Commit f78b0685 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fixed sport

parent 0d8cd6aa
...@@ -25,11 +25,17 @@ class MedicalBoardController extends Controller ...@@ -25,11 +25,17 @@ class MedicalBoardController extends Controller
} }
$records = $db->select( $records = $db->select(
"SELECT pmr.*, p.full_name_ar as player_name, p.card_status, "SELECT pmr.*,
p.phone as player_phone, d.file_path as document_path COALESCE(p.full_name_ar, sp.full_name_ar) as player_name,
COALESCE(p.card_status, 'N/A') as card_status,
COALESCE(p.phone, sp.phone) as player_phone,
d.file_path as document_path,
spd.file_path as sa_document_path
FROM player_medical_records pmr FROM player_medical_records pmr
JOIN players p ON p.id = pmr.player_id LEFT JOIN players p ON p.id = pmr.player_id AND pmr.source = 'membership'
LEFT JOIN sa_players sp ON sp.id = pmr.sa_player_id AND pmr.source = 'sports_activity'
LEFT JOIN documents d ON d.id = pmr.document_id LEFT JOIN documents d ON d.id = pmr.document_id
LEFT JOIN sa_player_documents spd ON spd.id = pmr.sa_document_id
WHERE {$where} WHERE {$where}
ORDER BY pmr.created_at DESC ORDER BY pmr.created_at DESC
LIMIT 100", LIMIT 100",
...@@ -65,10 +71,28 @@ class MedicalBoardController extends Controller ...@@ -65,10 +71,28 @@ class MedicalBoardController extends Controller
'approved_at' => date('Y-m-d H:i:s'), 'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]); ], 'id = ?', [(int) $id]);
$db->update('players', [ if (($record['source'] ?? 'membership') === 'sports_activity' && !empty($record['sa_player_id'])) {
'medical_status' => 'fit', $db->update('sa_players', [
'medical_expiry_date' => $expiryDate, 'medical_status' => 'fit',
], 'id = ?', [(int) $record['player_id']]); 'medical_expiry_date' => $expiryDate,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $record['sa_player_id']]);
if (!empty($record['sa_document_id'])) {
$db->update('sa_player_documents', [
'approval_status' => 'approved',
'expiry_date' => $expiryDate,
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $record['sa_document_id']]);
}
} else {
$db->update('players', [
'medical_status' => 'fit',
'medical_expiry_date' => $expiryDate,
], 'id = ?', [(int) $record['player_id']]);
}
return $this->redirect('/medical-board')->withSuccess('تم اعتماد الشهادة الطبية'); return $this->redirect('/medical-board')->withSuccess('تم اعتماد الشهادة الطبية');
} }
...@@ -92,9 +116,23 @@ class MedicalBoardController extends Controller ...@@ -92,9 +116,23 @@ class MedicalBoardController extends Controller
'approved_at' => date('Y-m-d H:i:s'), 'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]); ], 'id = ?', [(int) $id]);
$db->update('players', [ if (($record['source'] ?? 'membership') === 'sports_activity' && !empty($record['sa_player_id'])) {
'medical_status' => 'unfit', $db->update('sa_players', [
], 'id = ?', [(int) $record['player_id']]); 'medical_status' => 'unfit',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $record['sa_player_id']]);
if (!empty($record['sa_document_id'])) {
$db->update('sa_player_documents', [
'approval_status' => 'rejected',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $record['sa_document_id']]);
}
} else {
$db->update('players', [
'medical_status' => 'unfit',
], 'id = ?', [(int) $record['player_id']]);
}
return $this->redirect('/medical-board')->withSuccess('تم رفض الشهادة الطبية'); return $this->redirect('/medical-board')->withSuccess('تم رفض الشهادة الطبية');
} }
......
...@@ -49,6 +49,9 @@ ...@@ -49,6 +49,9 @@
<tr> <tr>
<td> <td>
<strong><?= e($record['player_name'] ?? '') ?></strong> <strong><?= e($record['player_name'] ?? '') ?></strong>
<?php if (($record['source'] ?? 'membership') === 'sports_activity'): ?>
<span style="padding:2px 6px;border-radius:8px;font-size:10px;font-weight:600;background:#EFF6FF;color:#2563EB;margin-right:4px;">نشاط رياضي</span>
<?php endif; ?>
<?php if (!empty($record['player_phone'])): ?> <?php if (!empty($record['player_phone'])): ?>
<br><small style="color:#6B7280;"><?= e($record['player_phone']) ?></small> <br><small style="color:#6B7280;"><?= e($record['player_phone']) ?></small>
<?php endif; ?> <?php endif; ?>
...@@ -59,8 +62,11 @@ ...@@ -59,8 +62,11 @@
<td style="direction:ltr;text-align:center;"><?= e($record['expiry_date'] ?? '-') ?></td> <td style="direction:ltr;text-align:center;"><?= e($record['expiry_date'] ?? '-') ?></td>
<td><?= e($record['doctor_name'] ?? $record['clinic_name'] ?? '-') ?></td> <td><?= e($record['doctor_name'] ?? $record['clinic_name'] ?? '-') ?></td>
<td> <td>
<?php if (!empty($record['document_path'])): ?> <?php
<a href="/storage/<?= e($record['document_path']) ?>" target="_blank" style="color:#2563EB;"> $docPath = $record['document_path'] ?? $record['sa_document_path'] ?? '';
?>
<?php if (!empty($docPath)): ?>
<a href="/<?= e($docPath) ?>" target="_blank" style="color:#2563EB;">
<i data-lucide="file-text" style="width:14px;height:14px;vertical-align:middle;"></i> عرض <i data-lucide="file-text" style="width:14px;height:14px;vertical-align:middle;"></i> عرض
</a> </a>
<?php else: ?> <?php else: ?>
......
...@@ -60,9 +60,10 @@ class ActivityBrowserApiController extends Controller ...@@ -60,9 +60,10 @@ class ActivityBrowserApiController extends Controller
$groups = $db->select( $groups = $db->select(
"SELECT g.id, g.code, g.name_ar, g.current_count, g.max_capacity, g.status, "SELECT g.id, g.code, g.name_ar, g.current_count, g.max_capacity, g.status,
g.monthly_fee_member, g.monthly_fee_nonmember, p.monthly_fee_member, p.monthly_fee_nonmember,
c.full_name_ar as coach_name c.full_name_ar as coach_name
FROM sa_groups g FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id LEFT JOIN sa_coaches c ON c.id = g.coach_id
WHERE g.program_id = ? AND g.is_archived = 0 WHERE g.program_id = ? AND g.is_archived = 0
ORDER BY g.name_ar ASC", ORDER BY g.name_ar ASC",
...@@ -97,6 +98,31 @@ class ActivityBrowserApiController extends Controller ...@@ -97,6 +98,31 @@ class ActivityBrowserApiController extends Controller
return $this->json(['success' => true, 'programs' => $programs]); return $this->json(['success' => true, 'programs' => $programs]);
} }
public function programPricing(Request $request, int $id): Response
{
$db = App::getInstance()->db();
$program = $db->selectOne(
"SELECT monthly_fee_member, monthly_fee_nonmember, max_capacity, min_capacity
FROM sa_programs WHERE id = ? AND is_archived = 0",
[$id]
);
if (!$program) {
return $this->json(['success' => false, 'error' => 'البرنامج غير موجود']);
}
return $this->json([
'success' => true,
'pricing' => [
'monthly_fee_member' => number_format((float) $program['monthly_fee_member'], 2),
'monthly_fee_nonmember' => number_format((float) $program['monthly_fee_nonmember'], 2),
'max_capacity' => (int) $program['max_capacity'],
'min_capacity' => (int) $program['min_capacity'],
],
]);
}
public function coachesByDiscipline(Request $request): Response public function coachesByDiscipline(Request $request): Response
{ {
$disciplineId = (int) $request->get('discipline_id', 0); $disciplineId = (int) $request->get('discipline_id', 0);
......
...@@ -24,10 +24,11 @@ class SubscriptionPreviewApiController extends Controller ...@@ -24,10 +24,11 @@ class SubscriptionPreviewApiController extends Controller
$groupPlayers = $db->select( $groupPlayers = $db->select(
"SELECT gp.player_id, gp.group_id, gp.enrolled_at, gp.paused_months, "SELECT gp.player_id, gp.group_id, gp.enrolled_at, gp.paused_months,
sp.player_type, sp.full_name_ar as player_name, sp.player_type, sp.full_name_ar as player_name,
g.monthly_fee_member, g.monthly_fee_nonmember, g.name_ar as group_name p.monthly_fee_member, p.monthly_fee_nonmember, g.name_ar as group_name
FROM sa_group_players gp FROM sa_group_players gp
JOIN sa_players sp ON sp.id = gp.player_id JOIN sa_players sp ON sp.id = gp.player_id
JOIN sa_groups g ON g.id = gp.group_id JOIN sa_groups g ON g.id = gp.group_id
JOIN sa_programs p ON p.id = g.program_id
WHERE gp.status = ? WHERE gp.status = ?
AND g.status = ? AND g.is_archived = 0", AND g.status = ? AND g.is_archived = 0",
[SaConstants::STATUS_ACTIVE, SaConstants::GROUP_ACTIVE] [SaConstants::STATUS_ACTIVE, SaConstants::GROUP_ACTIVE]
......
...@@ -88,7 +88,7 @@ class AttendanceController extends Controller ...@@ -88,7 +88,7 @@ class AttendanceController extends Controller
$players = []; $players = [];
if ($booking['group_id']) { if ($booking['group_id']) {
$players = $db->select( $players = $db->select(
"SELECT gp.player_id, p.full_name_ar as player_name, p.registration_serial as player_code "SELECT gp.player_id, p.full_name_ar as player_name, p.registration_serial as player_code, p.medical_status
FROM sa_group_players gp FROM sa_group_players gp
JOIN sa_players p ON p.id = gp.player_id JOIN sa_players p ON p.id = gp.player_id
WHERE gp.group_id = ? AND gp.status = 'active' WHERE gp.group_id = ? AND gp.status = 'active'
......
...@@ -65,7 +65,8 @@ class GroupController extends Controller ...@@ -65,7 +65,8 @@ class GroupController extends Controller
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
$groups = $db->select( $groups = $db->select(
"SELECT g.*, p.name_ar as program_name, c.full_name_ar as coach_name "SELECT g.*, p.name_ar as program_name, p.monthly_fee_member as program_fee, c.full_name_ar as coach_name,
EXISTS(SELECT 1 FROM sa_group_schedule gs WHERE gs.group_id = g.id AND gs.is_active = 1) as has_schedule
FROM sa_groups g FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id LEFT JOIN sa_coaches c ON c.id = g.coach_id
...@@ -116,10 +117,6 @@ class GroupController extends Controller ...@@ -116,10 +117,6 @@ class GroupController extends Controller
$nameEn = trim((string) $request->post('name_en', '')); $nameEn = trim((string) $request->post('name_en', ''));
$programId = (int) $request->post('program_id', 0); $programId = (int) $request->post('program_id', 0);
$coachId = (int) $request->post('coach_id', 0); $coachId = (int) $request->post('coach_id', 0);
$minCapacity = (int) $request->post('min_capacity', 0);
$maxCapacity = (int) $request->post('max_capacity', 0);
$monthlyFeeMember = (float) $request->post('monthly_fee_member', 0);
$monthlyFeeNonmember = (float) $request->post('monthly_fee_nonmember', 0);
$seasonStart = trim((string) $request->post('season_start', '')); $seasonStart = trim((string) $request->post('season_start', ''));
$seasonEnd = trim((string) $request->post('season_end', '')); $seasonEnd = trim((string) $request->post('season_end', ''));
...@@ -142,8 +139,8 @@ class GroupController extends Controller ...@@ -142,8 +139,8 @@ class GroupController extends Controller
} }
// Check unique code // Check unique code
$db = App::getInstance()->db();
if ($code !== '') { if ($code !== '') {
$db = App::getInstance()->db();
$existing = $db->selectOne("SELECT id FROM sa_groups WHERE code = ?", [$code]); $existing = $db->selectOne("SELECT id FROM sa_groups WHERE code = ?", [$code]);
if ($existing) { if ($existing) {
$errors[] = 'كود المجموعة مستخدم بالفعل'; $errors[] = 'كود المجموعة مستخدم بالفعل';
...@@ -157,21 +154,22 @@ class GroupController extends Controller ...@@ -157,21 +154,22 @@ class GroupController extends Controller
return $this->redirect('/sa/groups/create'); return $this->redirect('/sa/groups/create');
} }
// Inherit capacity from program
$programData = $db->selectOne("SELECT max_capacity, min_capacity FROM sa_programs WHERE id = ?", [$programId]);
$group = Group::create([ $group = Group::create([
'code' => $code, 'code' => $code,
'name_ar' => $nameAr, 'name_ar' => $nameAr,
'name_en' => $nameEn ?: null, 'name_en' => $nameEn ?: null,
'program_id' => $programId, 'program_id' => $programId,
'coach_id' => $coachId, 'coach_id' => $coachId,
'min_capacity' => $minCapacity, 'min_capacity' => (int) ($programData['min_capacity'] ?? 1),
'max_capacity' => $maxCapacity, 'max_capacity' => (int) ($programData['max_capacity'] ?? 20),
'current_count' => 0, 'current_count' => 0,
'is_full' => 0, 'is_full' => 0,
'monthly_fee_member' => $monthlyFeeMember, 'season_start' => $seasonStart ?: null,
'monthly_fee_nonmember' => $monthlyFeeNonmember, 'season_end' => $seasonEnd ?: null,
'season_start' => $seasonStart ?: null, 'status' => 'active',
'season_end' => $seasonEnd ?: null,
'status' => 'active',
]); ]);
return $this->redirect('/sa/groups/' . $group->id)->withSuccess('تم إضافة المجموعة بنجاح'); return $this->redirect('/sa/groups/' . $group->id)->withSuccess('تم إضافة المجموعة بنجاح');
...@@ -262,10 +260,6 @@ class GroupController extends Controller ...@@ -262,10 +260,6 @@ class GroupController extends Controller
$nameEn = trim((string) $request->post('name_en', '')); $nameEn = trim((string) $request->post('name_en', ''));
$programId = (int) $request->post('program_id', 0); $programId = (int) $request->post('program_id', 0);
$coachId = (int) $request->post('coach_id', 0); $coachId = (int) $request->post('coach_id', 0);
$minCapacity = (int) $request->post('min_capacity', 0);
$maxCapacity = (int) $request->post('max_capacity', 0);
$monthlyFeeMember = (float) $request->post('monthly_fee_member', 0);
$monthlyFeeNonmember = (float) $request->post('monthly_fee_nonmember', 0);
$seasonStart = trim((string) $request->post('season_start', '')); $seasonStart = trim((string) $request->post('season_start', ''));
$seasonEnd = trim((string) $request->post('season_end', '')); $seasonEnd = trim((string) $request->post('season_end', ''));
...@@ -288,8 +282,8 @@ class GroupController extends Controller ...@@ -288,8 +282,8 @@ class GroupController extends Controller
} }
// Check unique code (exclude current) // Check unique code (exclude current)
$db = App::getInstance()->db();
if ($code !== '') { if ($code !== '') {
$db = App::getInstance()->db();
$existing = $db->selectOne("SELECT id FROM sa_groups WHERE code = ? AND id != ?", [$code, (int) $id]); $existing = $db->selectOne("SELECT id FROM sa_groups WHERE code = ? AND id != ?", [$code, (int) $id]);
if ($existing) { if ($existing) {
$errors[] = 'كود المجموعة مستخدم بالفعل'; $errors[] = 'كود المجموعة مستخدم بالفعل';
...@@ -303,18 +297,19 @@ class GroupController extends Controller ...@@ -303,18 +297,19 @@ class GroupController extends Controller
return $this->redirect('/sa/groups/' . $id . '/edit'); return $this->redirect('/sa/groups/' . $id . '/edit');
} }
// Inherit capacity from program
$programData = $db->selectOne("SELECT max_capacity, min_capacity FROM sa_programs WHERE id = ?", [$programId]);
$group->update([ $group->update([
'code' => $code, 'code' => $code,
'name_ar' => $nameAr, 'name_ar' => $nameAr,
'name_en' => $nameEn ?: null, 'name_en' => $nameEn ?: null,
'program_id' => $programId, 'program_id' => $programId,
'coach_id' => $coachId, 'coach_id' => $coachId,
'min_capacity' => $minCapacity, 'min_capacity' => (int) ($programData['min_capacity'] ?? 1),
'max_capacity' => $maxCapacity, 'max_capacity' => (int) ($programData['max_capacity'] ?? 20),
'monthly_fee_member' => $monthlyFeeMember, 'season_start' => $seasonStart ?: null,
'monthly_fee_nonmember' => $monthlyFeeNonmember, 'season_end' => $seasonEnd ?: null,
'season_start' => $seasonStart ?: null,
'season_end' => $seasonEnd ?: null,
]); ]);
return $this->redirect('/sa/groups/' . $id)->withSuccess('تم تحديث المجموعة بنجاح'); return $this->redirect('/sa/groups/' . $id)->withSuccess('تم تحديث المجموعة بنجاح');
...@@ -337,7 +332,14 @@ class GroupController extends Controller ...@@ -337,7 +332,14 @@ class GroupController extends Controller
if (!empty($result['request_number'])) { if (!empty($result['request_number'])) {
$msg .= ' (طلب دفع: ' . $result['request_number'] . ')'; $msg .= ' (طلب دفع: ' . $result['request_number'] . ')';
} }
return $this->redirect('/sa/groups/' . $id . '?enrollment_pending=1')->withSuccess($msg); $redirect = $this->redirect('/sa/groups/' . $id . '?enrollment_pending=1')->withSuccess($msg);
if (!empty($result['medical_warning'])) {
$session = App::getInstance()->session();
$alerts = $session->get('_alerts', []);
$alerts[] = ['type' => 'warning', 'message' => $result['medical_warning']];
$session->flash('_alerts', $alerts);
}
return $redirect;
} }
return $this->redirect('/sa/groups/' . $id)->withError($result['error']); return $this->redirect('/sa/groups/' . $id)->withError($result['error']);
......
...@@ -38,10 +38,22 @@ class MirrorController extends Controller ...@@ -38,10 +38,22 @@ class MirrorController extends Controller
$grouped[$dName][] = $f; $grouped[$dName][] = $f;
} }
$unscheduledGroups = $db->select(
"SELECT g.id, g.code, g.name_ar, p.name_ar as program_name
FROM sa_groups g
JOIN sa_programs p ON p.id = g.program_id
WHERE g.status = 'active' AND g.is_archived = 0
AND NOT EXISTS (
SELECT 1 FROM sa_group_schedule gs WHERE gs.group_id = g.id AND gs.is_active = 1
)
ORDER BY g.name_ar"
);
return $this->view('SportsActivity.Views.mirror.index', [ return $this->view('SportsActivity.Views.mirror.index', [
'facilities' => $facilities, 'facilities' => $facilities,
'disciplines' => $disciplines, 'disciplines' => $disciplines,
'grouped' => $grouped, 'grouped' => $grouped,
'unscheduledGroups' => $unscheduledGroups,
]); ]);
} }
......
...@@ -90,8 +90,23 @@ class PlayerDocumentController extends Controller ...@@ -90,8 +90,23 @@ class PlayerDocumentController extends Controller
]; ];
$db->insert('sa_player_documents', $data); $db->insert('sa_player_documents', $data);
$docId = (int) $db->lastInsertId();
if ($documentType === 'medical_cert') {
$db->insert('player_medical_records', [
'player_id' => null,
'sa_player_id' => (int) $pid,
'record_type' => 'fitness_cert',
'sa_document_id' => $docId,
'approval_status' => 'pending',
'source' => 'sports_activity',
'created_by' => (int) (App::getInstance()->session()->get('employee_id') ?? 0),
'created_at' => now(),
'updated_at' => now(),
]);
}
return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم رفع المستند بنجاح'); return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم رفع المستند بنجاح' . ($documentType === 'medical_cert' ? ' — تم إرساله للجنة الطبية للمراجعة' : ''));
} }
/** /**
......
...@@ -102,6 +102,10 @@ class ProgramController extends Controller ...@@ -102,6 +102,10 @@ class ProgramController extends Controller
$sessionDuration = $request->post('session_duration_minutes', '') !== '' ? (int) $request->post('session_duration_minutes', 0) : null; $sessionDuration = $request->post('session_duration_minutes', '') !== '' ? (int) $request->post('session_duration_minutes', 0) : null;
$sessionsPerWeek = $request->post('sessions_per_week', '') !== '' ? (int) $request->post('sessions_per_week', 0) : null; $sessionsPerWeek = $request->post('sessions_per_week', '') !== '' ? (int) $request->post('sessions_per_week', 0) : null;
$descriptionAr = trim((string) $request->post('description_ar', '')); $descriptionAr = trim((string) $request->post('description_ar', ''));
$monthlyFeeMember = $request->post('monthly_fee_member', '') !== '' ? (float) $request->post('monthly_fee_member', 0) : 0.0;
$monthlyFeeNonmember = $request->post('monthly_fee_nonmember', '') !== '' ? (float) $request->post('monthly_fee_nonmember', 0) : 0.0;
$maxCapacity = $request->post('max_capacity', '') !== '' ? (int) $request->post('max_capacity', 20) : 20;
$minCapacity = $request->post('min_capacity', '') !== '' ? (int) $request->post('min_capacity', 1) : 1;
$errors = []; $errors = [];
if ($code === '') { if ($code === '') {
...@@ -113,6 +117,18 @@ class ProgramController extends Controller ...@@ -113,6 +117,18 @@ class ProgramController extends Controller
if ($disciplineId === 0) { if ($disciplineId === 0) {
$errors[] = 'النشاط الرياضي (اللعبة) مطلوب'; $errors[] = 'النشاط الرياضي (اللعبة) مطلوب';
} }
if ($monthlyFeeMember < 0) {
$errors[] = 'رسوم الأعضاء لا يمكن أن تكون سالبة';
}
if ($monthlyFeeNonmember < 0) {
$errors[] = 'رسوم غير الأعضاء لا يمكن أن تكون سالبة';
}
if ($maxCapacity < 1) {
$errors[] = 'الحد الأقصى للسعة يجب أن يكون 1 على الأقل';
}
if ($minCapacity > $maxCapacity) {
$errors[] = 'الحد الأدنى لا يمكن أن يتجاوز الأقصى';
}
// Check unique code // Check unique code
if ($code !== '') { if ($code !== '') {
...@@ -143,6 +159,10 @@ class ProgramController extends Controller ...@@ -143,6 +159,10 @@ class ProgramController extends Controller
'skill_level' => $skillLevel ?: null, 'skill_level' => $skillLevel ?: null,
'session_duration_minutes' => $sessionDuration, 'session_duration_minutes' => $sessionDuration,
'sessions_per_week' => $sessionsPerWeek, 'sessions_per_week' => $sessionsPerWeek,
'monthly_fee_member' => $monthlyFeeMember,
'monthly_fee_nonmember' => $monthlyFeeNonmember,
'max_capacity' => $maxCapacity,
'min_capacity' => $minCapacity,
'description_ar' => $descriptionAr ?: null, 'description_ar' => $descriptionAr ?: null,
'is_active' => 1, 'is_active' => 1,
]); ]);
...@@ -233,6 +253,10 @@ class ProgramController extends Controller ...@@ -233,6 +253,10 @@ class ProgramController extends Controller
$sessionDuration = $request->post('session_duration_minutes', '') !== '' ? (int) $request->post('session_duration_minutes', 0) : null; $sessionDuration = $request->post('session_duration_minutes', '') !== '' ? (int) $request->post('session_duration_minutes', 0) : null;
$sessionsPerWeek = $request->post('sessions_per_week', '') !== '' ? (int) $request->post('sessions_per_week', 0) : null; $sessionsPerWeek = $request->post('sessions_per_week', '') !== '' ? (int) $request->post('sessions_per_week', 0) : null;
$descriptionAr = trim((string) $request->post('description_ar', '')); $descriptionAr = trim((string) $request->post('description_ar', ''));
$monthlyFeeMember = $request->post('monthly_fee_member', '') !== '' ? (float) $request->post('monthly_fee_member', 0) : 0.0;
$monthlyFeeNonmember = $request->post('monthly_fee_nonmember', '') !== '' ? (float) $request->post('monthly_fee_nonmember', 0) : 0.0;
$maxCapacity = $request->post('max_capacity', '') !== '' ? (int) $request->post('max_capacity', 20) : 20;
$minCapacity = $request->post('min_capacity', '') !== '' ? (int) $request->post('min_capacity', 1) : 1;
$errors = []; $errors = [];
if ($code === '') { if ($code === '') {
...@@ -244,6 +268,18 @@ class ProgramController extends Controller ...@@ -244,6 +268,18 @@ class ProgramController extends Controller
if ($disciplineId === 0) { if ($disciplineId === 0) {
$errors[] = 'النشاط الرياضي (اللعبة) مطلوب'; $errors[] = 'النشاط الرياضي (اللعبة) مطلوب';
} }
if ($monthlyFeeMember < 0) {
$errors[] = 'رسوم الأعضاء لا يمكن أن تكون سالبة';
}
if ($monthlyFeeNonmember < 0) {
$errors[] = 'رسوم غير الأعضاء لا يمكن أن تكون سالبة';
}
if ($maxCapacity < 1) {
$errors[] = 'الحد الأقصى للسعة يجب أن يكون 1 على الأقل';
}
if ($minCapacity > $maxCapacity) {
$errors[] = 'الحد الأدنى لا يمكن أن يتجاوز الأقصى';
}
// Check unique code (exclude current) // Check unique code (exclude current)
if ($code !== '') { if ($code !== '') {
...@@ -274,9 +310,21 @@ class ProgramController extends Controller ...@@ -274,9 +310,21 @@ class ProgramController extends Controller
'skill_level' => $skillLevel ?: null, 'skill_level' => $skillLevel ?: null,
'session_duration_minutes' => $sessionDuration, 'session_duration_minutes' => $sessionDuration,
'sessions_per_week' => $sessionsPerWeek, 'sessions_per_week' => $sessionsPerWeek,
'monthly_fee_member' => $monthlyFeeMember,
'monthly_fee_nonmember' => $monthlyFeeNonmember,
'max_capacity' => $maxCapacity,
'min_capacity' => $minCapacity,
'description_ar' => $descriptionAr ?: null, 'description_ar' => $descriptionAr ?: null,
]); ]);
// Sync capacity to all active groups of this program
$db = App::getInstance()->db();
$db->query(
"UPDATE sa_groups SET max_capacity = ?, is_full = IF(current_count >= ?, 1, 0), updated_at = NOW()
WHERE program_id = ? AND status = 'active' AND is_archived = 0",
[$maxCapacity, $maxCapacity, (int) $id]
);
return $this->redirect('/sa/programs/' . $id)->withSuccess('تم تحديث البرنامج بنجاح'); return $this->redirect('/sa/programs/' . $id)->withSuccess('تم تحديث البرنامج بنجاح');
} }
} }
...@@ -114,7 +114,8 @@ class RegistrationWizardController extends Controller ...@@ -114,7 +114,8 @@ class RegistrationWizardController extends Controller
$registration = $db->selectOne( $registration = $db->selectOne(
"SELECT r.*, p.full_name_ar, p.full_name_en, p.national_id as player_nid, "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.date_of_birth, p.gender, p.phone, p.photo_path, p.player_type as p_type,
p.guardian_name, p.guardian_phone, p.guardian_national_id, p.guardian_relationship p.guardian_name, p.guardian_phone, p.guardian_national_id, p.guardian_relationship,
p.medical_status
FROM sa_registrations r FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ?", WHERE r.id = ?",
...@@ -126,7 +127,8 @@ class RegistrationWizardController extends Controller ...@@ -126,7 +127,8 @@ class RegistrationWizardController extends Controller
} }
$groups = $db->select( $groups = $db->select(
"SELECT g.*, p.name_ar as program_name, p.discipline_id, d.name_ar as discipline_name "SELECT g.*, p.name_ar as program_name, p.discipline_id, d.name_ar as discipline_name,
p.monthly_fee_member, p.monthly_fee_nonmember
FROM sa_groups g FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
...@@ -207,13 +209,13 @@ class RegistrationWizardController extends Controller ...@@ -207,13 +209,13 @@ class RegistrationWizardController extends Controller
$programs = $db->select( $programs = $db->select(
"SELECT p.id, p.name_ar, p.name_en, p.description_ar, "SELECT p.id, p.name_ar, p.name_en, p.description_ar,
COUNT(g.id) as group_count, p.monthly_fee_member as min_fee_member,
MIN(g.monthly_fee_member) as min_fee_member, p.monthly_fee_nonmember as min_fee_nonmember,
MIN(g.monthly_fee_nonmember) as min_fee_nonmember COUNT(g.id) as group_count
FROM sa_programs p FROM sa_programs p
LEFT JOIN sa_groups g ON g.program_id = p.id AND g.status = 'active' AND g.is_archived = 0 LEFT JOIN sa_groups g ON g.program_id = p.id AND g.status = 'active' AND g.is_archived = 0
WHERE p.discipline_id = ? AND p.is_archived = 0 WHERE p.discipline_id = ? AND p.is_archived = 0
GROUP BY p.id, p.name_ar, p.name_en, p.description_ar GROUP BY p.id, p.name_ar, p.name_en, p.description_ar, p.monthly_fee_member, p.monthly_fee_nonmember
ORDER BY p.name_ar", ORDER BY p.name_ar",
[(int) $disciplineId] [(int) $disciplineId]
); );
......
...@@ -17,7 +17,6 @@ class Group extends Model ...@@ -17,7 +17,6 @@ class Group extends Model
protected static array $fillable = [ protected static array $fillable = [
'code', 'name_ar', 'name_en', 'program_id', 'coach_id', 'code', 'name_ar', 'name_en', 'program_id', 'coach_id',
'min_capacity', 'max_capacity', 'current_count', 'is_full', 'min_capacity', 'max_capacity', 'current_count', 'is_full',
'monthly_fee_member', 'monthly_fee_nonmember',
'season_start', 'season_end', 'status', 'season_start', 'season_end', 'status',
]; ];
...@@ -48,7 +47,7 @@ class Group extends Model ...@@ -48,7 +47,7 @@ class Group extends Model
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
return $db->select( return $db->select(
"SELECT gp.*, p.full_name_ar as player_name, p.registration_serial as player_code, p.phone, "SELECT gp.*, p.full_name_ar as player_name, p.registration_serial as player_code, p.phone, p.medical_status,
(SELECT COUNT(*) FROM sa_subscriptions s (SELECT COUNT(*) FROM sa_subscriptions s
WHERE s.player_id = gp.player_id AND s.group_id = gp.group_id WHERE s.player_id = gp.player_id AND s.group_id = gp.group_id
AND s.payment_status IN ('unpaid','overdue','partial')) as unpaid_count, AND s.payment_status IN ('unpaid','overdue','partial')) as unpaid_count,
......
...@@ -18,7 +18,8 @@ class Program extends Model ...@@ -18,7 +18,8 @@ class Program extends Model
'code', 'name_ar', 'name_en', 'discipline_id', 'academy_id', 'code', 'name_ar', 'name_en', 'discipline_id', 'academy_id',
'program_type', 'age_from', 'age_to', 'gender_restriction', 'program_type', 'age_from', 'age_to', 'gender_restriction',
'skill_level', 'description_ar', 'session_duration_minutes', 'skill_level', 'description_ar', 'session_duration_minutes',
'sessions_per_week', 'is_active', 'sessions_per_week', 'monthly_fee_member', 'monthly_fee_nonmember',
'max_capacity', 'min_capacity', 'is_active',
]; ];
public static function getProgramTypeOptions(): array public static function getProgramTypeOptions(): array
......
...@@ -57,10 +57,10 @@ return [ ...@@ -57,10 +57,10 @@ return [
['GET', '/sa/players/{pid:\d+}/documents', 'SportsActivity\Controllers\PlayerDocumentController@index', ['auth'], 'sa.player.view'], ['GET', '/sa/players/{pid:\d+}/documents', 'SportsActivity\Controllers\PlayerDocumentController@index', ['auth'], 'sa.player.view'],
['POST', '/sa/players/{pid:\d+}/documents', 'SportsActivity\Controllers\PlayerDocumentController@upload', ['auth', 'csrf'], 'sa.player.manage'], ['POST', '/sa/players/{pid:\d+}/documents', 'SportsActivity\Controllers\PlayerDocumentController@upload', ['auth', 'csrf'], 'sa.player.manage'],
['POST', '/sa/players/{pid:\d+}/documents/upload', 'SportsActivity\Controllers\PlayerDocumentController@upload', ['auth', 'csrf'], 'sa.player.manage'], ['POST', '/sa/players/{pid:\d+}/documents/upload', 'SportsActivity\Controllers\PlayerDocumentController@upload', ['auth', 'csrf'], 'sa.player.manage'],
['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/approve', 'SportsActivity\Controllers\PlayerDocumentController@approve', ['auth', 'csrf'], 'sa.medical.approve'], ['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/approve', 'SportsActivity\Controllers\PlayerDocumentController@approve', ['auth', 'csrf'], 'medical.board.approve'],
['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/reject', 'SportsActivity\Controllers\PlayerDocumentController@reject', ['auth', 'csrf'], 'sa.medical.approve'], ['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/reject', 'SportsActivity\Controllers\PlayerDocumentController@reject', ['auth', 'csrf'], 'medical.board.approve'],
['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/approve-conditional', 'SportsActivity\Controllers\PlayerDocumentController@approveConditional', ['auth', 'csrf'], 'sa.medical.approve'], ['POST', '/sa/players/{pid:\d+}/documents/{id:\d+}/approve-conditional', 'SportsActivity\Controllers\PlayerDocumentController@approveConditional', ['auth', 'csrf'], 'medical.board.approve'],
['POST', '/sa/players/{pid:\d+}/medical/update', 'SportsActivity\Controllers\PlayerDocumentController@updateMedical', ['auth', 'csrf'], 'sa.medical.approve'], ['POST', '/sa/players/{pid:\d+}/medical/update', 'SportsActivity\Controllers\PlayerDocumentController@updateMedical', ['auth', 'csrf'], 'medical.board.approve'],
// Academies // Academies
['GET', '/sa/academies', 'SportsActivity\Controllers\AcademyController@index', ['auth'], 'sa.academy.view'], ['GET', '/sa/academies', 'SportsActivity\Controllers\AcademyController@index', ['auth'], 'sa.academy.view'],
...@@ -269,6 +269,9 @@ return [ ...@@ -269,6 +269,9 @@ return [
['GET', '/api/sa/activities/programs-by-discipline','SportsActivity\Controllers\Api\ActivityBrowserApiController@programsByDiscipline', ['auth'], 'sa.discipline.view'], ['GET', '/api/sa/activities/programs-by-discipline','SportsActivity\Controllers\Api\ActivityBrowserApiController@programsByDiscipline', ['auth'], 'sa.discipline.view'],
['GET', '/api/sa/activities/coaches-by-discipline', 'SportsActivity\Controllers\Api\ActivityBrowserApiController@coachesByDiscipline', ['auth'], 'sa.discipline.view'], ['GET', '/api/sa/activities/coaches-by-discipline', 'SportsActivity\Controllers\Api\ActivityBrowserApiController@coachesByDiscipline', ['auth'], 'sa.discipline.view'],
// ─── Program Pricing API ────────────────────────────────────────────────────
['GET', '/api/sa/programs/{id:\d+}/pricing', 'SportsActivity\Controllers\Api\ActivityBrowserApiController@programPricing', ['auth'], 'sa.program.view'],
// ─── Subscription Preview APIs ────────────────────────────────────────────── // ─── Subscription Preview APIs ──────────────────────────────────────────────
['GET', '/api/sa/subscriptions/preview', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@preview', ['auth'], 'sa.subscription.view'], ['GET', '/api/sa/subscriptions/preview', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@preview', ['auth'], 'sa.subscription.view'],
['POST', '/api/sa/subscriptions/pause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@pause', ['auth', 'csrf'], 'sa.subscription.generate'], ['POST', '/api/sa/subscriptions/pause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@pause', ['auth', 'csrf'], 'sa.subscription.generate'],
......
...@@ -21,14 +21,11 @@ final class EnrollmentService ...@@ -21,14 +21,11 @@ final class EnrollmentService
return ['success' => false, 'error' => 'اللاعب غير مسجل في النظام']; return ['success' => false, 'error' => 'اللاعب غير مسجل في النظام'];
} }
$medicalWarning = null;
if (!in_array($player['medical_status'], [SaConstants::MEDICAL_FIT, SaConstants::MEDICAL_CONDITIONAL])) { if (!in_array($player['medical_status'], [SaConstants::MEDICAL_FIT, SaConstants::MEDICAL_CONDITIONAL])) {
return ['success' => false, 'error' => 'الحالة الطبية للاعب لا تسمح بالتسجيل (' . $player['medical_status'] . ')']; $medicalWarning = 'اللاعب بدون شهادة طبية سارية';
} } elseif ($player['medical_status'] === SaConstants::MEDICAL_FIT && !empty($player['medical_expiry_date']) && $player['medical_expiry_date'] < date('Y-m-d')) {
$medicalWarning = 'الشهادة الطبية منتهية الصلاحية';
if ($player['medical_status'] === SaConstants::MEDICAL_FIT && $player['medical_expiry_date']) {
if ($player['medical_expiry_date'] < date('Y-m-d')) {
return ['success' => false, 'error' => 'الشهادة الطبية منتهية الصلاحية'];
}
} }
$group = $db->selectOne("SELECT * FROM sa_groups WHERE id = ? AND is_archived = 0", [$groupId]); $group = $db->selectOne("SELECT * FROM sa_groups WHERE id = ? AND is_archived = 0", [$groupId]);
...@@ -67,13 +64,21 @@ final class EnrollmentService ...@@ -67,13 +64,21 @@ final class EnrollmentService
return ['success' => false, 'error' => 'اللاعب مسجل بالفعل في هذه المجموعة']; return ['success' => false, 'error' => 'اللاعب مسجل بالفعل في هذه المجموعة'];
} }
if ((int) $group['current_count'] >= (int) $group['max_capacity']) { $program = $db->selectOne(
"SELECT monthly_fee_member, monthly_fee_nonmember, max_capacity FROM sa_programs WHERE id = ?",
[(int) $group['program_id']]
);
if (!$program) {
return ['success' => false, 'error' => 'البرنامج المرتبط بالمجموعة غير موجود'];
}
if ((int) $group['current_count'] >= (int) $program['max_capacity']) {
return ['success' => false, 'error' => 'المجموعة ممتلئة', 'suggest_waitlist' => true]; return ['success' => false, 'error' => 'المجموعة ممتلئة', 'suggest_waitlist' => true];
} }
$fee = $player['player_type'] === SaConstants::PLAYER_MEMBER $fee = $player['player_type'] === SaConstants::PLAYER_MEMBER
? (string) $group['monthly_fee_member'] ? (string) $program['monthly_fee_member']
: (string) $group['monthly_fee_nonmember']; : (string) $program['monthly_fee_nonmember'];
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0); $employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
...@@ -115,11 +120,12 @@ final class EnrollmentService ...@@ -115,11 +120,12 @@ final class EnrollmentService
} }
return [ return [
'success' => true, 'success' => true,
'enrollment_id' => $enrollmentId, 'enrollment_id' => $enrollmentId,
'request_id' => $requestResult['request_id'] ?? null, 'request_id' => $requestResult['request_id'] ?? null,
'request_number' => $requestResult['request_number'] ?? null, 'request_number' => $requestResult['request_number'] ?? null,
'fee' => $fee, 'fee' => $fee,
'medical_warning' => $medicalWarning,
]; ];
} }
...@@ -244,9 +250,14 @@ final class EnrollmentService ...@@ -244,9 +250,14 @@ final class EnrollmentService
return ['success' => false, 'error' => 'اللاعب مسجل بالفعل في هذه المجموعة']; return ['success' => false, 'error' => 'اللاعب مسجل بالفعل في هذه المجموعة'];
} }
$program = $db->selectOne(
"SELECT monthly_fee_member, monthly_fee_nonmember FROM sa_programs WHERE id = ?",
[(int) $group['program_id']]
);
$fee = $player['player_type'] === SaConstants::PLAYER_MEMBER $fee = $player['player_type'] === SaConstants::PLAYER_MEMBER
? (string) $group['monthly_fee_member'] ? (string) ($program['monthly_fee_member'] ?? '0')
: (string) $group['monthly_fee_nonmember']; : (string) ($program['monthly_fee_nonmember'] ?? '0');
$employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0); $employeeId = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
...@@ -347,11 +358,11 @@ final class EnrollmentService ...@@ -347,11 +358,11 @@ final class EnrollmentService
'program_id' => $programId, 'program_id' => $programId,
'facility_unit_id' => $sourceGroup['facility_unit_id'], 'facility_unit_id' => $sourceGroup['facility_unit_id'],
'coach_id' => $sourceGroup['coach_id'], 'coach_id' => $sourceGroup['coach_id'],
'monthly_fee_member' => $sourceGroup['monthly_fee_member'], 'monthly_fee_member' => 0,
'monthly_fee_nonmember' => $sourceGroup['monthly_fee_nonmember'], 'monthly_fee_nonmember' => 0,
'min_age' => $sourceGroup['min_age'], 'min_age' => $sourceGroup['min_age'],
'max_age' => $sourceGroup['max_age'], 'max_age' => $sourceGroup['max_age'],
'max_capacity' => $sourceGroup['max_capacity'], 'max_capacity' => (int) ($program['max_capacity'] ?? 20),
'current_count' => 0, 'current_count' => 0,
'is_full' => 0, 'is_full' => 0,
'status' => 'active', 'status' => 'active',
......
...@@ -187,25 +187,30 @@ final class RegistrationWizardService ...@@ -187,25 +187,30 @@ final class RegistrationWizardService
return ['success' => false, 'error' => 'المجموعة غير موجودة أو غير نشطة']; return ['success' => false, 'error' => 'المجموعة غير موجودة أو غير نشطة'];
} }
if ((int) $group['current_count'] >= (int) $group['max_capacity']) { $program = $db->selectOne(
"SELECT monthly_fee_member, monthly_fee_nonmember, max_capacity FROM sa_programs WHERE id = ?",
[(int) $group['program_id']]
);
if (!$program) {
return ['success' => false, 'error' => 'البرنامج المرتبط بالمجموعة غير موجود'];
}
if ((int) $group['current_count'] >= (int) $program['max_capacity']) {
return ['success' => false, 'error' => 'المجموعة ممتلئة']; return ['success' => false, 'error' => 'المجموعة ممتلئة'];
} }
$playerType = $registration['player_type']; $playerType = $registration['player_type'];
$monthlyFee = $playerType === 'member' $monthlyFee = $playerType === 'member'
? (float) $group['monthly_fee_member'] ? (float) $program['monthly_fee_member']
: (float) $group['monthly_fee_nonmember']; : (float) $program['monthly_fee_nonmember'];
$subscriptionAmount = $monthlyFee * max(1, $months); $subscriptionAmount = $monthlyFee * max(1, $months);
// Resolve academy code for discount scoping // Resolve academy code for discount scoping
$academyCode = null; $academyCode = null;
if (!empty($group['program_id'])) { if (!empty($program['academy_id'])) {
$program = $db->selectOne("SELECT academy_id FROM sa_programs WHERE id = ?", [(int) $group['program_id']]); $academy = $db->selectOne("SELECT code FROM sa_academies WHERE id = ?", [(int) $program['academy_id']]);
if ($program) { $academyCode = $academy['code'] ?? null;
$academy = $db->selectOne("SELECT code FROM sa_academies WHERE id = ?", [(int) $program['academy_id']]);
$academyCode = $academy['code'] ?? null;
}
} }
$discountResult = DiscountCalculatorService::calculateDiscounts( $discountResult = DiscountCalculatorService::calculateDiscounts(
......
...@@ -18,10 +18,11 @@ final class SubscriptionGeneratorService ...@@ -18,10 +18,11 @@ final class SubscriptionGeneratorService
$groupPlayers = $db->select( $groupPlayers = $db->select(
"SELECT gp.player_id, gp.group_id, gp.enrolled_at, gp.paused_months, "SELECT gp.player_id, gp.group_id, gp.enrolled_at, gp.paused_months,
sp.player_type, sp.full_name_ar as player_name, sp.player_type, sp.full_name_ar as player_name,
g.monthly_fee_member, g.monthly_fee_nonmember, g.name_ar as group_name p.monthly_fee_member, p.monthly_fee_nonmember, g.name_ar as group_name
FROM sa_group_players gp FROM sa_group_players gp
JOIN sa_players sp ON sp.id = gp.player_id JOIN sa_players sp ON sp.id = gp.player_id
JOIN sa_groups g ON g.id = gp.group_id JOIN sa_groups g ON g.id = gp.group_id
JOIN sa_programs p ON p.id = g.program_id
WHERE gp.status = ? WHERE gp.status = ?
AND g.status = ? AND g.is_archived = 0", AND g.status = ? AND g.is_archived = 0",
[SaConstants::STATUS_ACTIVE, SaConstants::GROUP_ACTIVE] [SaConstants::STATUS_ACTIVE, SaConstants::GROUP_ACTIVE]
......
...@@ -45,7 +45,12 @@ ...@@ -45,7 +45,12 @@
?> ?>
<tr> <tr>
<td><?= $i + 1 ?></td> <td><?= $i + 1 ?></td>
<td style="font-weight:600;"><?= e($player['player_name'] ?? '') ?></td> <td style="font-weight:600;">
<?= e($player['player_name'] ?? '') ?>
<?php if (!in_array($player['medical_status'] ?? 'pending', ['fit', 'conditional'])): ?>
<i data-lucide="alert-triangle" style="width:13px;height:13px;color:#D97706;vertical-align:middle;margin-right:3px;" title="بدون شهادة طبية سارية"></i>
<?php endif; ?>
</td>
<td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($player['player_code'] ?? '') ?></code></td> <td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($player['player_code'] ?? '') ?></code></td>
<td style="text-align:center;"> <td style="text-align:center;">
<input type="hidden" name="player_ids[<?= $i ?>]" value="<?= $playerId ?>"> <input type="hidden" name="player_ids[<?= $i ?>]" value="<?= $playerId ?>">
......
...@@ -64,30 +64,15 @@ ...@@ -64,30 +64,15 @@
</div> </div>
</div> </div>
<!-- Capacity & Fees --> <!-- Inherited Pricing & Capacity (read-only from program) -->
<div class="card" style="margin-bottom:20px;"> <div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;"> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#0D7377;"></i> <i data-lucide="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">السعة والرسوم</h3> <h3 style="margin:0;color:#6B7280;font-size:15px;">السعة والرسوم (موروثة من البرنامج)</h3>
</div> </div>
<div style="padding:20px;"> <div style="padding:20px;background:#F9FAFB;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;"> <div id="inherited-pricing" style="font-size:13px;color:#6B7280;">
<div class="form-group"> اختر البرنامج أولاً لعرض الرسوم والسعة الموروثة
<label class="form-label">الحد الأدنى <span style="color:#DC2626;">*</span></label>
<input type="number" name="min_capacity" value="<?= e(old('min_capacity', '5')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى <span style="color:#DC2626;">*</span></label>
<input type="number" name="max_capacity" value="<?= e(old('max_capacity', '20')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رسوم شهرية (أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_member" value="<?= e(old('monthly_fee_member', '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رسوم شهرية (غير أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_nonmember" value="<?= e(old('monthly_fee_nonmember', '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -150,6 +135,28 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -150,6 +135,28 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
}); });
programEl.addEventListener('change', function() {
var pid = this.value;
var panel = document.getElementById('inherited-pricing');
if (!pid) {
panel.innerHTML = 'اختر البرنامج أولاً لعرض الرسوم والسعة الموروثة';
return;
}
fetch('/api/sa/programs/' + pid + '/pricing')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
var p = data.pricing;
panel.innerHTML = '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:16px;">'
+ '<div><span style="color:#9CA3AF;">رسوم أعضاء:</span> <strong>' + p.monthly_fee_member + '</strong></div>'
+ '<div><span style="color:#9CA3AF;">رسوم غير أعضاء:</span> <strong>' + p.monthly_fee_nonmember + '</strong></div>'
+ '<div><span style="color:#9CA3AF;">أدنى سعة:</span> <strong>' + p.min_capacity + '</strong></div>'
+ '<div><span style="color:#9CA3AF;">أقصى سعة:</span> <strong>' + p.max_capacity + '</strong></div>'
+ '</div>';
}
});
});
function rebuildSelect(el, items, selectedId) { function rebuildSelect(el, items, selectedId) {
var placeholder = el === programEl ? '-- اختر البرنامج --' : '-- اختر المدرب --'; var placeholder = el === programEl ? '-- اختر البرنامج --' : '-- اختر المدرب --';
var html = '<option value="">' + placeholder + '</option>'; var html = '<option value="">' + placeholder + '</option>';
......
...@@ -64,30 +64,15 @@ ...@@ -64,30 +64,15 @@
</div> </div>
</div> </div>
<!-- Capacity & Fees --> <!-- Inherited Pricing & Capacity (read-only from program) -->
<div class="card" style="margin-bottom:20px;"> <div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;"> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#0D7377;"></i> <i data-lucide="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">السعة والرسوم</h3> <h3 style="margin:0;color:#6B7280;font-size:15px;">السعة والرسوم (موروثة من البرنامج)</h3>
</div> </div>
<div style="padding:20px;"> <div style="padding:20px;background:#F9FAFB;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;"> <div id="inherited-pricing" style="font-size:13px;color:#6B7280;">
<div class="form-group"> جاري التحميل...
<label class="form-label">الحد الأدنى <span style="color:#DC2626;">*</span></label>
<input type="number" name="min_capacity" value="<?= e(old('min_capacity', $group->min_capacity ?? '5')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى <span style="color:#DC2626;">*</span></label>
<input type="number" name="max_capacity" value="<?= e(old('max_capacity', $group->max_capacity ?? '20')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رسوم شهرية (أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_member" value="<?= e(old('monthly_fee_member', $group->monthly_fee_member ?? '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رسوم شهرية (غير أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_nonmember" value="<?= e(old('monthly_fee_nonmember', $group->monthly_fee_nonmember ?? '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -167,6 +152,30 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -167,6 +152,30 @@ document.addEventListener('DOMContentLoaded', function() {
d.textContent = str || ''; d.textContent = str || '';
return d.innerHTML; return d.innerHTML;
} }
// Inherited pricing display
var pricingPanel = document.getElementById('inherited-pricing');
function loadProgramPricing(pid) {
if (!pid) {
pricingPanel.innerHTML = 'اختر البرنامج أولاً لعرض الرسوم والسعة الموروثة';
return;
}
fetch('/api/sa/programs/' + pid + '/pricing')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
var p = data.pricing;
pricingPanel.innerHTML = '<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:16px;">'
+ '<div><span style="color:#9CA3AF;">رسوم أعضاء:</span> <strong>' + p.monthly_fee_member + '</strong></div>'
+ '<div><span style="color:#9CA3AF;">رسوم غير أعضاء:</span> <strong>' + p.monthly_fee_nonmember + '</strong></div>'
+ '<div><span style="color:#9CA3AF;">أدنى سعة:</span> <strong>' + p.min_capacity + '</strong></div>'
+ '<div><span style="color:#9CA3AF;">أقصى سعة:</span> <strong>' + p.max_capacity + '</strong></div>'
+ '</div>';
}
});
}
programEl.addEventListener('change', function() { loadProgramPricing(this.value); });
if (currentProgramId) loadProgramPricing(currentProgramId);
}); });
</script> </script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
...@@ -86,6 +86,9 @@ ...@@ -86,6 +86,9 @@
<a href="/sa/groups/<?= (int) $g['id'] ?>" style="text-decoration:none;color:#1A1A2E;font-weight:600;"> <a href="/sa/groups/<?= (int) $g['id'] ?>" style="text-decoration:none;color:#1A1A2E;font-weight:600;">
<?= e($g['name_ar']) ?> <?= e($g['name_ar']) ?>
</a> </a>
<?php if (empty($g['has_schedule']) && ($g['status'] ?? '') === 'active'): ?>
<i data-lucide="calendar-x" style="width:13px;height:13px;color:#D97706;vertical-align:middle;margin-right:4px;" title="بدون جدول زمني"></i>
<?php endif; ?>
<?php if (!empty($g['name_en'])): ?> <?php if (!empty($g['name_en'])): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e($g['name_en']) ?></div> <div style="font-size:11px;color:#9CA3AF;"><?= e($g['name_en']) ?></div>
<?php endif; ?> <?php endif; ?>
...@@ -98,7 +101,7 @@ ...@@ -98,7 +101,7 @@
</span> </span>
/ <?= (int) $g['max_capacity'] ?> / <?= (int) $g['max_capacity'] ?>
</td> </td>
<td style="direction:ltr;text-align:right;"><?= money((float) ($g['monthly_fee_member'] ?? 0)) ?></td> <td style="direction:ltr;text-align:right;"><?= money((float) ($g['program_fee'] ?? 0)) ?></td>
<td> <td>
<?php $st = $g['status'] ?? 'active'; ?> <?php $st = $g['status'] ?? 'active'; ?>
<span style="background:<?= $statusBgs[$st] ?? '#F3F4F6' ?>;color:<?= $statusColors[$st] ?? '#6B7280' ?>;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;"> <span style="background:<?= $statusBgs[$st] ?? '#F3F4F6' ?>;color:<?= $statusColors[$st] ?? '#6B7280' ?>;padding:3px 10px;border-radius:10px;font-size:12px;font-weight:600;">
......
...@@ -121,6 +121,9 @@ $st = $group['status'] ?? 'active'; ...@@ -121,6 +121,9 @@ $st = $group['status'] ?? 'active';
<td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($pl['player_code'] ?? '') ?></code></td> <td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($pl['player_code'] ?? '') ?></code></td>
<td style="font-weight:600;"> <td style="font-weight:600;">
<a href="/sa/players/<?= (int) $pl['player_id'] ?>" style="color:inherit;text-decoration:none;"><?= e($pl['player_name']) ?></a> <a href="/sa/players/<?= (int) $pl['player_id'] ?>" style="color:inherit;text-decoration:none;"><?= e($pl['player_name']) ?></a>
<?php if (!in_array($pl['medical_status'] ?? 'pending', ['fit', 'conditional'])): ?>
<i data-lucide="alert-triangle" style="width:13px;height:13px;color:#D97706;vertical-align:middle;margin-right:4px;" title="بدون شهادة طبية سارية"></i>
<?php endif; ?>
<?php if (($pl['status'] ?? '') === 'pending_payment'): ?> <?php if (($pl['status'] ?? '') === 'pending_payment'): ?>
<span style="padding:2px 6px;border-radius:8px;font-size:10px;font-weight:700;background:#FEF3C7;color:#D97706;margin-right:5px;">في انتظار الدفع</span> <span style="padding:2px 6px;border-radius:8px;font-size:10px;font-weight:700;background:#FEF3C7;color:#D97706;margin-right:5px;">في انتظار الدفع</span>
<?php endif; ?> <?php endif; ?>
......
...@@ -3,6 +3,22 @@ ...@@ -3,6 +3,22 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<?php if (!empty($unscheduledGroups)): ?>
<div style="background:#FEF3C7;border:1px solid #F59E0B;border-radius:8px;padding:14px 18px;margin-bottom:16px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<i data-lucide="alert-triangle" style="width:18px;height:18px;color:#D97706;"></i>
<strong style="color:#92400E;font-size:14px;">مجموعات بدون جدول زمني (<?= count($unscheduledGroups) ?>)</strong>
</div>
<div style="display:flex;flex-wrap:wrap;gap:8px;">
<?php foreach ($unscheduledGroups as $ug): ?>
<a href="/sa/groups/<?= (int) $ug['id'] ?>" style="background:#FFF;border:1px solid #F59E0B;border-radius:6px;padding:4px 10px;font-size:12px;text-decoration:none;color:#92400E;">
<?= e($ug['name_ar']) ?> <span style="color:#6B7280;">(<?= e($ug['program_name']) ?>)</span>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php if (!empty($disciplines)): ?> <?php if (!empty($disciplines)): ?>
<div class="card" style="margin-bottom:20px;padding:15px;"> <div class="card" style="margin-bottom:20px;padding:15px;">
<div style="font-weight:700;margin-bottom:10px;font-size:14px;">تصفية بالرياضة</div> <div style="font-weight:700;margin-bottom:10px;font-size:14px;">تصفية بالرياضة</div>
......
...@@ -87,6 +87,9 @@ $__template->layout('Layout.main'); ...@@ -87,6 +87,9 @@ $__template->layout('Layout.main');
<a href="/sa/players/<?= (int) $player['id'] ?>" style="text-decoration:none;color:#0D7377;font-weight:600;"> <a href="/sa/players/<?= (int) $player['id'] ?>" style="text-decoration:none;color:#0D7377;font-weight:600;">
<?= e($player['full_name_ar'] ?? '') ?> <?= e($player['full_name_ar'] ?? '') ?>
</a> </a>
<?php if (!in_array($player['medical_status'] ?? 'pending', ['fit', 'conditional'])): ?>
<i data-lucide="alert-triangle" style="width:14px;height:14px;color:#D97706;vertical-align:middle;margin-right:4px;" title="بدون شهادة طبية سارية"></i>
<?php endif; ?>
</td> </td>
<td> <td>
<?php <?php
......
...@@ -22,6 +22,13 @@ $genderLabels = ['male' => 'ذكر', 'female' => 'أنثى']; ...@@ -22,6 +22,13 @@ $genderLabels = ['male' => 'ذكر', 'female' => 'أنثى'];
$relationLabels = ['father' => 'أب', 'mother' => 'أم', 'brother' => 'أخ', 'sister' => 'أخت', 'uncle' => 'عم / خال', 'other' => 'أخرى']; $relationLabels = ['father' => 'أب', 'mother' => 'أم', 'brother' => 'أخ', 'sister' => 'أخت', 'uncle' => 'عم / خال', 'other' => 'أخرى'];
?> ?>
<?php if (!in_array($player['medical_status'] ?? 'pending', ['fit', 'conditional'])): ?>
<div style="background:#FEF3C7;border:1px solid #F59E0B;border-radius:8px;padding:12px 16px;margin-bottom:16px;display:flex;align-items:center;gap:10px;">
<i data-lucide="alert-triangle" style="width:20px;height:20px;color:#D97706;flex-shrink:0;"></i>
<div style="font-size:13px;color:#92400E;font-weight:600;">هذا اللاعب بدون شهادة طبية سارية — يمكنه التدريب لكن يجب تقديم الشهادة في أقرب وقت</div>
</div>
<?php endif; ?>
<!-- Player Info Card --> <!-- Player Info Card -->
<div class="card" style="margin-bottom:20px;"> <div class="card" style="margin-bottom:20px;">
<div style="padding:20px;display:flex;align-items:center;gap:20px;border-bottom:1px solid #E5E7EB;"> <div style="padding:20px;display:flex;align-items:center;gap:20px;border-bottom:1px solid #E5E7EB;">
......
...@@ -124,6 +124,35 @@ ...@@ -124,6 +124,35 @@
</div> </div>
</div> </div>
<!-- Pricing & Capacity -->
<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="banknote" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">التسعير والسعة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">رسوم شهرية (أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_member" value="<?= e(old('monthly_fee_member', '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رسوم شهرية (غير أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_nonmember" value="<?= e(old('monthly_fee_nonmember', '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأدنى للسعة <span style="color:#DC2626;">*</span></label>
<input type="number" name="min_capacity" value="<?= e(old('min_capacity', '1')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للسعة <span style="color:#DC2626;">*</span></label>
<input type="number" name="max_capacity" value="<?= e(old('max_capacity', '20')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
</div>
<p style="margin:10px 0 0;font-size:12px;color:#6B7280;">البرنامج هو المنتج المباع — السعر والسعة يرثهما كل المجموعات التابعة تلقائياً</p>
</div>
</div>
<!-- Submit --> <!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;"> <div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;"> <button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
......
...@@ -124,6 +124,35 @@ ...@@ -124,6 +124,35 @@
</div> </div>
</div> </div>
<!-- Pricing & Capacity -->
<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="banknote" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">التسعير والسعة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">رسوم شهرية (أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_member" value="<?= e(old('monthly_fee_member', $program->monthly_fee_member ?? '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">رسوم شهرية (غير أعضاء) <span style="color:#DC2626;">*</span></label>
<input type="number" name="monthly_fee_nonmember" value="<?= e(old('monthly_fee_nonmember', $program->monthly_fee_nonmember ?? '0')) ?>" class="form-input" required min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأدنى للسعة <span style="color:#DC2626;">*</span></label>
<input type="number" name="min_capacity" value="<?= e(old('min_capacity', $program->min_capacity ?? '1')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للسعة <span style="color:#DC2626;">*</span></label>
<input type="number" name="max_capacity" value="<?= e(old('max_capacity', $program->max_capacity ?? '20')) ?>" class="form-input" required min="1" style="direction:ltr;text-align:left;">
</div>
</div>
<p style="margin:10px 0 0;font-size:12px;color:#6B7280;">البرنامج هو المنتج المباع — السعر والسعة يرثهما كل المجموعات التابعة تلقائياً</p>
</div>
</div>
<!-- Submit --> <!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;"> <div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;"> <button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
......
...@@ -61,6 +61,18 @@ $genderLabels = ['mixed' => 'مختلط', 'male' => 'ذكور فقط', 'female' ...@@ -61,6 +61,18 @@ $genderLabels = ['mixed' => 'مختلط', 'male' => 'ذكور فقط', 'female'
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الحصص أسبوعياً</div> <div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الحصص أسبوعياً</div>
<div style="font-weight:600;"><?= $program['sessions_per_week'] ?? '—' ?></div> <div style="font-weight:600;"><?= $program['sessions_per_week'] ?? '—' ?></div>
</div> </div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">رسوم الأعضاء (شهري)</div>
<div style="font-weight:600;color:#059669;"><?= money((float) ($program['monthly_fee_member'] ?? 0)) ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">رسوم غير الأعضاء (شهري)</div>
<div style="font-weight:600;color:#D97706;"><?= money((float) ($program['monthly_fee_nonmember'] ?? 0)) ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">السعة (أدنى / أقصى)</div>
<div style="font-weight:600;"><?= (int) ($program['min_capacity'] ?? 1) ?> / <?= (int) ($program['max_capacity'] ?? 20) ?></div>
</div>
</div> </div>
<?php if (!empty($program['description_ar'])): ?> <?php if (!empty($program['description_ar'])): ?>
<div style="margin-top:15px;padding-top:15px;border-top:1px solid #F3F4F6;"> <div style="margin-top:15px;padding-top:15px;border-top:1px solid #F3F4F6;">
...@@ -108,7 +120,7 @@ $genderLabels = ['mixed' => 'مختلط', 'male' => 'ذكور فقط', 'female' ...@@ -108,7 +120,7 @@ $genderLabels = ['mixed' => 'مختلط', 'male' => 'ذكور فقط', 'female'
</span> </span>
/ <?= (int) $g['max_capacity'] ?> / <?= (int) $g['max_capacity'] ?>
</td> </td>
<td><?= money((float) ($g['monthly_fee_member'] ?? 0)) ?></td> <td><?= money((float) ($program['monthly_fee_member'] ?? 0)) ?></td>
<td> <td>
<?php <?php
$statusColors = ['active' => '#059669', 'paused' => '#D97706', 'completed' => '#6B7280', 'cancelled' => '#DC2626']; $statusColors = ['active' => '#059669', 'paused' => '#D97706', 'completed' => '#6B7280', 'cancelled' => '#DC2626'];
......
...@@ -129,6 +129,12 @@ ...@@ -129,6 +129,12 @@
<!-- Step 3: Activity/Group Selection --> <!-- Step 3: Activity/Group Selection -->
<div id="step-activity" class="wizard-step" style="<?= ($step ?? 1) == 3 ? '' : 'display:none;' ?>"> <div id="step-activity" class="wizard-step" style="<?= ($step ?? 1) == 3 ? '' : 'display:none;' ?>">
<?php if (!in_array($registration['medical_status'] ?? 'pending', ['fit', 'conditional'])): ?>
<div style="background:#FEF3C7;border:1px solid #F59E0B;border-radius:8px;padding:10px 14px;margin-bottom:12px;display:flex;align-items:center;gap:8px;">
<i data-lucide="alert-triangle" style="width:16px;height:16px;color:#D97706;flex-shrink:0;"></i>
<span style="font-size:12px;color:#92400E;font-weight:600;">اللاعب بدون شهادة طبية سارية — يمكنه التسجيل لكن سيظهر بعلامة تحذير</span>
</div>
<?php endif; ?>
<div class="card" style="margin-bottom:16px;"> <div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;"> <div style="padding:16px 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> <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>
......
...@@ -53,7 +53,7 @@ PermissionRegistry::register('sports_activity', [ ...@@ -53,7 +53,7 @@ PermissionRegistry::register('sports_activity', [
'sa.coach.manage' => ['ar' => 'إدارة المدربين', 'en' => 'Manage Coaches'], 'sa.coach.manage' => ['ar' => 'إدارة المدربين', 'en' => 'Manage Coaches'],
'sa.player.view' => ['ar' => 'عرض اللاعبين', 'en' => 'View Players'], 'sa.player.view' => ['ar' => 'عرض اللاعبين', 'en' => 'View Players'],
'sa.player.manage' => ['ar' => 'إدارة اللاعبين', 'en' => 'Manage Players'], 'sa.player.manage' => ['ar' => 'إدارة اللاعبين', 'en' => 'Manage Players'],
'sa.medical.approve' => ['ar' => 'اعتماد الشهادات الطبية', 'en' => 'Approve Medical Certificates'], 'sa.medical.approve' => ['ar' => '(ملغي) اعتماد الشهادات الطبية — استخدم medical.board.approve', 'en' => '(Deprecated) Use medical.board.approve'],
'sa.academy.view' => ['ar' => 'عرض الأكاديميات', 'en' => 'View Academies'], 'sa.academy.view' => ['ar' => 'عرض الأكاديميات', 'en' => 'View Academies'],
'sa.academy.manage' => ['ar' => 'إدارة الأكاديميات', 'en' => 'Manage Academies'], 'sa.academy.manage' => ['ar' => 'إدارة الأكاديميات', 'en' => 'Manage Academies'],
'sa.contract.view' => ['ar' => 'عرض العقود', 'en' => 'View Contracts'], 'sa.contract.view' => ['ar' => 'عرض العقود', 'en' => 'View Contracts'],
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<div style="margin-bottom:20px;border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;"><img src="/assets/tutorials/screenshots/sa-groups.png" alt="لقطة شاشة" style="width:100%;display:block;" loading="lazy"></div> <div style="margin-bottom:20px;border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;"><img src="/assets/tutorials/screenshots/sa-groups.png" alt="لقطة شاشة" style="width:100%;display:block;" loading="lazy"></div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">إضافة مجموعة</h3><div class="tut-step-body">من القائمة: <span class="field">الأنشطة الرياضية</span> > <span class="field">المجموعات</span> > إضافة مجموعة.</div></div> <div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">إضافة مجموعة</h3><div class="tut-step-body">من القائمة: <span class="field">الأنشطة الرياضية</span> > <span class="field">المجموعات</span> > إضافة مجموعة.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">البيانات والربط</h3><div class="tut-step-body"><ul><li><span class="field">code / name_ar / name_en</span> — كود واسم المجموعة</li><li><span class="field">النشاط الرياضي</span> — اختياري لتصفية البرامج والمدربين</li><li><span class="field">program_id</span> — البرنامج (يحدد المستوى والفئة العمرية)</li><li><span class="field">coach_id</span> — المدرب (يتم تصفيته حسب النشاط المختار)</li><li><span class="field">الحد الأدنى / الحد الأقصى</span> — سعة المجموعة</li><li><span class="field">رسوم شهرية (أعضاء) / رسوم شهرية (غير أعضاء)</span> — الرسوم</li><li><span class="field">بداية الموسم / نهاية الموسم</span> — مدة الموسم</li></ul><span class="info">اختيار النشاط الرياضي اختياري — لكنه يصفي قائمة البرامج والمدربين المتاحة تلقائياً.</span></div></div> <div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">البيانات والربط</h3><div class="tut-step-body"><ul><li><span class="field">code / name_ar / name_en</span> — كود واسم المجموعة</li><li><span class="field">النشاط الرياضي</span> — اختياري لتصفية البرامج والمدربين</li><li><span class="field">program_id</span> — البرنامج (يحدد المستوى والفئة العمرية والسعر والسعة)</li><li><span class="field">coach_id</span> — المدرب (يتم تصفيته حسب النشاط المختار)</li><li><span class="field">بداية الموسم / نهاية الموسم</span> — مدة الموسم</li></ul><span class="info">اختيار النشاط الرياضي اختياري — لكنه يصفي قائمة البرامج والمدربين المتاحة تلقائياً.</span><span class="success">السعة والرسوم موروثة تلقائياً من البرنامج المحدد — لا حاجة لتحديدها عند إنشاء المجموعة. عند تعديل سعة البرنامج، تتحدث جميع مجموعاته تلقائياً.</span></div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">إعداد الجدول الأسبوعي</h3><div class="tut-step-body">من صفحة المجموعة > الجدول > إضافة حصة:<ul><li><span class="field">day_of_week</span> — اليوم</li><li><span class="field">facility_unit_id</span> — الوحدة/الحارة/الملعب</li><li><span class="field">start_time / end_time</span> — الوقت</li></ul>مثال: أحد + ثلاثاء + خميس — حارة 3 — من 16:00 إلى 17:00<span class="warn">الجدول الأسبوعي يتحقق من التعارض — لو الوحدة محجوزة في نفس الوقت، النظام يرفض ويوضح التعارض.</span><span class="success">بعد إنشاء الجدول، يمكنك استخدام "توليد الحجوزات" لإنشاء حجوزات تلقائية لكل حصة.</span></div></div> <div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">إعداد الجدول الأسبوعي</h3><div class="tut-step-body">من صفحة المجموعة > الجدول > إضافة حصة:<ul><li><span class="field">day_of_week</span> — اليوم</li><li><span class="field">facility_unit_id</span> — الوحدة/الحارة/الملعب</li><li><span class="field">start_time / end_time</span> — الوقت</li></ul>مثال: أحد + ثلاثاء + خميس — حارة 3 — من 16:00 إلى 17:00<span class="warn">الجدول الأسبوعي يتحقق من التعارض — لو الوحدة محجوزة في نفس الوقت، النظام يرفض ويوضح التعارض.</span><span class="success">بعد إنشاء الجدول، يمكنك استخدام "توليد الحجوزات" لإنشاء حجوزات تلقائية لكل حصة.</span></div></div>
<div class="tut-nav"> <div class="tut-nav">
<a href="/tutorials/sports-activity/create-program"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> إنشاء برنامج تدريبي</a> <a href="/tutorials/sports-activity/create-program"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> إنشاء برنامج تدريبي</a>
......
...@@ -24,8 +24,9 @@ ...@@ -24,8 +24,9 @@
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">إضافة برنامج</h3><div class="tut-step-body">من القائمة: <span class="field">الأنشطة الرياضية</span> > <span class="field">البرامج</span> > إضافة برنامج.</div></div> <div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">إضافة برنامج</h3><div class="tut-step-body">من القائمة: <span class="field">الأنشطة الرياضية</span> > <span class="field">البرامج</span> > إضافة برنامج.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">البيانات الأساسية</h3><div class="tut-step-body"><ul><li><span class="field">code / name_ar</span> — الكود والاسم</li><li><span class="field">discipline_id</span> — اللعبة</li><li><span class="field">academy_id</span> — الأكاديمية (NULL = يديره النادي مباشرة)</li><li><span class="field">program_type</span> — training / recreational / competitive / rehabilitation</li></ul></div></div> <div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">البيانات الأساسية</h3><div class="tut-step-body"><ul><li><span class="field">code / name_ar</span> — الكود والاسم</li><li><span class="field">discipline_id</span> — اللعبة</li><li><span class="field">academy_id</span> — الأكاديمية (NULL = يديره النادي مباشرة)</li><li><span class="field">program_type</span> — training / recreational / competitive / rehabilitation</li></ul></div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">الفئة المستهدفة</h3><div class="tut-step-body"><ul><li><span class="field">age_from / age_to</span> — الفئة العمرية (مثلاً 6-10)</li><li><span class="field">gender_restriction</span> — male / female / NULL (مختلط)</li><li><span class="field">skill_level</span> — beginner / intermediate / advanced / elite</li></ul></div></div> <div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">الفئة المستهدفة</h3><div class="tut-step-body"><ul><li><span class="field">age_from / age_to</span> — الفئة العمرية (مثلاً 6-10)</li><li><span class="field">gender_restriction</span> — male / female / NULL (مختلط)</li><li><span class="field">skill_level</span> — beginner / intermediate / advanced / elite</li></ul></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">تفاصيل الحصص</h3><div class="tut-step-body"><ul><li><span class="field">session_duration_minutes</span> — مدة الحصة (مثل 60 دقيقة)</li><li><span class="field">sessions_per_week</span> — عدد الحصص أسبوعياً (مثل 3)</li></ul><span class="info">البرنامج هو القالب — يحدد ماذا ندرّب ولمن. المجموعة هي التنفيذ الفعلي — من المدرب ومتى وأين.</span></div></div> <div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">تفاصيل الحصص</h3><div class="tut-step-body"><ul><li><span class="field">session_duration_minutes</span> — مدة الحصة (مثل 60 دقيقة)</li><li><span class="field">sessions_per_week</span> — عدد الحصص أسبوعياً (مثل 3)</li></ul></div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">العلاقة بين البرنامج والمجموعة</h3><div class="tut-step-body"><div class="tut-diagram">البرنامج (Template) المجموعة (Instance) <div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">التسعير والسعة</h3><div class="tut-step-body"><ul><li><span class="field">monthly_fee_member</span> — الرسوم الشهرية للأعضاء</li><li><span class="field">monthly_fee_nonmember</span> — الرسوم الشهرية لغير الأعضاء</li><li><span class="field">min_capacity</span> — الحد الأدنى للسعة</li><li><span class="field">max_capacity</span> — الحد الأقصى للسعة</li></ul><span class="warn">البرنامج هو المنتج المباع — كل المجموعات التابعة ترث السعر والسعة تلقائياً. لا يمكن تحديد أسعار مختلفة لمجموعات نفس البرنامج.</span><span class="info">البرنامج هو القالب — يحدد ماذا ندرّب ولمن وبكم. المجموعة هي التنفيذ الفعلي — من المدرب ومتى وأين.</span></div></div>
<div class="tut-step"><div class="tut-step-num">6</div><h3 class="tut-step-title">العلاقة بين البرنامج والمجموعة</h3><div class="tut-step-body"><div class="tut-diagram">البرنامج (Template) المجموعة (Instance)
────────────────── ────────────────────── ────────────────── ──────────────────────
سباحة مستوى 1 مجموعة أ - صباحية سباحة مستوى 1 مجموعة أ - صباحية
العمر: 6-10 المدرب: أحمد العمر: 6-10 المدرب: أحمد
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح تسجيل اللاعبين</h3><div class="tut-step-body">من صفحة المجموعة > <span class="field">تسجيل لاعب</span>.</div></div> <div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح تسجيل اللاعبين</h3><div class="tut-step-body">من صفحة المجموعة > <span class="field">تسجيل لاعب</span>.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">البحث عن اللاعب</h3><div class="tut-step-body">ابحث بالاسم أو الرقم التسلسلي أو الرقم القومي.</div></div> <div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">البحث عن اللاعب</h3><div class="tut-step-body">ابحث بالاسم أو الرقم التسلسلي أو الرقم القومي.</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">التحقق التلقائي</h3><div class="tut-step-body">النظام يتحقق من:<ul><li>الحالة الطبية: لازم تكون <span class="field">fit</span> أو <span class="field">conditional</span></li><li>السعة: المجموعة لسه فيها مكان</li><li>العمر: ضمن الفئة العمرية للبرنامج</li><li>عدم التكرار: مش مسجل بالفعل</li></ul><span class="warn">إذا اللاعب حالته الطبية "pending" أو "unfit" أو "expired"، التسجيل مرفوض تلقائياً.</span></div></div> <div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">التحقق التلقائي</h3><div class="tut-step-body">النظام يتحقق من:<ul><li>السعة: المجموعة لسه فيها مكان (موروثة من البرنامج)</li><li>العمر: ضمن الفئة العمرية للبرنامج</li><li>عدم التكرار: مش مسجل بالفعل</li></ul><span class="info">الرسوم الشهرية تُحسب من البرنامج تلقائياً حسب نوع اللاعب (عضو / غير عضو).</span><span class="warn">الحالة الطبية لا تمنع التسجيل — لكن اللاعب بدون شهادة طبية سارية سيظهر بعلامة تحذير ⚠ في كل مكان (قائمة اللاعبين، صفحة المجموعة، كشف الحضور).</span></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">نتيجة التسجيل</h3><div class="tut-step-body">إذا كل الشروط تمام → يُسجل اللاعب بحالة <span class="field">pending_payment</span> (في انتظار الدفع). إذا المجموعة ممتلئة → يُعرض خيار "قائمة الانتظار".<span class="info">التسجيل لا يُفعّل مباشرة — يجب دفع أول اشتراك من خزنة الأنشطة أولاً. انظر: <a href="/tutorials/sports-activity/enrollment-payment" style="color:#1E40AF;font-weight:600;">الدفع الإجباري عند التسجيل</a>.</span><span class="success">بعد التحصيل: اللاعب يظهر في كشف الحضور اليومي ويتم توليد اشتراك شهري له.</span></div></div> <div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">نتيجة التسجيل</h3><div class="tut-step-body">إذا كل الشروط تمام → يُسجل اللاعب بحالة <span class="field">pending_payment</span> (في انتظار الدفع). إذا المجموعة ممتلئة → يُعرض خيار "قائمة الانتظار".<span class="info">التسجيل لا يُفعّل مباشرة — يجب دفع أول اشتراك من خزنة الأنشطة أولاً. انظر: <a href="/tutorials/sports-activity/enrollment-payment" style="color:#1E40AF;font-weight:600;">الدفع الإجباري عند التسجيل</a>.</span><span class="success">بعد التحصيل: اللاعب يظهر في كشف الحضور اليومي ويتم توليد اشتراك شهري له.</span></div></div>
<div class="tut-nav"> <div class="tut-nav">
<a href="/tutorials/sports-activity/create-group"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> إنشاء مجموعة تدريبية</a> <a href="/tutorials/sports-activity/create-group"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> إنشاء مجموعة تدريبية</a>
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح التوليد</h3><div class="tut-step-body">من صفحة الاشتراكات الشهرية، اضغط زر <span class="field">توليد الاشتراكات</span> أعلى الصفحة. أو يمكنك الضغط على <span class="field">معاينة الشهر القادم</span> أولاً لرؤية ما سيتم توليده.</div></div> <div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">فتح التوليد</h3><div class="tut-step-body">من صفحة الاشتراكات الشهرية، اضغط زر <span class="field">توليد الاشتراكات</span> أعلى الصفحة. أو يمكنك الضغط على <span class="field">معاينة الشهر القادم</span> أولاً لرؤية ما سيتم توليده.</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">اختيار الشهر</h3><div class="tut-step-body">اختر الشهر والسنة (مثلاً: يونيو 2026).</div></div> <div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">اختيار الشهر</h3><div class="tut-step-body">اختر الشهر والسنة (مثلاً: يونيو 2026).</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">المعالجة</h3><div class="tut-step-body">لكل لاعب active في كل مجموعة نشطة:<ul><li>يتحقق: هل سبق توليد اشتراك لنفس الفترة؟</li><li>يحدد المبلغ حسب <span class="field">player_type</span></li><li>ينشئ سجل بحالة <span class="field">unpaid</span></li></ul><div class="tut-diagram">المجموعة "سباحة صباحي": <div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">المعالجة</h3><div class="tut-step-body">لكل لاعب active في كل مجموعة نشطة:<ul><li>يتحقق: هل سبق توليد اشتراك لنفس الفترة؟</li><li>يحدد المبلغ من <strong>البرنامج</strong> حسب <span class="field">player_type</span> (عضو/غير عضو)</li><li>ينشئ سجل بحالة <span class="field">unpaid</span></li></ul><div class="tut-diagram">المجموعة "سباحة صباحي":
├── أحمد (عضو) 300 ج → اشتراك #SUB-001 unpaid ├── أحمد (عضو) 300 ج → اشتراك #SUB-001 unpaid
├── محمد (غير عضو) 450 ج → اشتراك #SUB-002 unpaid ├── محمد (غير عضو) 450 ج → اشتراك #SUB-002 unpaid
├── سارة (عضو) 300 ج → اشتراك #SUB-003 unpaid ├── سارة (عضو) 300 ج → اشتراك #SUB-003 unpaid
......
...@@ -21,18 +21,20 @@ ...@@ -21,18 +21,20 @@
<div style="margin-bottom:20px;border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;"><img src="/assets/tutorials/screenshots/sa-players.png" alt="لقطة شاشة" style="width:100%;display:block;" loading="lazy"></div> <div style="margin-bottom:20px;border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;"><img src="/assets/tutorials/screenshots/sa-players.png" alt="لقطة شاشة" style="width:100%;display:block;" loading="lazy"></div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">رفع شهادة طبية</h3><div class="tut-step-body">من صفحة اللاعب > المستندات > <span class="field">رفع شهادة طبية</span>.</div></div> <div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">مبدأ أساسي: الشهادة الطبية ليست شرطاً إجبارياً</h3><div class="tut-step-body"><span class="info">الشهادة الطبية <strong>لا تمنع</strong> التسجيل أو التدريب. اللاعب بدون شهادة سارية يظهر بعلامة تحذير ⚠ في كل مكان — لكنه يستطيع التسجيل والتدريب والحضور بشكل طبيعي.</span><span class="warn">التحذير يظهر في: قائمة اللاعبين، صفحة اللاعب، قائمة المجموعة المسجلين، كشف الحضور، ومعالج التسجيل.</span></div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">بيانات الشهادة</h3><div class="tut-step-body"><ul><li>نوع المستند: <span class="field">medical_cert</span></li><li>الملف (PDF أو صورة)</li><li>تاريخ الفحص وتاريخ الانتهاء</li><li>اسم الطبيب واسم العيادة/المستشفى</li></ul></div></div> <div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">رفع شهادة طبية</h3><div class="tut-step-body">من صفحة اللاعب > المستندات > <span class="field">رفع شهادة طبية</span>.<ul><li>نوع المستند: <span class="field">medical_cert</span></li><li>الملف (PDF أو صورة)</li></ul></div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">انتظار اللجنة الطبية</h3><div class="tut-step-body">الشهادة تذهب للحالة <span class="field">pending</span> — بانتظار اللجنة الطبية.</div></div> <div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">الإرسال التلقائي للجنة الطبية</h3><div class="tut-step-body">عند رفع شهادة طبية، يتم تلقائياً:<ul><li>حفظ المستند في ملفات اللاعب بحالة <span class="field">pending</span></li><li>إنشاء سجل مراجعة في قائمة اللجنة الطبية <span class="field">/medical-board</span></li></ul><span class="warn">لا يمكن اعتماد الشهادة من نفس صفحة اللاعب — الاعتماد حصري للجنة الطبية فقط من خلال صفحة اللجنة.</span></div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">دور اللجنة الطبية</h3><div class="tut-step-body">صاحب صلاحية <span class="field">sa.medical.approve</span> يراجع ويختار: معتمدة (approved) أو مرفوضة (rejected مع سبب).<span class="warn">مدير النشاط الرياضي يقدر يشوف حالة الشهادة الطبية بس (read-only) — الاعتماد حصري للجنة الطبية فقط.</span></div></div> <div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">دور اللجنة الطبية</h3><div class="tut-step-body">من صفحة <span class="field">/medical-board</span>، صاحب صلاحية <span class="field">medical.board.approve</span> يراجع ويختار:<ul><li><strong>اعتماد:</strong> يحدد تاريخ الانتهاء ← medical_status = fit</li><li><strong>رفض:</strong> يحدد السبب ← medical_status = unfit (التحذير يستمر)</li></ul></div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">بعد الاعتماد</h3><div class="tut-step-body">medical_status يتغير لـ <span class="field">fit</span> ويتم تعيين medical_expiry_date.<span class="success">لما اللاعب حالته "fit"، يقدر يتسجل في مجموعات ويحجز ساعات. النظام بيتحقق تلقائياً.</span></div></div> <div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">بعد الاعتماد</h3><div class="tut-step-body">medical_status يتغير لـ <span class="field">fit</span> ويتم تعيين medical_expiry_date.<span class="success">بعد الاعتماد: علامة التحذير تختفي تلقائياً من كل الشاشات. لما ينتهي التاريخ، العلامة تعود.</span></div></div>
<div class="tut-step"><div class="tut-step-num">6</div><h3 class="tut-step-title">دورة الحياة الكاملة</h3><div class="tut-step-body"><div class="tut-diagram">رفع الشهادة ──→ pending ──→ اللجنة الطبية ──→ approved ──→ fit <div class="tut-step"><div class="tut-step-num">6</div><h3 class="tut-step-title">دورة الحياة الكاملة</h3><div class="tut-step-body"><div class="tut-diagram">رفع الشهادة ──→ pending ──→ اللجنة الطبية ──→ approved ──→ fit (⚠ تختفي)
│ │ │ │
▼ ▼ ▼ ▼
rejected expired (تلقائي) rejected (⚠ تستمر) expired (تلقائي — ⚠ تعود)
│ عند انتهاء التاريخ │ عند انتهاء التاريخ
إعادة الرفع مرة أخرى</div></div></div> إعادة الرفع مرة أخرى
ملاحظة: في جميع الحالات، اللاعب يستطيع التدريب — التحذير إداري فقط</div></div></div>
<div class="tut-nav"> <div class="tut-nav">
<a href="/tutorials/sports-activity/register-player"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> تسجيل لاعب جديد</a> <a href="/tutorials/sports-activity/register-player"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> تسجيل لاعب جديد</a>
<a href="/tutorials/sports-activity/setup-academy">إنشاء أكاديمية وعقدها <i data-lucide="arrow-left" style="width:14px;height:14px;"></i></a> <a href="/tutorials/sports-activity/setup-academy">إنشاء أكاديمية وعقدها <i data-lucide="arrow-left" style="width:14px;height:14px;"></i></a>
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_programs
ADD COLUMN monthly_fee_member DECIMAL(10,2) NULL AFTER sessions_per_week,
ADD COLUMN monthly_fee_nonmember DECIMAL(10,2) NULL AFTER monthly_fee_member,
ADD COLUMN max_capacity INT UNSIGNED NULL AFTER monthly_fee_nonmember,
ADD COLUMN min_capacity INT UNSIGNED NULL AFTER max_capacity
",
'down' => "
ALTER TABLE sa_programs
DROP COLUMN monthly_fee_member,
DROP COLUMN monthly_fee_nonmember,
DROP COLUMN max_capacity,
DROP COLUMN min_capacity
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE sa_programs
MODIFY monthly_fee_member DECIMAL(10,2) NOT NULL DEFAULT 0.00,
MODIFY monthly_fee_nonmember DECIMAL(10,2) NOT NULL DEFAULT 0.00,
MODIFY max_capacity INT UNSIGNED NOT NULL DEFAULT 20,
MODIFY min_capacity INT UNSIGNED NOT NULL DEFAULT 1
",
'down' => "
ALTER TABLE sa_programs
MODIFY monthly_fee_member DECIMAL(10,2) NULL,
MODIFY monthly_fee_nonmember DECIMAL(10,2) NULL,
MODIFY max_capacity INT UNSIGNED NULL,
MODIFY min_capacity INT UNSIGNED NULL
",
];
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE player_medical_records
DROP FOREIGN KEY fk_pmr_player;
ALTER TABLE player_medical_records
MODIFY player_id BIGINT UNSIGNED NULL,
ADD COLUMN sa_player_id INT UNSIGNED NULL AFTER player_id,
ADD COLUMN sa_document_id INT UNSIGNED NULL AFTER document_id,
ADD COLUMN source VARCHAR(30) NOT NULL DEFAULT 'membership' AFTER approval_status,
ADD INDEX idx_pmr_sa_player (sa_player_id),
ADD INDEX idx_pmr_source (source)
",
'down' => "
ALTER TABLE player_medical_records
DROP INDEX idx_pmr_source,
DROP INDEX idx_pmr_sa_player,
DROP COLUMN source,
DROP COLUMN sa_document_id,
DROP COLUMN sa_player_id,
MODIFY player_id BIGINT UNSIGNED NOT NULL;
ALTER TABLE player_medical_records
ADD CONSTRAINT fk_pmr_player FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE
",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$programs = $db->select(
"SELECT p.id FROM sa_programs p
WHERE EXISTS (SELECT 1 FROM sa_groups g WHERE g.program_id = p.id AND g.is_archived = 0)",
[]
);
foreach ($programs as $program) {
$bestGroup = $db->selectOne(
"SELECT monthly_fee_member, monthly_fee_nonmember, max_capacity, min_capacity
FROM sa_groups
WHERE program_id = ? AND is_archived = 0
ORDER BY current_count DESC, id DESC
LIMIT 1",
[(int) $program['id']]
);
if (!$bestGroup) {
continue;
}
$db->update('sa_programs', [
'monthly_fee_member' => $bestGroup['monthly_fee_member'] ?? '0.00',
'monthly_fee_nonmember' => $bestGroup['monthly_fee_nonmember'] ?? '0.00',
'max_capacity' => (int) ($bestGroup['max_capacity'] ?? 20),
'min_capacity' => (int) ($bestGroup['min_capacity'] ?? 1),
], 'id = ?', [(int) $program['id']]);
}
// Programs without any groups get sensible defaults
$db->query(
"UPDATE sa_programs SET monthly_fee_member = 0.00, monthly_fee_nonmember = 0.00, max_capacity = 20, min_capacity = 1
WHERE monthly_fee_member IS 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