Commit d28573d7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

something Updated

parent 085a50cb
......@@ -3,41 +3,198 @@ declare(strict_types=1);
namespace App\Modules\Academies\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
class EnrollmentService
{
/**
* Enroll a player in an academy level.
* Placeholder — full logic in PlayerAffairs Phase 19.
*/
public static function enroll(int $playerId, int $academyId, int $levelId, ?int $scheduleId = null): bool
public static function enroll(int $playerId, int $academyId, int $levelId, ?int $scheduleId = null, ?string $season = null): int
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$ts = date('Y-m-d H:i:s');
// Check for duplicate active enrollment
$existing = $db->selectOne(
"SELECT id FROM academy_enrollments WHERE player_id = ? AND academy_id = ? AND status = 'active'",
[$playerId, $academyId]
);
if ($existing) {
throw new \RuntimeException('اللاعب مسجل بالفعل في هذه الأكاديمية');
}
$enrollmentId = $db->insert('academy_enrollments', [
'player_id' => $playerId,
'academy_id' => $academyId,
'level_id' => $levelId,
'schedule_id' => $scheduleId,
'season' => $season,
'enrolled_at' => $ts,
'enrollment_day' => (int) date('j'),
'status' => 'active',
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => $ts,
'updated_at' => $ts,
]);
EventBus::dispatch('academy.enrollment_created', [
'enrollment_id' => $enrollmentId,
'player_id' => $playerId,
'academy_id' => $academyId,
'level_id' => $levelId,
]);
Logger::info("Player #{$playerId} enrolled in academy #{$academyId} level #{$levelId}");
return $enrollmentId;
}
/**
* Promote an enrollment to a new level.
* Placeholder — full logic in PlayerAffairs Phase 19.
* Creates a new enrollment record linked to the previous one via promoted_from_id.
*/
public static function promote(int $enrollmentId, int $newLevelId): bool
public static function promote(int $enrollmentId, int $newLevelId): int
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$ts = date('Y-m-d H:i:s');
$enrollment = $db->selectOne("SELECT * FROM academy_enrollments WHERE id = ?", [$enrollmentId]);
if (!$enrollment) {
throw new \RuntimeException('التسجيل غير موجود');
}
if ($enrollment['status'] !== 'active') {
throw new \RuntimeException('لا يمكن ترقية تسجيل غير نشط');
}
// Mark current enrollment as promoted
$db->update('academy_enrollments', [
'status' => 'promoted',
'updated_at' => $ts,
], 'id = ?', [$enrollmentId]);
// Create new enrollment at the higher level
$newEnrollmentId = $db->insert('academy_enrollments', [
'player_id' => (int) $enrollment['player_id'],
'academy_id' => (int) $enrollment['academy_id'],
'level_id' => $newLevelId,
'schedule_id' => $enrollment['schedule_id'] ? (int) $enrollment['schedule_id'] : null,
'season' => $enrollment['season'],
'enrolled_at' => $ts,
'enrollment_day' => (int) date('j'),
'status' => 'active',
'promoted_from_id'=> $enrollmentId,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => $ts,
'updated_at' => $ts,
]);
EventBus::dispatch('academy.enrollment_promoted', [
'old_enrollment_id' => $enrollmentId,
'new_enrollment_id' => $newEnrollmentId,
'player_id' => (int) $enrollment['player_id'],
'academy_id' => (int) $enrollment['academy_id'],
'new_level_id' => $newLevelId,
]);
Logger::info("Enrollment #{$enrollmentId} promoted to level #{$newLevelId} → new enrollment #{$newEnrollmentId}");
return $newEnrollmentId;
}
/**
* Suspend an enrollment.
* Placeholder — full logic in PlayerAffairs Phase 19.
*/
public static function suspend(int $enrollmentId): bool
public static function suspend(int $enrollmentId, ?string $reason = null): void
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
$db = App::getInstance()->db();
$ts = date('Y-m-d H:i:s');
$enrollment = $db->selectOne("SELECT * FROM academy_enrollments WHERE id = ?", [$enrollmentId]);
if (!$enrollment) {
throw new \RuntimeException('التسجيل غير موجود');
}
if ($enrollment['status'] !== 'active') {
throw new \RuntimeException('لا يمكن إيقاف تسجيل غير نشط');
}
$db->update('academy_enrollments', [
'status' => 'suspended',
'updated_at' => $ts,
], 'id = ?', [$enrollmentId]);
EventBus::dispatch('academy.enrollment_suspended', [
'enrollment_id' => $enrollmentId,
'player_id' => (int) $enrollment['player_id'],
'academy_id' => (int) $enrollment['academy_id'],
'reason' => $reason,
]);
Logger::info("Enrollment #{$enrollmentId} suspended" . ($reason ? ": {$reason}" : ''));
}
/**
* Drop an enrollment.
* Placeholder — full logic in PlayerAffairs Phase 19.
* Drop (withdraw) an enrollment.
*/
public static function drop(int $enrollmentId): bool
public static function drop(int $enrollmentId, ?string $reason = null): void
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
$db = App::getInstance()->db();
$ts = date('Y-m-d H:i:s');
$enrollment = $db->selectOne("SELECT * FROM academy_enrollments WHERE id = ?", [$enrollmentId]);
if (!$enrollment) {
throw new \RuntimeException('التسجيل غير موجود');
}
if (!in_array($enrollment['status'], ['active', 'suspended'])) {
throw new \RuntimeException('لا يمكن إلغاء هذا التسجيل');
}
$db->update('academy_enrollments', [
'status' => 'dropped',
'dropped_at' => $ts,
'dropped_reason' => $reason,
'updated_at' => $ts,
], 'id = ?', [$enrollmentId]);
EventBus::dispatch('academy.enrollment_dropped', [
'enrollment_id' => $enrollmentId,
'player_id' => (int) $enrollment['player_id'],
'academy_id' => (int) $enrollment['academy_id'],
'reason' => $reason,
]);
Logger::info("Enrollment #{$enrollmentId} dropped" . ($reason ? ": {$reason}" : ''));
}
/**
* Reactivate a suspended enrollment.
*/
public static function reactivate(int $enrollmentId): void
{
$db = App::getInstance()->db();
$ts = date('Y-m-d H:i:s');
$enrollment = $db->selectOne("SELECT * FROM academy_enrollments WHERE id = ?", [$enrollmentId]);
if (!$enrollment) {
throw new \RuntimeException('التسجيل غير موجود');
}
if ($enrollment['status'] !== 'suspended') {
throw new \RuntimeException('لا يمكن إعادة تفعيل إلا التسجيلات الموقفة');
}
$db->update('academy_enrollments', [
'status' => 'active',
'updated_at' => $ts,
], 'id = ?', [$enrollmentId]);
EventBus::dispatch('academy.enrollment_reactivated', [
'enrollment_id' => $enrollmentId,
'player_id' => (int) $enrollment['player_id'],
'academy_id' => (int) $enrollment['academy_id'],
]);
Logger::info("Enrollment #{$enrollmentId} reactivated");
}
}
......@@ -32,12 +32,32 @@ class ActivitySubscriptionController extends Controller
$page = max(1, (int) $request->get('page', 1));
$result = ActivitySubscription::search($filters, 25, $page);
// Collection summary for selected month (or current month)
$summaryMonth = $filters['subscription_month'] ?: date('Y-m');
$db = App::getInstance()->db();
$collectionSummary = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN status = 'overdue' THEN 1 ELSE 0 END) AS overdue,
SUM(CASE WHEN status = 'exempt' THEN 1 ELSE 0 END) AS exempt,
SUM(CASE WHEN status = 'revoked' THEN 1 ELSE 0 END) AS revoked,
COALESCE(SUM(total_amount), 0) AS total_expected,
COALESCE(SUM(CASE WHEN status = 'paid' THEN total_amount ELSE 0 END), 0) AS total_collected,
COALESCE(SUM(CASE WHEN status IN ('pending', 'overdue') THEN total_amount ELSE 0 END), 0) AS total_outstanding
FROM activity_subscriptions
WHERE subscription_month = ?
", [$summaryMonth]) ?: [];
return $this->view('ActivitySubscriptions.Views.index', [
'subscriptions' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => ActivitySubscription::getStatuses(),
'disciplines' => SportDiscipline::allActive(),
'subscriptions' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => ActivitySubscription::getStatuses(),
'disciplines' => SportDiscipline::allActive(),
'collectionSummary' => $collectionSummary,
'summaryMonth' => $summaryMonth,
]);
}
......
......@@ -38,6 +38,40 @@ $allStatuses = ActivitySubscription::getStatuses();
</div>
</div>
<!-- Collection Summary -->
<?php if (!empty($collectionSummary) && (int) ($collectionSummary['total'] ?? 0) > 0):
$cs = $collectionSummary;
$csTotal = (int) $cs['total'];
$csPaid = (int) $cs['paid'];
$csRate = $csTotal > 0 ? round(($csPaid / $csTotal) * 100, 1) : 0;
$csRateColor = $csRate >= 80 ? '#059669' : ($csRate >= 50 ? '#D97706' : '#DC2626');
?>
<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:12px;margin-bottom:20px;">
<div class="card" style="padding:15px;text-align:center;border-right:3px solid #0D7377;">
<div style="font-size:20px;font-weight:700;color:#0D7377;"><?= number_format((float) $cs['total_expected'], 0) ?></div>
<div style="font-size:11px;color:#6B7280;">ج.م المتوقع (<?= e($summaryMonth) ?>)</div>
</div>
<div class="card" style="padding:15px;text-align:center;border-right:3px solid #059669;">
<div style="font-size:20px;font-weight:700;color:#059669;"><?= number_format((float) $cs['total_collected'], 0) ?></div>
<div style="font-size:11px;color:#6B7280;">ج.م محصّل</div>
</div>
<div class="card" style="padding:15px;text-align:center;border-right:3px solid #DC2626;">
<div style="font-size:20px;font-weight:700;color:#DC2626;"><?= number_format((float) $cs['total_outstanding'], 0) ?></div>
<div style="font-size:11px;color:#6B7280;">ج.م مستحق</div>
</div>
<div class="card" style="padding:15px;text-align:center;border-right:3px solid <?= $csRateColor ?>;">
<div style="font-size:20px;font-weight:700;color:<?= $csRateColor ?>;"><?= $csRate ?>%</div>
<div style="font-size:11px;color:#6B7280;">نسبة التحصيل</div>
</div>
<div class="card" style="padding:15px;text-align:center;border-right:3px solid #6B7280;">
<div style="font-size:20px;font-weight:700;color:#374151;"><?= $csTotal ?></div>
<div style="font-size:11px;color:#6B7280;">
<?= $csPaid ?> مسدد &middot; <?= (int) $cs['pending'] ?> معلق &middot; <?= (int) $cs['overdue'] ?> متأخر
</div>
</div>
</div>
<?php endif; ?>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/activity-subscriptions" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;">
......
......@@ -115,8 +115,6 @@ class DisciplineController extends Controller
return $this->view('Disciplines.Views.show', [
'discipline' => $discipline,
'config' => $discipline->getConfig(),
'ageGroups' => $discipline->getAgeGroups(),
'skillLevels' => $discipline->getSkillLevels(),
'categories' => SportDiscipline::getCategories(),
]);
}
......@@ -221,59 +219,12 @@ class DisciplineController extends Controller
}
/**
* Build the config array from the request (age groups, skill levels, etc.).
* Build the config array from the request.
*/
private function buildConfigFromRequest(Request $request): array
{
$config = [];
// Age groups
$ageCodes = $request->post('age_group_code');
$ageLabels = $request->post('age_group_label');
$ageMinAges = $request->post('age_group_min_age');
$ageMaxAges = $request->post('age_group_max_age');
if (is_array($ageCodes)) {
$ageGroups = [];
foreach ($ageCodes as $i => $code) {
$code = trim((string) ($code ?? ''));
$label = trim((string) ($ageLabels[$i] ?? ''));
if ($code !== '' && $label !== '') {
$ageGroups[] = [
'code' => $code,
'label_ar' => $label,
'min_age' => (int) ($ageMinAges[$i] ?? 0),
'max_age' => (int) ($ageMaxAges[$i] ?? 99),
];
}
}
if (!empty($ageGroups)) {
$config['age_groups'] = $ageGroups;
}
}
// Skill levels
$skillCodes = $request->post('skill_level_code');
$skillLabels = $request->post('skill_level_label');
if (is_array($skillCodes)) {
$skillLevels = [];
foreach ($skillCodes as $i => $code) {
$code = trim((string) ($code ?? ''));
$label = trim((string) ($skillLabels[$i] ?? ''));
if ($code !== '' && $label !== '') {
$skillLevels[] = [
'code' => $code,
'label_ar' => $label,
];
}
}
if (!empty($skillLevels)) {
$config['skill_levels'] = $skillLevels;
}
}
// Additional config options
$requiresMedicalCert = $request->post('requires_medical_cert');
if ($requiresMedicalCert) {
$config['requires_medical_cert'] = true;
......
<?php
declare(strict_types=1);
namespace App\Modules\Disciplines\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class SportsDashboardController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
// Players summary
$playerStats = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN card_status = 'active' THEN 1 ELSE 0 END) AS active,
SUM(CASE WHEN card_status = 'suspended' THEN 1 ELSE 0 END) AS suspended,
SUM(CASE WHEN card_status = 'revoked' THEN 1 ELSE 0 END) AS revoked,
SUM(CASE WHEN card_status = 'inactive' THEN 1 ELSE 0 END) AS inactive
FROM players WHERE is_archived = 0
") ?: ['total' => 0, 'active' => 0, 'suspended' => 0, 'revoked' => 0, 'inactive' => 0];
// Active enrollments
$enrollmentCount = $db->selectOne(
"SELECT COUNT(*) AS total FROM academy_enrollments WHERE status = 'active'"
)['total'] ?? 0;
// This month subscriptions
$currentMonth = date('Y-m');
$subStats = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) AS paid,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN status = 'overdue' THEN 1 ELSE 0 END) AS overdue,
COALESCE(SUM(CASE WHEN status = 'paid' THEN total_amount ELSE 0 END), 0) AS collected,
COALESCE(SUM(CASE WHEN status IN ('pending', 'overdue') THEN total_amount ELSE 0 END), 0) AS outstanding
FROM activity_subscriptions
WHERE subscription_month = ?
", [$currentMonth]) ?: ['total' => 0, 'paid' => 0, 'pending' => 0, 'overdue' => 0, 'collected' => 0, 'outstanding' => 0];
$collectionRate = ($subStats['total'] > 0)
? round(((int) $subStats['paid'] / (int) $subStats['total']) * 100, 1)
: 0;
// Facility utilization (reservations this month)
$monthStart = date('Y-m-01');
$monthEnd = date('Y-m-t');
$facilityStats = $db->selectOne("
SELECT
COUNT(*) AS total_reservations,
COALESCE(SUM(total_amount), 0) AS revenue
FROM reservations
WHERE reservation_date BETWEEN ? AND ?
AND status NOT IN ('cancelled')
", [$monthStart, $monthEnd]) ?: ['total_reservations' => 0, 'revenue' => 0];
// Recent registrations (last 10)
$recentPlayers = $db->select("
SELECT id, full_name_ar, player_type, card_status, registration_serial, created_at
FROM players
WHERE is_archived = 0
ORDER BY created_at DESC
LIMIT 10
");
// Unpaid subscriptions (pending + overdue this month)
$unpaidSubs = $db->select("
SELECT s.id, s.subscription_month, s.total_amount, s.status, s.due_date,
p.full_name_ar AS player_name, p.id AS player_id
FROM activity_subscriptions s
JOIN players p ON p.id = s.player_id
WHERE s.subscription_month = ?
AND s.status IN ('pending', 'overdue')
ORDER BY s.due_date ASC
LIMIT 15
", [$currentMonth]);
// Expiring medical certs (next 30 days)
$expiringMedical = $db->select("
SELECT p.id, p.full_name_ar, p.medical_expiry_date, p.medical_status
FROM players p
WHERE p.is_archived = 0
AND p.medical_expiry_date IS NOT NULL
AND p.medical_expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)
ORDER BY p.medical_expiry_date ASC
LIMIT 10
");
// Academies count
$academyCount = $db->selectOne(
"SELECT COUNT(*) AS total FROM academies WHERE is_active = 1"
)['total'] ?? 0;
// Disciplines count
$disciplineCount = $db->selectOne(
"SELECT COUNT(*) AS total FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0"
)['total'] ?? 0;
return $this->view('Disciplines.Views.sports_dashboard', [
'playerStats' => $playerStats,
'enrollmentCount' => $enrollmentCount,
'subStats' => $subStats,
'collectionRate' => $collectionRate,
'facilityStats' => $facilityStats,
'recentPlayers' => $recentPlayers,
'unpaidSubs' => $unpaidSubs,
'expiringMedical' => $expiringMedical,
'academyCount' => $academyCount,
'disciplineCount' => $disciplineCount,
'currentMonth' => $currentMonth,
]);
}
}
......@@ -2,6 +2,7 @@
declare(strict_types=1);
return [
['GET', '/sports-dashboard', 'Disciplines\Controllers\SportsDashboardController@index', ['auth'], 'discipline.view'],
['GET', '/disciplines', 'Disciplines\Controllers\DisciplineController@index', ['auth'], 'discipline.view'],
['GET', '/disciplines/create', 'Disciplines\Controllers\DisciplineController@create', ['auth'], 'discipline.manage'],
['POST', '/disciplines', 'Disciplines\Controllers\DisciplineController@store', ['auth', 'csrf'], 'discipline.manage'],
......
......@@ -23,52 +23,6 @@ class DisciplineService
return SportDiscipline::getByCategory($category);
}
/**
* Get age groups for a specific discipline.
*/
public static function getAgeGroups(int $disciplineId): array
{
$discipline = SportDiscipline::find($disciplineId);
if (!$discipline) {
return [];
}
return $discipline->getAgeGroups();
}
/**
* Get skill levels for a specific discipline.
*/
public static function getSkillLevels(int $disciplineId): array
{
$discipline = SportDiscipline::find($disciplineId);
if (!$discipline) {
return [];
}
return $discipline->getSkillLevels();
}
/**
* Validate whether a given age is acceptable for a discipline.
*/
public static function validateAgeForDiscipline(int $disciplineId, int $age): bool
{
$ageGroups = self::getAgeGroups($disciplineId);
if (empty($ageGroups)) {
// No age groups defined — allow all ages
return true;
}
foreach ($ageGroups as $group) {
$minAge = (int) ($group['min_age'] ?? 0);
$maxAge = (int) ($group['max_age'] ?? 999);
if ($age >= $minAge && $age <= $maxAge) {
return true;
}
}
return false;
}
/**
* Get all active disciplines grouped by category.
*/
......
......@@ -99,42 +99,6 @@
</div>
</div>
<!-- Age Groups -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="users" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">الفئات العمرية</h3>
</div>
<button type="button" class="btn btn-sm btn-outline" onclick="addAgeGroup()">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة فئة
</button>
</div>
<div style="padding:20px;" id="ageGroupsContainer">
<div style="color:#9CA3AF;font-size:13px;text-align:center;padding:15px;" id="ageGroupsEmpty">
لم يتم إضافة فئات عمرية بعد. اضغط "إضافة فئة" لإضافة فئة عمرية.
</div>
</div>
</div>
<!-- Skill Levels -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<i data-lucide="bar-chart-2" style="width:18px;height:18px;color:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">مستويات المهارة</h3>
</div>
<button type="button" class="btn btn-sm btn-outline" onclick="addSkillLevel()">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة مستوى
</button>
</div>
<div style="padding:20px;" id="skillLevelsContainer">
<div style="color:#9CA3AF;font-size:13px;text-align:center;padding:15px;" id="skillLevelsEmpty">
لم يتم إضافة مستويات مهارة بعد. اضغط "إضافة مستوى" لإضافة مستوى.
</div>
</div>
</div>
<!-- Additional Settings -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
......@@ -212,85 +176,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
var ageGroupIndex = 0;
function addAgeGroup() {
var container = document.getElementById('ageGroupsContainer');
var emptyMsg = document.getElementById('ageGroupsEmpty');
if (emptyMsg) emptyMsg.style.display = 'none';
var row = document.createElement('div');
row.className = 'age-group-row';
row.style.cssText = 'display:grid;grid-template-columns:1fr 1.5fr 0.7fr 0.7fr auto;gap:10px;align-items:end;margin-bottom:10px;padding:12px;background:#F9FAFB;border-radius:8px;';
row.innerHTML =
'<div class="form-group" style="margin:0;">' +
'<label class="form-label" style="font-size:11px;">الكود</label>' +
'<input type="text" name="age_group_code[]" class="form-input" placeholder="U12" style="direction:ltr;text-align:left;font-size:13px;">' +
'</div>' +
'<div class="form-group" style="margin:0;">' +
'<label class="form-label" style="font-size:11px;">الاسم</label>' +
'<input type="text" name="age_group_label[]" class="form-input" placeholder="تحت 12 سنة" style="font-size:13px;">' +
'</div>' +
'<div class="form-group" style="margin:0;">' +
'<label class="form-label" style="font-size:11px;">من سن</label>' +
'<input type="number" name="age_group_min_age[]" class="form-input" value="0" min="0" max="99" style="direction:ltr;text-align:left;font-size:13px;">' +
'</div>' +
'<div class="form-group" style="margin:0;">' +
'<label class="form-label" style="font-size:11px;">إلى سن</label>' +
'<input type="number" name="age_group_max_age[]" class="form-input" value="99" min="0" max="99" style="direction:ltr;text-align:left;font-size:13px;">' +
'</div>' +
'<button type="button" onclick="this.parentElement.remove();checkAgeGroupsEmpty();" class="btn btn-sm" style="color:#DC2626;padding:8px;border:1px solid #FCA5A5;border-radius:6px;background:#FEF2F2;cursor:pointer;height:38px;">' +
'<i data-lucide="trash-2" style="width:14px;height:14px;"></i>' +
'</button>';
container.appendChild(row);
ageGroupIndex++;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
function checkAgeGroupsEmpty() {
var container = document.getElementById('ageGroupsContainer');
var emptyMsg = document.getElementById('ageGroupsEmpty');
var rows = container.querySelectorAll('.age-group-row');
if (emptyMsg) emptyMsg.style.display = rows.length === 0 ? 'block' : 'none';
}
var skillLevelIndex = 0;
function addSkillLevel() {
var container = document.getElementById('skillLevelsContainer');
var emptyMsg = document.getElementById('skillLevelsEmpty');
if (emptyMsg) emptyMsg.style.display = 'none';
var row = document.createElement('div');
row.className = 'skill-level-row';
row.style.cssText = 'display:grid;grid-template-columns:1fr 2fr auto;gap:10px;align-items:end;margin-bottom:10px;padding:12px;background:#F9FAFB;border-radius:8px;';
row.innerHTML =
'<div class="form-group" style="margin:0;">' +
'<label class="form-label" style="font-size:11px;">الكود</label>' +
'<input type="text" name="skill_level_code[]" class="form-input" placeholder="beginner" style="direction:ltr;text-align:left;font-size:13px;">' +
'</div>' +
'<div class="form-group" style="margin:0;">' +
'<label class="form-label" style="font-size:11px;">الاسم</label>' +
'<input type="text" name="skill_level_label[]" class="form-input" placeholder="مبتدئ" style="font-size:13px;">' +
'</div>' +
'<button type="button" onclick="this.parentElement.remove();checkSkillLevelsEmpty();" class="btn btn-sm" style="color:#DC2626;padding:8px;border:1px solid #FCA5A5;border-radius:6px;background:#FEF2F2;cursor:pointer;height:38px;">' +
'<i data-lucide="trash-2" style="width:14px;height:14px;"></i>' +
'</button>';
container.appendChild(row);
skillLevelIndex++;
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
function checkSkillLevelsEmpty() {
var container = document.getElementById('skillLevelsContainer');
var emptyMsg = document.getElementById('skillLevelsEmpty');
var rows = container.querySelectorAll('.skill-level-row');
if (emptyMsg) emptyMsg.style.display = rows.length === 0 ? 'block' : 'none';
}
</script>
<?php $__template->endSection(); ?>
This diff is collapsed.
......@@ -50,9 +50,6 @@ $allCategories = SportDiscipline::getCategories();
<?php if (!empty($disciplines)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:20px;margin-bottom:20px;">
<?php foreach ($disciplines as $d):
$config = json_decode($d['config_json'] ?? '{}', true) ?: [];
$ageGroupsCount = count($config['age_groups'] ?? []);
$skillLevelsCount = count($config['skill_levels'] ?? []);
$catColor = SportDiscipline::getCategoryColor($d['category'] ?? '');
$catLabel = SportDiscipline::getCategoryLabel($d['category'] ?? '');
$isActive = (int) ($d['is_active'] ?? 0);
......@@ -79,16 +76,11 @@ $allCategories = SportDiscipline::getCategories();
</div>
</div>
<!-- Card Body Stats -->
<div style="padding:0 20px 15px;display:flex;gap:15px;font-size:12px;color:#6B7280;">
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="users" style="width:14px;height:14px;"></i>
<?= $ageGroupsCount ?> فئة عمرية
</span>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="bar-chart-2" style="width:14px;height:14px;"></i>
<?= $skillLevelsCount ?> مستوى مهارة
</span>
<!-- Card Body -->
<div style="padding:0 20px 15px;font-size:12px;color:#6B7280;">
<?php if (!empty($d['description_ar'])): ?>
<span style="display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;"><?= e($d['description_ar']) ?></span>
<?php endif; ?>
</div>
</a>
......
......@@ -70,15 +70,7 @@ $isActive = (int) $discipline->is_active;
</div>
<!-- Stats Cards Row -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0D7377;"><?= count($ageGroups) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">فئة عمرية</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#7C3AED;"><?= count($skillLevels) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">مستوى مهارة</div>
</div>
<div style="display:grid;grid-template-columns:repeat(2, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0284C7;">0</div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">لاعب مسجل</div>
......@@ -89,80 +81,6 @@ $isActive = (int) $discipline->is_active;
</div>
</div>
<!-- Config Details Grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Age Groups Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="users" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">الفئات العمرية</h3>
</div>
<?php if (!empty($ageGroups)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الكود</th>
<th>الفئة</th>
<th>من سن</th>
<th>إلى سن</th>
</tr>
</thead>
<tbody>
<?php foreach ($ageGroups as $ag): ?>
<tr>
<td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($ag['code'] ?? '') ?></code></td>
<td style="font-weight:600;"><?= e($ag['label_ar'] ?? '') ?></td>
<td style="text-align:center;"><?= (int) ($ag['min_age'] ?? 0) ?></td>
<td style="text-align:center;"><?= (int) ($ag['max_age'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:14px;">
<i data-lucide="info" style="width:20px;height:20px;margin-bottom:8px;"></i>
<div>لم يتم تحديد فئات عمرية</div>
</div>
<?php endif; ?>
</div>
<!-- Skill Levels Table -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="bar-chart-2" style="width:18px;height:18px;color:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">مستويات المهارة</h3>
</div>
<?php if (!empty($skillLevels)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الكود</th>
<th>المستوى</th>
</tr>
</thead>
<tbody>
<?php foreach ($skillLevels as $sl): ?>
<tr>
<td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($sl['code'] ?? '') ?></code></td>
<td style="font-weight:600;"><?= e($sl['label_ar'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:14px;">
<i data-lucide="info" style="width:20px;height:20px;margin-bottom:8px;"></i>
<div>لم يتم تحديد مستويات مهارة</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Additional Config -->
<?php
$requiresMedical = !empty($config['requires_medical_cert']);
......
This diff is collapsed.
......@@ -18,10 +18,12 @@ MenuRegistry::register('sports_activities', [
'parent' => null,
'order' => 395,
'children' => [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Sports Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 0],
['label_ar' => 'الأنشطة الرياضية', 'label_en' => 'Disciplines', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 1],
['label_ar' => 'الملاعب والمرافق', 'label_en' => 'Facilities', 'route' => '/facilities', 'permission' => 'facility.view', 'order' => 2],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/academies', 'permission' => 'academy.view', 'order' => 3],
['label_ar' => 'شئون اللاعبين', 'label_en' => 'Players', 'route' => '/players', 'permission' => 'player.view', 'order' => 4],
['label_ar' => 'الحضور والغياب', 'label_en' => 'Attendance', 'route' => '/attendance', 'permission' => 'player.view', 'order' => 4.5],
['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 5],
['label_ar' => 'التأجير المؤسسي', 'label_en' => 'Corporate Rentals', 'route' => '/rentals', 'permission' => 'rental.view', 'order' => 6],
['label_ar' => 'اشتراكات الأنشطة', 'label_en' => 'Activity Subscriptions','route' => '/activity-subscriptions', 'permission' => 'activity_sub.view', 'order' => 7],
......
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class AttendanceController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$academyId = (int) $request->get('academy_id', 0);
$date = trim((string) $request->get('date', date('Y-m-d')));
// Get all active academies for the dropdown
$academies = $db->select(
"SELECT a.id, a.name_ar, sd.name_ar AS discipline_name
FROM academies a
JOIN sport_disciplines sd ON sd.id = a.discipline_id
WHERE a.is_active = 1
ORDER BY a.name_ar"
);
$players = [];
$existingAttendance = [];
if ($academyId > 0) {
// Get active players enrolled in this academy
$players = $db->select("
SELECT ae.id AS enrollment_id, ae.player_id,
p.full_name_ar, p.activity_id_number, p.card_status
FROM academy_enrollments ae
JOIN players p ON p.id = ae.player_id AND p.is_archived = 0
WHERE ae.academy_id = ? AND ae.status = 'active'
ORDER BY p.full_name_ar
", [$academyId]);
// Get existing attendance records for this date and academy's players
if (!empty($players)) {
$playerIds = array_map(fn($p) => (int) $p['player_id'], $players);
$placeholders = implode(',', array_fill(0, count($playerIds), '?'));
$rows = $db->select(
"SELECT player_id, status, notes FROM player_attendance
WHERE attendance_date = ? AND player_id IN ({$placeholders})",
array_merge([$date], $playerIds)
);
foreach ($rows as $row) {
$existingAttendance[(int) $row['player_id']] = $row;
}
}
}
// Attendance summary for this academy/date
$summary = null;
if ($academyId > 0 && !empty($existingAttendance)) {
$summary = ['present' => 0, 'absent' => 0, 'late' => 0, 'excused' => 0];
foreach ($existingAttendance as $att) {
$s = $att['status'] ?? 'present';
if (isset($summary[$s])) $summary[$s]++;
}
}
return $this->view('PlayerAffairs.Views.attendance', [
'academies' => $academies,
'selectedAcademyId' => $academyId,
'selectedDate' => $date,
'players' => $players,
'existingAttendance' => $existingAttendance,
'summary' => $summary,
]);
}
public function record(Request $request): Response
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$academyId = (int) $request->post('academy_id', 0);
$date = trim((string) $request->post('date', date('Y-m-d')));
$statuses = (array) ($request->post('status') ?? []);
$notes = (array) ($request->post('notes') ?? []);
if ($academyId <= 0 || empty($statuses)) {
return $this->redirect('/attendance?academy_id=' . $academyId . '&date=' . $date)
->withError('بيانات غير صالحة');
}
$validStatuses = ['present', 'absent', 'late', 'excused'];
$ts = date('Y-m-d H:i:s');
$recorded = 0;
// Get enrollment_id mapping for this academy
$enrollments = $db->select(
"SELECT id AS enrollment_id, player_id FROM academy_enrollments WHERE academy_id = ? AND status = 'active'",
[$academyId]
);
$enrollmentMap = [];
foreach ($enrollments as $e) {
$enrollmentMap[(int) $e['player_id']] = (int) $e['enrollment_id'];
}
foreach ($statuses as $playerId => $status) {
$playerId = (int) $playerId;
$status = trim((string) $status);
if (!in_array($status, $validStatuses)) continue;
$enrollmentId = $enrollmentMap[$playerId] ?? null;
$note = trim((string) ($notes[$playerId] ?? ''));
// Upsert: delete existing then insert
$db->delete('player_attendance', 'player_id = ? AND attendance_date = ? AND enrollment_id = ?', [
$playerId, $date, $enrollmentId
]);
$db->insert('player_attendance', [
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'attendance_date' => $date,
'status' => $status,
'notes' => $note ?: null,
'created_at' => $ts,
'updated_at' => $ts,
'created_by' => $employee ? (int) $employee->id : null,
]);
$recorded++;
}
return $this->redirect('/attendance?academy_id=' . $academyId . '&date=' . $date)
->withSuccess('تم تسجيل الحضور بنجاح (' . $recorded . ' لاعب)');
}
}
......@@ -8,7 +8,6 @@ use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PlayerAffairs\Models\Player;
use App\Modules\PlayerAffairs\Models\PlayerDiscipline;
use App\Modules\PlayerAffairs\Models\AcademyEnrollment;
use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord;
use App\Modules\PlayerAffairs\Services\PlayerRegistrationService;
......@@ -145,7 +144,18 @@ class PlayerController extends Controller
return $this->redirect('/players')->withError('اللاعب غير موجود');
}
$disciplines = PlayerDiscipline::getForPlayer((int) $id);
// Derive disciplines from active enrollments (not the redundant player_disciplines table)
$db = App::getInstance()->db();
$disciplines = $db->select(
"SELECT DISTINCT sd.id, sd.name_ar AS discipline_name, sd.icon,
a.name_ar AS academy_name, ae.season, ae.status
FROM academy_enrollments ae
JOIN academies a ON a.id = ae.academy_id
JOIN sport_disciplines sd ON sd.id = a.discipline_id
WHERE ae.player_id = ? AND ae.status IN ('active', 'suspended')
ORDER BY sd.name_ar",
[(int) $id]
);
$enrollments = AcademyEnrollment::getForPlayer((int) $id);
$medicalRecords = PlayerMedicalRecord::getForPlayer((int) $id);
......
......@@ -164,9 +164,22 @@ EventBus::listen('academy.enrollment_created', function (array $data) {
}
$baseRate = $pricing ? (string) $pricing['rate'] : '0.00';
$isHalfMonth = ($enrollmentDay > 15) ? 1 : 0;
// Half-month: only for FIRST subscription month AND enrollment after 15th
$isFirstMonth = !$db->selectOne(
"SELECT id FROM activity_subscriptions WHERE enrollment_id = ? AND subscription_month < ? LIMIT 1",
[$enrollmentId, $month]
);
$isHalfMonth = ($isFirstMonth && $enrollmentDay > 15) ? 1 : 0;
$appliedRate = $isHalfMonth ? bcdiv($baseRate, '2', 2) : $baseRate;
$dueDate = date('Y-m-07');
// Due date: for first month with enrollment after 7th, give 7 days grace
$defaultDue = date('Y-m-07');
if ($isFirstMonth && $enrollmentDay > 7) {
$dueDate = date('Y-m-d', strtotime('+7 days'));
} else {
$dueDate = $defaultDue;
}
$subId = $db->insert('activity_subscriptions', [
'player_id' => $playerId,
......
......@@ -229,10 +229,11 @@ class Player extends Model
$params[] = $filters['medical_status'];
}
// Discipline filter (join player_disciplines)
// Discipline filter (derived from enrollments → academies → disciplines)
if (!empty($filters['discipline_id'])) {
$joins .= ' INNER JOIN player_disciplines pd ON pd.player_id = p.id';
$where[] = 'pd.discipline_id = ?';
$joins .= ' INNER JOIN academy_enrollments ae_disc ON ae_disc.player_id = p.id AND ae_disc.status IN (\'active\', \'suspended\')';
$joins .= ' INNER JOIN academies a_disc ON a_disc.id = ae_disc.academy_id';
$where[] = 'a_disc.discipline_id = ?';
$params[] = (int) $filters['discipline_id'];
}
......
......@@ -14,4 +14,8 @@ return [
['POST', '/players/{id:\d+}/medical', 'PlayerAffairs\Controllers\PlayerController@addMedical', ['auth', 'csrf'], 'player.manage_medical'],
['POST', '/players/{id:\d+}/enroll', 'PlayerAffairs\Controllers\PlayerController@enroll', ['auth', 'csrf'], 'academy.enroll'],
['POST', '/players/{id:\d+}/enroll/drop', 'PlayerAffairs\Controllers\PlayerController@dropEnrollment', ['auth', 'csrf'], 'academy.enroll'],
// Attendance
['GET', '/attendance', 'PlayerAffairs\Controllers\AttendanceController@index', ['auth'], 'player.view'],
['POST', '/attendance/record', 'PlayerAffairs\Controllers\AttendanceController@record', ['auth', 'csrf'], 'player.edit'],
];
This diff is collapsed.
......@@ -236,20 +236,24 @@ $enrollmentStatuses = AcademyEnrollment::getStatuses();
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">اللعبة</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">الفئة العمرية</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">مستوى المهارة</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">الأكاديمية</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">الموسم</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($disciplines as $disc): ?>
<?php foreach ($disciplines as $disc):
$dStatus = $disc['status'] ?? 'active';
$dStatusColor = $dStatus === 'active' ? '#059669' : '#D97706';
$dStatusLabel = $dStatus === 'active' ? 'نشط' : ($dStatus === 'suspended' ? 'موقف' : $dStatus);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 15px;font-weight:600;"><?= e($disc['discipline_name'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($disc['age_group'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($disc['skill_level'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($disc['academy_name'] ?? '—') ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;"><?= e($disc['season'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($disc['status'] ?? '—') ?></td>
<td style="padding:12px 15px;">
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $dStatusColor ?>15;color:<?= $dStatusColor ?>;"><?= e($dStatusLabel) ?></span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
......
......@@ -7,6 +7,7 @@ use App\Core\App;
use App\Core\EventBus;
use App\Modules\Reservations\Models\Reservation;
use App\Modules\Facilities\Models\Facility;
use App\Modules\Rules\Services\RuleEngine;
class ReservationService
{
......@@ -42,7 +43,8 @@ class ReservationService
// Determine time tier if not provided
if (empty($data['time_tier'])) {
$hour = (int) date('H', strtotime($data['start_time']));
$data['time_tier'] = $hour < 12 ? 'AM' : 'PM';
$pmStartHour = (int) (RuleEngine::getValue('SPORTS_PM_START_HOUR', 'hour') ?? 17);
$data['time_tier'] = $hour < $pmStartHour ? 'AM' : 'PM';
}
// Calculate unit rate and total amount if facility is set
......
......@@ -24,14 +24,14 @@ class ActivitySubGeneratorJob
{
$month = date('Y-m'); // Current month YYYY-MM
$ts = date('Y-m-d H:i:s');
$dueDate = date('Y-m-07'); // 7th of the month
$defaultDueDate = date('Y-m-07'); // 7th of the month
$processed = 0;
$skipped = 0;
// Get all active enrollments with player info
$enrollments = $this->db->select("
SELECT ae.id AS enrollment_id, ae.player_id, ae.academy_id, ae.level_id,
ae.enrollment_day, ae.season,
ae.enrollment_day, ae.season, ae.enrolled_at,
p.player_type,
a.discipline_id
FROM academy_enrollments ae
......@@ -79,6 +79,14 @@ class ActivitySubGeneratorJob
$appliedRate = $isHalfMonth ? bcdiv($baseRate, '2', 2) : $baseRate;
// Due date: for first month with enrollment after 7th, give 7 days from enrollment
if ($isFirstMonth && $enrollmentDay > 7) {
$enrolledAt = $e['enrolled_at'] ?? date('Y-m-d');
$dueDate = date('Y-m-d', strtotime('+7 days', strtotime($enrolledAt)));
} else {
$dueDate = $defaultDueDate;
}
$this->db->insert('activity_subscriptions', [
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
......
......@@ -28,7 +28,7 @@ return function (Database $db): void {
['code' => 'SVC_SPOUSE_ACQUIRED', 'name_ar' => 'رسوم زوج مكتسب', 'name_en' => 'Acquired Spouse Fee', 'price_type' => 'percentage', 'percentage' => '50.00'],
['code' => 'SVC_SPOUSE_BASE', 'name_ar' => 'رسوم زوج أساسي', 'name_en' => 'Base Spouse Fee', 'price_type' => 'percentage', 'percentage' => '15.00'],
['code' => 'SVC_WAIVER', 'name_ar' => 'رسوم تنازل', 'name_en' => 'Waiver Fee', 'price_type' => 'percentage', 'percentage' => '30.00'],
['code' => 'SVC_SPORTS_CONV', 'name_ar' => 'رسوم تحويل رياضي', 'name_en' => 'Sports Conversion Fee', 'price_type' => 'percentage', 'percentage' => '50.00'],
// SVC_SPORTS_CONV removed — no implementation exists, pricing lives in business_rules
['code' => 'SVC_ANNUAL_MEMBER', 'name_ar' => 'اشتراك سنوي - عضو', 'name_en' => 'Annual Sub - Member', 'price_type' => 'fixed', 'base_amount' => '492.00'],
['code' => 'SVC_ANNUAL_SPOUSE', 'name_ar' => 'اشتراك سنوي - زوجة', 'name_en' => 'Annual Sub - Spouse', 'price_type' => 'fixed', 'base_amount' => '492.00'],
['code' => 'SVC_ANNUAL_CHILD', 'name_ar' => 'اشتراك سنوي - ابن/ابنة', 'name_en' => 'Annual Sub - Child', 'price_type' => 'fixed', 'base_amount' => '222.00'],
......
......@@ -3,75 +3,19 @@ declare(strict_types=1);
use App\Core\Database;
/**
* DEPRECATED — Sports service catalog entries removed.
*
* Pricing authority:
* - Registration fees → business_rules (SPORTS_REG_FEE_MEMBER / _NONMEMBER)
* - Subscription rates → activity_pricing table
* - Facility rates → facilities table (am_member_rate, pm_member_rate, etc.)
* - Rental contracts → rental_contracts.total_amount (per-contract)
*
* These service_catalog entries were never read by application code and duplicated
* pricing that already lives in the authoritative tables above. Keeping them caused
* admin confusion (two places showing different numbers for the same fee).
*/
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$now = date('Y-m-d');
$services = [
[
'code' => 'SVC_SPORTS_REG_MEMBER',
'name_ar' => 'رسوم تسجيل نشاط رياضي - عضو',
'name_en' => 'Sports Registration Fee - Member',
'price_type' => 'fixed',
'base_amount' => '50.00',
],
[
'code' => 'SVC_SPORTS_REG_NONMEMBER',
'name_ar' => 'رسوم تسجيل نشاط رياضي - غير عضو',
'name_en' => 'Sports Registration Fee - Non-Member',
'price_type' => 'fixed',
'base_amount' => '100.00',
],
[
'code' => 'SVC_FACILITY_RESERVATION',
'name_ar' => 'حجز ملعب',
'name_en' => 'Facility Reservation',
'price_type' => 'fixed',
'base_amount' => '0.00',
],
[
'code' => 'SVC_RENTAL_CONTRACT',
'name_ar' => 'عقد تأجير مؤسسي',
'name_en' => 'Institutional Rental Contract',
'price_type' => 'fixed',
'base_amount' => '0.00',
],
[
'code' => 'SVC_RENTAL_DEPOSIT',
'name_ar' => 'تأمين تأجير مؤسسي',
'name_en' => 'Institutional Rental Deposit',
'price_type' => 'fixed',
'base_amount' => '0.00',
],
[
'code' => 'SVC_ACTIVITY_SUB_MONTHLY',
'name_ar' => 'اشتراك نشاط رياضي شهري',
'name_en' => 'Monthly Sports Activity Subscription',
'price_type' => 'fixed',
'base_amount' => '0.00',
],
];
foreach ($services as $s) {
$existing = $db->selectOne("SELECT id FROM service_catalog WHERE service_code = ? AND branch_id IS NULL", [$s['code']]);
if ($existing) {
continue;
}
$db->insert('service_catalog', [
'service_code' => $s['code'],
'name_ar' => $s['name_ar'],
'name_en' => $s['name_en'] ?? null,
'price_type' => $s['price_type'],
'base_amount' => $s['base_amount'] ?? null,
'percentage' => null,
'annual_amount' => null,
'currency' => 'EGP',
'applies_to' => null,
'branch_id' => null,
'effective_from' => $now,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
// No-op: sports pricing is managed by business_rules, activity_pricing, and facilities tables.
};
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