Commit da42f02a authored by Mahmoud Aglan's avatar Mahmoud Aglan

sportsUpdate

parent 78ab767e
...@@ -3,7 +3,20 @@ ...@@ -3,7 +3,20 @@
"allow": [ "allow": [
"Bash(wc -l /Users/mahmoudaglan/clubphp/app/Core/*.php)", "Bash(wc -l /Users/mahmoudaglan/clubphp/app/Core/*.php)",
"Bash(grep -r \"icon\" /Users/mahmoudaglan/clubphp/app/Modules/*/bootstrap.php)", "Bash(grep -r \"icon\" /Users/mahmoudaglan/clubphp/app/Modules/*/bootstrap.php)",
"Bash(grep -h \"'icon'\" /Users/mahmoudaglan/clubphp/app/Modules/*/bootstrap.php)" "Bash(grep -h \"'icon'\" /Users/mahmoudaglan/clubphp/app/Modules/*/bootstrap.php)",
"Bash(grep -A 30 \"CREATE TABLE \\\\`sports_members\\\\`\" /Users/mahmoudaglan/clubphp/database/schema.sql)",
"Bash(grep -A 20 \"CREATE TABLE \\\\`subscriptions\\\\`\" /Users/mahmoudaglan/clubphp/database/schema.sql)",
"Bash(grep -A 20 \"CREATE TABLE \\\\`payments\\\\`\" /Users/mahmoudaglan/clubphp/database/schema.sql)",
"Bash(grep -A 60 \"^CREATE TABLE \\\\`members\\\\`\" /Users/mahmoudaglan/clubphp/database/schema.sql)",
"Bash(ls -1 /Users/mahmoudaglan/clubphp/database/migrations/*.php)",
"Bash(ls -1 /Users/mahmoudaglan/clubphp/database/seeds/*.php)",
"Bash(ls -la /Users/mahmoudaglan/clubphp/database/migrations/Phase_17*)",
"Bash(ls -la /Users/mahmoudaglan/clubphp/database/seeds/Phase_17*)",
"Bash(ls /Users/mahmoudaglan/clubphp/database/seeds/Phase_17_004*)",
"Bash(ls /Users/mahmoudaglan/clubphp/database/seeds/Phase_22*)",
"Bash(ls database/migrations/Phase_1[7-9]_* database/migrations/Phase_2[0-3]_*)",
"Bash(ls database/seeds/Phase_1[7-9]_* database/seeds/Phase_2[0-2]_*)",
"Bash(ls cron/jobs/*Job.php)"
] ]
} }
} }
<?php
declare(strict_types=1);
namespace App\Modules\Academies\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Academies\Models\Academy;
use App\Modules\Academies\Models\AcademyLevel;
use App\Modules\Academies\Models\AcademySchedule;
use App\Modules\Disciplines\Models\SportDiscipline;
use App\Modules\Facilities\Models\Facility;
class AcademyController extends Controller
{
/**
* List all academies with type filter tabs, search, and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'academy_type' => trim((string) $request->get('academy_type', '')),
'discipline_id' => trim((string) $request->get('discipline_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = Academy::search($filters, 25, $page);
return $this->view('Academies.Views.index', [
'academies' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'academyTypes' => Academy::getAcademyTypes(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Show the create academy form.
*/
public function create(Request $request): Response
{
return $this->view('Academies.Views.create', [
'academyTypes' => Academy::getAcademyTypes(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Validate and store a new academy.
*/
public function store(Request $request): Response
{
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$disciplineId = (int) $request->post('discipline_id', 0);
$academyType = trim((string) $request->post('academy_type', ''));
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود الأكاديمية مطلوب';
}
if ($disciplineId <= 0) {
$errors[] = 'النشاط الرياضي مطلوب';
} else {
$discipline = SportDiscipline::find($disciplineId);
if (!$discipline) {
$errors[] = 'النشاط الرياضي غير موجود';
}
}
if (!array_key_exists($academyType, Academy::getAcademyTypes())) {
$errors[] = 'نوع الأكاديمية غير صالح';
}
// Check unique code
if ($code !== '') {
$existing = Academy::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود الأكاديمية مستخدم بالفعل';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/academies/create');
}
// Build config_json
$config = $this->buildConfigFromRequest($request);
$academy = Academy::create([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'discipline_id' => $disciplineId,
'academy_type' => $academyType,
'description_ar' => $descriptionAr ?: null,
'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE),
'sort_order' => $sortOrder,
'is_active' => 1,
]);
return $this->redirect('/academies/' . $academy->id)->withSuccess('تم إضافة الأكاديمية بنجاح');
}
/**
* Show academy detail page with levels and schedules.
*/
public function show(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$discipline = SportDiscipline::find((int) $academy->discipline_id);
$levels = AcademyLevel::getForAcademy((int) $id);
$schedules = AcademySchedule::getForAcademy((int) $id);
return $this->view('Academies.Views.show', [
'academy' => $academy,
'config' => $academy->getConfig(),
'discipline' => $discipline,
'levels' => $levels,
'schedules' => $schedules,
'academyTypes' => Academy::getAcademyTypes(),
'dayLabels' => AcademySchedule::getDayLabels(),
]);
}
/**
* Show edit form for an academy.
*/
public function edit(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
return $this->view('Academies.Views.edit', [
'academy' => $academy,
'config' => $academy->getConfig(),
'academyTypes' => Academy::getAcademyTypes(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Validate and update an existing academy.
*/
public function update(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$disciplineId = (int) $request->post('discipline_id', 0);
$academyType = trim((string) $request->post('academy_type', ''));
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود الأكاديمية مطلوب';
}
if ($disciplineId <= 0) {
$errors[] = 'النشاط الرياضي مطلوب';
} else {
$discipline = SportDiscipline::find($disciplineId);
if (!$discipline) {
$errors[] = 'النشاط الرياضي غير موجود';
}
}
if (!array_key_exists($academyType, Academy::getAcademyTypes())) {
$errors[] = 'نوع الأكاديمية غير صالح';
}
// Check unique code (exclude current)
if ($code !== '') {
$existing = Academy::query()
->where('code', '=', $code)
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود الأكاديمية مستخدم بالفعل';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/academies/' . $id . '/edit');
}
// Build config_json
$config = $this->buildConfigFromRequest($request);
$academy->update([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'discipline_id' => $disciplineId,
'academy_type' => $academyType,
'description_ar' => $descriptionAr ?: null,
'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE),
'sort_order' => $sortOrder,
]);
return $this->redirect('/academies/' . $id)->withSuccess('تم تحديث الأكاديمية بنجاح');
}
/**
* Toggle the is_active status of an academy.
*/
public function toggle(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$newStatus = $academy->is_active ? 0 : 1;
$academy->update(['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل الأكاديمية' : 'تم إيقاف الأكاديمية';
return $this->redirect('/academies/' . $id)->withSuccess($message);
}
/**
* Add a level to an academy.
*/
public function addLevel(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$levelOrder = (int) $request->post('level_order', 0);
$ageMin = (int) $request->post('age_min', 0);
$ageMax = (int) $request->post('age_max', 99);
$maxCapacity = (int) $request->post('max_capacity', 0);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'اسم المستوى بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود المستوى مطلوب';
}
// Check unique code within this academy
if ($code !== '') {
$existing = AcademyLevel::query()
->where('academy_id', '=', (int) $id)
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود المستوى مستخدم بالفعل في هذه الأكاديمية';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/academies/' . $id);
}
AcademyLevel::create([
'academy_id' => (int) $id,
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'level_order' => $levelOrder,
'age_min' => $ageMin,
'age_max' => $ageMax,
'max_capacity' => $maxCapacity,
'config_json' => null,
'is_active' => 1,
]);
return $this->redirect('/academies/' . $id)->withSuccess('تم إضافة المستوى بنجاح');
}
/**
* Remove (delete) a level from an academy.
*/
public function removeLevel(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$levelId = (int) $request->post('level_id', 0);
$level = AcademyLevel::find($levelId);
if (!$level || (int) $level->academy_id !== (int) $id) {
return $this->redirect('/academies/' . $id)->withError('المستوى غير موجود');
}
$level->update(['is_active' => 0]);
return $this->redirect('/academies/' . $id)->withSuccess('تم حذف المستوى بنجاح');
}
/**
* Add a schedule to an academy.
*/
public function addSchedule(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$levelId = (int) $request->post('level_id', 0);
$facilityId = (int) $request->post('facility_id', 0);
$dayOfWeek = (int) $request->post('day_of_week', 0);
$startTime = trim((string) $request->post('start_time', ''));
$endTime = trim((string) $request->post('end_time', ''));
$coachName = trim((string) $request->post('coach_name', ''));
$season = trim((string) $request->post('season', ''));
// Validation
$errors = [];
if ($dayOfWeek < 0 || $dayOfWeek > 6) {
$errors[] = 'يوم الأسبوع غير صالح';
}
if ($startTime === '') {
$errors[] = 'وقت البدء مطلوب';
}
if ($endTime === '') {
$errors[] = 'وقت الانتهاء مطلوب';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/academies/' . $id);
}
AcademySchedule::create([
'academy_id' => (int) $id,
'level_id' => $levelId > 0 ? $levelId : null,
'facility_id' => $facilityId > 0 ? $facilityId : null,
'day_of_week' => $dayOfWeek,
'start_time' => $startTime,
'end_time' => $endTime,
'coach_name' => $coachName ?: null,
'season' => $season ?: null,
'is_active' => 1,
]);
return $this->redirect('/academies/' . $id)->withSuccess('تم إضافة الجدول بنجاح');
}
/**
* Remove (delete) a schedule from an academy.
*/
public function removeSchedule(Request $request, string $id): Response
{
$academy = Academy::find((int) $id);
if (!$academy) {
return $this->redirect('/academies')->withError('الأكاديمية غير موجودة');
}
$scheduleId = (int) $request->post('schedule_id', 0);
$schedule = AcademySchedule::find($scheduleId);
if (!$schedule || (int) $schedule->academy_id !== (int) $id) {
return $this->redirect('/academies/' . $id)->withError('الجدول غير موجود');
}
$schedule->update(['is_active' => 0]);
return $this->redirect('/academies/' . $id)->withSuccess('تم حذف الجدول بنجاح');
}
/**
* Build the config array from the request (age_min, age_max, capacity).
*/
private function buildConfigFromRequest(Request $request): array
{
$config = [];
$ageMin = $request->post('age_min');
if ($ageMin !== null && $ageMin !== '') {
$config['age_min'] = (int) $ageMin;
}
$ageMax = $request->post('age_max');
if ($ageMax !== null && $ageMax !== '') {
$config['age_max'] = (int) $ageMax;
}
$capacity = $request->post('capacity');
if ($capacity !== null && $capacity !== '') {
$config['capacity'] = (int) $capacity;
}
$requiresMedicalCert = $request->post('requires_medical_cert');
if ($requiresMedicalCert) {
$config['requires_medical_cert'] = true;
}
$guardianConsentAge = $request->post('requires_guardian_consent_under');
if ($guardianConsentAge !== null && $guardianConsentAge !== '') {
$config['requires_guardian_consent_under'] = (int) $guardianConsentAge;
}
return $config;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Academies\Models;
use App\Core\Model;
class Academy extends Model
{
protected static string $table = 'academies';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'discipline_id',
'academy_type',
'description_ar',
'config_json',
'sort_order',
'is_active',
];
/**
* Decode config_json into an associative array.
*/
public function getConfig(): array
{
$raw = $this->config_json;
if (empty($raw)) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Get all academy types with Arabic labels.
*/
public static function getAcademyTypes(): array
{
return [
'football' => 'كرة قدم',
'gymnastics' => 'جمباز',
'swimming' => 'سباحة',
'combat' => 'فنون قتالية',
'racket' => 'راكيت',
'general' => 'عام',
];
}
/**
* Get the Arabic label for an academy type.
*/
public static function getTypeLabel(string $type): string
{
$types = self::getAcademyTypes();
return $types[$type] ?? $type;
}
/**
* Get the badge color for an academy type.
*/
public static function getTypeColor(string $type): string
{
$colors = [
'football' => '#0D7377',
'gymnastics' => '#7C3AED',
'swimming' => '#2563EB',
'combat' => '#DC2626',
'racket' => '#D97706',
'general' => '#059669',
];
return $colors[$type] ?? '#6B7280';
}
/**
* Get all active academies ordered by sort_order, name_ar.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get active academies filtered by discipline.
*/
public static function getByDiscipline(int $disciplineId): array
{
return static::query()
->where('is_active', '=', 1)
->where('discipline_id', '=', $disciplineId)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get active academies filtered by academy_type.
*/
public static function getByType(string $type): array
{
return static::query()
->where('is_active', '=', 1)
->where('academy_type', '=', $type)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Search academies with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`name_ar` LIKE ? OR `name_en` LIKE ? OR `code` LIKE ?)',
[$search, $search, $search]
);
}
if (!empty($filters['academy_type'])) {
$query = $query->where('academy_type', '=', $filters['academy_type']);
}
if (!empty($filters['discipline_id'])) {
$query = $query->where('discipline_id', '=', (int) $filters['discipline_id']);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('sort_order', 'ASC')->orderBy('name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Academies\Models;
use App\Core\Model;
class AcademyLevel extends Model
{
protected static string $table = 'academy_levels';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'academy_id',
'code',
'name_ar',
'name_en',
'level_order',
'age_min',
'age_max',
'max_capacity',
'config_json',
'is_active',
];
/**
* Decode config_json into an associative array.
*/
public function getConfig(): array
{
$raw = $this->config_json;
if (empty($raw)) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Get all levels for a given academy, ordered by level_order.
*/
public static function getForAcademy(int $academyId): array
{
return static::query()
->where('academy_id', '=', $academyId)
->orderBy('level_order', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Academies\Models;
use App\Core\Model;
class AcademySchedule extends Model
{
protected static string $table = 'academy_schedules';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'academy_id',
'level_id',
'facility_id',
'day_of_week',
'start_time',
'end_time',
'coach_name',
'season',
'is_active',
];
/**
* Get all schedules for a given academy, ordered by day_of_week then start_time.
*/
public static function getForAcademy(int $academyId): array
{
return static::query()
->where('academy_id', '=', $academyId)
->orderBy('day_of_week', 'ASC')
->orderBy('start_time', 'ASC')
->get();
}
/**
* Get all schedules for a given level.
*/
public static function getForLevel(int $levelId): array
{
return static::query()
->where('level_id', '=', $levelId)
->orderBy('day_of_week', 'ASC')
->orderBy('start_time', 'ASC')
->get();
}
/**
* Get day-of-week labels in Arabic (0=Sunday .. 6=Saturday).
*/
public static function getDayLabels(): array
{
return [
0 => 'الأحد',
1 => 'الاثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
6 => 'السبت',
];
}
}
<?php
declare(strict_types=1);
return [
['GET', '/academies', 'Academies\Controllers\AcademyController@index', ['auth'], 'academy.view'],
['GET', '/academies/create', 'Academies\Controllers\AcademyController@create', ['auth'], 'academy.manage'],
['POST', '/academies', 'Academies\Controllers\AcademyController@store', ['auth', 'csrf'], 'academy.manage'],
['GET', '/academies/{id:\d+}', 'Academies\Controllers\AcademyController@show', ['auth'], 'academy.view'],
['GET', '/academies/{id:\d+}/edit', 'Academies\Controllers\AcademyController@edit', ['auth'], 'academy.manage'],
['POST', '/academies/{id:\d+}', 'Academies\Controllers\AcademyController@update', ['auth', 'csrf'], 'academy.manage'],
['POST', '/academies/{id:\d+}/toggle', 'Academies\Controllers\AcademyController@toggle', ['auth', 'csrf'], 'academy.manage'],
['POST', '/academies/{id:\d+}/levels', 'Academies\Controllers\AcademyController@addLevel', ['auth', 'csrf'], 'academy.manage'],
['POST', '/academies/{id:\d+}/levels/remove', 'Academies\Controllers\AcademyController@removeLevel', ['auth', 'csrf'], 'academy.manage'],
['POST', '/academies/{id:\d+}/schedules', 'Academies\Controllers\AcademyController@addSchedule', ['auth', 'csrf'], 'academy.manage'],
['POST', '/academies/{id:\d+}/schedules/remove', 'Academies\Controllers\AcademyController@removeSchedule', ['auth', 'csrf'], 'academy.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Academies\Services;
use App\Modules\Academies\Models\Academy;
use App\Modules\Academies\Models\AcademyLevel;
use App\Modules\Academies\Models\AcademySchedule;
class AcademyService
{
/**
* Get all active academies.
*/
public static function getActive(): array
{
return Academy::allActive();
}
/**
* Get active academies filtered by discipline.
*/
public static function getAcademiesForDiscipline(int $disciplineId): array
{
return Academy::getByDiscipline($disciplineId);
}
/**
* Get levels for a given academy.
*/
public static function getLevels(int $academyId): array
{
return AcademyLevel::getForAcademy($academyId);
}
/**
* Get schedules for a given academy.
*/
public static function getSchedule(int $academyId): array
{
return AcademySchedule::getForAcademy($academyId);
}
/**
* Check if an age is eligible for any level within an academy.
*/
public static function checkAgeEligibility(int $academyId, int $age): bool
{
$levels = AcademyLevel::getForAcademy($academyId);
if (empty($levels)) {
// No levels defined — allow all ages
return true;
}
foreach ($levels as $level) {
$minAge = (int) ($level['age_min'] ?? 0);
$maxAge = (int) ($level['age_max'] ?? 999);
if ($age >= $minAge && $age <= $maxAge) {
return true;
}
}
return false;
}
/**
* Get all active academies grouped by academy_type.
*/
public static function getGroupedByType(): array
{
$academies = Academy::allActive();
$grouped = [];
foreach (Academy::getAcademyTypes() as $key => $label) {
$grouped[$key] = [];
}
foreach ($academies as $a) {
$type = $a['academy_type'] ?? 'general';
if (!isset($grouped[$type])) {
$grouped[$type] = [];
}
$grouped[$type][] = $a;
}
return $grouped;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Academies\Services;
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
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
}
/**
* Promote an enrollment to a new level.
* Placeholder — full logic in PlayerAffairs Phase 19.
*/
public static function promote(int $enrollmentId, int $newLevelId): bool
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
}
/**
* Suspend an enrollment.
* Placeholder — full logic in PlayerAffairs Phase 19.
*/
public static function suspend(int $enrollmentId): bool
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
}
/**
* Drop an enrollment.
* Placeholder — full logic in PlayerAffairs Phase 19.
*/
public static function drop(int $enrollmentId): bool
{
throw new \RuntimeException('Enrollment features require the PlayerAffairs module.');
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة أكاديمية جديدة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academies" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/academies" id="academyForm">
<?= csrf_field() ?>
<!-- Basic Information -->
<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="file-text" 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;gap:20px;">
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar')) ?>" class="form-input" required placeholder="مثال: أكاديمية كرة القدم">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en')) ?>" class="form-input" id="nameEn" placeholder="e.g. Football Academy" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود الأكاديمية <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" id="codeInput" required placeholder="مثال: ACAD_FOOTBALL" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">يتم توليده تلقائيا من الاسم الإنجليزي</small>
</div>
<div class="form-group">
<label class="form-label">النشاط الرياضي <span style="color:#DC2626;">*</span></label>
<select name="discipline_id" class="form-select" required>
<option value="">-- اختر النشاط --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= old('discipline_id') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع الأكاديمية <span style="color:#DC2626;">*</span></label>
<select name="academy_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($academyTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('academy_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:3fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">وصف الأكاديمية</label>
<textarea name="description_ar" class="form-input" rows="3" placeholder="وصف مختصر للأكاديمية..."><?= e(old('description_ar')) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" value="<?= e(old('sort_order', '0')) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Config Card -->
<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="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الإعدادات</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">الحد الأدنى للسن</label>
<input type="number" name="age_min" value="<?= e(old('age_min')) ?>" class="form-input" min="0" max="99" placeholder="مثال: 5" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للسن</label>
<input type="number" name="age_max" value="<?= e(old('age_max')) ?>" class="form-input" min="0" max="99" placeholder="مثال: 18" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">السعة الاستيعابية</label>
<input type="number" name="capacity" value="<?= e(old('capacity')) ?>" class="form-input" min="1" placeholder="مثال: 100" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ الأكاديمية
</button>
<a href="/academies" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Auto-generate code from English name
var nameEnInput = document.getElementById('nameEn');
var codeInput = document.getElementById('codeInput');
if (nameEnInput && codeInput) {
nameEnInput.addEventListener('input', function() {
if (codeInput.dataset.manuallyEdited !== 'true') {
codeInput.value = this.value
.toUpperCase()
.replace(/[^A-Z0-9\s]/g, '')
.replace(/\s+/g, '_')
.substring(0, 30);
}
});
codeInput.addEventListener('input', function() {
this.dataset.manuallyEdited = 'true';
});
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل: <?= e($academy->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academies/<?= (int) $academy->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض</a>
<a href="/academies" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$ageMin = $config['age_min'] ?? '';
$ageMax = $config['age_max'] ?? '';
$capacity = $config['capacity'] ?? '';
?>
<form method="POST" action="/academies/<?= (int) $academy->id ?>" id="academyForm">
<?= csrf_field() ?>
<!-- Basic Information -->
<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="file-text" 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;gap:20px;">
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar') ?: $academy->name_ar) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en') ?: ($academy->name_en ?? '')) ?>" class="form-input" id="nameEn" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود الأكاديمية <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $academy->code) ?>" class="form-input" id="codeInput" required style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">النشاط الرياضي <span style="color:#DC2626;">*</span></label>
<select name="discipline_id" class="form-select" required>
<option value="">-- اختر النشاط --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= (old('discipline_id') ?: $academy->discipline_id) == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع الأكاديمية <span style="color:#DC2626;">*</span></label>
<select name="academy_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($academyTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('academy_type') ?: ($academy->academy_type ?? '')) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:3fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">وصف الأكاديمية</label>
<textarea name="description_ar" class="form-input" rows="3"><?= e(old('description_ar') ?: ($academy->description_ar ?? '')) ?></textarea>
</div>
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" value="<?= e(old('sort_order') ?: (string)($academy->sort_order ?? 0)) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Config Card -->
<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="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الإعدادات</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">الحد الأدنى للسن</label>
<input type="number" name="age_min" value="<?= e(old('age_min') ?: (string) $ageMin) ?>" class="form-input" min="0" max="99" placeholder="مثال: 5" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للسن</label>
<input type="number" name="age_max" value="<?= e(old('age_max') ?: (string) $ageMax) ?>" class="form-input" min="0" max="99" placeholder="مثال: 18" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">السعة الاستيعابية</label>
<input type="number" name="capacity" value="<?= e(old('capacity') ?: (string) $capacity) ?>" class="form-input" min="1" placeholder="مثال: 100" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات
</button>
<a href="/academies/<?= (int) $academy->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Academies\Models\Academy;
use App\Modules\Academies\Models\AcademyLevel;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الأكاديميات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academies/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة أكاديمية</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentType = $filters['academy_type'] ?? '';
$allTypes = Academy::getAcademyTypes();
?>
<!-- Academy Type Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/academies<?= ($filters['discipline_id'] ?? '') !== '' ? '?discipline_id=' . urlencode($filters['discipline_id']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentType === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allTypes as $typeKey => $typeLabel): ?>
<a href="/academies?academy_type=<?= e($typeKey) ?><?= ($filters['q'] ?? '') !== '' ? '&q=' . urlencode($filters['q']) : '' ?><?= ($filters['discipline_id'] ?? '') !== '' ? '&discipline_id=' . urlencode($filters['discipline_id']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === $typeKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentType === $typeKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($typeLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search Bar -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/academies" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الكود..." class="form-input" style="min-width:200px;">
</div>
<div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">النشاط الرياضي</label>
<select name="discipline_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentType !== ''): ?>
<input type="hidden" name="academy_type" value="<?= e($currentType) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/academies" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Academies Grid -->
<?php if (!empty($academies)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:20px;margin-bottom:20px;">
<?php foreach ($academies as $a):
$typeColor = Academy::getTypeColor($a['academy_type'] ?? '');
$typeLabel = Academy::getTypeLabel($a['academy_type'] ?? '');
$isActive = (int) ($a['is_active'] ?? 0);
$levels = AcademyLevel::getForAcademy((int) $a['id']);
$levelCount = count($levels);
$descExcerpt = mb_substr($a['description_ar'] ?? '', 0, 80);
if (mb_strlen($a['description_ar'] ?? '') > 80) {
$descExcerpt .= '...';
}
// Find discipline name from the passed disciplines array
$discName = '';
foreach ($disciplines as $disc) {
if ((int) $disc['id'] === (int) ($a['discipline_id'] ?? 0)) {
$discName = $disc['name_ar'];
break;
}
}
?>
<div class="card" style="transition:box-shadow 0.2s;position:relative;overflow:hidden;<?= !$isActive ? 'opacity:0.65;' : '' ?>">
<?php if (!$isActive): ?>
<div style="position:absolute;top:12px;left:12px;z-index:2;">
<span class="badge" style="background:#FEE2E2;color:#DC2626;font-size:11px;padding:3px 10px;border-radius:10px;">معطّل</span>
</div>
<?php endif; ?>
<a href="/academies/<?= (int) $a['id'] ?>" style="text-decoration:none;color:inherit;display:block;">
<!-- Card Header -->
<div style="padding:20px 20px 15px;display:flex;align-items:start;gap:15px;">
<div style="width:52px;height:52px;border-radius:12px;background:linear-gradient(135deg, <?= $typeColor ?>15, <?= $typeColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="graduation-cap" style="width:26px;height:26px;color:<?= $typeColor ?>;"></i>
</div>
<div style="flex:1;min-width:0;">
<h3 style="margin:0 0 4px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($a['name_ar']) ?></h3>
<?php if (!empty($a['name_en'])): ?>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:6px;"><?= e($a['name_en']) ?></div>
<?php endif; ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $typeColor ?>15;color:<?= $typeColor ?>;"><?= e($typeLabel) ?></span>
</div>
</div>
<!-- Card Body Stats -->
<div style="padding:0 20px 10px;display:flex;gap:15px;font-size:12px;color:#6B7280;">
<?php if ($discName !== ''): ?>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="activity" style="width:14px;height:14px;"></i>
<?= e($discName) ?>
</span>
<?php endif; ?>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="layers" style="width:14px;height:14px;"></i>
<?= $levelCount ?> مستوى
</span>
</div>
<!-- Description Excerpt -->
<?php if (!empty($descExcerpt)): ?>
<div style="padding:0 20px 15px;font-size:13px;color:#6B7280;line-height:1.6;">
<?= e($descExcerpt) ?>
</div>
<?php endif; ?>
</a>
<!-- Card Footer -->
<div style="padding:10px 20px;border-top:1px solid #F3F4F6;display:flex;justify-content:space-between;align-items:center;">
<code style="font-size:11px;color:#9CA3AF;background:#F9FAFB;padding:2px 6px;border-radius:4px;"><?= e($a['code'] ?? '') ?></code>
<div style="display:flex;gap:6px;">
<a href="/academies/<?= (int) $a['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/academies/<?= (int) $a['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="graduation-cap" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أكاديميات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['academy_type']) || !empty($filters['discipline_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة أكاديمية جديدة للنادي.
<?php endif; ?>
</p>
<a href="/academies/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة أكاديمية</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Academies\Models\Academy;
use App\Modules\Academies\Models\AcademySchedule;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($academy->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academies/<?= (int) $academy->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/academies" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$typeColor = Academy::getTypeColor($academy->academy_type ?? '');
$typeLabel = Academy::getTypeLabel($academy->academy_type ?? '');
$isActive = (int) $academy->is_active;
$ageMin = $config['age_min'] ?? null;
$ageMax = $config['age_max'] ?? null;
$capacity = $config['capacity'] ?? null;
$dayLabels = AcademySchedule::getDayLabels();
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<div style="padding:25px;display:flex;align-items:start;gap:20px;">
<div style="width:72px;height:72px;border-radius:16px;background:linear-gradient(135deg, <?= $typeColor ?>15, <?= $typeColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="graduation-cap" style="width:36px;height:36px;color:<?= $typeColor ?>;"></i>
</div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;">
<h2 style="margin:0;font-size:22px;font-weight:700;color:#1A1A2E;"><?= e($academy->name_ar) ?></h2>
<span style="display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;background:<?= $typeColor ?>15;color:<?= $typeColor ?>;"><?= e($typeLabel) ?></span>
<?php if ($isActive): ?>
<span class="badge" style="background:#ECFDF5;color:#059669;font-size:12px;padding:4px 12px;border-radius:12px;font-weight:600;">فعّال</span>
<?php else: ?>
<span class="badge" style="background:#FEE2E2;color:#DC2626;font-size:12px;padding:4px 12px;border-radius:12px;font-weight:600;">معطّل</span>
<?php endif; ?>
</div>
<?php if ($academy->name_en): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;"><?= e($academy->name_en) ?></div>
<?php endif; ?>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:13px;color:#6B7280;">
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="tag" style="width:14px;height:14px;"></i>
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;"><?= e($academy->code) ?></code>
</span>
<?php if (!empty($discipline)): ?>
<a href="/disciplines/<?= (int) $discipline['id'] ?>" style="display:inline-flex;align-items:center;gap:4px;color:#0D7377;text-decoration:none;">
<i data-lucide="activity" style="width:14px;height:14px;"></i>
<?= e($discipline['name_ar'] ?? '') ?>
</a>
<?php endif; ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="arrow-up-down" style="width:14px;height:14px;"></i>
ترتيب: <?= (int) $academy->sort_order ?>
</span>
</div>
<?php if ($academy->description_ar): ?>
<p style="margin:12px 0 0;font-size:14px;color:#4B5563;line-height:1.7;"><?= e($academy->description_ar) ?></p>
<?php endif; ?>
</div>
<div style="flex-shrink:0;">
<form method="POST" action="/academies/<?= (int) $academy->id ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<?php if ($isActive): ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إيقاف هذه الأكاديمية؟')">
<i data-lucide="pause-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إيقاف
</button>
<?php else: ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تفعيل هذه الأكاديمية؟')">
<i data-lucide="play-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تفعيل
</button>
<?php endif; ?>
</form>
</div>
</div>
</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($levels) ?></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;">0</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:#0284C7;"><?= count($schedules) ?></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:#D97706;">
<?php if ($ageMin !== null && $ageMax !== null): ?>
<?= (int) $ageMin ?> - <?= (int) $ageMax ?>
<?php else: ?>
<?php endif; ?>
</div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">الفئة العمرية</div>
</div>
</div>
<!-- Config Card -->
<?php
$hasConfig = $ageMin !== null || $ageMax !== null || $capacity !== null;
?>
<?php if ($hasConfig): ?>
<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="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">الإعدادات</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:15px;">
<?php if ($ageMin !== null): ?>
<div style="padding:15px;background:#EFF6FF;border-radius:10px;display:flex;align-items:center;gap:10px;">
<i data-lucide="user-check" style="width:20px;height:20px;color:#0284C7;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:#0284C7;">الحد الأدنى للسن</div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $ageMin ?> سنة</div>
</div>
</div>
<?php endif; ?>
<?php if ($ageMax !== null): ?>
<div style="padding:15px;background:#FFF7ED;border-radius:10px;display:flex;align-items:center;gap:10px;">
<i data-lucide="user-x" style="width:20px;height:20px;color:#D97706;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:#D97706;">الحد الأقصى للسن</div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $ageMax ?> سنة</div>
</div>
</div>
<?php endif; ?>
<?php if ($capacity !== null): ?>
<div style="padding:15px;background:#ECFDF5;border-radius:10px;display:flex;align-items:center;gap:10px;">
<i data-lucide="users" style="width:20px;height:20px;color:#059669;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:#059669;">السعة الاستيعابية</div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $capacity ?> لاعب</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Levels Section -->
<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="layers" 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" id="toggleAddLevelBtn" onclick="toggleAddLevel()">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة مستوى
</button>
</div>
<!-- Add Level Inline Form (hidden by default) -->
<div id="addLevelForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<form method="POST" action="/academies/<?= (int) $academy->id ?>/levels">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" class="form-input" required placeholder="مثال: مبتدئ">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الاسم بالإنجليزي</label>
<input type="text" name="name_en" class="form-input" placeholder="e.g. Beginner" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الكود</label>
<input type="text" name="code" class="form-input" placeholder="مثال: LVL01" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;margin-top:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">ترتيب المستوى</label>
<input type="number" name="level_order" class="form-input" value="0" min="0" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">من سن</label>
<input type="number" name="age_min" class="form-input" min="0" max="99" placeholder="0" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">إلى سن</label>
<input type="number" name="age_max" class="form-input" min="0" max="99" placeholder="99" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الحد الأقصى</label>
<input type="number" name="max_capacity" class="form-input" min="1" placeholder="30" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ المستوى
</button>
<button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleAddLevel()">إلغاء</button>
</div>
</form>
</div>
<?php if (!empty($levels)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الترتيب</th>
<th>الكود</th>
<th>الاسم بالعربي</th>
<th>الاسم بالإنجليزي</th>
<th>الفئة العمرية</th>
<th>الحد الأقصى</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
<?php foreach ($levels as $level): ?>
<tr>
<td style="text-align:center;font-weight:600;"><?= (int) ($level['level_order'] ?? 0) ?></td>
<td><code style="font-size:11px;background:#F3F4F6;padding:2px 6px;border-radius:4px;"><?= e($level['code'] ?? '') ?></code></td>
<td style="font-weight:600;"><?= e($level['name_ar'] ?? '') ?></td>
<td style="color:#6B7280;direction:ltr;text-align:left;"><?= e($level['name_en'] ?? '') ?></td>
<td style="text-align:center;">
<?php if (isset($level['age_min']) && isset($level['age_max'])): ?>
<?= (int) $level['age_min'] ?> - <?= (int) $level['age_max'] ?>
<?php else: ?>
<?php endif; ?>
</td>
<td style="text-align:center;"><?= isset($level['max_capacity']) ? (int) $level['max_capacity'] : '—' ?></td>
<td>
<form method="POST" action="/academies/<?= (int) $academy->id ?>/levels/remove" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="level_id" value="<?= (int) $level['id'] ?>">
<button type="submit" class="btn btn-sm" style="color:#DC2626;padding:4px 8px;border:1px solid #FCA5A5;border-radius:6px;background:#FEF2F2;cursor:pointer;" onclick="return confirm('هل أنت متأكد من حذف هذا المستوى؟')">
<i data-lucide="trash-2" style="width:14px;height:14px;"></i>
</button>
</form>
</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>
<!-- Schedules Section -->
<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="calendar-clock" 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" id="toggleAddScheduleBtn" onclick="toggleAddSchedule()">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة جدول
</button>
</div>
<!-- Add Schedule Inline Form (hidden by default) -->
<div id="addScheduleForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<form method="POST" action="/academies/<?= (int) $academy->id ?>/schedules">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اليوم <span style="color:#DC2626;">*</span></label>
<select name="day_of_week" class="form-select" required>
<option value="">-- اختر اليوم --</option>
<?php foreach ($dayLabels as $dayKey => $dayLabel): ?>
<option value="<?= (int) $dayKey ?>"><?= e($dayLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">وقت البداية <span style="color:#DC2626;">*</span></label>
<input type="time" name="start_time" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">وقت النهاية <span style="color:#DC2626;">*</span></label>
<input type="time" name="end_time" class="form-input" required style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;margin-top:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">المستوى</label>
<select name="level_id" class="form-select">
<option value="">— الكل —</option>
<?php foreach ($levels as $lvl): ?>
<option value="<?= (int) $lvl['id'] ?>"><?= e($lvl['name_ar'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">رقم المنشأة</label>
<input type="number" name="facility_id" class="form-input" min="1" placeholder="رقم المنشأة" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اسم المدرب</label>
<input type="text" name="coach_name" class="form-input" placeholder="اسم المدرب">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الموسم</label>
<input type="text" name="season" class="form-input" placeholder="مثال: 2025-2026" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ الجدول
</button>
<button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleAddSchedule()">إلغاء</button>
</div>
</form>
</div>
<?php if (!empty($schedules)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>اليوم</th>
<th>وقت البداية</th>
<th>وقت النهاية</th>
<th>المستوى</th>
<th>المنشأة</th>
<th>المدرب</th>
<th>الموسم</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
<?php foreach ($schedules as $schedule):
$dayName = $dayLabels[(int) ($schedule['day_of_week'] ?? 0)] ?? '—';
// Find level name
$levelName = '—';
if (!empty($schedule['level_id'])) {
foreach ($levels as $lvl) {
if ((int) $lvl['id'] === (int) $schedule['level_id']) {
$levelName = $lvl['name_ar'] ?? '—';
break;
}
}
}
?>
<tr>
<td style="font-weight:600;"><?= e($dayName) ?></td>
<td style="direction:ltr;text-align:left;"><?= e($schedule['start_time'] ?? '') ?></td>
<td style="direction:ltr;text-align:left;"><?= e($schedule['end_time'] ?? '') ?></td>
<td><?= e($levelName) ?></td>
<td><?= !empty($schedule['facility_id']) ? '#' . (int) $schedule['facility_id'] : '—' ?></td>
<td><?= !empty($schedule['coach_name']) ? e($schedule['coach_name']) : '—' ?></td>
<td style="direction:ltr;text-align:left;"><?= !empty($schedule['season']) ? e($schedule['season']) : '—' ?></td>
<td>
<form method="POST" action="/academies/<?= (int) $academy->id ?>/schedules/remove" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="schedule_id" value="<?= (int) $schedule['id'] ?>">
<button type="submit" class="btn btn-sm" style="color:#DC2626;padding:4px 8px;border:1px solid #FCA5A5;border-radius:6px;background:#FEF2F2;cursor:pointer;" onclick="return confirm('هل أنت متأكد من حذف هذا الجدول؟')">
<i data-lucide="trash-2" style="width:14px;height:14px;"></i>
</button>
</form>
</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>
<!-- Metadata -->
<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="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">معلومات النظام</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:30%;">رقم السجل</td>
<td style="padding:6px 0;font-weight:600;">#<?= (int) $academy->id ?></td>
</tr>
<?php if ($academy->created_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:6px 0;"><?= e($academy->created_at) ?></td>
</tr>
<?php endif; ?>
<?php if ($academy->updated_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">آخر تحديث</td>
<td style="padding:6px 0;"><?= e($academy->updated_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
function toggleAddLevel() {
var form = document.getElementById('addLevelForm');
if (form.style.display === 'none') {
form.style.display = 'block';
} else {
form.style.display = 'none';
}
}
function toggleAddSchedule() {
var form = document.getElementById('addScheduleForm');
if (form.style.display === 'none') {
form.style.display = 'block';
} else {
form.style.display = 'none';
}
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Academies — Permissions only (sidebar owned by Disciplines bootstrap.php)
// ────────────────────────────────────────────────────────────
PermissionRegistry::register('academies', [
'academy.view' => ['ar' => 'عرض الأكاديميات', 'en' => 'View Academies'],
'academy.manage' => ['ar' => 'إدارة الأكاديميات', 'en' => 'Manage Academies'],
'academy.enroll' => ['ar' => 'تسجيل في الأكاديمية', 'en' => 'Enroll in Academy'],
]);
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\ActivitySubscriptions\Models\ActivitySubscription;
use App\Modules\ActivitySubscriptions\Models\ActivityPricing;
use App\Modules\ActivitySubscriptions\Services\ActivitySubscriptionService;
use App\Modules\ActivitySubscriptions\Services\ActivityPricingService;
use App\Modules\Payments\Services\PaymentService;
use App\Modules\Disciplines\Models\SportDiscipline;
class ActivitySubscriptionController extends Controller
{
/**
* List activity subscriptions with status tabs, month filter, search, and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => trim((string) $request->get('status', '')),
'subscription_month' => trim((string) $request->get('subscription_month', '')),
'discipline_id' => trim((string) $request->get('discipline_id', '')),
'player_type' => trim((string) $request->get('player_type', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = ActivitySubscription::search($filters, 25, $page);
return $this->view('ActivitySubscriptions.Views.index', [
'subscriptions' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => ActivitySubscription::getStatuses(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Show subscription detail with payment/exemption actions.
*/
public function show(Request $request, string $id): Response
{
$sub = ActivitySubscription::find((int) $id);
if (!$sub) {
return $this->redirect('/activity-subscriptions')->withError('الاشتراك غير موجود');
}
$db = App::getInstance()->db();
$player = $db->selectOne("SELECT * FROM `players` WHERE `id` = ?", [(int) $sub->player_id]);
$discipline = $sub->discipline_id ? SportDiscipline::find((int) $sub->discipline_id) : null;
return $this->view('ActivitySubscriptions.Views.show', [
'subscription' => $sub,
'player' => $player,
'discipline' => $discipline,
'statuses' => ActivitySubscription::getStatuses(),
]);
}
/**
* Process payment for a subscription.
*/
public function pay(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$sub = $db->selectOne(
"SELECT s.*, p.full_name_ar as player_name
FROM `activity_subscriptions` s
JOIN `players` p ON p.id = s.player_id
WHERE s.`id` = ?",
[(int) $id]
);
if (!$sub) {
return $this->redirect('/activity-subscriptions')->withError('الاشتراك غير موجود');
}
if ($sub['status'] === 'paid') {
return $this->redirect('/activity-subscriptions')->withError('الاشتراك مدفوع بالفعل');
}
$amount = (string) ($sub['total_amount'] ?? '0.00');
$data = $request->all();
$data['member_id'] = null;
$data['player_id'] = (int) $sub['player_id'];
$data['amount'] = $amount;
$data['payment_type'] = 'activity_subscription';
$data['payment_method'] = $data['payment_method'] ?? 'cash';
$data['related_entity_type'] = 'activity_subscriptions';
$data['related_entity_id'] = (int) $id;
$data['description'] = 'اشتراك نشاط ' . ($sub['subscription_month'] ?? '') . ' — ' . ($sub['player_name'] ?? '');
$result = PaymentService::processPayment($data);
if (!$result['success']) {
return $this->redirect('/activity-subscriptions/' . $id)->withError($result['error']);
}
ActivitySubscriptionService::paySubscription((int) $id, (int) $result['payment_id']);
return $this->redirect('/activity-subscriptions/' . $id)
->withSuccess('تم تسجيل الدفع بنجاح — إيصال: ' . $result['receipt_number']);
}
/**
* Grant an exemption on a subscription.
*/
public function exempt(Request $request, string $id): Response
{
$sub = ActivitySubscription::find((int) $id);
if (!$sub) {
return $this->redirect('/activity-subscriptions')->withError('الاشتراك غير موجود');
}
$reason = trim((string) $request->post('exemption_reason', ''));
if ($reason === '') {
return $this->redirect('/activity-subscriptions/' . $id)->withError('يجب إدخال سبب الإعفاء');
}
$employee = App::getInstance()->currentEmployee();
$exemptedBy = $employee ? (int) ($employee->id ?? ($employee['id'] ?? 0)) : 0;
ActivitySubscriptionService::grantExemption((int) $id, $exemptedBy, $reason);
return $this->redirect('/activity-subscriptions/' . $id)->withSuccess('تم إعفاء الاشتراك بنجاح');
}
/**
* Generate monthly subscriptions for a specified month.
*/
public function generate(Request $request): Response
{
$month = trim((string) $request->post('month', ''));
if ($month === '' || !preg_match('/^\d{4}-\d{2}$/', $month)) {
return $this->redirect('/activity-subscriptions')->withError('يجب تحديد شهر صحيح بصيغة YYYY-MM');
}
$count = ActivitySubscriptionService::generateMonthlySubscriptions($month);
return $this->redirect('/activity-subscriptions?subscription_month=' . urlencode($month))
->withSuccess('تم توليد ' . $count . ' اشتراك لشهر ' . $month);
}
/**
* List all pricing records with edit capability.
*/
public function pricing(Request $request): Response
{
$filters = [
'pricing_type' => trim((string) $request->get('pricing_type', '')),
'is_active' => $request->get('is_active', ''),
];
$page = max(1, (int) $request->get('page', 1));
$result = ActivityPricing::search($filters, 25, $page);
return $this->view('ActivitySubscriptions.Views.pricing', [
'pricings' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'pricingTypes' => ActivityPricing::getPricingTypes(),
]);
}
/**
* Update pricing rates.
*/
public function updatePricing(Request $request): Response
{
$pricingType = trim((string) $request->post('pricing_type', ''));
$referenceId = (int) $request->post('reference_id', 0);
if (!array_key_exists($pricingType, ActivityPricing::getPricingTypes())) {
return $this->redirect('/activity-subscriptions/pricing')->withError('نوع التسعير غير صالح');
}
if ($referenceId <= 0) {
return $this->redirect('/activity-subscriptions/pricing')->withError('المرجع غير صالح');
}
$rates = [
'member_rate' => trim((string) $request->post('member_rate', '0')),
'nonmember_rate' => trim((string) $request->post('nonmember_rate', '0')),
'member_rate_pm' => trim((string) $request->post('member_rate_pm', '0')),
'nonmember_rate_pm' => trim((string) $request->post('nonmember_rate_pm', '0')),
'effective_from' => trim((string) $request->post('effective_from', date('Y-m-d'))),
'effective_to' => $request->post('effective_to') ?: null,
];
ActivityPricingService::setRate($pricingType, $referenceId, $rates);
return $this->redirect('/activity-subscriptions/pricing')->withSuccess('تم تحديث التسعير بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Models;
use App\Core\Model;
use App\Core\App;
class ActivityPricing extends Model
{
protected static string $table = 'activity_pricing';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'pricing_type',
'reference_id',
'member_rate',
'nonmember_rate',
'member_rate_pm',
'nonmember_rate_pm',
'effective_from',
'effective_to',
'is_active',
'created_by',
];
/**
* Get all pricing types with Arabic labels.
*/
public static function getPricingTypes(): array
{
return [
'academy' => 'أكاديمية',
'discipline' => 'نشاط',
'facility' => 'مرفق',
];
}
/**
* Get the Arabic label for a pricing type key.
*/
public static function getPricingTypeLabel(string $type): string
{
$types = self::getPricingTypes();
return $types[$type] ?? $type;
}
/**
* Get the effective rate for a pricing type and reference.
*
* Finds the active pricing record where effective_from <= today
* and (effective_to IS NULL OR effective_to >= today), then returns
* the appropriate rate based on membership status and time tier.
*/
public static function getEffectiveRate(
string $type,
int $refId,
bool $isMember,
?string $timeTier = null
): float {
$today = date('Y-m-d');
$query = static::query()
->where('pricing_type', '=', $type)
->where('reference_id', '=', $refId)
->where('is_active', '=', 1)
->whereRaw('`effective_from` <= ?', [$today])
->whereRaw('(`effective_to` IS NULL OR `effective_to` >= ?)', [$today])
->orderBy('effective_from', 'DESC')
->limit(1);
$rows = $query->get();
$row = $rows[0] ?? null;
if (!$row) {
return 0.0;
}
if ($timeTier === 'PM') {
return (float) ($isMember ? $row['member_rate_pm'] : $row['nonmember_rate_pm']);
}
return (float) ($isMember ? $row['member_rate'] : $row['nonmember_rate']);
}
/**
* Search pricing records with filters and pagination.
*/
public static function search(array $filters = [], int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['pricing_type'])) {
$query = $query->where('pricing_type', '=', $filters['pricing_type']);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('pricing_type', 'ASC')->orderBy('reference_id', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Models;
use App\Core\Model;
use App\Core\App;
class ActivitySubscription extends Model
{
protected static string $table = 'activity_subscriptions';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'player_id',
'enrollment_id',
'discipline_id',
'subscription_month',
'player_type',
'base_rate',
'is_half_month',
'applied_rate',
'discount',
'total_amount',
'payment_id',
'status',
'due_date',
'paid_at',
'revoked_at',
'exempted_by',
'exemption_reason',
'notes',
'created_by',
];
/**
* Get all subscription statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'pending' => 'مستحق',
'paid' => 'مدفوع',
'overdue' => 'متأخر',
'exempted' => 'معفى',
'revoked' => 'ملغى',
];
}
/**
* Get the Arabic label for a status key.
*/
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a status.
*/
public static function getStatusColor(string $status): string
{
$colors = [
'pending' => '#D97706',
'paid' => '#059669',
'overdue' => '#DC2626',
'exempted' => '#7C3AED',
'revoked' => '#6B7280',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Search activity subscriptions with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query()
->leftJoin('players', '`activity_subscriptions`.`player_id` = `players`.`id`')
->leftJoin('sport_disciplines', '`activity_subscriptions`.`discipline_id` = `sport_disciplines`.`id`')
->select('`activity_subscriptions`.*, `players`.`full_name_ar` as player_name, `sport_disciplines`.`name_ar` as discipline_name');
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`players`.`full_name_ar` LIKE ? OR `players`.`code` LIKE ?)',
[$search, $search]
);
}
if (!empty($filters['status'])) {
$query = $query->where('status', '=', $filters['status']);
}
if (!empty($filters['subscription_month'])) {
$query = $query->where('subscription_month', '=', $filters['subscription_month']);
}
if (!empty($filters['discipline_id'])) {
$query = $query->where('discipline_id', '=', (int) $filters['discipline_id']);
}
if (!empty($filters['player_type'])) {
$query = $query->where('player_type', '=', $filters['player_type']);
}
$query = $query->orderBy('subscription_month', 'DESC')->orderBy('player_name', 'ASC');
return $query->paginate($perPage, $page);
}
/**
* Get all subscriptions for a specific player, ordered by month descending.
*/
public static function getForPlayer(int $playerId): array
{
return static::query()
->leftJoin('sport_disciplines', '`activity_subscriptions`.`discipline_id` = `sport_disciplines`.`id`')
->select('`activity_subscriptions`.*, `sport_disciplines`.`name_ar` as discipline_name')
->where('player_id', '=', $playerId)
->orderBy('subscription_month', 'DESC')
->get();
}
}
<?php
declare(strict_types=1);
return [
['GET', '/activity-subscriptions', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@index', ['auth'], 'activity_sub.view'],
['GET', '/activity-subscriptions/pricing', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@pricing', ['auth'], 'activity_sub.manage_pricing'],
['POST', '/activity-subscriptions/pricing', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@updatePricing', ['auth', 'csrf'], 'activity_sub.manage_pricing'],
['POST', '/activity-subscriptions/generate', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@generate', ['auth', 'csrf'], 'activity_sub.generate'],
['GET', '/activity-subscriptions/{id:\d+}', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@show', ['auth'], 'activity_sub.view'],
['POST', '/activity-subscriptions/{id:\d+}/pay', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@pay', ['auth', 'csrf'], 'activity_sub.collect'],
['POST', '/activity-subscriptions/{id:\d+}/exempt', 'ActivitySubscriptions\Controllers\ActivitySubscriptionController@exempt', ['auth', 'csrf'], 'activity_sub.exempt'],
];
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Services;
use App\Core\App;
use App\Modules\ActivitySubscriptions\Models\ActivityPricing;
class ActivityPricingService
{
/**
* Get the rate pair (member / nonmember) for a pricing type and reference.
*
* @return array{member_rate: string, nonmember_rate: string}
*/
public static function getRate(string $pricingType, int $referenceId): array
{
$today = date('Y-m-d');
$row = ActivityPricing::query()
->where('pricing_type', '=', $pricingType)
->where('reference_id', '=', $referenceId)
->where('is_active', '=', 1)
->whereRaw('`effective_from` <= ?', [$today])
->whereRaw('(`effective_to` IS NULL OR `effective_to` >= ?)', [$today])
->orderBy('effective_from', 'DESC')
->first();
if (!$row) {
return ['member_rate' => '0.00', 'nonmember_rate' => '0.00'];
}
return [
'member_rate' => (string) ($row['member_rate'] ?? '0.00'),
'nonmember_rate' => (string) ($row['nonmember_rate'] ?? '0.00'),
];
}
/**
* Get the effective rate for a specific pricing type, reference, membership, and time tier.
*/
public static function getEffectiveRate(
string $pricingType,
int $referenceId,
bool $isMember,
?string $timeTier = 'AM'
): float {
return ActivityPricing::getEffectiveRate($pricingType, $referenceId, $isMember, $timeTier);
}
/**
* Upsert a pricing record.
*
* If an active record exists for the same type + reference, deactivates it
* and creates a new one.
*
* @param array $rates Keys: member_rate, nonmember_rate, member_rate_pm, nonmember_rate_pm, effective_from, effective_to
*/
public static function setRate(string $pricingType, int $referenceId, array $rates): object
{
$db = App::getInstance()->db();
// Deactivate any current active pricing for this type + reference
$db->query(
"UPDATE `activity_pricing` SET `is_active` = 0, `updated_at` = ? WHERE `pricing_type` = ? AND `reference_id` = ? AND `is_active` = 1",
[date('Y-m-d H:i:s'), $pricingType, $referenceId]
);
$employee = App::getInstance()->currentEmployee();
return ActivityPricing::create([
'pricing_type' => $pricingType,
'reference_id' => $referenceId,
'member_rate' => $rates['member_rate'] ?? '0.00',
'nonmember_rate' => $rates['nonmember_rate'] ?? '0.00',
'member_rate_pm' => $rates['member_rate_pm'] ?? '0.00',
'nonmember_rate_pm' => $rates['nonmember_rate_pm'] ?? '0.00',
'effective_from' => $rates['effective_from'] ?? date('Y-m-d'),
'effective_to' => $rates['effective_to'] ?? null,
'is_active' => 1,
'created_by' => $employee ? (int) ($employee->id ?? ($employee['id'] ?? null)) : null,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\ActivitySubscriptions\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\ActivitySubscriptions\Models\ActivitySubscription;
use App\Modules\ActivitySubscriptions\Models\ActivityPricing;
class ActivitySubscriptionService
{
/**
* Generate monthly subscriptions for all active enrollments in the given month.
*
* For each active enrollment, creates a subscription record. If the enrollment
* started after the 15th of its first month, marks it as half-month with half rate.
*
* @param string $month Format YYYY-MM
* @return int Number of subscriptions generated
*/
public static function generateMonthlySubscriptions(string $month): int
{
$db = App::getInstance()->db();
$count = 0;
// Get all active enrollments
$enrollments = $db->select(
"SELECT e.*, p.player_type, a.discipline_id
FROM `enrollments` e
JOIN `players` p ON p.id = e.player_id
JOIN `academy_levels` al ON al.id = e.level_id
JOIN `academies` a ON a.id = al.academy_id
WHERE e.`status` = 'active'
AND (e.`end_date` IS NULL OR e.`end_date` >= ?)
AND e.`start_date` <= ?",
[$month . '-01', $month . '-28']
);
foreach ($enrollments as $enrollment) {
$playerId = (int) $enrollment['player_id'];
$enrollmentId = (int) $enrollment['id'];
$disciplineId = (int) ($enrollment['discipline_id'] ?? 0);
// Skip if subscription already exists for this enrollment + month
$existing = ActivitySubscription::query()
->where('enrollment_id', '=', $enrollmentId)
->where('subscription_month', '=', $month)
->first();
if ($existing) {
continue;
}
// Check if this is the first month and enrollment started after the 15th
$enrollmentDay = (int) date('d', strtotime($enrollment['start_date'] ?? ''));
$enrollmentMonth = date('Y-m', strtotime($enrollment['start_date'] ?? ''));
$isHalfMonth = ($enrollmentMonth === $month && $enrollmentDay > 15);
// Calculate rate
$rate = self::calculateRate($enrollmentId, $isHalfMonth);
$baseRate = $rate['base'];
$appliedRate = $rate['applied'];
// Due date is the 7th of the subscription month
$dueDate = $month . '-07';
$sub = ActivitySubscription::create([
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'discipline_id' => $disciplineId,
'subscription_month' => $month,
'player_type' => $enrollment['player_type'] ?? 'member',
'base_rate' => $baseRate,
'is_half_month' => $isHalfMonth ? 1 : 0,
'applied_rate' => $appliedRate,
'discount' => bcsub((string) $baseRate, (string) $appliedRate, 2),
'total_amount' => $appliedRate,
'status' => 'pending',
'due_date' => $dueDate,
]);
EventBus::dispatch('activity_sub.generated', [
'subscription_id' => (int) $sub->id,
'player_id' => $playerId,
'month' => $month,
]);
$count++;
}
return $count;
}
/**
* Calculate the rate for an enrollment.
*
* Looks up the enrollment's academy pricing and returns base and applied rates.
*
* @return array{base: string, applied: string}
*/
public static function calculateRate(int $enrollmentId, bool $isHalfMonth = false): array
{
$db = App::getInstance()->db();
$enrollment = $db->selectOne(
"SELECT e.*, p.player_type, a.id as academy_id, a.discipline_id
FROM `enrollments` e
JOIN `players` p ON p.id = e.player_id
JOIN `academy_levels` al ON al.id = e.level_id
JOIN `academies` a ON a.id = al.academy_id
WHERE e.`id` = ?",
[$enrollmentId]
);
if (!$enrollment) {
return ['base' => '0.00', 'applied' => '0.00'];
}
$isMember = ($enrollment['player_type'] ?? '') === 'member';
$academyId = (int) ($enrollment['academy_id'] ?? 0);
// Try academy pricing first, then discipline pricing
$rate = ActivityPricing::getEffectiveRate('academy', $academyId, $isMember);
if ($rate <= 0 && !empty($enrollment['discipline_id'])) {
$rate = ActivityPricing::getEffectiveRate('discipline', (int) $enrollment['discipline_id'], $isMember);
}
$baseRate = number_format($rate, 2, '.', '');
$appliedRate = $isHalfMonth
? number_format($rate / 2, 2, '.', '')
: $baseRate;
return ['base' => $baseRate, 'applied' => $appliedRate];
}
/**
* Mark a subscription as paid.
*/
public static function paySubscription(int $subscriptionId, int $paymentId): bool
{
$sub = ActivitySubscription::find($subscriptionId);
if (!$sub) {
return false;
}
$sub->update([
'status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
'payment_id' => $paymentId,
]);
EventBus::dispatch('activity_sub.paid', [
'subscription_id' => $subscriptionId,
'player_id' => (int) $sub->player_id,
'payment_id' => $paymentId,
]);
return true;
}
/**
* Revoke overdue subscriptions for a given month.
*
* Finds subscriptions where status is 'pending' or 'overdue' and
* due_date + 7 days < today, then sets them to 'revoked'.
*/
public static function revokeOverdue(string $month): int
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
$count = 0;
$rows = ActivitySubscription::query()
->where('subscription_month', '=', $month)
->whereRaw("`status` IN ('pending', 'overdue')")
->whereRaw('DATE_ADD(`due_date`, INTERVAL 7 DAY) < ?', [$today])
->get();
foreach ($rows as $row) {
$sub = ActivitySubscription::find((int) $row['id']);
if (!$sub) {
continue;
}
$sub->update([
'status' => 'revoked',
'revoked_at' => date('Y-m-d H:i:s'),
]);
EventBus::dispatch('activity_sub.revoked', [
'subscription_id' => (int) $row['id'],
'player_id' => (int) $row['player_id'],
'month' => $month,
]);
$count++;
}
return $count;
}
/**
* Grant an exemption on a subscription.
*/
public static function grantExemption(int $subscriptionId, int $exemptedBy, string $reason): bool
{
$sub = ActivitySubscription::find($subscriptionId);
if (!$sub) {
return false;
}
$sub->update([
'status' => 'exempted',
'exempted_by' => $exemptedBy,
'exemption_reason' => $reason,
]);
EventBus::dispatch('activity_sub.exempted', [
'subscription_id' => $subscriptionId,
'player_id' => (int) $sub->player_id,
'exempted_by' => $exemptedBy,
]);
return true;
}
}
<?php
use App\Modules\ActivitySubscriptions\Models\ActivitySubscription;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>اشتراكات الأنشطة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<form method="POST" action="/activity-subscriptions/generate" style="display:inline-flex;gap:6px;align-items:center;">
<?= csrf_field() ?>
<input type="month" name="month" value="<?= e(date('Y-m')) ?>" class="form-input" style="width:160px;font-size:13px;">
<button type="submit" class="btn btn-outline" onclick="return confirm('هل تريد توليد الاشتراكات لهذا الشهر؟')">
<i data-lucide="refresh-cw" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> توليد
</button>
</form>
<a href="/activity-subscriptions/pricing" class="btn btn-outline"><i data-lucide="settings" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> التسعير</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentStatus = $filters['status'] ?? '';
$allStatuses = ActivitySubscription::getStatuses();
?>
<!-- Status Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/activity-subscriptions?<?= http_build_query(array_merge($filters, ['status' => '', 'page' => 1])) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allStatuses as $sKey => $sLabel): ?>
<a href="/activity-subscriptions?<?= http_build_query(array_merge($filters, ['status' => $sKey, 'page' => 1])) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === $sKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === $sKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($sLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- 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;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="اسم اللاعب أو الكود..." class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">الشهر</label>
<input type="month" name="subscription_month" value="<?= e($filters['subscription_month'] ?? '') ?>" class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">النشاط</label>
<select name="discipline_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentStatus !== ''): ?>
<input type="hidden" name="status" value="<?= e($currentStatus) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/activity-subscriptions" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Subscriptions Table -->
<?php if (!empty($subscriptions)): ?>
<div class="card" style="padding:0;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">اللاعب</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الشهر</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">النشاط</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">نوع اللاعب</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">السعر الأساسي</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">السعر المطبق</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الخصم</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الإجمالي</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($subscriptions as $s):
$statusColor = ActivitySubscription::getStatusColor($s['status'] ?? 'pending');
$statusLabel = ActivitySubscription::getStatusLabel($s['status'] ?? 'pending');
$canPay = in_array($s['status'] ?? '', ['pending', 'overdue']);
$canExempt = in_array($s['status'] ?? '', ['pending', 'overdue']);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;">
<div style="font-weight:600;"><?= e($s['player_name'] ?? '—') ?></div>
</td>
<td style="padding:12px 16px;"><?= e($s['subscription_month'] ?? '') ?></td>
<td style="padding:12px 16px;"><?= e($s['discipline_name'] ?? '—') ?></td>
<td style="padding:12px 16px;"><?= e($s['player_type'] ?? '') ?></td>
<td style="padding:12px 16px;"><?= money($s['base_rate'] ?? 0) ?></td>
<td style="padding:12px 16px;"><?= money($s['applied_rate'] ?? 0) ?></td>
<td style="padding:12px 16px;">
<?php if (((float) ($s['discount'] ?? 0)) > 0): ?>
<span style="color:#059669;font-weight:600;"><?= money($s['discount'] ?? 0) ?>-</span>
<?php else: ?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
<td style="padding:12px 16px;font-weight:700;"><?= money($s['total_amount'] ?? 0) ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= $statusColor ?>15;color:<?= $statusColor ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($statusLabel) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<div style="display:flex;gap:4px;justify-content:center;">
<a href="/activity-subscriptions/<?= (int) $s['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i>
</a>
<?php if ($canPay): ?>
<form method="POST" action="/activity-subscriptions/<?= (int) $s['id'] ?>/pay" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-primary" style="font-size:12px;padding:4px 10px;" onclick="return confirm('تأكيد الدفع؟')">
<i data-lucide="credit-card" style="width:13px;height:13px;vertical-align:middle;"></i> دفع
</button>
</form>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="receipt" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد اشتراكات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['subscription_month']) || !empty($filters['discipline_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
لم يتم توليد اشتراكات بعد. استخدم زر "توليد" لإنشاء اشتراكات الشهر.
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\ActivitySubscriptions\Models\ActivityPricing;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تسعير الأنشطة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/activity-subscriptions" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للاشتراكات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filter Bar -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/activity-subscriptions/pricing" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;">
<div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">نوع التسعير</label>
<select name="pricing_type" class="form-input">
<option value="">الكل</option>
<?php foreach ($pricingTypes as $ptKey => $ptLabel): ?>
<option value="<?= e($ptKey) ?>" <?= ($filters['pricing_type'] ?? '') === $ptKey ? 'selected' : '' ?>><?= e($ptLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="is_active" class="form-input">
<option value="" <?= ($filters['is_active'] ?? '') === '' ? 'selected' : '' ?>>الكل</option>
<option value="1" <?= ($filters['is_active'] ?? '') === '1' ? 'selected' : '' ?>>فعّال</option>
<option value="0" <?= ($filters['is_active'] ?? '') === '0' ? 'selected' : '' ?>>غير فعّال</option>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/activity-subscriptions/pricing" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Pricing Table -->
<?php if (!empty($pricings)): ?>
<div class="card" style="padding:0;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;" id="pricing-table">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">النوع</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">المرجع</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">عضو</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">غير عضو</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">عضو (م)</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">غير عضو (م)</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">من</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">إلى</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($pricings as $idx => $p):
$typeLabel = ActivityPricing::getPricingTypeLabel($p['pricing_type'] ?? '');
$isActive = (int) ($p['is_active'] ?? 0);
?>
<tr style="border-bottom:1px solid #F3F4F6;" id="row-<?= $idx ?>">
<td style="padding:12px 16px;">
<span style="background:#0D737715;color:#0D7377;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($typeLabel) ?>
</span>
</td>
<td style="padding:12px 16px;font-weight:600;">#<?= (int) ($p['reference_id'] ?? 0) ?></td>
<td style="padding:12px 16px;"><?= money($p['member_rate'] ?? 0) ?></td>
<td style="padding:12px 16px;"><?= money($p['nonmember_rate'] ?? 0) ?></td>
<td style="padding:12px 16px;"><?= money($p['member_rate_pm'] ?? 0) ?></td>
<td style="padding:12px 16px;"><?= money($p['nonmember_rate_pm'] ?? 0) ?></td>
<td style="padding:12px 16px;font-size:13px;"><?= e($p['effective_from'] ?? '—') ?></td>
<td style="padding:12px 16px;font-size:13px;"><?= e($p['effective_to'] ?? '—') ?></td>
<td style="padding:12px 16px;text-align:center;">
<?php if ($isActive): ?>
<span class="badge" style="background:#05966915;color:#059669;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">فعّال</span>
<?php else: ?>
<span class="badge" style="background:#6B728015;color:#6B7280;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">غير فعّال</span>
<?php endif; ?>
</td>
<td style="padding:12px 16px;text-align:center;">
<button type="button" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;" onclick="toggleEditRow(<?= $idx ?>)">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</button>
</td>
</tr>
<!-- Inline Edit Row -->
<tr id="edit-row-<?= $idx ?>" style="display:none;background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<td colspan="10" style="padding:20px;">
<form method="POST" action="/activity-subscriptions/pricing" style="display:flex;flex-wrap:wrap;gap:12px;align-items:end;">
<?= csrf_field() ?>
<input type="hidden" name="pricing_type" value="<?= e($p['pricing_type'] ?? '') ?>">
<input type="hidden" name="reference_id" value="<?= (int) ($p['reference_id'] ?? 0) ?>">
<div style="flex:1;min-width:120px;">
<label class="form-label" style="font-size:11px;">سعر العضو</label>
<input type="number" step="0.01" min="0" name="member_rate" value="<?= e($p['member_rate'] ?? '0') ?>" class="form-input" style="font-size:13px;">
</div>
<div style="flex:1;min-width:120px;">
<label class="form-label" style="font-size:11px;">سعر غير العضو</label>
<input type="number" step="0.01" min="0" name="nonmember_rate" value="<?= e($p['nonmember_rate'] ?? '0') ?>" class="form-input" style="font-size:13px;">
</div>
<div style="flex:1;min-width:120px;">
<label class="form-label" style="font-size:11px;">عضو (مسائي)</label>
<input type="number" step="0.01" min="0" name="member_rate_pm" value="<?= e($p['member_rate_pm'] ?? '0') ?>" class="form-input" style="font-size:13px;">
</div>
<div style="flex:1;min-width:120px;">
<label class="form-label" style="font-size:11px;">غير عضو (مسائي)</label>
<input type="number" step="0.01" min="0" name="nonmember_rate_pm" value="<?= e($p['nonmember_rate_pm'] ?? '0') ?>" class="form-input" style="font-size:13px;">
</div>
<div style="flex:1;min-width:130px;">
<label class="form-label" style="font-size:11px;">سريان من</label>
<input type="date" name="effective_from" value="<?= e($p['effective_from'] ?? date('Y-m-d')) ?>" class="form-input" style="font-size:13px;">
</div>
<div style="flex:1;min-width:130px;">
<label class="form-label" style="font-size:11px;">سريان إلى</label>
<input type="date" name="effective_to" value="<?= e($p['effective_to'] ?? '') ?>" class="form-input" style="font-size:13px;" placeholder="مفتوح">
</div>
<div style="display:flex;gap:6px;">
<button type="submit" class="btn btn-sm btn-primary" style="font-size:12px;">
<i data-lucide="check" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> حفظ
</button>
<button type="button" class="btn btn-sm btn-outline" style="font-size:12px;color:#6B7280;" onclick="toggleEditRow(<?= $idx ?>)">
إلغاء
</button>
</div>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="settings" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد سجلات تسعير</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">
<?php if (!empty($filters['pricing_type']) || ($filters['is_active'] ?? '') !== ''): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير البحث.
<?php else: ?>
لم يتم إعداد تسعير بعد.
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
function toggleEditRow(idx) {
var editRow = document.getElementById('edit-row-' + idx);
if (!editRow) return;
if (editRow.style.display === 'none' || editRow.style.display === '') {
// Close all other edit rows first
var allEditRows = document.querySelectorAll('[id^="edit-row-"]');
for (var i = 0; i < allEditRows.length; i++) {
allEditRows[i].style.display = 'none';
}
editRow.style.display = 'table-row';
} else {
editRow.style.display = 'none';
}
}
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\ActivitySubscriptions\Models\ActivitySubscription;
$__template->layout('Layout.main');
$status = $subscription->status ?? 'pending';
$statusColor = ActivitySubscription::getStatusColor($status);
$statusLabel = ActivitySubscription::getStatusLabel($status);
$isHalf = (int) ($subscription->is_half_month ?? 0);
$canAct = in_array($status, ['pending', 'overdue']);
$playerType = ($subscription->player_type === 'member') ? 'عضو' : 'غير عضو';
?>
<?php $__template->section('title'); ?>اشتراك #<?= (int) $subscription->id ?><?= e($subscription->subscription_month) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/activity-subscriptions" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<div style="padding:25px;display:flex;align-items:start;gap:20px;flex-wrap:wrap;">
<div style="width:64px;height:64px;border-radius:14px;background:linear-gradient(135deg, <?= $statusColor ?>15, <?= $statusColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="receipt" style="width:32px;height:32px;color:<?= $statusColor ?>;"></i>
</div>
<div style="flex:1;min-width:200px;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;">
<h2 style="margin:0;font-size:22px;font-weight:700;color:#1A1A2E;">اشتراك #<?= (int) $subscription->id ?></h2>
<span class="badge" style="background:<?= $statusColor ?>15;color:<?= $statusColor ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($statusLabel) ?>
</span>
<?php if ($isHalf): ?>
<span class="badge" style="background:#D9770615;color:#D97706;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<i data-lucide="clock" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> نصف شهر
</span>
<?php endif; ?>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap;font-size:14px;color:#6B7280;">
<span style="display:inline-flex;align-items:center;gap:5px;">
<i data-lucide="user" style="width:15px;height:15px;"></i>
<strong style="color:#374151;"><?= e($player['full_name_ar'] ?? '—') ?></strong>
</span>
<span style="display:inline-flex;align-items:center;gap:5px;">
<i data-lucide="calendar" style="width:15px;height:15px;"></i>
<?= e($subscription->subscription_month) ?>
</span>
<?php if ($discipline): ?>
<span style="display:inline-flex;align-items:center;gap:5px;">
<i data-lucide="activity" style="width:15px;height:15px;"></i>
<?= e($discipline->name_ar) ?>
</span>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Financial Info -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(150px, 1fr));gap:15px;margin-bottom:20px;">
<div class="card" style="padding:18px;text-align:center;border-right:4px solid #0D7377;">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">السعر الأساسي</div>
<div style="font-size:20px;font-weight:700;color:#0D7377;"><?= money($subscription->base_rate ?? 0) ?></div>
</div>
<?php if (((float) ($subscription->applied_rate ?? 0)) !== ((float) ($subscription->base_rate ?? 0))): ?>
<div class="card" style="padding:18px;text-align:center;border-right:4px solid #D97706;">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">السعر المطبق <?= $isHalf ? '(نصف شهر)' : '' ?></div>
<div style="font-size:20px;font-weight:700;color:#D97706;"><?= money($subscription->applied_rate ?? 0) ?></div>
</div>
<?php endif; ?>
<div class="card" style="padding:18px;text-align:center;border-right:4px solid #059669;">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">الخصم</div>
<div style="font-size:20px;font-weight:700;color:<?= ((float) ($subscription->discount ?? 0)) > 0 ? '#059669' : '#9CA3AF' ?>;">
<?php if (((float) ($subscription->discount ?? 0)) > 0): ?>
<?= money($subscription->discount ?? 0) ?>-
<?php else: ?>
<?php endif; ?>
</div>
</div>
<div class="card" style="padding:18px;text-align:center;border-right:4px solid #1A1A2E;background:linear-gradient(135deg, #F9FAFB, #F3F4F6);">
<div style="font-size:11px;color:#6B7280;margin-bottom:6px;">الإجمالي</div>
<div style="font-size:26px;font-weight:800;color:#1A1A2E;"><?= money($subscription->total_amount ?? 0) ?></div>
</div>
</div>
<!-- Info Grid -->
<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="info" 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;gap:0;">
<!-- Player Type -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">نوع اللاعب</div>
<div style="font-size:14px;font-weight:600;color:#374151;"><?= e($playerType) ?></div>
</div>
<!-- Due Date -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">تاريخ الاستحقاق</div>
<div style="font-size:14px;font-weight:600;color:#374151;"><?= e($subscription->due_date ?? '—') ?></div>
</div>
<?php if ($status === 'paid' && $subscription->paid_at): ?>
<!-- Paid At -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">تاريخ الدفع</div>
<div style="font-size:14px;font-weight:600;color:#059669;"><?= e($subscription->paid_at) ?></div>
</div>
<?php endif; ?>
<?php if ($status === 'revoked' && $subscription->revoked_at): ?>
<!-- Revoked At -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">تاريخ الإلغاء</div>
<div style="font-size:14px;font-weight:600;color:#6B7280;"><?= e($subscription->revoked_at) ?></div>
</div>
<?php endif; ?>
<?php if ($status === 'exempted'): ?>
<!-- Exempted By -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">أعفي بواسطة</div>
<div style="font-size:14px;font-weight:600;color:#7C3AED;"><?= e($subscription->exempted_by ?? '—') ?></div>
</div>
<!-- Exemption Reason -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">سبب الإعفاء</div>
<div style="font-size:14px;font-weight:600;color:#7C3AED;"><?= e($subscription->exemption_reason ?? '—') ?></div>
</div>
<?php endif; ?>
<?php if ($subscription->payment_id): ?>
<!-- Payment ID -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">رقم عملية الدفع</div>
<div style="font-size:14px;font-weight:600;color:#374151;">#<?= (int) $subscription->payment_id ?></div>
</div>
<?php endif; ?>
<!-- Enrollment ID -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">رقم التسجيل</div>
<div style="font-size:14px;font-weight:600;color:#374151;"><?= $subscription->enrollment_id ? '#' . (int) $subscription->enrollment_id : '—' ?></div>
</div>
<!-- Notes -->
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;">
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:14px;color:#374151;"><?= e($subscription->notes ?? '') ?: '<span style="color:#9CA3AF;">—</span>' ?></div>
</div>
</div>
</div>
</div>
<?php if ($canAct): ?>
<!-- Actions Section -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Pay Form -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="credit-card" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">تسجيل الدفع</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/activity-subscriptions/<?= (int) $subscription->id ?>/pay">
<?= csrf_field() ?>
<div style="margin-bottom:16px;">
<label class="form-label" style="font-size:13px;display:block;margin-bottom:6px;">طريقة الدفع</label>
<select name="payment_method" class="form-input" style="width:100%;">
<option value="cash">نقدي</option>
<option value="card">بطاقة</option>
<option value="transfer">تحويل</option>
</select>
</div>
<div style="background:#F0FDF4;border-radius:8px;padding:12px;margin-bottom:16px;text-align:center;">
<div style="font-size:12px;color:#6B7280;">المبلغ المستحق</div>
<div style="font-size:22px;font-weight:800;color:#059669;"><?= money($subscription->total_amount ?? 0) ?></div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;" onclick="return confirm('تأكيد دفع <?= money($subscription->total_amount ?? 0) ?>؟')">
<i data-lucide="check-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i> تأكيد الدفع
</button>
</form>
</div>
</div>
<!-- Exempt Form -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="shield-check" style="width:18px;height:18px;color:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">إعفاء من الاشتراك</h3>
</div>
<div style="padding:20px;">
<form method="POST" action="/activity-subscriptions/<?= (int) $subscription->id ?>/exempt">
<?= csrf_field() ?>
<div style="margin-bottom:16px;">
<label class="form-label" style="font-size:13px;display:block;margin-bottom:6px;">سبب الإعفاء <span style="color:#DC2626;">*</span></label>
<textarea name="exemption_reason" class="form-input" rows="4" style="width:100%;resize:vertical;" placeholder="أدخل سبب الإعفاء..." required></textarea>
</div>
<button type="submit" class="btn btn-outline" style="width:100%;color:#7C3AED;border-color:#7C3AED;" onclick="return confirm('هل أنت متأكد من إعفاء هذا الاشتراك؟')">
<i data-lucide="shield-check" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;"></i> تأكيد الإعفاء
</button>
</form>
</div>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
PermissionRegistry::register('activity_subscriptions', [
'activity_sub.view' => ['ar' => 'عرض اشتراكات الأنشطة', 'en' => 'View Activity Subscriptions'],
'activity_sub.collect' => ['ar' => 'تحصيل اشتراكات الأنشطة', 'en' => 'Collect Activity Subscriptions'],
'activity_sub.exempt' => ['ar' => 'إعفاء اشتراكات الأنشطة', 'en' => 'Exempt Activity Subscriptions'],
'activity_sub.generate' => ['ar' => 'توليد اشتراكات الأنشطة', 'en' => 'Generate Activity Subscriptions'],
'activity_sub.manage_pricing' => ['ar' => 'إدارة تسعير الأنشطة', 'en' => 'Manage Activity Pricing'],
]);
<?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;
use App\Modules\Disciplines\Models\SportDiscipline;
class DisciplineController extends Controller
{
/**
* List all disciplines with category filter tabs and search.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'category' => trim((string) $request->get('category', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = SportDiscipline::search($filters, 25, $page);
return $this->view('Disciplines.Views.index', [
'disciplines' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'categories' => SportDiscipline::getCategories(),
]);
}
/**
* Show the create discipline form.
*/
public function create(Request $request): Response
{
return $this->view('Disciplines.Views.create', [
'categories' => SportDiscipline::getCategories(),
]);
}
/**
* Validate and store a new discipline.
*/
public function store(Request $request): Response
{
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$category = trim((string) $request->post('category', ''));
$icon = trim((string) $request->post('icon', 'activity'));
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود النشاط مطلوب';
}
if (!array_key_exists($category, SportDiscipline::getCategories())) {
$errors[] = 'فئة النشاط غير صالحة';
}
// Check unique code
if ($code !== '') {
$existing = SportDiscipline::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود النشاط مستخدم بالفعل';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/disciplines/create');
}
// Build config_json
$config = $this->buildConfigFromRequest($request);
$discipline = SportDiscipline::create([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'category' => $category,
'icon' => $icon,
'description_ar' => $descriptionAr ?: null,
'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE),
'sort_order' => $sortOrder,
'is_active' => 1,
]);
return $this->redirect('/disciplines/' . $discipline->id)->withSuccess('تم إضافة النشاط الرياضي بنجاح');
}
/**
* Show discipline detail page.
*/
public function show(Request $request, string $id): Response
{
$discipline = SportDiscipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/disciplines')->withError('النشاط الرياضي غير موجود');
}
return $this->view('Disciplines.Views.show', [
'discipline' => $discipline,
'config' => $discipline->getConfig(),
'ageGroups' => $discipline->getAgeGroups(),
'skillLevels' => $discipline->getSkillLevels(),
'categories' => SportDiscipline::getCategories(),
]);
}
/**
* Show edit form for a discipline.
*/
public function edit(Request $request, string $id): Response
{
$discipline = SportDiscipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/disciplines')->withError('النشاط الرياضي غير موجود');
}
return $this->view('Disciplines.Views.edit', [
'discipline' => $discipline,
'config' => $discipline->getConfig(),
'categories' => SportDiscipline::getCategories(),
]);
}
/**
* Validate and update an existing discipline.
*/
public function update(Request $request, string $id): Response
{
$discipline = SportDiscipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/disciplines')->withError('النشاط الرياضي غير موجود');
}
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$category = trim((string) $request->post('category', ''));
$icon = trim((string) $request->post('icon', 'activity'));
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود النشاط مطلوب';
}
if (!array_key_exists($category, SportDiscipline::getCategories())) {
$errors[] = 'فئة النشاط غير صالحة';
}
// Check unique code (exclude current)
if ($code !== '') {
$existing = SportDiscipline::query()
->where('code', '=', $code)
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود النشاط مستخدم بالفعل';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/disciplines/' . $id . '/edit');
}
// Build config_json
$config = $this->buildConfigFromRequest($request);
$discipline->update([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'category' => $category,
'icon' => $icon,
'description_ar' => $descriptionAr ?: null,
'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE),
'sort_order' => $sortOrder,
]);
return $this->redirect('/disciplines/' . $id)->withSuccess('تم تحديث النشاط الرياضي بنجاح');
}
/**
* Toggle the is_active status of a discipline.
*/
public function toggle(Request $request, string $id): Response
{
$discipline = SportDiscipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/disciplines')->withError('النشاط الرياضي غير موجود');
}
$newStatus = $discipline->is_active ? 0 : 1;
$discipline->update(['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل النشاط الرياضي' : 'تم إيقاف النشاط الرياضي';
return $this->redirect('/disciplines/' . $id)->withSuccess($message);
}
/**
* Build the config array from the request (age groups, skill levels, etc.).
*/
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;
}
$guardianConsentAge = $request->post('requires_guardian_consent_under');
if ($guardianConsentAge !== null && $guardianConsentAge !== '') {
$config['requires_guardian_consent_under'] = (int) $guardianConsentAge;
}
$maxPlayers = $request->post('max_players_per_session');
if ($maxPlayers !== null && $maxPlayers !== '') {
$config['max_players_per_session'] = (int) $maxPlayers;
}
return $config;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Disciplines\Models;
use App\Core\Model;
use App\Core\App;
class SportDiscipline extends Model
{
protected static string $table = 'sport_disciplines';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'category',
'icon',
'description_ar',
'config_json',
'sort_order',
'is_active',
];
/**
* Decode config_json into an associative array.
*/
public function getConfig(): array
{
$raw = $this->config_json;
if (empty($raw)) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Extract age groups from the config.
*/
public function getAgeGroups(): array
{
$config = $this->getConfig();
return $config['age_groups'] ?? [];
}
/**
* Extract skill levels from the config.
*/
public function getSkillLevels(): array
{
$config = $this->getConfig();
return $config['skill_levels'] ?? [];
}
/**
* Get all discipline categories with Arabic labels.
*/
public static function getCategories(): array
{
return [
'individual' => 'الفردية',
'team' => 'الجماعية',
'racket' => 'الراكيت',
'leisure' => 'الترفيهية',
'emerging' => 'الناشئة',
];
}
/**
* Get the Arabic label for a category key.
*/
public static function getCategoryLabel(string $category): string
{
$categories = self::getCategories();
return $categories[$category] ?? $category;
}
/**
* Get the badge color for a category.
*/
public static function getCategoryColor(string $category): string
{
$colors = [
'individual' => '#0D7377',
'team' => '#0284C7',
'racket' => '#7C3AED',
'leisure' => '#D97706',
'emerging' => '#059669',
];
return $colors[$category] ?? '#6B7280';
}
/**
* Get all active disciplines ordered by sort_order.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get disciplines by category.
*/
public static function getByCategory(string $category): array
{
return static::query()
->where('is_active', '=', 1)
->where('category', '=', $category)
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Search disciplines with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`name_ar` LIKE ? OR `name_en` LIKE ? OR `code` LIKE ?)',
[$search, $search, $search]
);
}
if (!empty($filters['category'])) {
$query = $query->where('category', '=', $filters['category']);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('sort_order', 'ASC')->orderBy('name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
return [
['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'],
['GET', '/disciplines/{id:\d+}', 'Disciplines\Controllers\DisciplineController@show', ['auth'], 'discipline.view'],
['GET', '/disciplines/{id:\d+}/edit', 'Disciplines\Controllers\DisciplineController@edit', ['auth'], 'discipline.manage'],
['POST', '/disciplines/{id:\d+}', 'Disciplines\Controllers\DisciplineController@update', ['auth', 'csrf'], 'discipline.manage'],
['POST', '/disciplines/{id:\d+}/toggle','Disciplines\Controllers\DisciplineController@toggle', ['auth', 'csrf'], 'discipline.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Disciplines\Services;
use App\Modules\Disciplines\Models\SportDiscipline;
class DisciplineService
{
/**
* Get all active disciplines.
*/
public static function getActive(): array
{
return SportDiscipline::allActive();
}
/**
* Get active disciplines filtered by category.
*/
public static function getByCategory(string $category): array
{
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.
*/
public static function getGroupedByCategory(): array
{
$disciplines = SportDiscipline::allActive();
$grouped = [];
foreach (SportDiscipline::getCategories() as $key => $label) {
$grouped[$key] = [];
}
foreach ($disciplines as $d) {
$cat = $d['category'] ?? 'individual';
if (!isset($grouped[$cat])) {
$grouped[$cat] = [];
}
$grouped[$cat][] = $d;
}
return $grouped;
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة نشاط رياضي جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/disciplines" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/disciplines" id="disciplineForm">
<?= csrf_field() ?>
<!-- Basic Information -->
<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="file-text" 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;gap:20px;">
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar')) ?>" class="form-input" required placeholder="مثال: كرة القدم">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en')) ?>" class="form-input" id="nameEn" placeholder="e.g. Football" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود النشاط <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" id="codeInput" required placeholder="مثال: FOOTBALL" style="direction:ltr;text-align:left;text-transform:uppercase;">
<small style="color:#9CA3AF;font-size:11px;">يتم توليده تلقائيا من الاسم الإنجليزي</small>
</div>
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
<select name="category" class="form-select" required>
<option value="">-- اختر الفئة --</option>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('category') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" value="<?= e(old('sort_order', '0')) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">الأيقونة</label>
<select name="icon" class="form-select" id="iconSelect">
<?php
$icons = [
'activity' => 'Activity (نشاط)',
'trophy' => 'Trophy (كأس)',
'target' => 'Target (هدف)',
'zap' => 'Zap (طاقة)',
'flame' => 'Flame (شعلة)',
'heart' => 'Heart (قلب)',
'star' => 'Star (نجمة)',
'award' => 'Award (جائزة)',
'shield' => 'Shield (درع)',
'swords' => 'Swords (سيوف)',
'bike' => 'Bike (دراجة)',
'dumbbell' => 'Dumbbell (دمبل)',
'waves' => 'Waves (أمواج)',
'wind' => 'Wind (رياح)',
'mountain' => 'Mountain (جبل)',
'flag' => 'Flag (علم)',
'timer' => 'Timer (مؤقت)',
'gauge' => 'Gauge (مقياس)',
'volleyball' => 'Volleyball (كرة طائرة)',
'dribbble' => 'Ball (كرة)',
'circle-dot' => 'Circle Dot (دائرة)',
'move' => 'Move (حركة)',
'grab' => 'Grab (قبضة)',
'footprints' => 'Footprints (خطوات)',
];
$selectedIcon = old('icon', 'activity');
foreach ($icons as $iconKey => $iconLabel):
?>
<option value="<?= e($iconKey) ?>" <?= $selectedIcon === $iconKey ? 'selected' : '' ?>><?= e($iconLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="display:flex;align-items:end;gap:10px;">
<div id="iconPreview" style="width:48px;height:48px;border-radius:10px;background:#F3F4F6;display:flex;align-items:center;justify-content:center;">
<i data-lucide="activity" style="width:24px;height:24px;color:#0D7377;" id="iconPreviewIcon"></i>
</div>
<span style="font-size:12px;color:#9CA3AF;">معاينة الأيقونة</span>
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">وصف النشاط</label>
<textarea name="description_ar" class="form-input" rows="3" placeholder="وصف مختصر للنشاط الرياضي..."><?= e(old('description_ar')) ?></textarea>
</div>
</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;">
<i data-lucide="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">إعدادات إضافية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px;border:1px solid #E5E7EB;border-radius:8px;">
<input type="checkbox" name="requires_medical_cert" value="1" <?= old('requires_medical_cert') ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;">
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">شهادة طبية مطلوبة</div>
<div style="font-size:11px;color:#9CA3AF;">يجب تقديم شهادة لياقة طبية</div>
</div>
</label>
</div>
<div class="form-group">
<label class="form-label">موافقة ولي الأمر لمن هم أقل من (سنة)</label>
<input type="number" name="requires_guardian_consent_under" value="<?= e(old('requires_guardian_consent_under')) ?>" class="form-input" min="0" max="21" placeholder="مثال: 18" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للاعبين في الحصة</label>
<input type="number" name="max_players_per_session" value="<?= e(old('max_players_per_session')) ?>" class="form-input" min="1" placeholder="مثال: 20" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ النشاط الرياضي
</button>
<a href="/disciplines" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Auto-generate code from English name
var nameEnInput = document.getElementById('nameEn');
var codeInput = document.getElementById('codeInput');
if (nameEnInput && codeInput) {
nameEnInput.addEventListener('input', function() {
if (codeInput.dataset.manuallyEdited !== 'true') {
codeInput.value = this.value
.toUpperCase()
.replace(/[^A-Z0-9\s]/g, '')
.replace(/\s+/g, '_')
.substring(0, 30);
}
});
codeInput.addEventListener('input', function() {
this.dataset.manuallyEdited = 'true';
});
}
// Icon preview
var iconSelect = document.getElementById('iconSelect');
if (iconSelect) {
iconSelect.addEventListener('change', function() {
var preview = document.getElementById('iconPreviewIcon');
if (preview) {
preview.setAttribute('data-lucide', this.value);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
});
}
});
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(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل: <?= e($discipline->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/disciplines/<?= (int) $discipline->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض</a>
<a href="/disciplines" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$ageGroups = $config['age_groups'] ?? [];
$skillLevels = $config['skill_levels'] ?? [];
$requiresMedical = !empty($config['requires_medical_cert']);
$guardianAge = $config['requires_guardian_consent_under'] ?? '';
$maxPlayers = $config['max_players_per_session'] ?? '';
?>
<form method="POST" action="/disciplines/<?= (int) $discipline->id ?>" id="disciplineForm">
<?= csrf_field() ?>
<!-- Basic Information -->
<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="file-text" 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;gap:20px;">
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar') ?: $discipline->name_ar) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en') ?: ($discipline->name_en ?? '')) ?>" class="form-input" id="nameEn" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">كود النشاط <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $discipline->code) ?>" class="form-input" id="codeInput" required style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الفئة <span style="color:#DC2626;">*</span></label>
<select name="category" class="form-select" required>
<option value="">-- اختر الفئة --</option>
<?php foreach ($categories as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('category') ?: $discipline->category) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">ترتيب العرض</label>
<input type="number" name="sort_order" value="<?= e(old('sort_order') ?: (string)($discipline->sort_order ?? 0)) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">الأيقونة</label>
<select name="icon" class="form-select" id="iconSelect">
<?php
$icons = [
'activity' => 'Activity (نشاط)',
'trophy' => 'Trophy (كأس)',
'target' => 'Target (هدف)',
'zap' => 'Zap (طاقة)',
'flame' => 'Flame (شعلة)',
'heart' => 'Heart (قلب)',
'star' => 'Star (نجمة)',
'award' => 'Award (جائزة)',
'shield' => 'Shield (درع)',
'swords' => 'Swords (سيوف)',
'bike' => 'Bike (دراجة)',
'dumbbell' => 'Dumbbell (دمبل)',
'waves' => 'Waves (أمواج)',
'wind' => 'Wind (رياح)',
'mountain' => 'Mountain (جبل)',
'flag' => 'Flag (علم)',
'timer' => 'Timer (مؤقت)',
'gauge' => 'Gauge (مقياس)',
'volleyball' => 'Volleyball (كرة طائرة)',
'dribbble' => 'Ball (كرة)',
'circle-dot' => 'Circle Dot (دائرة)',
'move' => 'Move (حركة)',
'grab' => 'Grab (قبضة)',
'footprints' => 'Footprints (خطوات)',
];
$selectedIcon = old('icon') ?: ($discipline->icon ?? 'activity');
foreach ($icons as $iconKey => $iconLabel):
?>
<option value="<?= e($iconKey) ?>" <?= $selectedIcon === $iconKey ? 'selected' : '' ?>><?= e($iconLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="display:flex;align-items:end;gap:10px;">
<div id="iconPreview" style="width:48px;height:48px;border-radius:10px;background:#F3F4F6;display:flex;align-items:center;justify-content:center;">
<i data-lucide="<?= e($selectedIcon) ?>" style="width:24px;height:24px;color:#0D7377;" id="iconPreviewIcon"></i>
</div>
<span style="font-size:12px;color:#9CA3AF;">معاينة الأيقونة</span>
</div>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">وصف النشاط</label>
<textarea name="description_ar" class="form-input" rows="3"><?= e(old('description_ar') ?: ($discipline->description_ar ?? '')) ?></textarea>
</div>
</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;<?= !empty($ageGroups) ? 'display:none;' : '' ?>" id="ageGroupsEmpty">
لم يتم إضافة فئات عمرية بعد. اضغط "إضافة فئة" لإضافة فئة عمرية.
</div>
<?php foreach ($ageGroups as $ag): ?>
<div class="age-group-row" style="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;">
<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" value="<?= e($ag['code'] ?? '') ?>" 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" value="<?= e($ag['label_ar'] ?? '') ?>" 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="<?= (int) ($ag['min_age'] ?? 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="<?= (int) ($ag['max_age'] ?? 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>
</div>
<?php endforeach; ?>
</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;<?= !empty($skillLevels) ? 'display:none;' : '' ?>" id="skillLevelsEmpty">
لم يتم إضافة مستويات مهارة بعد. اضغط "إضافة مستوى" لإضافة مستوى.
</div>
<?php foreach ($skillLevels as $sl): ?>
<div class="skill-level-row" style="display:grid;grid-template-columns:1fr 2fr auto;gap:10px;align-items:end;margin-bottom:10px;padding:12px;background:#F9FAFB;border-radius:8px;">
<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" value="<?= e($sl['code'] ?? '') ?>" 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" value="<?= e($sl['label_ar'] ?? '') ?>" 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>
</div>
<?php endforeach; ?>
</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;">
<i data-lucide="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">إعدادات إضافية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:12px;border:1px solid #E5E7EB;border-radius:8px;">
<input type="checkbox" name="requires_medical_cert" value="1" <?= $requiresMedical ? 'checked' : '' ?> style="width:18px;height:18px;accent-color:#0D7377;">
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">شهادة طبية مطلوبة</div>
<div style="font-size:11px;color:#9CA3AF;">يجب تقديم شهادة لياقة طبية</div>
</div>
</label>
</div>
<div class="form-group">
<label class="form-label">موافقة ولي الأمر لمن هم أقل من (سنة)</label>
<input type="number" name="requires_guardian_consent_under" value="<?= e((string) $guardianAge) ?>" class="form-input" min="0" max="21" placeholder="مثال: 18" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للاعبين في الحصة</label>
<input type="number" name="max_players_per_session" value="<?= e((string) $maxPlayers) ?>" class="form-input" min="1" placeholder="مثال: 20" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات
</button>
<a href="/disciplines/<?= (int) $discipline->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Icon preview
var iconSelect = document.getElementById('iconSelect');
if (iconSelect) {
iconSelect.addEventListener('change', function() {
var preview = document.getElementById('iconPreviewIcon');
if (preview) {
preview.setAttribute('data-lucide', this.value);
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
}
});
}
});
var ageGroupIndex = <?= count($ageGroups) ?>;
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 = <?= count($skillLevels) ?>;
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(); ?>
<?php
use App\Modules\Disciplines\Models\SportDiscipline;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الأنشطة الرياضية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/disciplines/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة نشاط رياضي</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentCategory = $filters['category'] ?? '';
$allCategories = SportDiscipline::getCategories();
?>
<!-- Category Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/disciplines"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentCategory === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentCategory === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allCategories as $catKey => $catLabel): ?>
<a href="/disciplines?category=<?= e($catKey) ?><?= ($filters['q'] ?? '') !== '' ? '&q=' . urlencode($filters['q']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentCategory === $catKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentCategory === $catKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($catLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search Bar -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/disciplines" style="display:flex;gap:10px;align-items:end;">
<div style="flex:1;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الكود..." class="form-input" style="min-width:250px;">
</div>
<?php if ($currentCategory !== ''): ?>
<input type="hidden" name="category" value="<?= e($currentCategory) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/disciplines" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Disciplines Grid -->
<?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);
?>
<div class="card" style="transition:box-shadow 0.2s;position:relative;overflow:hidden;<?= !$isActive ? 'opacity:0.65;' : '' ?>">
<?php if (!$isActive): ?>
<div style="position:absolute;top:12px;left:12px;z-index:2;">
<span class="badge" style="background:#FEE2E2;color:#DC2626;font-size:11px;padding:3px 10px;border-radius:10px;">معطّل</span>
</div>
<?php endif; ?>
<a href="/disciplines/<?= (int) $d['id'] ?>" style="text-decoration:none;color:inherit;display:block;">
<!-- Card Header with Icon -->
<div style="padding:20px 20px 15px;display:flex;align-items:start;gap:15px;">
<div style="width:52px;height:52px;border-radius:12px;background:linear-gradient(135deg, <?= $catColor ?>15, <?= $catColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="<?= e($d['icon'] ?? 'activity') ?>" style="width:26px;height:26px;color:<?= $catColor ?>;"></i>
</div>
<div style="flex:1;min-width:0;">
<h3 style="margin:0 0 4px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($d['name_ar']) ?></h3>
<?php if (!empty($d['name_en'])): ?>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:6px;"><?= e($d['name_en']) ?></div>
<?php endif; ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $catColor ?>15;color:<?= $catColor ?>;"><?= e($catLabel) ?></span>
</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>
</div>
</a>
<!-- Card Footer -->
<div style="padding:10px 20px;border-top:1px solid #F3F4F6;display:flex;justify-content:space-between;align-items:center;">
<code style="font-size:11px;color:#9CA3AF;background:#F9FAFB;padding:2px 6px;border-radius:4px;"><?= e($d['code'] ?? '') ?></code>
<div style="display:flex;gap:6px;">
<a href="/disciplines/<?= (int) $d['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/disciplines/<?= (int) $d['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="activity" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أنشطة رياضية</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['category'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة نشاط رياضي جديد للنادي.
<?php endif; ?>
</p>
<a href="/disciplines/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة نشاط رياضي</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Disciplines\Models\SportDiscipline;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($discipline->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/disciplines/<?= (int) $discipline->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/disciplines" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$catColor = SportDiscipline::getCategoryColor($discipline->category ?? '');
$catLabel = SportDiscipline::getCategoryLabel($discipline->category ?? '');
$isActive = (int) $discipline->is_active;
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<div style="padding:25px;display:flex;align-items:start;gap:20px;">
<div style="width:72px;height:72px;border-radius:16px;background:linear-gradient(135deg, <?= $catColor ?>15, <?= $catColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="<?= e($discipline->icon ?? 'activity') ?>" style="width:36px;height:36px;color:<?= $catColor ?>;"></i>
</div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;">
<h2 style="margin:0;font-size:22px;font-weight:700;color:#1A1A2E;"><?= e($discipline->name_ar) ?></h2>
<?php if ($isActive): ?>
<span class="badge" style="background:#ECFDF5;color:#059669;font-size:12px;padding:4px 12px;border-radius:12px;font-weight:600;">فعّال</span>
<?php else: ?>
<span class="badge" style="background:#FEE2E2;color:#DC2626;font-size:12px;padding:4px 12px;border-radius:12px;font-weight:600;">معطّل</span>
<?php endif; ?>
</div>
<?php if ($discipline->name_en): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;"><?= e($discipline->name_en) ?></div>
<?php endif; ?>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:13px;color:#6B7280;">
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="tag" style="width:14px;height:14px;"></i>
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;"><?= e($discipline->code) ?></code>
</span>
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $catColor ?>15;color:<?= $catColor ?>;">
<?= e($catLabel) ?>
</span>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="arrow-up-down" style="width:14px;height:14px;"></i>
ترتيب: <?= (int) $discipline->sort_order ?>
</span>
</div>
<?php if ($discipline->description_ar): ?>
<p style="margin:12px 0 0;font-size:14px;color:#4B5563;line-height:1.7;"><?= e($discipline->description_ar) ?></p>
<?php endif; ?>
</div>
<div style="flex-shrink:0;">
<form method="POST" action="/disciplines/<?= (int) $discipline->id ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<?php if ($isActive): ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إيقاف هذا النشاط؟')">
<i data-lucide="pause-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إيقاف
</button>
<?php else: ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تفعيل هذا النشاط؟')">
<i data-lucide="play-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تفعيل
</button>
<?php endif; ?>
</form>
</div>
</div>
</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 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>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#D97706;">0</div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">أكاديمية</div>
</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']);
$guardianAge = $config['requires_guardian_consent_under'] ?? null;
$maxPlayers = $config['max_players_per_session'] ?? null;
$hasExtraConfig = $requiresMedical || $guardianAge || $maxPlayers;
?>
<?php if ($hasExtraConfig): ?>
<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="settings" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">إعدادات إضافية</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr));gap:15px;">
<?php if ($requiresMedical): ?>
<div style="padding:15px;background:#FEF2F2;border-radius:10px;display:flex;align-items:center;gap:10px;">
<i data-lucide="heart-pulse" style="width:20px;height:20px;color:#DC2626;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:#DC2626;">شهادة طبية مطلوبة</div>
<div style="font-size:11px;color:#6B7280;">يجب تقديم شهادة طبية للتسجيل</div>
</div>
</div>
<?php endif; ?>
<?php if ($guardianAge): ?>
<div style="padding:15px;background:#FFF7ED;border-radius:10px;display:flex;align-items:center;gap:10px;">
<i data-lucide="shield-check" style="width:20px;height:20px;color:#D97706;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:#D97706;">موافقة ولي الأمر</div>
<div style="font-size:11px;color:#6B7280;">مطلوبة لمن هم أقل من <?= (int) $guardianAge ?> سنة</div>
</div>
</div>
<?php endif; ?>
<?php if ($maxPlayers): ?>
<div style="padding:15px;background:#EFF6FF;border-radius:10px;display:flex;align-items:center;gap:10px;">
<i data-lucide="users" style="width:20px;height:20px;color:#0284C7;"></i>
<div>
<div style="font-size:13px;font-weight:600;color:#0284C7;">الحد الأقصى للاعبين</div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $maxPlayers ?> لاعب لكل حصة</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Metadata -->
<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="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">معلومات النظام</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:30%;">رقم السجل</td>
<td style="padding:6px 0;font-weight:600;">#<?= (int) $discipline->id ?></td>
</tr>
<?php if ($discipline->created_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:6px 0;"><?= e($discipline->created_at) ?></td>
</tr>
<?php endif; ?>
<?php if ($discipline->updated_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">آخر تحديث</td>
<td style="padding:6px 0;"><?= e($discipline->updated_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\MenuRegistry;
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Sports Activities — Parent sidebar menu with all 7 module children
// This file owns the entire sports_activities menu group.
// ────────────────────────────────────────────────────────────
MenuRegistry::register('sports_activities', [
'label_ar' => 'الأنشطة الرياضية',
'label_en' => 'Sports Activities',
'icon' => 'activity',
'route' => '/disciplines',
'permission' => 'discipline.view',
'parent' => null,
'order' => 395,
'children' => [
['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' => '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],
['label_ar' => 'تسعير الأنشطة', 'label_en' => 'Activity Pricing', 'route' => '/activity-subscriptions/pricing', 'permission' => 'activity_sub.manage_pricing', 'order' => 8],
],
]);
PermissionRegistry::register('disciplines', [
'discipline.view' => ['ar' => 'عرض الأنشطة الرياضية', 'en' => 'View Disciplines'],
'discipline.manage' => ['ar' => 'إدارة الأنشطة الرياضية', 'en' => 'Manage Disciplines'],
]);
...@@ -64,6 +64,14 @@ class Document extends Model ...@@ -64,6 +64,14 @@ class Document extends Model
'photo' => 'صورة شخصية', 'photo' => 'صورة شخصية',
'disability_doc' => 'مستند إعاقة', 'disability_doc' => 'مستند إعاقة',
'championship_doc' => 'مستند بطولة', 'championship_doc' => 'مستند بطولة',
'medical_certificate'=> 'شهادة طبية',
'fitness_cert' => 'شهادة لياقة بدنية',
'guardian_consent' => 'موافقة ولي الأمر',
'federation_card' => 'كارنيه الاتحاد',
'player_photo' => 'صورة اللاعب',
'rental_letter' => 'خطاب رسمي للتأجير',
'technical_report' => 'تقرير فني',
'contract_scan' => 'نسخة عقد',
'other' => 'مستند آخر', 'other' => 'مستند آخر',
]; ];
} }
......
<?php
declare(strict_types=1);
namespace App\Modules\Facilities\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Facilities\Models\Facility;
use App\Modules\Facilities\Models\FacilityTimeSlot;
use App\Modules\Facilities\Models\FacilityBlackoutDate;
use App\Modules\Disciplines\Models\SportDiscipline;
class FacilityController extends Controller
{
/**
* List all facilities with type filter tabs, location filter, search, and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'facility_type' => trim((string) $request->get('facility_type', '')),
'location' => trim((string) $request->get('location', '')),
'linked_discipline_id' => trim((string) $request->get('linked_discipline_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = Facility::search($filters, 25, $page);
return $this->view('Facilities.Views.index', [
'facilities' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'types' => Facility::getTypes(),
'locations' => Facility::getLocations(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Show the create facility form.
*/
public function create(Request $request): Response
{
return $this->view('Facilities.Views.create', [
'types' => Facility::getTypes(),
'locations' => Facility::getLocations(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Validate and store a new facility.
*/
public function store(Request $request): Response
{
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$facilityType = trim((string) $request->post('facility_type', ''));
$location = trim((string) $request->post('location', ''));
$capacity = (int) $request->post('capacity', 0);
$rateM = (float) $request->post('hourly_rate_member', 0);
$rateNM = (float) $request->post('hourly_rate_nonmember', 0);
$rateMPM = (float) $request->post('hourly_rate_member_pm', 0);
$rateNMPM = (float) $request->post('hourly_rate_nonmember_pm', 0);
$disciplineId = $request->post('linked_discipline_id', '');
$disciplineId = $disciplineId !== '' ? (int) $disciplineId : null;
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'اسم المرفق بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود المرفق مطلوب';
}
if (!array_key_exists($facilityType, Facility::getTypes())) {
$errors[] = 'نوع المرفق غير صالح';
}
if (!array_key_exists($location, Facility::getLocations())) {
$errors[] = 'الموقع غير صالح';
}
// Check unique code
if ($code !== '') {
$existing = Facility::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود المرفق مستخدم بالفعل';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/facilities/create');
}
// Build config_json from request
$config = $this->buildConfigFromRequest($request);
$facility = Facility::create([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'facility_type' => $facilityType,
'location' => $location,
'capacity' => $capacity,
'hourly_rate_member' => $rateM,
'hourly_rate_nonmember' => $rateNM,
'hourly_rate_member_pm' => $rateMPM,
'hourly_rate_nonmember_pm'=> $rateNMPM,
'linked_discipline_id' => $disciplineId,
'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE),
'is_active' => 1,
]);
return $this->redirect('/facilities/' . $facility->id)->withSuccess('تم إضافة المرفق بنجاح');
}
/**
* Show facility detail page with time slots and blackout dates.
*/
public function show(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$timeSlots = FacilityTimeSlot::getForFacility((int) $id);
$blackoutDates = FacilityBlackoutDate::getForFacility((int) $id);
// Load linked discipline name if present
$linkedDiscipline = null;
if ($facility->linked_discipline_id) {
$linkedDiscipline = SportDiscipline::find((int) $facility->linked_discipline_id);
}
return $this->view('Facilities.Views.show', [
'facility' => $facility,
'config' => $facility->getConfig(),
'timeSlots' => $timeSlots,
'blackoutDates' => $blackoutDates,
'linkedDiscipline' => $linkedDiscipline,
'types' => Facility::getTypes(),
'locations' => Facility::getLocations(),
'slotTypes' => FacilityTimeSlot::getSlotTypes(),
'dayLabels' => FacilityTimeSlot::getDayLabels(),
]);
}
/**
* Show edit form for a facility.
*/
public function edit(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
return $this->view('Facilities.Views.edit', [
'facility' => $facility,
'config' => $facility->getConfig(),
'types' => Facility::getTypes(),
'locations' => Facility::getLocations(),
'disciplines' => SportDiscipline::allActive(),
]);
}
/**
* Validate and update an existing facility.
*/
public function update(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$code = trim((string) $request->post('code', ''));
$facilityType = trim((string) $request->post('facility_type', ''));
$location = trim((string) $request->post('location', ''));
$capacity = (int) $request->post('capacity', 0);
$rateM = (float) $request->post('hourly_rate_member', 0);
$rateNM = (float) $request->post('hourly_rate_nonmember', 0);
$rateMPM = (float) $request->post('hourly_rate_member_pm', 0);
$rateNMPM = (float) $request->post('hourly_rate_nonmember_pm', 0);
$disciplineId = $request->post('linked_discipline_id', '');
$disciplineId = $disciplineId !== '' ? (int) $disciplineId : null;
// Validation
$errors = [];
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'اسم المرفق بالعربي مطلوب (حرفان على الأقل)';
}
if ($code === '') {
$errors[] = 'كود المرفق مطلوب';
}
if (!array_key_exists($facilityType, Facility::getTypes())) {
$errors[] = 'نوع المرفق غير صالح';
}
if (!array_key_exists($location, Facility::getLocations())) {
$errors[] = 'الموقع غير صالح';
}
// Check unique code (exclude current)
if ($code !== '') {
$existing = Facility::query()
->where('code', '=', $code)
->whereRaw('`id` != ?', [(int) $id])
->first();
if ($existing) {
$errors[] = 'كود المرفق مستخدم بالفعل';
}
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/facilities/' . $id . '/edit');
}
// Build config_json from request
$config = $this->buildConfigFromRequest($request);
$facility->update([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'facility_type' => $facilityType,
'location' => $location,
'capacity' => $capacity,
'hourly_rate_member' => $rateM,
'hourly_rate_nonmember' => $rateNM,
'hourly_rate_member_pm' => $rateMPM,
'hourly_rate_nonmember_pm'=> $rateNMPM,
'linked_discipline_id' => $disciplineId,
'config_json' => json_encode($config, JSON_UNESCAPED_UNICODE),
]);
return $this->redirect('/facilities/' . $id)->withSuccess('تم تحديث المرفق بنجاح');
}
/**
* Toggle the is_active status of a facility.
*/
public function toggle(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$newStatus = $facility->is_active ? 0 : 1;
$facility->update(['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل المرفق' : 'تم إيقاف المرفق';
return $this->redirect('/facilities/' . $id)->withSuccess($message);
}
/**
* Add a blackout date for a facility.
*/
public function addBlackout(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$blackoutDate = trim((string) $request->post('blackout_date', ''));
$startTime = trim((string) $request->post('start_time', ''));
$endTime = trim((string) $request->post('end_time', ''));
$reason = trim((string) $request->post('reason', ''));
$errors = [];
if ($blackoutDate === '') {
$errors[] = 'تاريخ الحظر مطلوب';
}
if ($reason === '') {
$errors[] = 'سبب الحظر مطلوب';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
return $this->redirect('/facilities/' . $id);
}
FacilityBlackoutDate::create([
'facility_id' => (int) $id,
'blackout_date' => $blackoutDate,
'start_time' => $startTime ?: null,
'end_time' => $endTime ?: null,
'reason' => $reason,
'created_by' => $_SESSION['user_id'] ?? null,
]);
return $this->redirect('/facilities/' . $id)->withSuccess('تم إضافة تاريخ الحظر بنجاح');
}
/**
* Remove a blackout date.
*/
public function removeBlackout(Request $request, string $id): Response
{
$facility = Facility::find((int) $id);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$blackoutId = (int) $request->post('blackout_id', 0);
if ($blackoutId > 0) {
$blackout = FacilityBlackoutDate::find($blackoutId);
if ($blackout && (int) $blackout->facility_id === (int) $id) {
$blackout->update(['facility_id' => (int) $id]); // no-op to trigger; use direct delete
// Direct delete since no soft-delete
FacilityBlackoutDate::query()
->where('id', '=', $blackoutId)
->where('facility_id', '=', (int) $id)
->first(); // verify ownership
// Use the model's delete or a raw approach
$db = App::getInstance()->db();
$db->execute('DELETE FROM `facility_blackout_dates` WHERE `id` = ? AND `facility_id` = ?', [$blackoutId, (int) $id]);
}
}
return $this->redirect('/facilities/' . $id)->withSuccess('تم حذف تاريخ الحظر');
}
/**
* Build the config array from the request.
*/
private function buildConfigFromRequest(Request $request): array
{
$config = [];
// Surface type (e.g., grass, artificial, wood, etc.)
$surfaceType = trim((string) $request->post('surface_type', ''));
if ($surfaceType !== '') {
$config['surface_type'] = $surfaceType;
}
// Lighting availability
$hasLighting = $request->post('has_lighting');
if ($hasLighting) {
$config['has_lighting'] = true;
}
// Changing rooms
$hasChangingRooms = $request->post('has_changing_rooms');
if ($hasChangingRooms) {
$config['has_changing_rooms'] = true;
}
// Dimensions
$dimensions = trim((string) $request->post('dimensions', ''));
if ($dimensions !== '') {
$config['dimensions'] = $dimensions;
}
// Notes
$notes = trim((string) $request->post('config_notes', ''));
if ($notes !== '') {
$config['notes'] = $notes;
}
return $config;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Facilities\Models;
use App\Core\Model;
class Facility extends Model
{
protected static string $table = 'facilities';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'facility_type',
'location',
'capacity',
'hourly_rate_member',
'hourly_rate_nonmember',
'hourly_rate_member_pm',
'hourly_rate_nonmember_pm',
'linked_discipline_id',
'config_json',
'is_active',
];
/**
* Decode config_json into an associative array.
*/
public function getConfig(): array
{
$raw = $this->config_json;
if (empty($raw)) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Get all facility types with Arabic labels.
*/
public static function getTypes(): array
{
return [
'pitch' => 'ملعب',
'court' => 'كورت',
'hall' => 'قاعة',
'lane' => 'حارة',
'table' => 'طاولة',
'device' => 'جهاز',
'track' => 'مضمار',
'pool' => 'حمام سباحة',
];
}
/**
* Get location options with Arabic labels.
*/
public static function getLocations(): array
{
return [
'indoor' => 'داخلي',
'outdoor' => 'خارجي',
];
}
/**
* Get the Arabic label for a facility type.
*/
public static function getTypeLabel(string $type): string
{
$types = self::getTypes();
return $types[$type] ?? $type;
}
/**
* Get the badge color for a facility type.
*/
public static function getTypeColor(string $type): string
{
$colors = [
'pitch' => '#0D7377',
'court' => '#7C3AED',
'hall' => '#0284C7',
'lane' => '#059669',
'table' => '#D97706',
'device' => '#DC2626',
'track' => '#EA580C',
'pool' => '#2563EB',
];
return $colors[$type] ?? '#6B7280';
}
/**
* Get the Arabic label for a location key.
*/
public static function getLocationLabel(string $location): string
{
$locations = self::getLocations();
return $locations[$location] ?? $location;
}
/**
* Get all active facilities ordered by name_ar.
*/
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Get active facilities filtered by facility_type.
*/
public static function getByType(string $type): array
{
return static::query()
->where('is_active', '=', 1)
->where('facility_type', '=', $type)
->orderBy('name_ar', 'ASC')
->get();
}
/**
* Search facilities with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`name_ar` LIKE ? OR `name_en` LIKE ? OR `code` LIKE ?)',
[$search, $search, $search]
);
}
if (!empty($filters['facility_type'])) {
$query = $query->where('facility_type', '=', $filters['facility_type']);
}
if (!empty($filters['location'])) {
$query = $query->where('location', '=', $filters['location']);
}
if (!empty($filters['linked_discipline_id'])) {
$query = $query->where('linked_discipline_id', '=', (int) $filters['linked_discipline_id']);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
/**
* Get the correct hourly rate based on member status and time tier.
*
* @param bool $isMember Whether the person is a club member
* @param string $timeTier 'AM' for morning rates, 'PM' for evening rates
*/
public function getRate(bool $isMember, string $timeTier = 'AM'): float
{
if ($timeTier === 'PM') {
return (float) ($isMember ? $this->hourly_rate_member_pm : $this->hourly_rate_nonmember_pm);
}
return (float) ($isMember ? $this->hourly_rate_member : $this->hourly_rate_nonmember);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Facilities\Models;
use App\Core\Model;
class FacilityBlackoutDate extends Model
{
protected static string $table = 'facility_blackout_dates';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'facility_id',
'blackout_date',
'start_time',
'end_time',
'reason',
'created_by',
];
/**
* Get upcoming blackout dates for a facility (today and future), ordered by date.
*/
public static function getForFacility(int $facilityId): array
{
return static::query()
->where('facility_id', '=', $facilityId)
->whereRaw('`blackout_date` >= CURDATE()')
->orderBy('blackout_date', 'ASC')
->orderBy('start_time', 'ASC')
->get();
}
/**
* Check if a given date (and optionally time) is blocked for a facility.
*/
public static function isBlackedOut(int $facilityId, string $date, ?string $time = null): bool
{
$query = static::query()
->where('facility_id', '=', $facilityId)
->where('blackout_date', '=', $date);
$rows = $query->get();
if (empty($rows)) {
return false;
}
foreach ($rows as $row) {
// If the blackout has no start/end time, it blocks the entire day
if (empty($row['start_time']) && empty($row['end_time'])) {
return true;
}
// If no specific time to check, any blackout on this date counts
if ($time === null) {
return true;
}
// Check if the requested time falls within the blackout window
$start = $row['start_time'] ?? '00:00:00';
$end = $row['end_time'] ?? '23:59:59';
if ($time >= $start && $time <= $end) {
return true;
}
}
return false;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Facilities\Models;
use App\Core\Model;
class FacilityTimeSlot extends Model
{
protected static string $table = 'facility_time_slots';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'facility_id',
'day_of_week',
'start_time',
'end_time',
'slot_type',
'linked_academy_id',
'is_active',
];
/**
* Get all time slots for a specific facility, ordered by day_of_week then start_time.
*/
public static function getForFacility(int $facilityId): array
{
return static::query()
->where('facility_id', '=', $facilityId)
->orderBy('day_of_week', 'ASC')
->orderBy('start_time', 'ASC')
->get();
}
/**
* Get slot type options with Arabic labels.
*/
public static function getSlotTypes(): array
{
return [
'available' => 'متاح',
'maintenance' => 'صيانة',
'academy_reserved' => 'محجوز للأكاديمية',
'blocked' => 'محظور',
];
}
/**
* Get day-of-week labels in Arabic (0 = Sunday).
*/
public static function getDayLabels(): array
{
return [
0 => 'الأحد',
1 => 'الاثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
6 => 'السبت',
];
}
}
<?php
declare(strict_types=1);
return [
['GET', '/facilities', 'Facilities\Controllers\FacilityController@index', ['auth'], 'facility.view'],
['GET', '/facilities/create', 'Facilities\Controllers\FacilityController@create', ['auth'], 'facility.manage'],
['POST', '/facilities', 'Facilities\Controllers\FacilityController@store', ['auth', 'csrf'], 'facility.manage'],
['GET', '/facilities/{id:\d+}', 'Facilities\Controllers\FacilityController@show', ['auth'], 'facility.view'],
['GET', '/facilities/{id:\d+}/edit', 'Facilities\Controllers\FacilityController@edit', ['auth'], 'facility.manage'],
['POST', '/facilities/{id:\d+}', 'Facilities\Controllers\FacilityController@update', ['auth', 'csrf'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/toggle', 'Facilities\Controllers\FacilityController@toggle', ['auth', 'csrf'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/blackout', 'Facilities\Controllers\FacilityController@addBlackout', ['auth', 'csrf'], 'facility.manage'],
['POST', '/facilities/{id:\d+}/blackout/remove', 'Facilities\Controllers\FacilityController@removeBlackout', ['auth', 'csrf'], 'facility.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Facilities\Services;
use App\Modules\Facilities\Models\Facility;
use App\Modules\Facilities\Models\FacilityTimeSlot;
use App\Modules\Facilities\Models\FacilityBlackoutDate;
class FacilityService
{
/**
* Get all active facilities.
*/
public static function getActive(): array
{
return Facility::allActive();
}
/**
* Get active facilities filtered by facility type.
*/
public static function getByType(string $type): array
{
return Facility::getByType($type);
}
/**
* Get available time slots for a facility on a given date.
*
* Checks the day_of_week from the date, fetches matching time slots
* that are active and of type 'available', and excludes any that fall
* within blackout date windows.
*/
public static function getAvailableSlots(int $facilityId, string $date): array
{
// Determine day of week (0 = Sunday, 6 = Saturday)
$dayOfWeek = (int) date('w', strtotime($date));
// Get all active slots for this facility on this day
$slots = FacilityTimeSlot::query()
->where('facility_id', '=', $facilityId)
->where('day_of_week', '=', $dayOfWeek)
->where('slot_type', '=', 'available')
->where('is_active', '=', 1)
->orderBy('start_time', 'ASC')
->get();
if (empty($slots)) {
return [];
}
// Filter out slots that overlap with blackout dates
$available = [];
foreach ($slots as $slot) {
$startTime = $slot['start_time'] ?? null;
if (!FacilityBlackoutDate::isBlackedOut($facilityId, $date, $startTime)) {
$available[] = $slot;
}
}
return $available;
}
/**
* Get the correct hourly rate for a facility.
*
* @param int $facilityId The facility ID
* @param bool $isMember Whether the person is a club member
* @param string $timeTier 'AM' for morning rates, 'PM' for evening rates
*/
public static function getRate(int $facilityId, bool $isMember, string $timeTier = 'AM'): float
{
$facility = Facility::find($facilityId);
if (!$facility) {
return 0.0;
}
return $facility->getRate($isMember, $timeTier);
}
/**
* Get all active facilities grouped by facility_type.
*/
public static function getGroupedByType(): array
{
$facilities = Facility::allActive();
$grouped = [];
// Initialize all type keys
foreach (Facility::getTypes() as $key => $label) {
$grouped[$key] = [];
}
foreach ($facilities as $f) {
$type = $f['facility_type'] ?? 'pitch';
if (!isset($grouped[$type])) {
$grouped[$type] = [];
}
$grouped[$type][] = $f;
}
return $grouped;
}
/**
* Block a slot by creating a blackout date entry.
*/
public static function blockSlot(
int $facilityId,
string $date,
?string $startTime,
?string $endTime,
string $reason
): object {
return FacilityBlackoutDate::create([
'facility_id' => $facilityId,
'blackout_date' => $date,
'start_time' => $startTime ?: null,
'end_time' => $endTime ?: null,
'reason' => $reason,
'created_by' => $_SESSION['user_id'] ?? null,
]);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة مرفق جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/facilities" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/facilities" id="facilityForm">
<?= csrf_field() ?>
<!-- Basic Information -->
<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="file-text" 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;gap:20px;">
<div class="form-group">
<label class="form-label">كود المرفق <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code')) ?>" class="form-input" required placeholder="مثال: PITCH-01" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar')) ?>" class="form-input" required placeholder="مثال: ملعب 1">
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en')) ?>" class="form-input" placeholder="e.g. Pitch 1" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">نوع المرفق <span style="color:#DC2626;">*</span></label>
<select name="facility_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($types as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('facility_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الموقع <span style="color:#DC2626;">*</span></label>
<select name="location" class="form-select" required>
<option value="">-- اختر الموقع --</option>
<?php foreach ($locations as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('location') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">السعة</label>
<input type="number" name="capacity" value="<?= e(old('capacity')) ?>" class="form-input" min="0" placeholder="مثال: 22" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">النشاط المرتبط</label>
<select name="linked_discipline_id" class="form-select">
<option value="">-- بدون ربط --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= old('linked_discipline_id') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-input" rows="2" placeholder="وصف مختصر للمرفق..."><?= e(old('description')) ?></textarea>
</div>
</div>
</div>
</div>
<!-- Rates Card -->
<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:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">الأسعار بالساعة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">
<i data-lucide="sun" style="width:14px;height:14px;vertical-align:middle;color:#D97706;"></i>
سعر الأعضاء (صباحي)
</label>
<input type="number" name="hourly_rate_member" value="<?= e(old('hourly_rate_member')) ?>" class="form-input" min="0" step="0.01" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">
<i data-lucide="sun" style="width:14px;height:14px;vertical-align:middle;color:#D97706;"></i>
سعر غير الأعضاء (صباحي)
</label>
<input type="number" name="hourly_rate_nonmember" value="<?= e(old('hourly_rate_nonmember')) ?>" class="form-input" min="0" step="0.01" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">
<i data-lucide="moon" style="width:14px;height:14px;vertical-align:middle;color:#7C3AED;"></i>
سعر الأعضاء (مسائي)
</label>
<input type="number" name="hourly_rate_member_pm" value="<?= e(old('hourly_rate_member_pm')) ?>" class="form-input" min="0" step="0.01" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">
<i data-lucide="moon" style="width:14px;height:14px;vertical-align:middle;color:#7C3AED;"></i>
سعر غير الأعضاء (مسائي)
</label>
<input type="number" name="hourly_rate_nonmember_pm" value="<?= e(old('hourly_rate_nonmember_pm')) ?>" class="form-input" min="0" step="0.01" placeholder="0.00" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ المرفق
</button>
<a href="/facilities" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل المرفق: <?= e($facility->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/facilities/<?= (int) $facility->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض</a>
<a href="/facilities" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/facilities/<?= (int) $facility->id ?>" id="facilityForm">
<?= csrf_field() ?>
<!-- Basic Information -->
<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="file-text" 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;gap:20px;">
<div class="form-group">
<label class="form-label">كود المرفق <span style="color:#DC2626;">*</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $facility->code) ?>" class="form-input" required style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e(old('name_ar') ?: $facility->name_ar) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en') ?: ($facility->name_en ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">نوع المرفق <span style="color:#DC2626;">*</span></label>
<select name="facility_type" class="form-select" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($types as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('facility_type') ?: $facility->facility_type) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الموقع <span style="color:#DC2626;">*</span></label>
<select name="location" class="form-select" required>
<option value="">-- اختر الموقع --</option>
<?php foreach ($locations as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('location') ?: $facility->location) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">السعة</label>
<input type="number" name="capacity" value="<?= e(old('capacity') ?: (string)($facility->capacity ?? 0)) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">النشاط المرتبط</label>
<select name="linked_discipline_id" class="form-select">
<option value="">-- بدون ربط --</option>
<?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc['id'] ?>" <?= (old('linked_discipline_id') ?: ($facility->linked_discipline_id ?? '')) == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar'] ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-input" rows="2"><?= e(old('description') ?: ($facility->description ?? '')) ?></textarea>
</div>
</div>
</div>
</div>
<!-- Rates Card -->
<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:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">الأسعار بالساعة</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">
<i data-lucide="sun" style="width:14px;height:14px;vertical-align:middle;color:#D97706;"></i>
سعر الأعضاء (صباحي)
</label>
<input type="number" name="hourly_rate_member" value="<?= e(old('hourly_rate_member') ?: (string)($facility->hourly_rate_member ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">
<i data-lucide="sun" style="width:14px;height:14px;vertical-align:middle;color:#D97706;"></i>
سعر غير الأعضاء (صباحي)
</label>
<input type="number" name="hourly_rate_nonmember" value="<?= e(old('hourly_rate_nonmember') ?: (string)($facility->hourly_rate_nonmember ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">
<i data-lucide="moon" style="width:14px;height:14px;vertical-align:middle;color:#7C3AED;"></i>
سعر الأعضاء (مسائي)
</label>
<input type="number" name="hourly_rate_member_pm" value="<?= e(old('hourly_rate_member_pm') ?: (string)($facility->hourly_rate_member_pm ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">
<i data-lucide="moon" style="width:14px;height:14px;vertical-align:middle;color:#7C3AED;"></i>
سعر غير الأعضاء (مسائي)
</label>
<input type="number" name="hourly_rate_nonmember_pm" value="<?= e(old('hourly_rate_nonmember_pm') ?: (string)($facility->hourly_rate_nonmember_pm ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات
</button>
<a href="/facilities/<?= (int) $facility->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Facilities\Models\Facility;
use App\Modules\Disciplines\Models\SportDiscipline;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الملاعب والمرافق<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/facilities/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة مرفق</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentType = $filters['facility_type'] ?? '';
$currentLocation = $filters['location'] ?? '';
$allTypes = Facility::getTypes();
$allLocations = Facility::getLocations();
// Map facility types to Lucide icons
$typeIcons = [
'pitch' => 'layout-grid',
'court' => 'square',
'hall' => 'warehouse',
'lane' => 'move-horizontal',
'table' => 'table-2',
'device' => 'dumbbell',
'track' => 'route',
'pool' => 'waves',
];
?>
<!-- Type Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/facilities<?= $currentLocation !== '' ? '?location=' . urlencode($currentLocation) : '' ?><?= ($filters['q'] ?? '') !== '' ? ($currentLocation !== '' ? '&' : '?') . 'q=' . urlencode($filters['q']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentType === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allTypes as $typeKey => $typeLabel): ?>
<a href="/facilities?facility_type=<?= e($typeKey) ?><?= $currentLocation !== '' ? '&location=' . urlencode($currentLocation) : '' ?><?= ($filters['q'] ?? '') !== '' ? '&q=' . urlencode($filters['q']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === $typeKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentType === $typeKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($typeLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search Bar with Location Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/facilities" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الكود..." class="form-input" style="min-width:200px;">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الموقع</label>
<select name="location" class="form-input">
<option value="">الكل</option>
<?php foreach ($allLocations as $locKey => $locLabel): ?>
<option value="<?= e($locKey) ?>" <?= $currentLocation === $locKey ? 'selected' : '' ?>><?= e($locLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentType !== ''): ?>
<input type="hidden" name="facility_type" value="<?= e($currentType) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/facilities" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Facilities Grid -->
<?php if (!empty($facilities)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:20px;margin-bottom:20px;">
<?php foreach ($facilities as $f):
$fType = $f['facility_type'] ?? 'pitch';
$fLocation = $f['location'] ?? 'indoor';
$typeColor = Facility::getTypeColor($fType);
$typeLabel = Facility::getTypeLabel($fType);
$locLabel = Facility::getLocationLabel($fLocation);
$isActive = (int) ($f['is_active'] ?? 0);
$icon = $typeIcons[$fType] ?? 'building';
$capacity = (int) ($f['capacity'] ?? 0);
$rateAM = number_format((float) ($f['hourly_rate_member'] ?? 0), 0);
$ratePM = number_format((float) ($f['hourly_rate_member_pm'] ?? 0), 0);
// Get linked discipline name
$disciplineName = '';
if (!empty($f['linked_discipline_id']) && !empty($disciplines)) {
foreach ($disciplines as $disc) {
if ((int) $disc['id'] === (int) $f['linked_discipline_id']) {
$disciplineName = $disc['name_ar'] ?? '';
break;
}
}
}
?>
<div class="card" style="transition:box-shadow 0.2s;position:relative;overflow:hidden;<?= !$isActive ? 'opacity:0.65;' : '' ?>">
<?php if (!$isActive): ?>
<div style="position:absolute;top:12px;left:12px;z-index:2;">
<span class="badge" style="background:#FEE2E2;color:#DC2626;font-size:11px;padding:3px 10px;border-radius:10px;">معطّل</span>
</div>
<?php endif; ?>
<a href="/facilities/<?= (int) $f['id'] ?>" style="text-decoration:none;color:inherit;display:block;">
<!-- Card Header with Icon -->
<div style="padding:20px 20px 15px;display:flex;align-items:start;gap:15px;">
<div style="width:52px;height:52px;border-radius:12px;background:linear-gradient(135deg, <?= $typeColor ?>15, <?= $typeColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="<?= e($icon) ?>" style="width:26px;height:26px;color:<?= $typeColor ?>;"></i>
</div>
<div style="flex:1;min-width:0;">
<h3 style="margin:0 0 4px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($f['name_ar']) ?></h3>
<?php if (!empty($f['name_en'])): ?>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:6px;"><?= e($f['name_en']) ?></div>
<?php endif; ?>
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $typeColor ?>15;color:<?= $typeColor ?>;"><?= e($typeLabel) ?></span>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $fLocation === 'indoor' ? '#DBEAFE' : '#FEF3C7' ?>;color:<?= $fLocation === 'indoor' ? '#1D4ED8' : '#92400E' ?>;">
<i data-lucide="<?= $fLocation === 'indoor' ? 'home' : 'sun' ?>" style="width:11px;height:11px;vertical-align:middle;margin-left:2px;"></i>
<?= e($locLabel) ?>
</span>
</div>
</div>
</div>
<!-- Card Body Stats -->
<div style="padding:0 20px 15px;display:flex;gap:15px;font-size:12px;color:#6B7280;flex-wrap:wrap;">
<?php if ($capacity > 0): ?>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="users" style="width:14px;height:14px;"></i>
<?= $capacity ?> سعة
</span>
<?php endif; ?>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="sun" style="width:14px;height:14px;"></i>
صباحي: <?= $rateAM ?> ج.م
</span>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="moon" style="width:14px;height:14px;"></i>
مسائي: <?= $ratePM ?> ج.م
</span>
</div>
<?php if ($disciplineName !== ''): ?>
<div style="padding:0 20px 12px;">
<span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;color:#0D7377;background:#F0FDFA;padding:3px 10px;border-radius:8px;">
<i data-lucide="activity" style="width:12px;height:12px;"></i>
<?= e($disciplineName) ?>
</span>
</div>
<?php endif; ?>
</a>
<!-- Card Footer -->
<div style="padding:10px 20px;border-top:1px solid #F3F4F6;display:flex;justify-content:space-between;align-items:center;">
<code style="font-size:11px;color:#9CA3AF;background:#F9FAFB;padding:2px 6px;border-radius:4px;"><?= e($f['code'] ?? '') ?></code>
<div style="display:flex;gap:6px;">
<a href="/facilities/<?= (int) $f['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/facilities/<?= (int) $f['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="building" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد مرافق</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['facility_type']) || !empty($filters['location'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة مرفق جديد للنادي.
<?php endif; ?>
</p>
<a href="/facilities/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة مرفق</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Facilities\Models\Facility;
use App\Modules\Facilities\Models\FacilityTimeSlot;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($facility->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/facilities/<?= (int) $facility->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/facilities" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$fType = $facility->facility_type ?? 'pitch';
$fLocation = $facility->location ?? 'indoor';
$typeColor = Facility::getTypeColor($fType);
$typeLabel = Facility::getTypeLabel($fType);
$locLabel = Facility::getLocationLabel($fLocation);
$isActive = (int) $facility->is_active;
$typeIcons = [
'pitch' => 'goal',
'court' => 'rectangle-horizontal',
'hall' => 'building',
'lane' => 'arrow-right-circle',
'table' => 'table',
'device' => 'gamepad-2',
'track' => 'timer',
'pool' => 'waves',
];
$icon = $typeIcons[$fType] ?? 'building';
$dayLabels = FacilityTimeSlot::getDayLabels();
$slotTypes = FacilityTimeSlot::getSlotTypes();
$slotTypeColors = [
'available' => ['bg' => '#ECFDF5', 'color' => '#059669'],
'maintenance' => ['bg' => '#FEF3C7', 'color' => '#D97706'],
'academy_reserved' => ['bg' => '#DBEAFE', 'color' => '#2563EB'],
'blocked' => ['bg' => '#FEE2E2', 'color' => '#DC2626'],
];
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<div style="padding:25px;display:flex;align-items:start;gap:20px;">
<div style="width:72px;height:72px;border-radius:16px;background:linear-gradient(135deg, <?= $typeColor ?>15, <?= $typeColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="<?= e($icon) ?>" style="width:36px;height:36px;color:<?= $typeColor ?>;"></i>
</div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;">
<h2 style="margin:0;font-size:22px;font-weight:700;color:#1A1A2E;"><?= e($facility->name_ar) ?></h2>
<?php if ($isActive): ?>
<span class="badge" style="background:#ECFDF5;color:#059669;font-size:12px;padding:4px 12px;border-radius:12px;font-weight:600;">فعّال</span>
<?php else: ?>
<span class="badge" style="background:#FEE2E2;color:#DC2626;font-size:12px;padding:4px 12px;border-radius:12px;font-weight:600;">معطّل</span>
<?php endif; ?>
</div>
<?php if ($facility->name_en): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;"><?= e($facility->name_en) ?></div>
<?php endif; ?>
<div style="display:flex;gap:12px;flex-wrap:wrap;font-size:13px;color:#6B7280;">
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="tag" style="width:14px;height:14px;"></i>
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;"><?= e($facility->code) ?></code>
</span>
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $typeColor ?>15;color:<?= $typeColor ?>;">
<?= e($typeLabel) ?>
</span>
<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 10px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $fLocation === 'indoor' ? '#DBEAFE' : '#FEF3C7' ?>;color:<?= $fLocation === 'indoor' ? '#0284C7' : '#D97706' ?>;">
<i data-lucide="<?= $fLocation === 'indoor' ? 'home' : 'sun' ?>" style="width:12px;height:12px;"></i>
<?= e($locLabel) ?>
</span>
<?php if ((int) $facility->capacity > 0): ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="users" style="width:14px;height:14px;"></i>
السعة: <?= (int) $facility->capacity ?>
</span>
<?php endif; ?>
</div>
<?php if ($facility->description ?? ''): ?>
<p style="margin:12px 0 0;font-size:14px;color:#4B5563;line-height:1.7;"><?= e($facility->description) ?></p>
<?php endif; ?>
</div>
<div style="flex-shrink:0;">
<form method="POST" action="/facilities/<?= (int) $facility->id ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<?php if ($isActive): ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إيقاف هذا المرفق؟')">
<i data-lucide="pause-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إيقاف
</button>
<?php else: ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تفعيل هذا المرفق؟')">
<i data-lucide="play-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تفعيل
</button>
<?php endif; ?>
</form>
</div>
</div>
</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;"><?= (int) $facility->capacity ?></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:#059669;"><?= money((float) $facility->hourly_rate_member) ?></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:#D97706;"><?= money((float) $facility->hourly_rate_nonmember) ?></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;"><?= money((float) $facility->hourly_rate_member_pm) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">سعر الأعضاء مسائي</div>
</div>
</div>
<!-- Rates Card -->
<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:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">الأسعار بالساعة</h3>
</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>الفترة</th>
<th>سعر الأعضاء</th>
<th>سعر غير الأعضاء</th>
</tr>
</thead>
<tbody>
<tr>
<td style="font-weight:600;">
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="sun" style="width:14px;height:14px;color:#D97706;"></i>
صباحي (AM)
</span>
</td>
<td><?= money((float) $facility->hourly_rate_member) ?></td>
<td><?= money((float) $facility->hourly_rate_nonmember) ?></td>
</tr>
<tr>
<td style="font-weight:600;">
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="moon" style="width:14px;height:14px;color:#7C3AED;"></i>
مسائي (PM)
</span>
</td>
<td><?= money((float) $facility->hourly_rate_member_pm) ?></td>
<td><?= money((float) $facility->hourly_rate_nonmember_pm) ?></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Linked Discipline Card -->
<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="activity" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">النشاط المرتبط</h3>
</div>
<div style="padding:20px;">
<?php if ($discipline): ?>
<div style="display:flex;align-items:center;gap:15px;">
<div style="width:48px;height:48px;border-radius:12px;background:linear-gradient(135deg, #0D737715, #0D737730);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="<?= e($discipline['icon'] ?? 'activity') ?>" style="width:24px;height:24px;color:#0D7377;"></i>
</div>
<div style="flex:1;">
<div style="font-size:16px;font-weight:700;color:#1A1A2E;margin-bottom:4px;"><?= e($discipline['name_ar'] ?? '') ?></div>
<?php if (!empty($discipline['category'])): ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#F3F4F6;color:#6B7280;"><?= e($discipline['category']) ?></span>
<?php endif; ?>
</div>
<a href="/disciplines/<?= (int) $discipline['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;margin-left:4px;"></i> عرض النشاط
</a>
</div>
<?php else: ?>
<div style="text-align:center;color:#9CA3AF;font-size:14px;padding:15px;">
<i data-lucide="unlink" style="width:20px;height:20px;margin-bottom:8px;"></i>
<div>غير مرتبط بنشاط</div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Time Slots Section -->
<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="clock" style="width:18px;height:18px;color:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">الفترات الزمنية</h3>
</div>
<a href="/facilities/<?= (int) $facility->id ?>/timeslots/create" class="btn btn-sm btn-outline">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة فترة
</a>
</div>
<?php if (!empty($timeSlots)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>اليوم</th>
<th>من</th>
<th>إلى</th>
<th>النوع</th>
<th>الأكاديمية</th>
</tr>
</thead>
<tbody>
<?php foreach ($timeSlots as $slot): ?>
<tr>
<td style="font-weight:600;"><?= e($dayLabels[(int) ($slot['day_of_week'] ?? 0)] ?? '') ?></td>
<td style="direction:ltr;text-align:center;"><?= e($slot['start_time'] ?? '') ?></td>
<td style="direction:ltr;text-align:center;"><?= e($slot['end_time'] ?? '') ?></td>
<td>
<?php
$st = $slot['slot_type'] ?? 'available';
$stColors = $slotTypeColors[$st] ?? ['bg' => '#F3F4F6', 'color' => '#6B7280'];
?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $stColors['bg'] ?>;color:<?= $stColors['color'] ?>;">
<?= e($slotTypes[$st] ?? $st) ?>
</span>
</td>
<td>
<?php if (!empty($slot['linked_academy_id'])): ?>
<code style="font-size:11px;background:#F3F4F6;padding:1px 6px;border-radius:4px;">#<?= (int) $slot['linked_academy_id'] ?></code>
<?php else: ?>
<span style="color:#9CA3AF;">-</span>
<?php endif; ?>
</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>
<!-- Blackout Dates Section -->
<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="calendar-off" style="width:18px;height:18px;color:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">تواريخ الحظر</h3>
</div>
<button type="button" class="btn btn-sm btn-outline" onclick="document.getElementById('blackoutForm').style.display = document.getElementById('blackoutForm').style.display === 'none' ? 'block' : 'none';">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة حظر
</button>
</div>
<!-- Add Blackout Inline Form (hidden by default) -->
<div id="blackoutForm" style="display:none;padding:20px;border-bottom:1px solid #E5E7EB;background:#FEF2F2;">
<form method="POST" action="/facilities/<?= (int) $facility->id ?>/blackout">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-bottom:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">التاريخ <span style="color:#DC2626;">*</span></label>
<input type="date" name="blackout_date" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">من الساعة</label>
<input type="time" name="start_time" class="form-input" style="direction:ltr;text-align:left;">
<small style="color:#9CA3AF;font-size:11px;">اتركه فارغاً لحظر اليوم بالكامل</small>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">إلى الساعة</label>
<input type="time" name="end_time" class="form-input" style="direction:ltr;text-align:left;">
</div>
</div>
<div class="form-group" style="margin:0 0 15px;">
<label class="form-label" style="font-size:12px;">السبب</label>
<textarea name="reason" class="form-input" rows="2" placeholder="سبب الحظر..."></textarea>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ
</button>
<button type="button" class="btn btn-outline" style="font-size:13px;" onclick="document.getElementById('blackoutForm').style.display='none';">إلغاء</button>
</div>
</form>
</div>
<?php if (!empty($blackoutDates)): ?>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>التاريخ</th>
<th>الفترة</th>
<th>السبب</th>
<th style="width:80px;"></th>
</tr>
</thead>
<tbody>
<?php foreach ($blackoutDates as $bo): ?>
<tr>
<td style="font-weight:600;"><?= e($bo['blackout_date'] ?? '') ?></td>
<td>
<?php if (!empty($bo['start_time']) && !empty($bo['end_time'])): ?>
<span style="direction:ltr;display:inline-block;"><?= e($bo['start_time']) ?> - <?= e($bo['end_time']) ?></span>
<?php else: ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:#FEE2E2;color:#DC2626;">اليوم بالكامل</span>
<?php endif; ?>
</td>
<td style="color:#4B5563;"><?= e($bo['reason'] ?? '-') ?></td>
<td>
<form method="POST" action="/facilities/<?= (int) $facility->id ?>/blackout/remove" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="blackout_id" value="<?= (int) $bo['id'] ?>">
<button type="submit" class="btn btn-sm" style="color:#DC2626;padding:4px 8px;border:1px solid #FCA5A5;border-radius:6px;background:#FEF2F2;cursor:pointer;" onclick="return confirm('هل أنت متأكد من حذف هذا الحظر؟')">
<i data-lucide="trash-2" style="width:13px;height:13px;"></i>
</button>
</form>
</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>
<!-- Metadata -->
<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="info" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">معلومات النظام</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:30%;">رقم السجل</td>
<td style="padding:6px 0;font-weight:600;">#<?= (int) $facility->id ?></td>
</tr>
<?php if ($facility->created_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">تاريخ الإنشاء</td>
<td style="padding:6px 0;"><?= e($facility->created_at) ?></td>
</tr>
<?php endif; ?>
<?php if ($facility->updated_at): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">آخر تحديث</td>
<td style="padding:6px 0;"><?= e($facility->updated_at) ?></td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
// ────────────────────────────────────────────────────────────
// Facilities — Permissions only (sidebar menu owned by Disciplines bootstrap.php)
// ────────────────────────────────────────────────────────────
PermissionRegistry::register('facilities', [
'facility.view' => ['ar' => 'عرض الملاعب والمرافق', 'en' => 'View Facilities'],
'facility.manage' => ['ar' => 'إدارة الملاعب والمرافق', 'en' => 'Manage Facilities'],
]);
<?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;
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;
use App\Modules\PlayerAffairs\Services\PlayerCardService;
use App\Modules\PlayerAffairs\Services\MedicalRecordService;
class PlayerController extends Controller
{
/**
* List all players with tabs, status filter, search, and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'player_type' => trim((string) $request->get('player_type', '')),
'card_status' => trim((string) $request->get('card_status', '')),
'medical_status' => trim((string) $request->get('medical_status', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = Player::search($filters, 25, $page);
return $this->view('PlayerAffairs.Views.index', [
'players' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'playerTypes' => Player::getPlayerTypes(),
'cardStatuses' => Player::getCardStatuses(),
'medicalStatuses' => Player::getMedicalStatuses(),
]);
}
/**
* Show the player registration form.
*/
public function create(Request $request): Response
{
return $this->view('PlayerAffairs.Views.create', [
'playerTypes' => Player::getPlayerTypes(),
'genders' => Player::getGenders(),
'relationships' => Player::getGuardianRelationships(),
]);
}
/**
* Validate and register a new player.
*/
public function store(Request $request): Response
{
$playerType = trim((string) $request->post('player_type', ''));
$fullNameAr = trim((string) $request->post('full_name_ar', ''));
$fullNameEn = trim((string) $request->post('full_name_en', ''));
$nationalId = trim((string) $request->post('national_id', ''));
$passportNumber = trim((string) $request->post('passport_number', ''));
$dateOfBirth = trim((string) $request->post('date_of_birth', ''));
$gender = trim((string) $request->post('gender', ''));
$phone = trim((string) $request->post('phone', ''));
$email = trim((string) $request->post('email', ''));
$address = trim((string) $request->post('address', ''));
$guardianName = trim((string) $request->post('guardian_name', ''));
$guardianPhone = trim((string) $request->post('guardian_phone', ''));
$guardianNationalId = trim((string) $request->post('guardian_national_id', ''));
$guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$schoolName = trim((string) $request->post('school_name', ''));
$schoolGrade = trim((string) $request->post('school_grade', ''));
$notes = trim((string) $request->post('notes', ''));
$memberId = $request->post('member_id');
// Validation
$errors = [];
if ($fullNameAr === '' || mb_strlen($fullNameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if (!array_key_exists($playerType, Player::getPlayerTypes())) {
$errors[] = 'نوع اللاعب غير صالح';
}
if ($dateOfBirth === '') {
$errors[] = 'تاريخ الميلاد مطلوب';
}
if ($gender !== '' && !array_key_exists($gender, Player::getGenders())) {
$errors[] = 'الجنس غير صالح';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/players/create');
}
$data = [
'full_name_ar' => $fullNameAr,
'full_name_en' => $fullNameEn ?: null,
'national_id' => $nationalId ?: null,
'passport_number' => $passportNumber ?: null,
'date_of_birth' => $dateOfBirth,
'gender' => $gender ?: null,
'phone' => $phone ?: null,
'email' => $email ?: null,
'address' => $address ?: null,
'guardian_name' => $guardianName ?: null,
'guardian_phone' => $guardianPhone ?: null,
'guardian_national_id' => $guardianNationalId ?: null,
'guardian_relationship' => $guardianRelationship ?: null,
'school_name' => $schoolName ?: null,
'school_grade' => $schoolGrade ?: null,
'notes' => $notes ?: null,
];
try {
if ($playerType === 'member' && $memberId) {
$player = PlayerRegistrationService::registerMemberPlayer((int) $memberId, $data);
} else {
$player = PlayerRegistrationService::registerNonMemberPlayer($data);
}
} catch (\RuntimeException $e) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => $e->getMessage()]]);
$session->flash('_old_input', $request->all());
return $this->redirect('/players/create');
}
return $this->redirect('/players/' . $player->id)->withSuccess('تم تسجيل اللاعب بنجاح');
}
/**
* Show a player's full profile.
*/
public function show(Request $request, string $id): Response
{
$player = Player::find((int) $id);
if (!$player) {
return $this->redirect('/players')->withError('اللاعب غير موجود');
}
$disciplines = PlayerDiscipline::getForPlayer((int) $id);
$enrollments = AcademyEnrollment::getForPlayer((int) $id);
$medicalRecords = PlayerMedicalRecord::getForPlayer((int) $id);
return $this->view('PlayerAffairs.Views.show', [
'player' => $player,
'disciplines' => $disciplines,
'enrollments' => $enrollments,
'medicalRecords' => $medicalRecords,
'cardStatuses' => Player::getCardStatuses(),
'medicalStatuses' => Player::getMedicalStatuses(),
'playerTypes' => Player::getPlayerTypes(),
'genders' => Player::getGenders(),
]);
}
/**
* Show the edit form for a player.
*/
public function edit(Request $request, string $id): Response
{
$player = Player::find((int) $id);
if (!$player) {
return $this->redirect('/players')->withError('اللاعب غير موجود');
}
return $this->view('PlayerAffairs.Views.edit', [
'player' => $player,
'playerTypes' => Player::getPlayerTypes(),
'genders' => Player::getGenders(),
'relationships' => Player::getGuardianRelationships(),
]);
}
/**
* Update player information.
*/
public function update(Request $request, string $id): Response
{
$player = Player::find((int) $id);
if (!$player) {
return $this->redirect('/players')->withError('اللاعب غير موجود');
}
$fullNameAr = trim((string) $request->post('full_name_ar', ''));
$fullNameEn = trim((string) $request->post('full_name_en', ''));
$nationalId = trim((string) $request->post('national_id', ''));
$passportNumber = trim((string) $request->post('passport_number', ''));
$dateOfBirth = trim((string) $request->post('date_of_birth', ''));
$gender = trim((string) $request->post('gender', ''));
$phone = trim((string) $request->post('phone', ''));
$email = trim((string) $request->post('email', ''));
$address = trim((string) $request->post('address', ''));
$guardianName = trim((string) $request->post('guardian_name', ''));
$guardianPhone = trim((string) $request->post('guardian_phone', ''));
$guardianNationalId = trim((string) $request->post('guardian_national_id', ''));
$guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$schoolName = trim((string) $request->post('school_name', ''));
$schoolGrade = trim((string) $request->post('school_grade', ''));
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
if ($fullNameAr === '' || mb_strlen($fullNameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
if ($dateOfBirth === '') {
$errors[] = 'تاريخ الميلاد مطلوب';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/players/' . $id . '/edit');
}
$player->update([
'full_name_ar' => $fullNameAr,
'full_name_en' => $fullNameEn ?: null,
'national_id' => $nationalId ?: null,
'passport_number' => $passportNumber ?: null,
'date_of_birth' => $dateOfBirth,
'gender' => $gender ?: null,
'phone' => $phone ?: null,
'email' => $email ?: null,
'address' => $address ?: null,
'guardian_name' => $guardianName ?: null,
'guardian_phone' => $guardianPhone ?: null,
'guardian_national_id' => $guardianNationalId ?: null,
'guardian_relationship' => $guardianRelationship ?: null,
'school_name' => $schoolName ?: null,
'school_grade' => $schoolGrade ?: null,
'notes' => $notes ?: null,
]);
return $this->redirect('/players/' . $id)->withSuccess('تم تحديث بيانات اللاعب بنجاح');
}
/**
* Activate a player's card.
*/
public function activateCard(Request $request, string $id): Response
{
try {
PlayerCardService::activateCard((int) $id);
} catch (\RuntimeException $e) {
return $this->redirect('/players/' . $id)->withError($e->getMessage());
}
return $this->redirect('/players/' . $id)->withSuccess('تم تفعيل الكارنيه بنجاح');
}
/**
* Suspend a player's card.
*/
public function suspendCard(Request $request, string $id): Response
{
$reason = trim((string) $request->post('reason', ''));
try {
PlayerCardService::suspendCard((int) $id, $reason ?: null);
} catch (\RuntimeException $e) {
return $this->redirect('/players/' . $id)->withError($e->getMessage());
}
return $this->redirect('/players/' . $id)->withSuccess('تم إيقاف الكارنيه');
}
/**
* Revoke a player's card.
*/
public function revokeCard(Request $request, string $id): Response
{
try {
PlayerCardService::revokeCard((int) $id);
} catch (\RuntimeException $e) {
return $this->redirect('/players/' . $id)->withError($e->getMessage());
}
return $this->redirect('/players/' . $id)->withSuccess('تم إلغاء الكارنيه');
}
/**
* Add a medical record for a player.
*/
public function addMedical(Request $request, string $id): Response
{
$data = [
'record_type' => trim((string) $request->post('record_type', '')),
'exam_date' => trim((string) $request->post('exam_date', '')),
'expiry_date' => trim((string) $request->post('expiry_date', '')) ?: null,
'doctor_name' => trim((string) $request->post('doctor_name', '')) ?: null,
'clinic_name' => trim((string) $request->post('clinic_name', '')) ?: null,
'result' => trim((string) $request->post('result', '')),
'restrictions' => trim((string) $request->post('restrictions', '')) ?: null,
'document_id' => $request->post('document_id') ? (int) $request->post('document_id') : null,
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
// Validation
$errors = [];
if (!array_key_exists($data['record_type'], PlayerMedicalRecord::getRecordTypes())) {
$errors[] = 'نوع السجل الطبي غير صالح';
}
if (empty($data['exam_date'])) {
$errors[] = 'تاريخ الفحص مطلوب';
}
if (!empty($data['result']) && !array_key_exists($data['result'], PlayerMedicalRecord::getResults())) {
$errors[] = 'نتيجة الفحص غير صالحة';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
return $this->redirect('/players/' . $id);
}
try {
MedicalRecordService::addRecord((int) $id, $data);
} catch (\RuntimeException $e) {
return $this->redirect('/players/' . $id)->withError($e->getMessage());
}
return $this->redirect('/players/' . $id)->withSuccess('تم إضافة السجل الطبي بنجاح');
}
/**
* Enroll a player in an academy.
*/
public function enroll(Request $request, string $id): Response
{
$player = Player::find((int) $id);
if (!$player) {
return $this->redirect('/players')->withError('اللاعب غير موجود');
}
$academyId = (int) $request->post('academy_id', 0);
$levelId = $request->post('level_id') ? (int) $request->post('level_id') : null;
$scheduleId = $request->post('schedule_id') ? (int) $request->post('schedule_id') : null;
$season = trim((string) $request->post('season', ''));
if ($academyId <= 0) {
return $this->redirect('/players/' . $id)->withError('يجب اختيار أكاديمية');
}
$employee = App::getInstance()->currentEmployee();
AcademyEnrollment::create([
'player_id' => (int) $id,
'academy_id' => $academyId,
'level_id' => $levelId,
'schedule_id' => $scheduleId,
'season' => $season ?: null,
'enrolled_at' => date('Y-m-d H:i:s'),
'enrollment_day' => (int) date('j'),
'status' => 'active',
'created_by' => $employee ? (int) $employee->id : null,
]);
return $this->redirect('/players/' . $id)->withSuccess('تم تسجيل اللاعب في الأكاديمية بنجاح');
}
/**
* Drop (withdraw) a player from an academy enrollment.
*/
public function dropEnrollment(Request $request, string $id): Response
{
$enrollmentId = (int) $request->post('enrollment_id', 0);
$droppedReason = trim((string) $request->post('dropped_reason', ''));
if ($enrollmentId <= 0) {
return $this->redirect('/players/' . $id)->withError('يجب تحديد التسجيل');
}
$enrollment = AcademyEnrollment::find($enrollmentId);
if (!$enrollment || (int) $enrollment->player_id !== (int) $id) {
return $this->redirect('/players/' . $id)->withError('التسجيل غير موجود');
}
$enrollment->update([
'status' => 'dropped',
'dropped_at' => date('Y-m-d H:i:s'),
'dropped_reason' => $droppedReason ?: null,
]);
return $this->redirect('/players/' . $id)->withSuccess('تم انسحاب اللاعب من الأكاديمية');
}
}
<?php
declare(strict_types=1);
/**
* Sports Activities Event Listeners
*
* Cross-module integration wiring for the sports system.
* Included from PlayerAffairs/bootstrap.php.
*/
use App\Core\EventBus;
use App\Core\App;
use App\Core\Logger;
// ─────────────────────────────────────────────────────────────
// payment.completed → auto-set registration_fee_paid,
// auto-confirm reservations,
// auto-update rental deposits,
// auto-pay activity subscriptions
// ─────────────────────────────────────────────────────────────
EventBus::listen('payment.completed', function (array $data) {
try {
$db = App::getInstance()->db();
$paymentType = $data['payment_type'] ?? '';
$paymentId = (int) ($data['payment_id'] ?? 0);
$playerId = (int) ($data['player_id'] ?? 0);
$ts = date('Y-m-d H:i:s');
// Sports registration fee payment → mark player as fee paid
if (in_array($paymentType, ['sports_reg_member', 'sports_reg_nonmember']) && $playerId > 0) {
$db->update('players', [
'registration_fee_paid' => 1,
'updated_at' => $ts,
], 'id = ?', [$playerId]);
Logger::info("Player #{$playerId} registration fee marked as paid");
}
// Facility reservation payment → auto-confirm reservation
if ($paymentType === 'facility_reservation' && $paymentId > 0) {
$reservation = $db->selectOne(
"SELECT id FROM reservations WHERE payment_id = ? AND status = 'pending'",
[$paymentId]
);
if ($reservation) {
$db->update('reservations', [
'status' => 'confirmed',
'confirmed_at' => $ts,
'updated_at' => $ts,
], 'id = ?', [(int) $reservation['id']]);
EventBus::dispatch('reservation.confirmed', [
'reservation_id' => (int) $reservation['id'],
'payment_id' => $paymentId,
]);
Logger::info("Reservation #{$reservation['id']} auto-confirmed after payment");
}
}
// Rental deposit payment → update deposit status
if ($paymentType === 'rental_deposit' && $paymentId > 0) {
$contract = $db->selectOne(
"SELECT id FROM rental_contracts WHERE deposit_payment_id = ? AND deposit_status = 'pending'",
[$paymentId]
);
if ($contract) {
$db->update('rental_contracts', [
'deposit_status' => 'collected',
'deposit_payment_id' => $paymentId,
'updated_at' => $ts,
], 'id = ?', [(int) $contract['id']]);
EventBus::dispatch('rental.deposit_collected', [
'contract_id' => (int) $contract['id'],
'payment_id' => $paymentId,
]);
Logger::info("Rental deposit collected for contract #{$contract['id']}");
}
}
// Activity subscription payment → mark sub as paid + activate card
if ($paymentType === 'activity_subscription' && $paymentId > 0) {
$sub = $db->selectOne(
"SELECT id, player_id FROM activity_subscriptions WHERE payment_id = ? AND status IN ('pending', 'overdue')",
[$paymentId]
);
if ($sub) {
$db->update('activity_subscriptions', [
'status' => 'paid',
'paid_at' => $ts,
'updated_at' => $ts,
], 'id = ?', [(int) $sub['id']]);
// Activate the player's card
$db->update('players', [
'card_status' => 'active',
'updated_at' => $ts,
], 'id = ? AND card_status != ?', [(int) $sub['player_id'], 'active']);
EventBus::dispatch('activity_sub.paid', [
'subscription_id' => (int) $sub['id'],
'player_id' => (int) $sub['player_id'],
'payment_id' => $paymentId,
]);
EventBus::dispatch('player.card_activated', [
'player_id' => (int) $sub['player_id'],
'reason' => 'سداد اشتراك النشاط الرياضي',
]);
Logger::info("Activity sub #{$sub['id']} paid, player #{$sub['player_id']} card activated");
}
}
} catch (\Throwable $e) {
Logger::error("Sports payment.completed handler error: " . $e->getMessage(), $data);
}
}, 50); // Priority 50 (lower than the main payment handler at 100)
// ─────────────────────────────────────────────────────────────
// academy.enrollment_created → auto-generate first month subscription
// ─────────────────────────────────────────────────────────────
EventBus::listen('academy.enrollment_created', function (array $data) {
try {
$db = App::getInstance()->db();
$enrollmentId = (int) ($data['enrollment_id'] ?? 0);
$playerId = (int) ($data['player_id'] ?? 0);
$academyId = (int) ($data['academy_id'] ?? 0);
$ts = date('Y-m-d H:i:s');
if ($enrollmentId <= 0 || $playerId <= 0 || $academyId <= 0) return;
$enrollment = $db->selectOne("SELECT * FROM academy_enrollments WHERE id = ?", [$enrollmentId]);
if (!$enrollment) return;
$player = $db->selectOne("SELECT player_type FROM players WHERE id = ?", [$playerId]);
if (!$player) return;
$academy = $db->selectOne("SELECT discipline_id FROM academies WHERE id = ?", [$academyId]);
if (!$academy) return;
$month = date('Y-m');
$enrollmentDay = (int) ($enrollment['enrollment_day'] ?? (int) date('j'));
// Check if subscription already exists
$existing = $db->selectOne(
"SELECT id FROM activity_subscriptions WHERE player_id = ? AND enrollment_id = ? AND subscription_month = ?",
[$playerId, $enrollmentId, $month]
);
if ($existing) return;
// Look up pricing
$isMember = ($player['player_type'] === 'member');
$rateCol = $isMember ? 'member_rate' : 'nonmember_rate';
$pricing = $db->selectOne(
"SELECT {$rateCol} AS rate FROM activity_pricing WHERE pricing_type = 'academy' AND reference_id = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE())",
[$academyId]
);
if (!$pricing) {
$pricing = $db->selectOne(
"SELECT {$rateCol} AS rate FROM activity_pricing WHERE pricing_type = 'discipline' AND reference_id = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE())",
[(int) $academy['discipline_id']]
);
}
$baseRate = $pricing ? (string) $pricing['rate'] : '0.00';
$isHalfMonth = ($enrollmentDay > 15) ? 1 : 0;
$appliedRate = $isHalfMonth ? bcdiv($baseRate, '2', 2) : $baseRate;
$dueDate = date('Y-m-07');
$subId = $db->insert('activity_subscriptions', [
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'discipline_id' => (int) $academy['discipline_id'],
'subscription_month' => $month,
'player_type' => $player['player_type'],
'base_rate' => $baseRate,
'is_half_month' => $isHalfMonth,
'applied_rate' => $appliedRate,
'discount' => '0.00',
'total_amount' => $appliedRate,
'status' => 'pending',
'due_date' => $dueDate,
'created_at' => $ts,
'updated_at' => $ts,
]);
EventBus::dispatch('activity_sub.generated', [
'subscription_id' => $subId,
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'month' => $month,
'amount' => $appliedRate,
'is_half_month' => $isHalfMonth,
]);
Logger::info("Auto-generated activity sub #{$subId} for enrollment #{$enrollmentId} (half_month={$isHalfMonth})");
} catch (\Throwable $e) {
Logger::error("academy.enrollment_created handler error: " . $e->getMessage(), $data);
}
});
// ─────────────────────────────────────────────────────────────
// academy.enrollment_dropped → cancel pending unpaid subscriptions
// ─────────────────────────────────────────────────────────────
EventBus::listen('academy.enrollment_dropped', function (array $data) {
try {
$db = App::getInstance()->db();
$enrollmentId = (int) ($data['enrollment_id'] ?? 0);
$ts = date('Y-m-d H:i:s');
if ($enrollmentId <= 0) return;
// Cancel all pending (unpaid) subscriptions for this enrollment
$pending = $db->select(
"SELECT id, player_id, subscription_month FROM activity_subscriptions WHERE enrollment_id = ? AND status IN ('pending', 'overdue')",
[$enrollmentId]
);
foreach ($pending as $sub) {
$db->update('activity_subscriptions', [
'status' => 'revoked',
'revoked_at' => $ts,
'notes' => 'تم الإلغاء بسبب الانسحاب من الأكاديمية',
'updated_at' => $ts,
], 'id = ?', [(int) $sub['id']]);
Logger::info("Activity sub #{$sub['id']} cancelled due to enrollment #{$enrollmentId} drop");
}
} catch (\Throwable $e) {
Logger::error("academy.enrollment_dropped handler error: " . $e->getMessage(), $data);
}
});
// ─────────────────────────────────────────────────────────────
// activity_sub.paid → activate player card (backup if not already activated)
// ─────────────────────────────────────────────────────────────
EventBus::listen('activity_sub.paid', function (array $data) {
try {
$db = App::getInstance()->db();
$playerId = (int) ($data['player_id'] ?? 0);
$ts = date('Y-m-d H:i:s');
if ($playerId <= 0) return;
$player = $db->selectOne("SELECT card_status FROM players WHERE id = ?", [$playerId]);
if ($player && $player['card_status'] !== 'active') {
$db->update('players', [
'card_status' => 'active',
'updated_at' => $ts,
], 'id = ?', [$playerId]);
EventBus::dispatch('player.card_activated', [
'player_id' => $playerId,
'reason' => 'سداد اشتراك النشاط',
]);
}
} catch (\Throwable $e) {
Logger::error("activity_sub.paid card activation error: " . $e->getMessage());
}
});
// ─────────────────────────────────────────────────────────────
// player.registered → dispatch for SMS notification
// ─────────────────────────────────────────────────────────────
EventBus::listen('player.registered', function (array $data) {
try {
$playerId = (int) ($data['player_id'] ?? 0);
$playerName = $data['player_name'] ?? '';
$serial = $data['registration_serial'] ?? '';
$phone = $data['phone'] ?? '';
if ($phone && $playerName && $serial) {
EventBus::dispatchAsync('sms.send', [
'template_code' => 'SMS_SPORTS_REGISTRATION',
'phone' => $phone,
'variables' => [
'player_name' => $playerName,
'registration_serial' => $serial,
],
]);
}
} catch (\Throwable $e) {
Logger::error("player.registered SMS handler error: " . $e->getMessage());
}
});
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Models;
use App\Core\Model;
class AcademyEnrollment extends Model
{
protected static string $table = 'academy_enrollments';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static array $fillable = [
'player_id',
'academy_id',
'level_id',
'schedule_id',
'season',
'enrolled_at',
'enrollment_day',
'status',
'promoted_from_id',
'dropped_at',
'dropped_reason',
'created_by',
];
/**
* Get enrollment statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'active' => 'فعال',
'suspended' => 'موقوف',
'dropped' => 'منسحب',
'promoted' => 'ترقية',
'completed' => 'مكتمل',
];
}
/**
* Get the Arabic label for a status.
*/
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a status.
*/
public static function getStatusColor(string $status): string
{
$colors = [
'active' => '#059669',
'suspended' => '#D97706',
'dropped' => '#DC2626',
'promoted' => '#7C3AED',
'completed' => '#0284C7',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Get all enrollments for a specific player.
*/
public static function getForPlayer(int $playerId): array
{
return static::query()
->where('player_id', '=', $playerId)
->orderBy('created_at', 'DESC')
->get();
}
/**
* Get all enrollments for a specific academy.
*/
public static function getForAcademy(int $academyId): array
{
return static::query()
->where('academy_id', '=', $academyId)
->orderBy('created_at', 'DESC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Models;
use App\Core\Model;
use App\Core\App;
class Player extends Model
{
protected static string $table = 'players';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'player_type',
'member_id',
'activity_id_number',
'registration_serial',
'full_name_ar',
'full_name_en',
'national_id',
'passport_number',
'date_of_birth',
'gender',
'phone',
'email',
'address',
'guardian_name',
'guardian_phone',
'guardian_national_id',
'guardian_relationship',
'photo_path',
'medical_status',
'medical_expiry_date',
'school_name',
'school_grade',
'registration_fee_paid',
'card_status',
'access_window_minutes',
'notes',
'branch_id',
'is_archived',
'archived_at',
'archived_by',
];
/**
* Get player types with Arabic labels.
*/
public static function getPlayerTypes(): array
{
return [
'member' => 'عضو',
'non_member' => 'غير عضو',
];
}
/**
* Get genders with Arabic labels.
*/
public static function getGenders(): array
{
return [
'male' => 'ذكر',
'female' => 'أنثى',
];
}
/**
* Get card statuses with Arabic labels.
*/
public static function getCardStatuses(): array
{
return [
'inactive' => 'غير فعال',
'active' => 'فعال',
'suspended' => 'موقوف',
'revoked' => 'ملغى',
];
}
/**
* Get medical statuses with Arabic labels.
*/
public static function getMedicalStatuses(): array
{
return [
'pending' => 'قيد المراجعة',
'fit' => 'لائق',
'conditional' => 'لائق بشروط',
'unfit' => 'غير لائق',
'expired' => 'منتهي',
];
}
/**
* Get guardian relationships with Arabic labels.
*/
public static function getGuardianRelationships(): array
{
return [
'father' => 'الأب',
'mother' => 'الأم',
'brother' => 'الأخ',
'sister' => 'الأخت',
'other' => 'أخرى',
];
}
/**
* Get the Arabic label for a card status.
*/
public static function getCardStatusLabel(string $status): string
{
$statuses = self::getCardStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a card status.
*/
public static function getCardStatusColor(string $status): string
{
$colors = [
'inactive' => '#6B7280',
'active' => '#059669',
'suspended' => '#D97706',
'revoked' => '#DC2626',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Get the Arabic label for a medical status.
*/
public static function getMedicalStatusLabel(string $status): string
{
$statuses = self::getMedicalStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a medical status.
*/
public static function getMedicalStatusColor(string $status): string
{
$colors = [
'pending' => '#6B7280',
'fit' => '#059669',
'conditional' => '#D97706',
'unfit' => '#DC2626',
'expired' => '#9CA3AF',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Calculate age from date_of_birth.
*/
public function getAge(): ?int
{
if (empty($this->date_of_birth)) {
return null;
}
$dob = new \DateTime($this->date_of_birth);
$now = new \DateTime();
return (int) $dob->diff($now)->y;
}
/**
* Check if the player is a minor (under 18).
*/
public function isMinor(): bool
{
$age = $this->getAge();
return $age !== null && $age < 18;
}
/**
* Get all active (non-archived) players ordered by name.
*/
public static function allActive(): array
{
return static::query()
->where('is_archived', '=', 0)
->orderBy('full_name_ar', 'ASC')
->get();
}
/**
* Search players with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$joins = '';
$where = ['p.is_archived = 0'];
$params = [];
// Text search: name, national_id, registration_serial
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where[] = '(p.full_name_ar LIKE ? OR p.full_name_en LIKE ? OR p.national_id LIKE ? OR p.registration_serial LIKE ?)';
$params[] = $search;
$params[] = $search;
$params[] = $search;
$params[] = $search;
}
// Player type filter
if (!empty($filters['player_type'])) {
$where[] = 'p.player_type = ?';
$params[] = $filters['player_type'];
}
// Card status filter
if (!empty($filters['card_status'])) {
$where[] = 'p.card_status = ?';
$params[] = $filters['card_status'];
}
// Medical status filter
if (!empty($filters['medical_status'])) {
$where[] = 'p.medical_status = ?';
$params[] = $filters['medical_status'];
}
// Discipline filter (join player_disciplines)
if (!empty($filters['discipline_id'])) {
$joins .= ' INNER JOIN player_disciplines pd ON pd.player_id = p.id';
$where[] = 'pd.discipline_id = ?';
$params[] = (int) $filters['discipline_id'];
}
// Academy filter (join academy_enrollments)
if (!empty($filters['academy_id'])) {
$joins .= ' INNER JOIN academy_enrollments ae ON ae.player_id = p.id';
$where[] = 'ae.academy_id = ?';
$params[] = (int) $filters['academy_id'];
}
$whereClause = implode(' AND ', $where);
// Count total
$countSql = "SELECT COUNT(DISTINCT p.id) as total FROM players p {$joins} WHERE {$whereClause}";
$countRow = $db->selectOne($countSql, $params);
$total = (int) ($countRow['total'] ?? 0);
// Pagination
$lastPage = max(1, (int) ceil($total / $perPage));
$page = min($page, $lastPage);
$offset = ($page - 1) * $perPage;
// Fetch data
$dataSql = "SELECT DISTINCT p.* FROM players p {$joins} WHERE {$whereClause} ORDER BY p.full_name_ar ASC LIMIT {$perPage} OFFSET {$offset}";
$data = $db->select($dataSql, $params);
return [
'data' => $data,
'pagination' => [
'current_page' => $page,
'last_page' => $lastPage,
'per_page' => $perPage,
'total' => $total,
],
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Models;
use App\Core\Model;
class PlayerDiscipline extends Model
{
protected static string $table = 'player_disciplines';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'player_id',
'discipline_id',
'age_group',
'skill_level',
'season',
'status',
'registered_at',
];
/**
* Get discipline statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'active' => 'فعال',
'inactive' => 'غير فعال',
'transferred' => 'محوّل',
];
}
/**
* Get all disciplines for a specific player.
*/
public static function getForPlayer(int $playerId): array
{
return static::query()
->where('player_id', '=', $playerId)
->orderBy('created_at', 'DESC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Models;
use App\Core\Model;
class PlayerMedicalRecord extends Model
{
protected static string $table = 'player_medical_records';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'player_id',
'record_type',
'exam_date',
'expiry_date',
'doctor_name',
'clinic_name',
'result',
'restrictions',
'document_id',
'notes',
'created_by',
];
/**
* Get record types with Arabic labels.
*/
public static function getRecordTypes(): array
{
return [
'fitness_cert' => 'شهادة لياقة',
'medical_exam' => 'فحص طبي',
'blood_test' => 'تحليل دم',
'cardiac_screen' => 'فحص قلب',
'other' => 'أخرى',
];
}
/**
* Get medical results with Arabic labels.
*/
public static function getResults(): array
{
return [
'fit' => 'لائق',
'conditional' => 'لائق بشروط',
'unfit' => 'غير لائق',
];
}
/**
* Get all medical records for a specific player, ordered by exam_date DESC.
*/
public static function getForPlayer(int $playerId): array
{
return static::query()
->where('player_id', '=', $playerId)
->orderBy('exam_date', 'DESC')
->get();
}
}
<?php
declare(strict_types=1);
return [
['GET', '/players', 'PlayerAffairs\Controllers\PlayerController@index', ['auth'], 'player.view'],
['GET', '/players/create', 'PlayerAffairs\Controllers\PlayerController@create', ['auth'], 'player.register'],
['POST', '/players', 'PlayerAffairs\Controllers\PlayerController@store', ['auth', 'csrf'], 'player.register'],
['GET', '/players/{id:\d+}', 'PlayerAffairs\Controllers\PlayerController@show', ['auth'], 'player.view'],
['GET', '/players/{id:\d+}/edit', 'PlayerAffairs\Controllers\PlayerController@edit', ['auth'], 'player.edit'],
['POST', '/players/{id:\d+}', 'PlayerAffairs\Controllers\PlayerController@update', ['auth', 'csrf'], 'player.edit'],
['POST', '/players/{id:\d+}/card/activate', 'PlayerAffairs\Controllers\PlayerController@activateCard', ['auth', 'csrf'], 'player.manage_card'],
['POST', '/players/{id:\d+}/card/suspend', 'PlayerAffairs\Controllers\PlayerController@suspendCard', ['auth', 'csrf'], 'player.manage_card'],
['POST', '/players/{id:\d+}/card/revoke', 'PlayerAffairs\Controllers\PlayerController@revokeCard', ['auth', 'csrf'], 'player.manage_card'],
['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'],
];
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\PlayerAffairs\Models\Player;
use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord;
final class MedicalRecordService
{
/**
* Add a medical record for a player and update their medical status.
*/
public static function addRecord(int $playerId, array $data): PlayerMedicalRecord
{
$player = Player::find($playerId);
if (!$player) {
throw new \RuntimeException('اللاعب غير موجود');
}
$employee = App::getInstance()->currentEmployee();
$record = PlayerMedicalRecord::create([
'player_id' => $playerId,
'record_type' => $data['record_type'] ?? 'medical_exam',
'exam_date' => $data['exam_date'] ?? date('Y-m-d'),
'expiry_date' => $data['expiry_date'] ?? null,
'doctor_name' => $data['doctor_name'] ?? null,
'clinic_name' => $data['clinic_name'] ?? null,
'result' => $data['result'] ?? 'pending',
'restrictions' => $data['restrictions'] ?? null,
'document_id' => $data['document_id'] ?? null,
'notes' => $data['notes'] ?? null,
'created_by' => $employee ? (int) $employee->id : null,
]);
// Update the player's medical status and expiry based on the new record
$updateData = [];
if (!empty($data['result'])) {
$updateData['medical_status'] = $data['result'];
}
if (!empty($data['expiry_date'])) {
$updateData['medical_expiry_date'] = $data['expiry_date'];
}
if (!empty($updateData)) {
$player->update($updateData);
}
EventBus::dispatch('player.medical_updated', [
'player_id' => $playerId,
'record_id' => $record->id,
]);
return $record;
}
/**
* Check if a player has valid medical clearance.
*/
public static function checkClearance(int $playerId): bool
{
$player = Player::find($playerId);
if (!$player) {
return false;
}
$validStatuses = ['fit', 'conditional'];
if (!in_array($player->medical_status, $validStatuses, true)) {
return false;
}
if (empty($player->medical_expiry_date)) {
return false;
}
return $player->medical_expiry_date > date('Y-m-d');
}
/**
* Get players whose medical records expire within the given number of days.
*/
public static function getExpiringRecords(int $daysAhead = 30): array
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
$futureDate = date('Y-m-d', strtotime("+{$daysAhead} days"));
return $db->select(
"SELECT * FROM players
WHERE is_archived = 0
AND medical_expiry_date IS NOT NULL
AND medical_expiry_date BETWEEN ? AND ?
ORDER BY medical_expiry_date ASC",
[$today, $futureDate]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Services;
use App\Core\EventBus;
use App\Modules\PlayerAffairs\Models\Player;
final class PlayerCardService
{
/**
* Activate a player's card.
*/
public static function activateCard(int $playerId): void
{
$player = Player::find($playerId);
if (!$player) {
throw new \RuntimeException('اللاعب غير موجود');
}
$player->update(['card_status' => 'active']);
EventBus::dispatch('player.card_activated', ['player_id' => $playerId]);
}
/**
* Suspend a player's card.
*/
public static function suspendCard(int $playerId, ?string $reason = null): void
{
$player = Player::find($playerId);
if (!$player) {
throw new \RuntimeException('اللاعب غير موجود');
}
$player->update(['card_status' => 'suspended']);
EventBus::dispatch('player.card_suspended', [
'player_id' => $playerId,
'reason' => $reason,
]);
}
/**
* Revoke a player's card.
*/
public static function revokeCard(int $playerId): void
{
$player = Player::find($playerId);
if (!$player) {
throw new \RuntimeException('اللاعب غير موجود');
}
$player->update(['card_status' => 'revoked']);
EventBus::dispatch('player.card_revoked', ['player_id' => $playerId]);
}
/**
* Check if a player's card is active.
*/
public static function isCardActive(int $playerId): bool
{
$player = Player::find($playerId);
if (!$player) {
return false;
}
return $player->card_status === 'active';
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Services;
use App\Core\App;
use App\Modules\PlayerAffairs\Models\Player;
use App\Modules\PlayerAffairs\Models\PlayerDiscipline;
use App\Modules\Members\Models\Member;
use App\Modules\Rules\Services\RuleEngine;
final class PlayerRegistrationService
{
/**
* Register a player from an existing club member.
*/
public static function registerMemberPlayer(int $memberId, array $data): Player
{
$member = Member::find($memberId);
if (!$member) {
throw new \RuntimeException('العضو غير موجود');
}
$serial = self::nextSerial('member');
$playerData = array_merge($data, [
'player_type' => 'member',
'member_id' => $memberId,
'registration_serial' => $serial,
'full_name_ar' => $data['full_name_ar'] ?? $member->full_name_ar,
'full_name_en' => $data['full_name_en'] ?? $member->full_name_en,
'national_id' => $data['national_id'] ?? $member->national_id,
'date_of_birth' => $data['date_of_birth'] ?? $member->date_of_birth,
'gender' => $data['gender'] ?? $member->gender,
'phone' => $data['phone'] ?? $member->phone_mobile,
'email' => $data['email'] ?? $member->email,
'card_status' => 'inactive',
'medical_status' => 'pending',
'is_archived' => 0,
]);
return Player::create($playerData);
}
/**
* Register a non-member player from direct input.
*/
public static function registerNonMemberPlayer(array $data): Player
{
$serial = self::nextSerial('non_member');
$playerData = array_merge($data, [
'player_type' => 'non_member',
'member_id' => null,
'registration_serial' => $serial,
'access_window_minutes' => $data['access_window_minutes'] ?? 30,
'card_status' => 'inactive',
'medical_status' => 'pending',
'is_archived' => 0,
]);
return Player::create($playerData);
}
/**
* Generate the next registration serial for the given player type.
*/
public static function nextSerial(string $type): string
{
$ruleCode = $type === 'member'
? 'SPORTS_REG_SERIAL_START_MEMBER'
: 'SPORTS_REG_SERIAL_START_NONMEMBER';
$ruleValue = RuleEngine::getValue($ruleCode, 'value');
$start = $ruleValue !== null ? (int) $ruleValue : 1;
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT MAX(CAST(registration_serial AS UNSIGNED)) as max_serial FROM players WHERE player_type = ?",
[$type]
);
$maxSerial = (int) ($row['max_serial'] ?? 0);
return (string) max($start, $maxSerial + 1);
}
/**
* Add a discipline to a player.
*/
public static function addDiscipline(int $playerId, int $disciplineId, ?string $season = null): PlayerDiscipline
{
return PlayerDiscipline::create([
'player_id' => $playerId,
'discipline_id' => $disciplineId,
'season' => $season,
'status' => 'active',
'registered_at' => date('Y-m-d H:i:s'),
]);
}
/**
* Quick search for autocomplete: players matching name/national_id/serial.
*/
public static function search(string $query): array
{
$db = App::getInstance()->db();
$search = '%' . $query . '%';
return $db->select(
"SELECT id, full_name_ar, full_name_en, national_id, registration_serial, player_type
FROM players
WHERE is_archived = 0
AND (full_name_ar LIKE ? OR full_name_en LIKE ? OR national_id LIKE ? OR registration_serial LIKE ?)
ORDER BY full_name_ar ASC
LIMIT 10",
[$search, $search, $search, $search]
);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تسجيل لاعب جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/players" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/players" id="playerForm">
<?= csrf_field() ?>
<!-- Section 1: Basic Data -->
<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="user" 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;gap:20px;">
<div class="form-group">
<label class="form-label">نوع اللاعب <span style="color:#DC2626;">*</span></label>
<select name="player_type" id="playerType" class="form-input" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($playerTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('player_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" id="memberIdGroup" style="display:none;">
<label class="form-label">رقم العضوية</label>
<input type="number" name="member_id" value="<?= e(old('member_id')) ?>" class="form-input" min="1" style="direction:ltr;text-align:left;" placeholder="رقم العضوية في النادي">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e(old('full_name_ar')) ?>" class="form-input" required placeholder="الاسم رباعي بالعربي" style="font-size:16px;">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (إنجليزي)</label>
<input type="text" name="full_name_en" value="<?= e(old('full_name_en')) ?>" class="form-input" placeholder="Full name in English" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="أدخل 14 رقم">
</div>
<div class="form-group">
<label class="form-label">رقم جواز السفر</label>
<input type="text" name="passport_number" value="<?= e(old('passport_number')) ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="رقم جواز السفر (اختياري)">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($genders as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('gender') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Section 2: Contact & Guardian -->
<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="phone" 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;gap:20px;">
<div class="form-group">
<label class="form-label">رقم الهاتف</label>
<input type="tel" name="phone" value="<?= e(old('phone')) ?>" class="form-input" maxlength="15" style="direction:ltr;text-align:left;" placeholder="01XXXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e(old('email')) ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="email@example.com">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">العنوان</label>
<textarea name="address" class="form-input" rows="2" placeholder="عنوان السكن..."><?= e(old('address')) ?></textarea>
</div>
</div>
<div style="border-top:1px solid #E5E7EB;margin:20px 0;padding-top:20px;">
<h4 style="margin:0 0 15px;color:#374151;font-size:14px;"><i data-lucide="shield" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> بيانات ولي الأمر</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اسم ولي الأمر</label>
<input type="text" name="guardian_name" value="<?= e(old('guardian_name')) ?>" class="form-input" placeholder="الاسم الكامل لولي الأمر">
</div>
<div class="form-group">
<label class="form-label">هاتف ولي الأمر</label>
<input type="tel" name="guardian_phone" value="<?= e(old('guardian_phone')) ?>" class="form-input" maxlength="15" style="direction:ltr;text-align:left;" placeholder="01XXXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي لولي الأمر</label>
<input type="text" name="guardian_national_id" value="<?= e(old('guardian_national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;" placeholder="14 رقم">
</div>
<div class="form-group">
<label class="form-label">صلة القرابة</label>
<select name="guardian_relationship" class="form-input">
<option value="">-- اختر --</option>
<?php foreach ($relationships as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('guardian_relationship') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div style="border-top:1px solid #E5E7EB;margin:20px 0;padding-top:20px;">
<h4 style="margin:0 0 15px;color:#374151;font-size:14px;"><i data-lucide="graduation-cap" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> البيانات التعليمية</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اسم المدرسة</label>
<input type="text" name="school_name" value="<?= e(old('school_name')) ?>" class="form-input" placeholder="اسم المدرسة">
</div>
<div class="form-group">
<label class="form-label">الصف الدراسي</label>
<input type="text" name="school_grade" value="<?= e(old('school_grade')) ?>" class="form-input" placeholder="مثال: الصف الخامس الابتدائي">
</div>
</div>
</div>
<div class="form-group" style="margin-top:10px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="أي ملاحظات إضافية..."><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تسجيل اللاعب
</button>
<a href="/players" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var playerTypeSelect = document.getElementById('playerType');
var memberIdGroup = document.getElementById('memberIdGroup');
function toggleMemberId() {
if (playerTypeSelect.value === 'member') {
memberIdGroup.style.display = '';
} else {
memberIdGroup.style.display = 'none';
}
}
playerTypeSelect.addEventListener('change', toggleMemberId);
toggleMemberId();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل: <?= e($player->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/players/<?= (int) $player->id ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للملف</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/players/<?= (int) $player->id ?>" id="playerEditForm">
<?= csrf_field() ?>
<!-- Section 1: Basic Data -->
<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="user" 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;gap:20px;">
<div class="form-group">
<label class="form-label">نوع اللاعب</label>
<select name="player_type" class="form-input" disabled style="background:#F3F4F6;cursor:not-allowed;">
<?php foreach ($playerTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($player->player_type ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
<input type="hidden" name="player_type" value="<?= e($player->player_type ?? '') ?>">
<small style="color:#9CA3AF;font-size:11px;">لا يمكن تغيير نوع اللاعب بعد التسجيل</small>
</div>
<?php if (($player->player_type ?? '') === 'member'): ?>
<div class="form-group">
<label class="form-label">رقم العضوية</label>
<input type="number" name="member_id" value="<?= e(old('member_id') ?: ($player->member_id ?? '')) ?>" class="form-input" min="1" style="direction:ltr;text-align:left;" placeholder="رقم العضوية في النادي">
</div>
<?php endif; ?>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (عربي) <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e(old('full_name_ar') ?: ($player->full_name_ar ?? '')) ?>" class="form-input" required placeholder="الاسم رباعي بالعربي" style="font-size:16px;">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (إنجليزي)</label>
<input type="text" name="full_name_en" value="<?= e(old('full_name_en') ?: ($player->full_name_en ?? '')) ?>" class="form-input" placeholder="Full name in English" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?: ($player->national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="أدخل 14 رقم">
</div>
<div class="form-group">
<label class="form-label">رقم جواز السفر</label>
<input type="text" name="passport_number" value="<?= e(old('passport_number') ?: ($player->passport_number ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="رقم جواز السفر (اختياري)">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?: ($player->date_of_birth ?? '')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="gender" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($genders as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('gender') ?: ($player->gender ?? '')) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Section 2: Contact & Guardian -->
<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="phone" 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;gap:20px;">
<div class="form-group">
<label class="form-label">رقم الهاتف</label>
<input type="tel" name="phone" value="<?= e(old('phone') ?: ($player->phone ?? '')) ?>" class="form-input" maxlength="15" style="direction:ltr;text-align:left;" placeholder="01XXXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e(old('email') ?: ($player->email ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="email@example.com">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">العنوان</label>
<textarea name="address" class="form-input" rows="2" placeholder="عنوان السكن..."><?= e(old('address') ?: ($player->address ?? '')) ?></textarea>
</div>
</div>
<div style="border-top:1px solid #E5E7EB;margin:20px 0;padding-top:20px;">
<h4 style="margin:0 0 15px;color:#374151;font-size:14px;"><i data-lucide="shield" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> بيانات ولي الأمر</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اسم ولي الأمر</label>
<input type="text" name="guardian_name" value="<?= e(old('guardian_name') ?: ($player->guardian_name ?? '')) ?>" class="form-input" placeholder="الاسم الكامل لولي الأمر">
</div>
<div class="form-group">
<label class="form-label">هاتف ولي الأمر</label>
<input type="tel" name="guardian_phone" value="<?= e(old('guardian_phone') ?: ($player->guardian_phone ?? '')) ?>" class="form-input" maxlength="15" style="direction:ltr;text-align:left;" placeholder="01XXXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي لولي الأمر</label>
<input type="text" name="guardian_national_id" value="<?= e(old('guardian_national_id') ?: ($player->guardian_national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;" placeholder="14 رقم">
</div>
<div class="form-group">
<label class="form-label">صلة القرابة</label>
<select name="guardian_relationship" class="form-input">
<option value="">-- اختر --</option>
<?php foreach ($relationships as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('guardian_relationship') ?: ($player->guardian_relationship ?? '')) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<div style="border-top:1px solid #E5E7EB;margin:20px 0;padding-top:20px;">
<h4 style="margin:0 0 15px;color:#374151;font-size:14px;"><i data-lucide="graduation-cap" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> البيانات التعليمية</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اسم المدرسة</label>
<input type="text" name="school_name" value="<?= e(old('school_name') ?: ($player->school_name ?? '')) ?>" class="form-input" placeholder="اسم المدرسة">
</div>
<div class="form-group">
<label class="form-label">الصف الدراسي</label>
<input type="text" name="school_grade" value="<?= e(old('school_grade') ?: ($player->school_grade ?? '')) ?>" class="form-input" placeholder="مثال: الصف الخامس الابتدائي">
</div>
</div>
</div>
<div class="form-group" style="margin-top:10px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="أي ملاحظات إضافية..."><?= e(old('notes') ?: ($player->notes ?? '')) ?></textarea>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;justify-content:flex-start;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات
</button>
<a href="/players/<?= (int) $player->id ?>" class="btn btn-outline" style="padding:12px 30px;font-size:15px;">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\PlayerAffairs\Models\Player;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>شئون اللاعبين<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/players/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تسجيل لاعب جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentType = $filters['player_type'] ?? '';
$currentCardStatus = $filters['card_status'] ?? '';
$allPlayerTypes = Player::getPlayerTypes();
$allCardStatuses = Player::getCardStatuses();
?>
<!-- Player Type Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/players<?= ($filters['q'] ?? '') !== '' ? '?q=' . urlencode($filters['q']) : '' ?><?= $currentCardStatus !== '' ? (($filters['q'] ?? '') !== '' ? '&' : '?') . 'card_status=' . urlencode($currentCardStatus) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentType === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allPlayerTypes as $typeKey => $typeLabel): ?>
<?php
$tabParams = [];
if (($filters['q'] ?? '') !== '') $tabParams['q'] = $filters['q'];
$tabParams['player_type'] = $typeKey;
if ($currentCardStatus !== '') $tabParams['card_status'] = $currentCardStatus;
$tabQuery = http_build_query($tabParams);
?>
<a href="/players?<?= $tabQuery ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === $typeKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentType === $typeKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($typeLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search & Filter Bar -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/players" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم، الرقم القومي، رقم التسلسل..." class="form-input" style="min-width:250px;">
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">حالة الكارنيه</label>
<select name="card_status" class="form-input">
<option value="">الكل</option>
<?php foreach ($allCardStatuses as $csKey => $csLabel): ?>
<option value="<?= e($csKey) ?>" <?= $currentCardStatus === $csKey ? 'selected' : '' ?>><?= e($csLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentType !== ''): ?>
<input type="hidden" name="player_type" value="<?= e($currentType) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/players" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Players Table -->
<?php if (!empty($players)): ?>
<div class="card" style="padding:0;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">#</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">رقم التسلسل</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">الاسم</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">النوع</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">الرقم القومي</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">الهاتف</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">حالة الكارنيه</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">الحالة الطبية</th>
<th style="padding:12px 15px;text-align:right;font-weight:600;color:#374151;white-space:nowrap;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php
$startIndex = (($pagination['current_page'] ?? 1) - 1) * ($pagination['per_page'] ?? 25);
foreach ($players as $i => $p):
$cardStatus = $p['card_status'] ?? 'inactive';
$medicalStatus = $p['medical_status'] ?? 'pending';
$cardColor = Player::getCardStatusColor($cardStatus);
$cardLabel = Player::getCardStatusLabel($cardStatus);
$medicalColor = Player::getMedicalStatusColor($medicalStatus);
$medicalLabel = Player::getMedicalStatusLabel($medicalStatus);
$playerType = $p['player_type'] ?? '';
$typeLabel = ($allPlayerTypes[$playerType] ?? $playerType);
$typeColor = $playerType === 'member' ? '#0284C7' : '#7C3AED';
?>
<tr style="border-bottom:1px solid #F3F4F6;transition:background 0.15s;" onmouseover="this.style.background='#F9FAFB'" onmouseout="this.style.background='transparent'">
<td style="padding:12px 15px;color:#6B7280;"><?= $startIndex + $i + 1 ?></td>
<td style="padding:12px 15px;">
<code style="font-size:12px;color:#6B7280;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($p['registration_serial'] ?? '—') ?></code>
</td>
<td style="padding:12px 15px;">
<div style="font-weight:600;color:#1A1A2E;"><?= e($p['full_name_ar'] ?? '') ?></div>
<?php if (!empty($p['full_name_en'])): ?>
<div style="font-size:12px;color:#9CA3AF;"><?= e($p['full_name_en']) ?></div>
<?php endif; ?>
</td>
<td style="padding:12px 15px;">
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $typeColor ?>15;color:<?= $typeColor ?>;"><?= e($typeLabel) ?></span>
</td>
<td style="padding:12px 15px;color:#374151;font-size:13px;"><?= e($p['national_id'] ?? '—') ?></td>
<td style="padding:12px 15px;color:#374151;font-size:13px;" dir="ltr"><?= e($p['phone'] ?? '—') ?></td>
<td style="padding:12px 15px;">
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $cardColor ?>15;color:<?= $cardColor ?>;"><?= e($cardLabel) ?></span>
</td>
<td style="padding:12px 15px;">
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $medicalColor ?>15;color:<?= $medicalColor ?>;"><?= e($medicalLabel) ?></span>
</td>
<td style="padding:12px 15px;white-space:nowrap;">
<div style="display:flex;gap:6px;">
<a href="/players/<?= (int) $p['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
<a href="/players/<?= (int) $p['id'] ?>/edit" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="edit-3" style="width:13px;height:13px;vertical-align:middle;"></i> تعديل
</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="users" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد لاعبون</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['player_type']) || !empty($filters['card_status'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بتسجيل لاعب جديد في النادي.
<?php endif; ?>
</p>
<a href="/players/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تسجيل لاعب جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\PlayerAffairs\Models\Player;
use App\Modules\PlayerAffairs\Models\PlayerMedicalRecord;
use App\Modules\PlayerAffairs\Models\AcademyEnrollment;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($player->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/players/<?= (int) $player->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/players" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$cardStatus = $player->card_status ?? 'inactive';
$medicalStatus = $player->medical_status ?? 'pending';
$cardColor = Player::getCardStatusColor($cardStatus);
$cardLabel = Player::getCardStatusLabel($cardStatus);
$medicalColor = Player::getMedicalStatusColor($medicalStatus);
$medicalLabel = Player::getMedicalStatusLabel($medicalStatus);
$playerType = $player->player_type ?? '';
$typeLabel = ($playerTypes[$playerType] ?? $playerType);
$typeColor = $playerType === 'member' ? '#0284C7' : '#7C3AED';
$age = $player->getAge();
$recordTypes = PlayerMedicalRecord::getRecordTypes();
$resultTypes = PlayerMedicalRecord::getResults();
$enrollmentStatuses = AcademyEnrollment::getStatuses();
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;padding:25px;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;">
<h2 style="margin:0;font-size:22px;font-weight:700;color:#1A1A2E;"><?= e($player->full_name_ar) ?></h2>
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $typeColor ?>15;color:<?= $typeColor ?>;"><?= e($typeLabel) ?></span>
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $cardColor ?>15;color:<?= $cardColor ?>;"><?= e($cardLabel) ?></span>
</div>
<?php if ($player->full_name_en): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;"><?= e($player->full_name_en) ?></div>
<?php endif; ?>
<div style="display:flex;gap:15px;flex-wrap:wrap;font-size:13px;color:#6B7280;margin-top:10px;">
<?php if ($player->registration_serial): ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="hash" style="width:14px;height:14px;"></i>
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;color:#0D7377;font-weight:600;"><?= e($player->registration_serial) ?></code>
</span>
<?php endif; ?>
<?php if ($player->activity_id_number): ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="id-card" style="width:14px;height:14px;"></i>
رقم النشاط: <strong style="color:#374151;"><?= e($player->activity_id_number) ?></strong>
</span>
<?php endif; ?>
</div>
</div>
<div style="text-align:left;">
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $medicalColor ?>15;color:<?= $medicalColor ?>;"><?= e($medicalLabel) ?></span>
<?php if ($player->medical_expiry_date): ?>
<div style="margin-top:6px;font-size:11px;color:#9CA3AF;">انتهاء: <?= e($player->medical_expiry_date) ?></div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Stats 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;"><?= $age !== null ? $age : '—' ?></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:14px;font-weight:700;color:<?= $cardColor ?>;"><?= e($cardLabel) ?></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($disciplines) ?></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:#0284C7;"><?= count($enrollments) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">التسجيلات</div>
</div>
</div>
<!-- Personal Info -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="user" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">البيانات الشخصية</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">الرقم القومي</td><td style="padding:6px 0;direction:ltr;text-align:right;font-weight:600;"><?= e($player->national_id ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم جواز السفر</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($player->passport_number ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الميلاد</td><td style="padding:6px 0;"><?= e($player->date_of_birth ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">النوع</td><td style="padding:6px 0;"><?= e($genders[$player->gender] ?? $player->gender ?? '—') ?></td></tr>
</table>
</div>
</div>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="phone" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات التواصل</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">الهاتف</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($player->phone ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">البريد الإلكتروني</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($player->email ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">العنوان</td><td style="padding:6px 0;"><?= e($player->address ?? '—') ?></td></tr>
</table>
</div>
</div>
</div>
<!-- Guardian Info (if exists) -->
<?php if (!empty($player->guardian_name)): ?>
<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="shield" style="width:18px;height:18px;color:#D97706;"></i>
<h3 style="margin:0;color:#D97706;font-size:15px;">بيانات ولي الأمر</h3>
</div>
<div style="padding:15px 20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">اسم ولي الأمر</td><td style="padding:6px 0;font-weight:600;"><?= e($player->guardian_name) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">هاتف ولي الأمر</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($player->guardian_phone ?? '—') ?></td></tr>
</table>
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:40%;">الرقم القومي</td><td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($player->guardian_national_id ?? '—') ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">صلة القرابة</td><td style="padding:6px 0;">
<?php
$guardianRelationships = Player::getGuardianRelationships();
echo e($guardianRelationships[$player->guardian_relationship] ?? $player->guardian_relationship ?? '—');
?>
</td></tr>
</table>
</div>
</div>
</div>
<?php endif; ?>
<!-- Education Info (if exists) -->
<?php if (!empty($player->school_name)): ?>
<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="graduation-cap" style="width:18px;height:18px;color:#0284C7;"></i>
<h3 style="margin:0;color:#0284C7;font-size:15px;">البيانات التعليمية</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:25%;">اسم المدرسة</td><td style="padding:6px 0;font-weight:600;"><?= e($player->school_name) ?></td></tr>
<?php if ($player->school_grade): ?>
<tr><td style="padding:6px 0;color:#6B7280;">الصف الدراسي</td><td style="padding:6px 0;"><?= e($player->school_grade) ?></td></tr>
<?php endif; ?>
</table>
</div>
</div>
<?php endif; ?>
<!-- Notes -->
<?php if (!empty($player->notes)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">ملاحظات</h3>
</div>
<div style="padding:15px 20px;font-size:14px;color:#374151;line-height:1.7;"><?= e($player->notes) ?></div>
</div>
<?php endif; ?>
<!-- Card Management -->
<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="credit-card" 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:flex;align-items:center;gap:15px;margin-bottom:15px;">
<span style="font-size:14px;color:#6B7280;">الحالة الحالية:</span>
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $cardColor ?>15;color:<?= $cardColor ?>;"><?= e($cardLabel) ?></span>
</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<?php if (in_array($cardStatus, ['inactive', 'suspended', 'revoked'])): ?>
<form method="POST" action="/players/<?= (int) $player->id ?>/activate-card" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" style="font-size:13px;" onclick="return confirm('هل أنت متأكد من تفعيل الكارنيه؟')">
<i data-lucide="check-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تفعيل الكارنيه
</button>
</form>
<?php endif; ?>
<?php if ($cardStatus === 'active'): ?>
<button type="button" class="btn btn-outline" style="font-size:13px;color:#D97706;border-color:#D97706;" onclick="document.getElementById('suspendCardForm').style.display = document.getElementById('suspendCardForm').style.display === 'none' ? 'block' : 'none';">
<i data-lucide="pause-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إيقاف الكارنيه
</button>
<form method="POST" action="/players/<?= (int) $player->id ?>/revoke-card" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-danger" style="font-size:13px;" onclick="return confirm('هل أنت متأكد من إلغاء الكارنيه؟ هذا الإجراء خطير.')">
<i data-lucide="x-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> إلغاء الكارنيه
</button>
</form>
<?php endif; ?>
</div>
<?php if ($cardStatus === 'active'): ?>
<div id="suspendCardForm" style="display:none;margin-top:15px;padding:15px;background:#FFF7ED;border:1px solid #FDE68A;border-radius:8px;">
<form method="POST" action="/players/<?= (int) $player->id ?>/suspend-card">
<?= csrf_field() ?>
<div class="form-group" style="margin-bottom:10px;">
<label class="form-label" style="font-size:12px;">سبب الإيقاف</label>
<textarea name="reason" class="form-input" rows="2" placeholder="أدخل سبب إيقاف الكارنيه..."></textarea>
</div>
<button type="submit" class="btn btn-outline" style="font-size:13px;color:#D97706;border-color:#D97706;" onclick="return confirm('هل أنت متأكد من إيقاف الكارنيه؟')">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تأكيد الإيقاف
</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
<!-- Disciplines Table -->
<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="activity" style="width:18px;height:18px;color:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">الألعاب الرياضية</h3>
</div>
<?php if (!empty($disciplines)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<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>
</tr>
</thead>
<tbody>
<?php foreach ($disciplines as $disc): ?>
<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;direction:ltr;text-align:right;"><?= e($disc['season'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($disc['status'] ?? '—') ?></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>
<!-- Enrollments Table -->
<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="book-open" style="width:18px;height:18px;color:#0284C7;"></i>
<h3 style="margin:0;color:#0284C7;font-size:15px;">التسجيلات في الأكاديميات</h3>
</div>
<button type="button" class="btn btn-sm btn-outline" onclick="toggleEnrollForm()">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> تسجيل جديد
</button>
</div>
<!-- Enrollment Inline Form (hidden by default) -->
<div id="enrollForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<form method="POST" action="/players/<?= (int) $player->id ?>/enroll">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">رقم الأكاديمية <span style="color:#DC2626;">*</span></label>
<input type="number" name="academy_id" class="form-input" required min="1" style="direction:ltr;text-align:left;" placeholder="رقم الأكاديمية">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">رقم المستوى <span style="color:#DC2626;">*</span></label>
<input type="number" name="level_id" class="form-input" required min="1" style="direction:ltr;text-align:left;" placeholder="رقم المستوى">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">رقم الجدول</label>
<input type="number" name="schedule_id" class="form-input" min="1" style="direction:ltr;text-align:left;" placeholder="رقم الجدول">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الموسم</label>
<input type="text" name="season" class="form-input" placeholder="مثال: 2025-2026" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تسجيل
</button>
<button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleEnrollForm()">إلغاء</button>
</div>
</form>
</div>
<?php if (!empty($enrollments)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<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 ($enrollments as $enroll):
$eStatus = $enroll['status'] ?? 'active';
$eStatusLabel = AcademyEnrollment::getStatusLabel($eStatus);
$eStatusColor = AcademyEnrollment::getStatusColor($eStatus);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 15px;font-weight:600;"><?= e($enroll['academy_name'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($enroll['level_name'] ?? '—') ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;"><?= e($enroll['season'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($enroll['enrolled_at'] ?? '—') ?></td>
<td style="padding:12px 15px;">
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $eStatusColor ?>15;color:<?= $eStatusColor ?>;"><?= e($eStatusLabel) ?></span>
</td>
<td style="padding:12px 15px;">
<?php if ($eStatus === 'active'): ?>
<button type="button" class="btn btn-sm" style="color:#DC2626;padding:4px 10px;border:1px solid #FCA5A5;border-radius:6px;background:#FEF2F2;cursor:pointer;font-size:12px;" onclick="toggleDropForm(<?= (int) ($enroll['id'] ?? 0) ?>)">
<i data-lucide="log-out" style="width:13px;height:13px;vertical-align:middle;"></i> انسحاب
</button>
<div id="dropForm-<?= (int) ($enroll['id'] ?? 0) ?>" style="display:none;margin-top:8px;">
<form method="POST" action="/players/<?= (int) $player->id ?>/drop-enrollment">
<?= csrf_field() ?>
<input type="hidden" name="enrollment_id" value="<?= (int) ($enroll['id'] ?? 0) ?>">
<textarea name="dropped_reason" class="form-input" rows="2" placeholder="سبب الانسحاب..." style="font-size:12px;margin-bottom:6px;"></textarea>
<button type="submit" class="btn btn-sm btn-danger" style="font-size:11px;" onclick="return confirm('تأكيد الانسحاب من الأكاديمية؟')">تأكيد الانسحاب</button>
</form>
</div>
<?php endif; ?>
</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>
<!-- Medical Records -->
<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="heart-pulse" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">السجلات الطبية</h3>
</div>
<button type="button" class="btn btn-sm btn-outline" onclick="toggleMedicalForm()">
<i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;"></i> إضافة سجل طبي
</button>
</div>
<!-- Add Medical Record Inline Form (hidden by default) -->
<div id="addMedicalForm" style="display:none;padding:20px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<form method="POST" action="/players/<?= (int) $player->id ?>/medical">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">نوع السجل <span style="color:#DC2626;">*</span></label>
<select name="record_type" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($recordTypes as $rtKey => $rtLabel): ?>
<option value="<?= e($rtKey) ?>"><?= e($rtLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">تاريخ الفحص <span style="color:#DC2626;">*</span></label>
<input type="date" name="exam_date" class="form-input" required>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">تاريخ الانتهاء</label>
<input type="date" name="expiry_date" class="form-input">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-top:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اسم الطبيب</label>
<input type="text" name="doctor_name" class="form-input" placeholder="اسم الطبيب">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">اسم العيادة/المستشفى</label>
<input type="text" name="clinic_name" class="form-input" placeholder="اسم المكان">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">النتيجة <span style="color:#DC2626;">*</span></label>
<select name="result" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($resultTypes as $resKey => $resLabel): ?>
<option value="<?= e($resKey) ?>"><?= e($resLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-top:15px;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">قيود/شروط</label>
<textarea name="restrictions" class="form-input" rows="2" placeholder="أي قيود أو شروط طبية..."></textarea>
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2" placeholder="ملاحظات إضافية..."></textarea>
</div>
</div>
<div style="margin-top:15px;display:flex;gap:8px;">
<button type="submit" class="btn btn-primary" style="font-size:13px;padding:8px 20px;">
<i data-lucide="check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> حفظ السجل
</button>
<button type="button" class="btn btn-outline" style="font-size:13px;padding:8px 20px;" onclick="toggleMedicalForm()">إلغاء</button>
</div>
</form>
</div>
<?php if (!empty($medicalRecords)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<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>
</tr>
</thead>
<tbody>
<?php foreach ($medicalRecords as $rec):
$recType = $rec['record_type'] ?? '';
$recResult = $rec['result'] ?? '';
$resultLabel = $resultTypes[$recResult] ?? $recResult;
$resultColorMap = ['fit' => '#059669', 'conditional' => '#D97706', 'unfit' => '#DC2626'];
$resultColor = $resultColorMap[$recResult] ?? '#6B7280';
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 15px;font-weight:600;"><?= e($recordTypes[$recType] ?? $recType) ?></td>
<td style="padding:12px 15px;"><?= e($rec['exam_date'] ?? '—') ?></td>
<td style="padding:12px 15px;"><?= e($rec['expiry_date'] ?? '—') ?></td>
<td style="padding:12px 15px;">
<span style="display:inline-block;padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $resultColor ?>15;color:<?= $resultColor ?>;"><?= e($resultLabel) ?></span>
</td>
<td style="padding:12px 15px;"><?= e($rec['doctor_name'] ?? '—') ?></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>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
function toggleEnrollForm() {
var form = document.getElementById('enrollForm');
form.style.display = form.style.display === 'none' ? 'block' : 'none';
}
function toggleMedicalForm() {
var form = document.getElementById('addMedicalForm');
form.style.display = form.style.display === 'none' ? 'block' : 'none';
}
function toggleDropForm(enrollmentId) {
var form = document.getElementById('dropForm-' + enrollmentId);
if (form) {
form.style.display = form.style.display === 'none' ? 'block' : 'none';
}
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
PermissionRegistry::register('player_affairs', [
'player.view' => ['ar' => 'عرض اللاعبين', 'en' => 'View Players'],
'player.search' => ['ar' => 'بحث عن لاعبين', 'en' => 'Search Players'],
'player.register' => ['ar' => 'تسجيل لاعب', 'en' => 'Register Player'],
'player.edit' => ['ar' => 'تعديل بيانات لاعب', 'en' => 'Edit Player'],
'player.manage_card' => ['ar' => 'إدارة كارنيه اللاعب', 'en' => 'Manage Player Card'],
'player.view_medical' => ['ar' => 'عرض السجل الطبي', 'en' => 'View Medical Records'],
'player.manage_medical' => ['ar' => 'إدارة السجل الطبي', 'en' => 'Manage Medical Records'],
'player.record_attendance' => ['ar' => 'تسجيل الحضور', 'en' => 'Record Attendance'],
]);
// ── Cross-module event listeners for the sports system ──
require_once __DIR__ . '/EventListeners.php';
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Rentals\Models\RentalEntity;
use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalBooking;
use App\Modules\Rentals\Services\RentalContractService;
use App\Modules\Rentals\Services\RentalDepositService;
use App\Modules\Facilities\Models\Facility;
class RentalController extends Controller
{
// ─── Contracts ───────────────────────────────────────────────
/**
* List contracts with status tabs, entity filter, facility filter, search, pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => trim((string) $request->get('status', '')),
'entity_id' => trim((string) $request->get('entity_id', '')),
'facility_id' => trim((string) $request->get('facility_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = RentalContract::search($filters, 25, $page);
// Load entities and facilities for filter dropdowns
$entities = RentalEntity::search([], 1000, 1)['data'] ?? [];
$facilities = Facility::allActive();
// Load entity names map for display
$entityMap = [];
foreach ($entities as $e) {
$entityMap[(int) $e['id']] = $e['name_ar'];
}
// Load facility names map for display
$facilityMap = [];
foreach ($facilities as $f) {
$facilityMap[(int) $f['id']] = $f['name_ar'];
}
return $this->view('Rentals.Views.index', [
'contracts' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => RentalContract::getStatuses(),
'entities' => $entities,
'facilities' => $facilities,
'entityMap' => $entityMap,
'facilityMap' => $facilityMap,
]);
}
// ─── Entities ────────────────────────────────────────────────
/**
* List rental entities.
*/
public function entities(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'entity_type' => trim((string) $request->get('entity_type', '')),
'status' => trim((string) $request->get('status', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = RentalEntity::search($filters, 25, $page);
return $this->view('Rentals.Views.entities', [
'entities' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'entityTypes' => RentalEntity::getEntityTypes(),
'statuses' => RentalEntity::getStatuses(),
]);
}
/**
* Show the create entity form.
*/
public function createEntity(Request $request): Response
{
return $this->view('Rentals.Views.entity_form', [
'entity' => null,
'entityTypes' => RentalEntity::getEntityTypes(),
'statuses' => RentalEntity::getStatuses(),
]);
}
/**
* Validate and store a new rental entity.
*/
public function storeEntity(Request $request): Response
{
$data = $this->extractEntityData($request);
$errors = $this->validateEntityData($data);
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/rentals/entities/create');
}
$entity = RentalEntity::create($data);
return $this->redirect('/rentals/entities/' . $entity->id)->withSuccess('تم إضافة الجهة المستأجرة بنجاح');
}
/**
* Show entity detail page.
*/
public function showEntity(Request $request, string $id): Response
{
$entity = RentalEntity::find((int) $id);
if (!$entity) {
return $this->redirect('/rentals/entities')->withError('الجهة المستأجرة غير موجودة');
}
$contracts = RentalContract::getForEntity((int) $id);
return $this->view('Rentals.Views.entity_show', [
'entity' => $entity,
'contracts' => $contracts,
]);
}
/**
* Show edit form for an entity.
*/
public function editEntity(Request $request, string $id): Response
{
$entity = RentalEntity::find((int) $id);
if (!$entity) {
return $this->redirect('/rentals/entities')->withError('الجهة المستأجرة غير موجودة');
}
return $this->view('Rentals.Views.entity_form', [
'entity' => $entity,
'entityTypes' => RentalEntity::getEntityTypes(),
'statuses' => RentalEntity::getStatuses(),
]);
}
/**
* Validate and update an existing entity.
*/
public function updateEntity(Request $request, string $id): Response
{
$entity = RentalEntity::find((int) $id);
if (!$entity) {
return $this->redirect('/rentals/entities')->withError('الجهة المستأجرة غير موجودة');
}
$data = $this->extractEntityData($request);
$errors = $this->validateEntityData($data);
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/rentals/entities/' . $id . '/edit');
}
$entity->update($data);
return $this->redirect('/rentals/entities/' . $id)->withSuccess('تم تحديث الجهة المستأجرة بنجاح');
}
// ─── Contracts CRUD ──────────────────────────────────────────
/**
* Show the create contract form.
*/
public function createContract(Request $request): Response
{
$entities = RentalEntity::search(['status' => 'active'], 1000, 1)['data'] ?? [];
$facilities = Facility::allActive();
return $this->view('Rentals.Views.contract_form', [
'contract' => null,
'entities' => $entities,
'facilities' => $facilities,
'statuses' => RentalContract::getStatuses(),
]);
}
/**
* Validate and store a new rental contract.
*/
public function storeContract(Request $request): Response
{
$entityId = (int) $request->post('entity_id', 0);
$facilityId = (int) $request->post('facility_id', 0);
$activityType = trim((string) $request->post('activity_type', ''));
$startDate = trim((string) $request->post('start_date', ''));
$endDate = trim((string) $request->post('end_date', ''));
$totalUnits = (int) $request->post('total_units', 0);
$unitRate = (float) $request->post('unit_rate', 0);
$timeTier = trim((string) $request->post('time_tier', 'AM'));
$depositPercentage = (float) $request->post('deposit_percentage', 0);
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
$entity = RentalEntity::find($entityId);
if (!$entity) {
$errors[] = 'يجب اختيار جهة مستأجرة صالحة';
}
$facility = Facility::find($facilityId);
if (!$facility) {
$errors[] = 'يجب اختيار مرفق صالح';
}
if ($startDate === '' || $endDate === '') {
$errors[] = 'تاريخ البدء والانتهاء مطلوبان';
} elseif ($startDate >= $endDate) {
$errors[] = 'تاريخ الانتهاء يجب أن يكون بعد تاريخ البدء';
}
if ($totalUnits <= 0) {
$errors[] = 'عدد الوحدات يجب أن يكون أكبر من صفر';
}
if ($unitRate <= 0) {
$errors[] = 'سعر الوحدة يجب أن يكون أكبر من صفر';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/rentals/contracts/create');
}
$contract = RentalContractService::createContract([
'entity_id' => $entityId,
'facility_id' => $facilityId,
'activity_type' => $activityType ?: null,
'start_date' => $startDate,
'end_date' => $endDate,
'total_units' => $totalUnits,
'unit_rate' => $unitRate,
'time_tier' => $timeTier,
'deposit_percentage' => $depositPercentage,
'notes' => $notes ?: null,
]);
return $this->redirect('/rentals/contracts/' . $contract->id)->withSuccess('تم إنشاء العقد بنجاح');
}
/**
* Show contract detail page with bookings and deposit status.
*/
public function showContract(Request $request, string $id): Response
{
$contract = RentalContract::find((int) $id);
if (!$contract) {
return $this->redirect('/rentals')->withError('العقد غير موجود');
}
$entityId = (int) ($contract->entity_id ?? $contract['entity_id']);
$facilityId = (int) ($contract->facility_id ?? $contract['facility_id']);
$entity = RentalEntity::find($entityId);
$facility = Facility::find($facilityId);
$bookings = RentalBooking::getForContract((int) $id);
return $this->view('Rentals.Views.contract_show', [
'contract' => $contract,
'entity' => $entity,
'facility' => $facility,
'bookings' => $bookings,
]);
}
/**
* Approve a rental contract.
*/
public function approveContract(Request $request, string $id): Response
{
$contract = RentalContract::find((int) $id);
if (!$contract) {
return $this->redirect('/rentals')->withError('العقد غير موجود');
}
$employee = App::getInstance()->currentEmployee();
$contract->update([
'status' => 'approved',
'approved_by' => $employee ? (int) $employee->id : null,
'approved_at' => date('Y-m-d H:i:s'),
]);
return $this->redirect('/rentals/contracts/' . $id)->withSuccess('تم اعتماد العقد بنجاح');
}
/**
* Collect deposit for a contract.
*/
public function collectDeposit(Request $request, string $id): Response
{
$paymentId = (int) $request->post('payment_id', 0);
if ($paymentId <= 0) {
return $this->redirect('/rentals/contracts/' . $id)->withError('رقم الدفعة مطلوب');
}
try {
RentalDepositService::collectDeposit((int) $id, $paymentId);
return $this->redirect('/rentals/contracts/' . $id)->withSuccess('تم تحصيل التأمين بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/rentals/contracts/' . $id)->withError($e->getMessage());
}
}
/**
* Refund deposit for a contract.
*/
public function refundDeposit(Request $request, string $id): Response
{
$paymentId = (int) $request->post('payment_id', 0);
try {
if ($paymentId > 0) {
RentalDepositService::processRefund((int) $id, $paymentId);
} else {
RentalDepositService::requestRefund((int) $id);
}
return $this->redirect('/rentals/contracts/' . $id)->withSuccess('تم معالجة استرداد التأمين بنجاح');
} catch (\RuntimeException $e) {
return $this->redirect('/rentals/contracts/' . $id)->withError($e->getMessage());
}
}
// ─── Private helpers ─────────────────────────────────────────
/**
* Extract entity data from request.
*/
private function extractEntityData(Request $request): array
{
return [
'entity_type' => trim((string) $request->post('entity_type', '')),
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'contact_person' => trim((string) $request->post('contact_person', '')) ?: null,
'contact_phone' => trim((string) $request->post('contact_phone', '')) ?: null,
'contact_email' => trim((string) $request->post('contact_email', '')) ?: null,
'address' => trim((string) $request->post('address', '')) ?: null,
'tax_number' => trim((string) $request->post('tax_number', '')) ?: null,
'official_letter_received' => (int) $request->post('official_letter_received', 0),
'board_approved' => (int) $request->post('board_approved', 0),
'approval_date' => trim((string) $request->post('approval_date', '')) ?: null,
'status' => trim((string) $request->post('status', 'pending')),
'notes' => trim((string) $request->post('notes', '')) ?: null,
];
}
/**
* Validate entity data, return errors array.
*/
private function validateEntityData(array $data): array
{
$errors = [];
if (empty($data['name_ar']) || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم الجهة بالعربي مطلوب (حرفان على الأقل)';
}
if (!array_key_exists($data['entity_type'], RentalEntity::getEntityTypes())) {
$errors[] = 'نوع الجهة غير صالح';
}
if (!array_key_exists($data['status'], RentalEntity::getStatuses())) {
$errors[] = 'الحالة غير صالحة';
}
return $errors;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Models;
use App\Core\Model;
class RentalBooking extends Model
{
protected static string $table = 'rental_bookings';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'contract_id',
'facility_id',
'booking_date',
'start_time',
'end_time',
'status',
'notes',
];
/**
* Get all booking statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'scheduled' => 'مجدول',
'completed' => 'مكتمل',
'cancelled' => 'ملغى',
'no_show' => 'لم يحضر',
];
}
/**
* Get all bookings for a specific contract, ordered by date and time.
*/
public static function getForContract(int $contractId): array
{
return static::query()
->where('contract_id', '=', $contractId)
->orderBy('booking_date', 'ASC')
->orderBy('start_time', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Models;
use App\Core\Model;
use App\Core\App;
class RentalContract extends Model
{
protected static string $table = 'rental_contracts';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'contract_number',
'entity_id',
'facility_id',
'activity_type',
'start_date',
'end_date',
'total_units',
'unit_rate',
'time_tier',
'subtotal',
'discount_percentage',
'discount_amount',
'total_amount',
'deposit_percentage',
'deposit_amount',
'deposit_status',
'deposit_payment_id',
'technical_report_submitted',
'status',
'approved_by',
'approved_at',
'terms_json',
'notes',
];
/**
* Get all contract statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'pending_approval' => 'بانتظار الاعتماد',
'approved' => 'مُعتمد',
'active' => 'فعال',
'completed' => 'مكتمل',
'cancelled' => 'ملغى',
'terminated' => 'منهي',
];
}
/**
* Get all deposit statuses with Arabic labels.
*/
public static function getDepositStatuses(): array
{
return [
'pending' => 'قيد التحصيل',
'collected' => 'محصّل',
'partially_refunded' => 'مسترد جزئياً',
'refunded' => 'مسترد',
];
}
/**
* Get the Arabic label for a status key.
*/
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a contract status.
*/
public static function getStatusColor(string $status): string
{
$colors = [
'draft' => '#6B7280',
'pending_approval' => '#D97706',
'approved' => '#7C3AED',
'active' => '#059669',
'completed' => '#0284C7',
'cancelled' => '#DC2626',
'terminated' => '#991B1B',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Get the Arabic label for a deposit status key.
*/
public static function getDepositStatusLabel(string $status): string
{
$statuses = self::getDepositStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a deposit status.
*/
public static function getDepositStatusColor(string $status): string
{
$colors = [
'pending' => '#D97706',
'collected' => '#059669',
'partially_refunded' => '#7C3AED',
'refunded' => '#0284C7',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Decode terms_json into an associative array.
*/
public function getTerms(): array
{
$raw = $this->terms_json;
if (empty($raw)) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
/**
* Generate a contract number in RC-YYYYMM-XXXX format.
*/
public static function generateNumber(): string
{
$prefix = 'RC-' . date('Ym') . '-';
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT contract_number FROM rental_contracts WHERE contract_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
$nextSeq = 1;
if ($row) {
$number = is_object($row) ? $row->contract_number : ($row['contract_number'] ?? '');
$parts = explode('-', (string) $number);
$lastSeq = (int) ($parts[2] ?? 0);
$nextSeq = $lastSeq + 1;
}
return $prefix . str_pad((string) $nextSeq, 4, '0', STR_PAD_LEFT);
}
/**
* Search contracts with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`contract_number` LIKE ? OR `notes` LIKE ?)',
[$search, $search]
);
}
if (!empty($filters['entity_id'])) {
$query = $query->where('entity_id', '=', (int) $filters['entity_id']);
}
if (!empty($filters['facility_id'])) {
$query = $query->where('facility_id', '=', (int) $filters['facility_id']);
}
if (!empty($filters['status'])) {
$query = $query->where('status', '=', $filters['status']);
}
$query = $query->orderBy('created_at', 'DESC');
return $query->paginate($perPage, $page);
}
/**
* Get all contracts for a specific entity.
*/
public static function getForEntity(int $entityId): array
{
return static::query()
->where('entity_id', '=', $entityId)
->orderBy('created_at', 'DESC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Models;
use App\Core\Model;
class RentalEntity extends Model
{
protected static string $table = 'rental_entities';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'entity_type',
'name_ar',
'name_en',
'contact_person',
'contact_phone',
'contact_email',
'address',
'tax_number',
'official_letter_received',
'board_approved',
'approval_date',
'status',
'notes',
];
/**
* Get all entity types with Arabic labels.
*/
public static function getEntityTypes(): array
{
return [
'school' => 'مدرسة',
'university' => 'جامعة',
'embassy' => 'سفارة',
'federation' => 'اتحاد',
'private' => 'خاص',
'corporate' => 'شركة',
];
}
/**
* Get all statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'pending' => 'قيد المراجعة',
'approved' => 'مُعتمد',
'active' => 'فعال',
'suspended' => 'موقوف',
'blacklisted' => 'محظور',
];
}
/**
* Get the Arabic label for a status key.
*/
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a status.
*/
public static function getStatusColor(string $status): string
{
$colors = [
'pending' => '#D97706',
'approved' => '#7C3AED',
'active' => '#059669',
'suspended' => '#DC2626',
'blacklisted' => '#991B1B',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Search rental entities with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`name_ar` LIKE ? OR `name_en` LIKE ? OR `contact_person` LIKE ? OR `contact_phone` LIKE ?)',
[$search, $search, $search, $search]
);
}
if (!empty($filters['entity_type'])) {
$query = $query->where('entity_type', '=', $filters['entity_type']);
}
if (!empty($filters['status'])) {
$query = $query->where('status', '=', $filters['status']);
}
$query = $query->orderBy('created_at', 'DESC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
return [
['GET', '/rentals', 'Rentals\Controllers\RentalController@index', ['auth'], 'rental.view'],
['GET', '/rentals/entities', 'Rentals\Controllers\RentalController@entities', ['auth'], 'rental.view'],
['GET', '/rentals/entities/create', 'Rentals\Controllers\RentalController@createEntity', ['auth'], 'rental.manage_entity'],
['POST', '/rentals/entities', 'Rentals\Controllers\RentalController@storeEntity', ['auth', 'csrf'], 'rental.manage_entity'],
['GET', '/rentals/entities/{id:\d+}', 'Rentals\Controllers\RentalController@showEntity', ['auth'], 'rental.view'],
['GET', '/rentals/entities/{id:\d+}/edit', 'Rentals\Controllers\RentalController@editEntity', ['auth'], 'rental.manage_entity'],
['POST', '/rentals/entities/{id:\d+}', 'Rentals\Controllers\RentalController@updateEntity', ['auth', 'csrf'], 'rental.manage_entity'],
['GET', '/rentals/contracts/create', 'Rentals\Controllers\RentalController@createContract', ['auth'], 'rental.manage_contract'],
['POST', '/rentals/contracts', 'Rentals\Controllers\RentalController@storeContract', ['auth', 'csrf'], 'rental.manage_contract'],
['GET', '/rentals/contracts/{id:\d+}', 'Rentals\Controllers\RentalController@showContract', ['auth'], 'rental.view'],
['POST', '/rentals/contracts/{id:\d+}/approve', 'Rentals\Controllers\RentalController@approveContract', ['auth', 'csrf'], 'rental.approve'],
['POST', '/rentals/contracts/{id:\d+}/deposit/collect', 'Rentals\Controllers\RentalController@collectDeposit', ['auth', 'csrf'], 'rental.manage_deposit'],
['POST', '/rentals/contracts/{id:\d+}/deposit/refund', 'Rentals\Controllers\RentalController@refundDeposit', ['auth', 'csrf'], 'rental.manage_deposit'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalBooking;
use App\Modules\Rules\Services\RuleEngine;
final class RentalContractService
{
/**
* Create a new rental contract.
*
* Generates the contract number, calculates totals (subtotal = units * rate),
* applies bulk discount if eligible, and calculates deposit.
*/
public static function createContract(array $data): object
{
$contractNumber = RentalContract::generateNumber();
$totalUnits = (int) ($data['total_units'] ?? 0);
$unitRate = (float) ($data['unit_rate'] ?? 0);
$subtotal = $totalUnits * $unitRate;
// Calculate discount
$startDate = $data['start_date'] ?? '';
$endDate = $data['end_date'] ?? '';
$months = 0;
if ($startDate && $endDate) {
$start = new \DateTimeImmutable($startDate);
$end = new \DateTimeImmutable($endDate);
$diff = $start->diff($end);
$months = ($diff->y * 12) + $diff->m + ($diff->d > 0 ? 1 : 0);
}
$discountPercentage = self::calculateBulkDiscount($totalUnits, $months);
$discountAmount = round($subtotal * ($discountPercentage / 100), 2);
$totalAmount = $subtotal - $discountAmount;
// Calculate deposit
$depositPercentage = (float) ($data['deposit_percentage'] ?? 0);
$depositAmount = round($totalAmount * ($depositPercentage / 100), 2);
$contract = RentalContract::create([
'contract_number' => $contractNumber,
'entity_id' => (int) $data['entity_id'],
'facility_id' => (int) $data['facility_id'],
'activity_type' => $data['activity_type'] ?? null,
'start_date' => $startDate,
'end_date' => $endDate,
'total_units' => $totalUnits,
'unit_rate' => $unitRate,
'time_tier' => $data['time_tier'] ?? 'AM',
'subtotal' => $subtotal,
'discount_percentage' => $discountPercentage,
'discount_amount' => $discountAmount,
'total_amount' => $totalAmount,
'deposit_percentage' => $depositPercentage,
'deposit_amount' => $depositAmount,
'deposit_status' => 'pending',
'technical_report_submitted' => 0,
'status' => 'draft',
'terms_json' => $data['terms_json'] ?? null,
'notes' => $data['notes'] ?? null,
]);
EventBus::dispatch('rental.contract_created', [
'contract_id' => (int) $contract->id,
'entity_id' => (int) $data['entity_id'],
]);
Logger::info('Rental contract created', ['contract_id' => $contract->id, 'number' => $contractNumber]);
return $contract;
}
/**
* Activate a rental contract.
*/
public static function activateContract(int $contractId): void
{
$contract = RentalContract::find($contractId);
if (!$contract) {
throw new \RuntimeException('Contract not found');
}
$contract->update(['status' => 'active']);
EventBus::dispatch('rental.contract_activated', [
'contract_id' => $contractId,
]);
Logger::info('Rental contract activated', ['contract_id' => $contractId]);
}
/**
* Generate booking slots from a schedule array.
*
* @param int $contractId
* @param array $schedule Array of [{date, start_time, end_time}]
*/
public static function generateBookingSlots(int $contractId, array $schedule): void
{
$contract = RentalContract::find($contractId);
if (!$contract) {
throw new \RuntimeException('Contract not found');
}
$facilityId = (int) ($contract->facility_id ?? $contract['facility_id']);
foreach ($schedule as $slot) {
RentalBooking::create([
'contract_id' => $contractId,
'facility_id' => $facilityId,
'booking_date' => $slot['date'],
'start_time' => $slot['start_time'],
'end_time' => $slot['end_time'],
'status' => 'scheduled',
]);
}
Logger::info('Rental booking slots generated', [
'contract_id' => $contractId,
'count' => count($schedule),
]);
}
/**
* Calculate bulk discount based on total units and contract duration.
*
* Default: if units >= 24 AND months >= 2, return 15%.
* Values are read from business_rules: RENTAL_BULK_MIN_UNITS,
* RENTAL_BULK_MIN_MONTHS, RENTAL_BULK_DISCOUNT_PCT.
*/
public static function calculateBulkDiscount(int $totalUnits, int $months): float
{
$minUnits = (int) (RuleEngine::getValue('RENTAL_BULK_MIN_UNITS') ?? 24);
$minMonths = (int) (RuleEngine::getValue('RENTAL_BULK_MIN_MONTHS') ?? 2);
$discountPct = (float) (RuleEngine::getValue('RENTAL_BULK_DISCOUNT_PCT') ?? 15.00);
if ($totalUnits >= $minUnits && $months >= $minMonths) {
return $discountPct;
}
return 0.00;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Rentals\Services;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Rentals\Models\RentalContract;
final class RentalDepositService
{
/**
* Record deposit collection for a contract.
*/
public static function collectDeposit(int $contractId, int $paymentId): void
{
$contract = RentalContract::find($contractId);
if (!$contract) {
throw new \RuntimeException('Contract not found');
}
$contract->update([
'deposit_status' => 'collected',
'deposit_payment_id' => $paymentId,
]);
EventBus::dispatch('rental.deposit_collected', [
'contract_id' => $contractId,
'payment_id' => $paymentId,
]);
Logger::info('Rental deposit collected', ['contract_id' => $contractId, 'payment_id' => $paymentId]);
}
/**
* Request a deposit refund.
*
* Validates that the technical report has been submitted.
* Sets deposit_status to 'partially_refunded' or 'refunded' depending on context.
*/
public static function requestRefund(int $contractId): void
{
$contract = RentalContract::find($contractId);
if (!$contract) {
throw new \RuntimeException('Contract not found');
}
$techReport = (int) ($contract->technical_report_submitted ?? $contract['technical_report_submitted'] ?? 0);
if (!$techReport) {
throw new \RuntimeException('Technical report must be submitted before requesting deposit refund');
}
$contract->update([
'deposit_status' => 'partially_refunded',
]);
EventBus::dispatch('rental.deposit_refund_requested', [
'contract_id' => $contractId,
]);
Logger::info('Rental deposit refund requested', ['contract_id' => $contractId]);
}
/**
* Process a deposit refund.
*/
public static function processRefund(int $contractId, int $paymentId): void
{
$contract = RentalContract::find($contractId);
if (!$contract) {
throw new \RuntimeException('Contract not found');
}
$contract->update([
'deposit_status' => 'refunded',
]);
EventBus::dispatch('rental.deposit_refunded', [
'contract_id' => $contractId,
'payment_id' => $paymentId,
]);
Logger::info('Rental deposit refunded', ['contract_id' => $contractId, 'payment_id' => $paymentId]);
}
}
<?php
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>عقد تأجير جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rentals" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للعقود</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/rentals/contracts">
<?= \App\Core\CSRF::field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">بيانات العقد</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">الجهة المستأجرة <span style="color:#DC2626;">*</span></label>
<select name="entity_id" class="form-input" required>
<option value="">-- اختر الجهة --</option>
<?php foreach ($entities as $ent): ?>
<option value="<?= (int) $ent['id'] ?>" <?= (string) old('entity_id') === (string) $ent['id'] ? 'selected' : '' ?>><?= e($ent['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">المرفق <span style="color:#DC2626;">*</span></label>
<select name="facility_id" class="form-input" required>
<option value="">-- اختر المرفق --</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= (string) old('facility_id') === (string) $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">نوع النشاط</label>
<select name="activity_type" class="form-input">
<option value="">-- اختر --</option>
<option value="practice" <?= old('activity_type') === 'practice' ? 'selected' : '' ?>>تدريب</option>
<option value="competitive" <?= old('activity_type') === 'competitive' ? 'selected' : '' ?>>تنافسي</option>
</select>
</div>
<div>
<label class="form-label">الفترة الزمنية</label>
<select name="time_tier" class="form-input">
<option value="AM" <?= old('time_tier', 'AM') === 'AM' ? 'selected' : '' ?>>صباحي</option>
<option value="PM" <?= old('time_tier', 'AM') === 'PM' ? 'selected' : '' ?>>مسائي</option>
</select>
</div>
<div>
<label class="form-label">تاريخ البدء <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" value="<?= e(old('start_date')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">تاريخ الانتهاء <span style="color:#DC2626;">*</span></label>
<input type="date" name="end_date" value="<?= e(old('end_date')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">عدد الوحدات <span style="color:#DC2626;">*</span></label>
<input type="number" name="total_units" value="<?= e(old('total_units')) ?>" class="form-input" min="1" required>
</div>
<div>
<label class="form-label">سعر الوحدة <span style="color:#DC2626;">*</span></label>
<input type="number" name="unit_rate" value="<?= e(old('unit_rate')) ?>" class="form-input" step="0.01" min="0.01" required>
</div>
<div>
<label class="form-label">نسبة التأمين (%)</label>
<input type="number" name="deposit_percentage" value="<?= e(old('deposit_percentage')) ?>" class="form-input" step="0.01" min="0" placeholder="10%">
</div>
<div></div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" style="resize:vertical;"><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">إنشاء العقد</button>
<a href="/rentals" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Rentals\Models\RentalContract;
use App\Modules\Rentals\Models\RentalBooking;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($contract->contract_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rentals" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للعقود</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$cStatus = $contract->status ?? 'draft';
$depositStatus = $contract->deposit_status ?? 'pending';
$activityTypes = ['practice' => 'تدريب', 'competitive' => 'تنافسي'];
$timeTiers = ['AM' => 'صباحي', 'PM' => 'مسائي'];
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div>
<div style="margin-bottom:8px;">
<code style="font-size:14px;background:#F0F9FF;color:#0284C7;padding:4px 12px;border-radius:4px;font-weight:600;"><?= e($contract->contract_number) ?></code>
</div>
<div style="font-size:14px;color:#374151;">
<strong>الجهة:</strong> <?= $entity ? e($entity->name_ar) : '—' ?>
&nbsp;|&nbsp;
<strong>المرفق:</strong> <?= $facility ? e($facility->name_ar) : '—' ?>
</div>
</div>
<span class="badge" style="background:<?= RentalContract::getStatusColor($cStatus) ?>15;color:<?= RentalContract::getStatusColor($cStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalContract::getStatusLabel($cStatus)) ?>
</span>
</div>
</div>
<!-- Financial Summary -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;"><i data-lucide="calculator" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> الملخص المالي</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;">
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">عدد الوحدات</div>
<div style="font-size:22px;font-weight:700;color:#374151;"><?= (int) ($contract->total_units ?? 0) ?></div>
</div>
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">سعر الوحدة</div>
<div style="font-size:22px;font-weight:700;color:#374151;"><?= money((float) ($contract->unit_rate ?? 0)) ?></div>
</div>
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المجموع الفرعي</div>
<div style="font-size:22px;font-weight:700;color:#374151;"><?= money((float) ($contract->subtotal ?? 0)) ?></div>
</div>
<?php
$discountPct = (float) ($contract->discount_percentage ?? 0);
$discountAmt = (float) ($contract->discount_amount ?? 0);
?>
<?php if ($discountPct > 0 || $discountAmt > 0): ?>
<div style="background:#FEF2F2;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">الخصم <?php if ($discountPct > 0): ?>(<?= $discountPct ?>%)<?php endif; ?></div>
<div style="font-size:22px;font-weight:700;color:#DC2626;">-<?= money($discountAmt) ?></div>
</div>
<?php endif; ?>
</div>
<div style="padding:0 20px 20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div style="background:#0D7377;padding:15px;border-radius:8px;text-align:center;">
<div style="font-size:12px;color:rgba(255,255,255,0.8);margin-bottom:4px;">الإجمالي</div>
<div style="font-size:26px;font-weight:700;color:#fff;"><?= money((float) ($contract->total_amount ?? 0)) ?></div>
</div>
<div style="background:#F9FAFB;padding:15px;border-radius:8px;text-align:center;border:1px solid #E5E7EB;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">
التأمين (<?= (float) ($contract->deposit_percentage ?? 0) ?>%)
</div>
<div style="font-size:22px;font-weight:700;color:#374151;margin-bottom:6px;"><?= money((float) ($contract->deposit_amount ?? 0)) ?></div>
<span class="badge" style="background:<?= RentalContract::getDepositStatusColor($depositStatus) ?>15;color:<?= RentalContract::getDepositStatusColor($depositStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalContract::getDepositStatusLabel($depositStatus)) ?>
</span>
</div>
</div>
</div>
</div>
<!-- Info Grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin:0 0 15px;">تفاصيل العقد</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">تاريخ البدء</td><td style="padding:8px 0;"><?= e($contract->start_date ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">تاريخ الانتهاء</td><td style="padding:8px 0;"><?= e($contract->end_date ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">نوع النشاط</td><td style="padding:8px 0;"><?= e($activityTypes[$contract->activity_type ?? ''] ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الفترة الزمنية</td><td style="padding:8px 0;"><?= e($timeTiers[$contract->time_tier ?? ''] ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">التقرير الفني</td><td style="padding:8px 0;"><?= ($contract->technical_report_submitted ?? 0) ? '<span style="color:#059669;font-weight:600;">نعم</span>' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
</table>
</div>
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin:0 0 15px;">بيانات الاعتماد</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">اعتمد بواسطة</td><td style="padding:8px 0;"><?= e($contract->approved_by ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">تاريخ الاعتماد</td><td style="padding:8px 0;"><?= e($contract->approved_at ?? '—') ?></td></tr>
</table>
<?php if (!empty($contract->notes)): ?>
<div style="margin-top:15px;padding-top:12px;border-top:1px solid #E5E7EB;">
<h4 style="color:#6B7280;font-size:13px;margin:0 0 6px;">ملاحظات</h4>
<p style="font-size:14px;margin:0;color:#374151;"><?= nl2br(e($contract->notes)) ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Action Buttons -->
<?php if ($cStatus === 'pending_approval'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;align-items:center;gap:15px;background:#FFFBEB;border:1px solid #FDE68A;">
<i data-lucide="alert-circle" style="width:20px;height:20px;color:#D97706;"></i>
<span style="font-size:14px;color:#92400E;flex:1;">هذا العقد بانتظار الاعتماد</span>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/approve" style="margin:0;">
<?= \App\Core\CSRF::field() ?>
<button type="submit" class="btn btn-primary" style="background:#059669;"><i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> اعتماد العقد</button>
</form>
</div>
<?php endif; ?>
<?php if (in_array($cStatus, ['approved', 'active']) && $depositStatus === 'pending'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;align-items:center;gap:15px;background:#F0FDF4;border:1px solid #BBF7D0;">
<i data-lucide="banknote" style="width:20px;height:20px;color:#059669;"></i>
<span style="font-size:14px;color:#166534;flex:1;">التأمين قيد التحصيل — أدخل رقم الدفعة لتسجيل التحصيل</span>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/deposit" style="margin:0;display:flex;gap:8px;align-items:center;">
<?= \App\Core\CSRF::field() ?>
<input type="number" name="payment_id" class="form-input" placeholder="رقم الدفعة" required style="width:150px;">
<button type="submit" class="btn btn-primary">تحصيل التأمين</button>
</form>
</div>
<?php endif; ?>
<?php if ($depositStatus === 'collected'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;align-items:center;gap:15px;background:#EFF6FF;border:1px solid #BFDBFE;">
<i data-lucide="undo-2" style="width:20px;height:20px;color:#0284C7;"></i>
<span style="font-size:14px;color:#1E40AF;flex:1;">تم تحصيل التأمين — يمكنك استرداد المبلغ</span>
<form method="POST" action="/rentals/contracts/<?= (int) $contract->id ?>/refund" style="margin:0;">
<?= \App\Core\CSRF::field() ?>
<button type="submit" class="btn btn-outline" style="color:#0284C7;border-color:#0284C7;"><i data-lucide="undo-2" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> استرداد التأمين</button>
</form>
</div>
<?php endif; ?>
<!-- Bookings Table -->
<div class="card" style="padding:0;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#1A1A2E;"><i data-lucide="calendar" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> الحجوزات</h3>
</div>
<?php if (!empty($bookings)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">التاريخ</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">من</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">إلى</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($bookings as $b):
$bStatus = $b['status'] ?? 'pending';
$bStatusColors = [
'pending' => '#D97706',
'confirmed' => '#059669',
'cancelled' => '#DC2626',
'completed' => '#0284C7',
];
$bStatusLabels = [
'pending' => 'قيد الانتظار',
'confirmed' => 'مؤكد',
'cancelled' => 'ملغى',
'completed' => 'مكتمل',
];
$bColor = $bStatusColors[$bStatus] ?? '#6B7280';
$bLabel = $bStatusLabels[$bStatus] ?? $bStatus;
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;"><?= e($b['booking_date'] ?? '') ?></td>
<td style="padding:12px 16px;white-space:nowrap;"><?= e(substr($b['start_time'] ?? '', 0, 5)) ?></td>
<td style="padding:12px 16px;white-space:nowrap;"><?= e(substr($b['end_time'] ?? '', 0, 5)) ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= $bColor ?>15;color:<?= $bColor ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($bLabel) ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;">
<i data-lucide="calendar-x" style="width:40px;height:40px;color:#D1D5DB;"></i>
<p style="color:#9CA3AF;font-size:14px;margin:10px 0 0;">لا توجد حجوزات لهذا العقد</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Rentals\Models\RentalEntity;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الجهات المستأجرة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rentals/entities/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جهة جديدة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentStatus = $filters['status'] ?? '';
$allStatuses = RentalEntity::getStatuses();
?>
<!-- Status Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/rentals/entities?<?= http_build_query(array_merge($filters, ['status' => '', 'page' => 1])) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allStatuses as $sKey => $sLabel): ?>
<a href="/rentals/entities?<?= http_build_query(array_merge($filters, ['status' => $sKey, 'page' => 1])) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === $sKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === $sKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($sLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/rentals/entities" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="اسم الجهة، جهة الاتصال، الهاتف..." class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">نوع الجهة</label>
<select name="entity_type" class="form-input">
<option value="">الكل</option>
<?php foreach ($entityTypes as $tKey => $tLabel): ?>
<option value="<?= e($tKey) ?>" <?= ($filters['entity_type'] ?? '') === $tKey ? 'selected' : '' ?>><?= e($tLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentStatus !== ''): ?>
<input type="hidden" name="status" value="<?= e($currentStatus) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/rentals/entities" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Entities Table -->
<?php if (!empty($entities)): ?>
<div class="card" style="padding:0;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الاسم</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">نوع الجهة</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">جهة الاتصال</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الهاتف</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($entities as $ent):
$entStatus = $ent['status'] ?? 'pending';
$entType = $ent['entity_type'] ?? '';
$typeLabel = $entityTypes[$entType] ?? $entType;
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;font-weight:600;">
<a href="/rentals/entities/<?= (int) $ent['id'] ?>" style="color:#0D7377;text-decoration:none;"><?= e($ent['name_ar'] ?? '') ?></a>
</td>
<td style="padding:12px 16px;"><?= e($typeLabel) ?></td>
<td style="padding:12px 16px;"><?= e($ent['contact_person'] ?? '—') ?></td>
<td style="padding:12px 16px;direction:ltr;text-align:right;"><?= e($ent['contact_phone'] ?? '—') ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= RentalEntity::getStatusColor($entStatus) ?>15;color:<?= RentalEntity::getStatusColor($entStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalEntity::getStatusLabel($entStatus)) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<a href="/rentals/entities/<?= (int) $ent['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="building-2" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد جهات مستأجرة</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['entity_type']) || !empty($filters['status'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة جهة مستأجرة جديدة.
<?php endif; ?>
</p>
<a href="/rentals/entities/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جهة جديدة</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
$__template->layout('Layout.main');
$isEdit = $entity !== null;
?>
<?php $__template->section('title'); ?><?= $isEdit ? 'تعديل: ' . e($entity->name_ar) : 'إضافة جهة مستأجرة' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($isEdit): ?>
<a href="/rentals/entities/<?= (int) $entity->id ?>" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للجهة</a>
<?php else: ?>
<a href="/rentals/entities" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
// Pre-populate: old input takes priority, then entity values for edit, then empty
$val = function(string $field, $default = '') use ($entity, $isEdit) {
$oldInput = $_SESSION['_old_input'] ?? [];
if (isset($oldInput[$field])) {
return $oldInput[$field];
}
if ($isEdit && $entity !== null) {
return $entity->$field ?? $default;
}
return $default;
};
?>
<form method="POST" action="<?= $isEdit ? '/rentals/entities/' . (int) $entity->id : '/rentals/entities' ?>">
<?= \App\Core\CSRF::field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">بيانات الجهة المستأجرة</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<label class="form-label">نوع الجهة <span style="color:#DC2626;">*</span></label>
<select name="entity_type" class="form-input" required>
<option value="">-- اختر --</option>
<?php foreach ($entityTypes as $tKey => $tLabel): ?>
<option value="<?= e($tKey) ?>" <?= (string) $val('entity_type') === $tKey ? 'selected' : '' ?>><?= e($tLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الحالة <span style="color:#DC2626;">*</span></label>
<select name="status" class="form-input" required>
<?php foreach ($statuses as $sKey => $sLabel): ?>
<option value="<?= e($sKey) ?>" <?= (string) $val('status', 'pending') === $sKey ? 'selected' : '' ?>><?= e($sLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e($val('name_ar')) ?>" class="form-input" required>
</div>
<div>
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e($val('name_en')) ?>" class="form-input">
</div>
<div>
<label class="form-label">جهة الاتصال</label>
<input type="text" name="contact_person" value="<?= e($val('contact_person')) ?>" class="form-input">
</div>
<div>
<label class="form-label">هاتف الاتصال</label>
<input type="text" name="contact_phone" value="<?= e($val('contact_phone')) ?>" class="form-input">
</div>
<div>
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="contact_email" value="<?= e($val('contact_email')) ?>" class="form-input">
</div>
<div>
<label class="form-label">الرقم الضريبي</label>
<input type="text" name="tax_number" value="<?= e($val('tax_number')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div style="grid-column:1/-1;">
<label class="form-label">العنوان</label>
<textarea name="address" class="form-input" rows="2" style="resize:vertical;"><?= e($val('address')) ?></textarea>
</div>
<div>
<label class="form-label">تاريخ الاعتماد</label>
<input type="date" name="approval_date" value="<?= e($val('approval_date')) ?>" class="form-input">
</div>
<div style="display:flex;align-items:end;gap:20px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding-bottom:8px;">
<input type="checkbox" name="official_letter_received" value="1" <?= $val('official_letter_received') ? 'checked' : '' ?>>
<span style="font-size:14px;">تم استلام الخطاب الرسمي</span>
</label>
</div>
<div style="display:flex;align-items:end;gap:20px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding-bottom:8px;">
<input type="checkbox" name="board_approved" value="1" <?= $val('board_approved') ? 'checked' : '' ?>>
<span style="font-size:14px;">معتمد من مجلس الإدارة</span>
</label>
</div>
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" style="resize:vertical;"><?= e($val('notes')) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">حفظ</button>
<a href="<?= $isEdit ? '/rentals/entities/' . (int) $entity->id : '/rentals/entities' ?>" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Rentals\Models\RentalEntity;
use App\Modules\Rentals\Models\RentalContract;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($entity->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rentals/entities/<?= (int) $entity->id ?>/edit" class="btn btn-outline"><i data-lucide="edit" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/rentals/entities" class="btn btn-outline" style="margin-right:8px;"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$entStatus = $entity->status ?? 'pending';
$entType = $entity->entity_type ?? '';
$typeLabel = RentalEntity::getEntityTypes()[$entType] ?? $entType;
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div>
<h2 style="margin:0 0 8px;color:#1A1A2E;"><?= e($entity->name_ar) ?></h2>
<?php if (!empty($entity->name_en)): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;"><?= e($entity->name_en) ?></div>
<?php endif; ?>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<span class="badge" style="background:#0D737715;color:#0D7377;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($typeLabel) ?>
</span>
<span class="badge" style="background:<?= RentalEntity::getStatusColor($entStatus) ?>15;color:<?= RentalEntity::getStatusColor($entStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalEntity::getStatusLabel($entStatus)) ?>
</span>
</div>
</div>
</div>
<!-- Info Grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin:0 0 15px;">بيانات الاتصال</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">جهة الاتصال</td><td style="padding:8px 0;font-weight:600;"><?= e($entity->contact_person ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الهاتف</td><td style="padding:8px 0;direction:ltr;text-align:right;"><?= e($entity->contact_phone ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">البريد الإلكتروني</td><td style="padding:8px 0;direction:ltr;text-align:right;"><?= e($entity->contact_email ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">العنوان</td><td style="padding:8px 0;"><?= e($entity->address ?? '—') ?></td></tr>
</table>
</div>
<div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin:0 0 15px;">بيانات إدارية</h4>
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;width:40%;">الرقم الضريبي</td><td style="padding:8px 0;direction:ltr;text-align:right;"><?= e($entity->tax_number ?? '—') ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">الخطاب الرسمي</td><td style="padding:8px 0;"><?= $entity->official_letter_received ? '<span style="color:#059669;font-weight:600;">نعم</span>' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">اعتماد مجلس الإدارة</td><td style="padding:8px 0;"><?= $entity->board_approved ? '<span style="color:#059669;font-weight:600;">نعم</span>' : '<span style="color:#DC2626;">لا</span>' ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">تاريخ الاعتماد</td><td style="padding:8px 0;"><?= e($entity->approval_date ?? '—') ?></td></tr>
</table>
<?php if (!empty($entity->notes)): ?>
<div style="margin-top:15px;padding-top:12px;border-top:1px solid #E5E7EB;">
<h4 style="color:#6B7280;font-size:13px;margin:0 0 6px;">ملاحظات</h4>
<p style="font-size:14px;margin:0;color:#374151;"><?= nl2br(e($entity->notes)) ?></p>
</div>
<?php endif; ?>
</div>
</div>
<!-- Contracts Section -->
<div class="card" style="padding:0;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;color:#1A1A2E;"><i data-lucide="file-text" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> العقود</h3>
<a href="/rentals/contracts/create" class="btn btn-sm btn-primary"><i data-lucide="plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> عقد جديد</a>
</div>
<?php if (!empty($contracts)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">رقم العقد</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">من</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">إلى</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الإجمالي</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($contracts as $c):
$cStatus = $c['status'] ?? 'draft';
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;">
<a href="/rentals/contracts/<?= (int) $c['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;"><?= e($c['contract_number'] ?? '') ?></a>
</td>
<td style="padding:12px 16px;direction:ltr;text-align:right;"><?= e($c['start_date'] ?? '') ?></td>
<td style="padding:12px 16px;direction:ltr;text-align:right;"><?= e($c['end_date'] ?? '') ?></td>
<td style="padding:12px 16px;font-weight:600;"><?= money((float) ($c['total_amount'] ?? 0)) ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= RentalContract::getStatusColor($cStatus) ?>15;color:<?= RentalContract::getStatusColor($cStatus) ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e(RentalContract::getStatusLabel($cStatus)) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<a href="/rentals/contracts/<?= (int) $c['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;">
<i data-lucide="file-x" style="width:40px;height:40px;color:#D1D5DB;"></i>
<p style="color:#9CA3AF;font-size:14px;margin:10px 0 0;">لا توجد عقود</p>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Rentals\Models\RentalContract;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>التأجير المؤسسي<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/rentals/contracts/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عقد جديد</a>
<a href="/rentals/entities" class="btn btn-outline" style="margin-right:8px;"><i data-lucide="building-2" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> الجهات المستأجرة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentStatus = $filters['status'] ?? '';
$allStatuses = RentalContract::getStatuses();
?>
<!-- Status Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/rentals"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allStatuses as $sKey => $sLabel): ?>
<a href="/rentals?status=<?= e($sKey) ?><?= ($filters['q'] ?? '') !== '' ? '&q=' . urlencode($filters['q']) : '' ?><?= ($filters['entity_id'] ?? '') !== '' ? '&entity_id=' . urlencode($filters['entity_id']) : '' ?><?= ($filters['facility_id'] ?? '') !== '' ? '&facility_id=' . urlencode($filters['facility_id']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === $sKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === $sKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($sLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search & Filters Bar -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/rentals" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث برقم العقد..." class="form-input">
</div>
<div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">الجهة المستأجرة</label>
<select name="entity_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($entities as $ent): ?>
<option value="<?= (int) $ent['id'] ?>" <?= ($filters['entity_id'] ?? '') == $ent['id'] ? 'selected' : '' ?>><?= e($ent['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">المرفق</label>
<select name="facility_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= ($filters['facility_id'] ?? '') == $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentStatus !== ''): ?>
<input type="hidden" name="status" value="<?= e($currentStatus) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/rentals" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Contracts Table -->
<?php if (!empty($contracts)): ?>
<div class="card" style="padding:0;overflow-x:auto;">
<table class="table" style="width:100%;border-collapse:collapse;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:right;white-space:nowrap;">رقم العقد</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:right;white-space:nowrap;">الجهة المستأجرة</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:right;white-space:nowrap;">المرفق</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:right;white-space:nowrap;">من</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:right;white-space:nowrap;">إلى</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:right;white-space:nowrap;">الإجمالي</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:center;white-space:nowrap;">التأمين</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:center;white-space:nowrap;">الحالة</th>
<th style="padding:12px 16px;font-size:13px;font-weight:600;color:#374151;text-align:center;white-space:nowrap;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($contracts as $c):
$contractStatus = $c['status'] ?? 'draft';
$depositStatus = $c['deposit_status'] ?? 'pending';
$entityName = $entityMap[(int) ($c['entity_id'] ?? 0)] ?? '-';
$facilityName = $facilityMap[(int) ($c['facility_id'] ?? 0)] ?? '-';
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;font-size:13px;">
<a href="/rentals/contracts/<?= (int) $c['id'] ?>" style="color:#0D7377;font-weight:600;text-decoration:none;">
<?= e($c['contract_number'] ?? '') ?>
</a>
</td>
<td style="padding:12px 16px;font-size:13px;"><?= e($entityName) ?></td>
<td style="padding:12px 16px;font-size:13px;"><?= e($facilityName) ?></td>
<td style="padding:12px 16px;font-size:13px;direction:ltr;text-align:right;"><?= e($c['start_date'] ?? '') ?></td>
<td style="padding:12px 16px;font-size:13px;direction:ltr;text-align:right;"><?= e($c['end_date'] ?? '') ?></td>
<td style="padding:12px 16px;font-size:13px;font-weight:600;"><?= money((float) ($c['total_amount'] ?? 0)) ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= RentalContract::getDepositStatusColor($depositStatus) ?>15;color:<?= RentalContract::getDepositStatusColor($depositStatus) ?>;font-size:11px;padding:3px 10px;border-radius:10px;">
<?= e(RentalContract::getDepositStatusLabel($depositStatus)) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= RentalContract::getStatusColor($contractStatus) ?>15;color:<?= RentalContract::getStatusColor($contractStatus) ?>;font-size:11px;padding:3px 10px;border-radius:10px;">
<?= e(RentalContract::getStatusLabel($contractStatus)) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<a href="/rentals/contracts/<?= (int) $c['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="file-text" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد عقود تأجير</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['status']) || !empty($filters['entity_id']) || !empty($filters['facility_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء عقد تأجير جديد.
<?php endif; ?>
</p>
<a href="/rentals/contracts/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عقد جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
PermissionRegistry::register('rentals', [
'rental.view' => ['ar' => 'عرض التأجير المؤسسي', 'en' => 'View Corporate Rentals'],
'rental.manage_entity' => ['ar' => 'إدارة جهات التأجير', 'en' => 'Manage Rental Entities'],
'rental.approve' => ['ar' => 'اعتماد عقود التأجير', 'en' => 'Approve Rental Contracts'],
'rental.manage_contract'=> ['ar' => 'إدارة عقود التأجير', 'en' => 'Manage Rental Contracts'],
'rental.manage_deposit' => ['ar' => 'إدارة تأمينات التأجير', 'en' => 'Manage Rental Deposits'],
]);
...@@ -20,5 +20,7 @@ PermissionRegistry::register('reports', [ ...@@ -20,5 +20,7 @@ PermissionRegistry::register('reports', [
'report.view_financial' => ['ar' => 'تقارير مالية', 'en' => 'Financial Reports'], 'report.view_financial' => ['ar' => 'تقارير مالية', 'en' => 'Financial Reports'],
'report.view_operations' => ['ar' => 'تقارير العمليات', 'en' => 'Operations Reports'], 'report.view_operations' => ['ar' => 'تقارير العمليات', 'en' => 'Operations Reports'],
'report.view_audit' => ['ar' => 'تقارير المراجعة', 'en' => 'Audit Reports'], 'report.view_audit' => ['ar' => 'تقارير المراجعة', 'en' => 'Audit Reports'],
'report.view_sports' => ['ar' => 'تقارير الأنشطة الرياضية', 'en' => 'Sports Activity Reports'],
'report.view_sports_financial' => ['ar' => 'تقارير مالية رياضية', 'en' => 'Sports Financial Reports'],
'report.export' => ['ar' => 'تصدير التقارير', 'en' => 'Export Reports'], 'report.export' => ['ar' => 'تصدير التقارير', 'en' => 'Export Reports'],
]); ]);
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Reservations\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Reservations\Models\Reservation;
use App\Modules\Reservations\Services\ReservationService;
use App\Modules\Reservations\Services\FacilityConflictDetector;
use App\Modules\Facilities\Models\Facility;
class ReservationController extends Controller
{
/**
* List all reservations with status tabs, facility filter, date range, and search.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'facility_id' => trim((string) $request->get('facility_id', '')),
'status' => trim((string) $request->get('status', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = Reservation::search($filters, 25, $page);
return $this->view('Reservations.Views.index', [
'reservations' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => Reservation::getStatuses(),
'facilities' => Facility::allActive(),
]);
}
/**
* Show the reservation creation form.
*/
public function create(Request $request): Response
{
return $this->view('Reservations.Views.create', [
'facilities' => Facility::allActive(),
'bookerTypes' => Reservation::getBookerTypes(),
]);
}
/**
* Validate and store a new reservation.
*/
public function store(Request $request): Response
{
$facilityId = (int) $request->post('facility_id', 0);
$reservationDate = trim((string) $request->post('reservation_date', ''));
$startTime = trim((string) $request->post('start_time', ''));
$endTime = trim((string) $request->post('end_time', ''));
$bookerType = trim((string) $request->post('booker_type', ''));
$bookerName = trim((string) $request->post('booker_name', ''));
$bookerPhone = trim((string) $request->post('booker_phone', ''));
$playerId = $request->post('player_id') ? (int) $request->post('player_id') : null;
$memberId = $request->post('member_id') ? (int) $request->post('member_id') : null;
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
if ($facilityId <= 0) {
$errors[] = 'يجب اختيار المرفق';
}
if ($reservationDate === '') {
$errors[] = 'تاريخ الحجز مطلوب';
}
if ($startTime === '' || $endTime === '') {
$errors[] = 'وقت البداية والنهاية مطلوب';
}
if ($startTime !== '' && $endTime !== '' && $startTime >= $endTime) {
$errors[] = 'وقت النهاية يجب أن يكون بعد وقت البداية';
}
if (!array_key_exists($bookerType, Reservation::getBookerTypes())) {
$errors[] = 'نوع الحاجز غير صالح';
}
if ($bookerName === '') {
$errors[] = 'اسم الحاجز مطلوب';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/reservations/create');
}
$result = ReservationService::create([
'facility_id' => $facilityId,
'reservation_date' => $reservationDate,
'start_time' => $startTime,
'end_time' => $endTime,
'booker_type' => $bookerType,
'booker_name' => $bookerName,
'booker_phone' => $bookerPhone,
'player_id' => $playerId,
'member_id' => $memberId,
'notes' => $notes ?: null,
]);
if (!$result['success']) {
$session = App::getInstance()->session();
$conflictMessages = array_map(
fn($c) => ['type' => 'error', 'message' => $c['message']],
$result['conflicts']
);
$session->flash('_alerts', $conflictMessages);
$session->flash('_old_input', $request->all());
return $this->redirect('/reservations/create');
}
return $this->redirect('/reservations/' . $result['reservation']->id)
->withSuccess('تم إنشاء الحجز بنجاح — رقم: ' . $result['reservation']->reservation_number);
}
/**
* Show reservation detail with action buttons.
*/
public function show(Request $request, string $id): Response
{
$reservation = Reservation::find((int) $id);
if (!$reservation) {
return $this->redirect('/reservations')->withError('الحجز غير موجود');
}
$facility = Facility::find((int) $reservation->facility_id);
return $this->view('Reservations.Views.show', [
'reservation' => $reservation,
'facility' => $facility,
'statuses' => Reservation::getStatuses(),
'bookerTypes' => Reservation::getBookerTypes(),
]);
}
/**
* Confirm a reservation.
*/
public function confirm(Request $request, string $id): Response
{
$reservation = Reservation::find((int) $id);
if (!$reservation) {
return $this->redirect('/reservations')->withError('الحجز غير موجود');
}
ReservationService::confirm((int) $id);
return $this->redirect('/reservations/' . $id)->withSuccess('تم تأكيد الحجز بنجاح');
}
/**
* Cancel a reservation with an optional reason.
*/
public function cancel(Request $request, string $id): Response
{
$reservation = Reservation::find((int) $id);
if (!$reservation) {
return $this->redirect('/reservations')->withError('الحجز غير موجود');
}
$reason = trim((string) $request->post('cancel_reason', ''));
ReservationService::cancel((int) $id, $reason ?: null);
return $this->redirect('/reservations/' . $id)->withSuccess('تم إلغاء الحجز');
}
/**
* Check in a reservation.
*/
public function checkIn(Request $request, string $id): Response
{
$reservation = Reservation::find((int) $id);
if (!$reservation) {
return $this->redirect('/reservations')->withError('الحجز غير موجود');
}
ReservationService::checkIn((int) $id);
return $this->redirect('/reservations/' . $id)->withSuccess('تم تسجيل الدخول بنجاح');
}
/**
* Calendar view for reservations.
*/
public function calendar(Request $request): Response
{
$dateFrom = trim((string) $request->get('date_from', date('Y-m-d')));
$dateTo = trim((string) $request->get('date_to', date('Y-m-d', strtotime('+6 days'))));
$facilities = Facility::allActive();
$db = App::getInstance()->db();
$reservations = $db->select(
"SELECT r.*, f.name_ar as facility_name
FROM `reservations` r
LEFT JOIN `facilities` f ON f.id = r.facility_id
WHERE r.`reservation_date` BETWEEN ? AND ?
AND r.`status` NOT IN ('cancelled', 'no_show')
AND r.`is_archived` = 0
ORDER BY r.`reservation_date` ASC, r.`start_time` ASC",
[$dateFrom, $dateTo]
);
return $this->view('Reservations.Views.calendar', [
'facilities' => $facilities,
'reservations' => $reservations,
'dateFrom' => $dateFrom,
'dateTo' => $dateTo,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Reservations\Models;
use App\Core\Model;
use App\Core\App;
class Reservation extends Model
{
protected static string $table = 'reservations';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'reservation_number',
'facility_id',
'booker_type',
'player_id',
'member_id',
'booker_name',
'booker_phone',
'reservation_date',
'start_time',
'end_time',
'duration_hours',
'time_tier',
'unit_rate',
'total_amount',
'payment_id',
'status',
'confirmed_at',
'cancelled_at',
'cancel_reason',
'notes',
'created_by',
];
/**
* Get all reservation statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'pending' => 'قيد الانتظار',
'confirmed' => 'مؤكد',
'checked_in' => 'تم الدخول',
'completed' => 'مكتمل',
'cancelled' => 'ملغى',
'no_show' => 'لم يحضر',
];
}
/**
* Get the Arabic label for a status key.
*/
public static function getStatusLabel(string $status): string
{
$statuses = self::getStatuses();
return $statuses[$status] ?? $status;
}
/**
* Get the badge color for a status.
*/
public static function getStatusColor(string $status): string
{
$colors = [
'pending' => '#D97706',
'confirmed' => '#0284C7',
'checked_in' => '#7C3AED',
'completed' => '#059669',
'cancelled' => '#DC2626',
'no_show' => '#6B7280',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Get all booker types with Arabic labels.
*/
public static function getBookerTypes(): array
{
return [
'member' => 'عضو',
'non_member' => 'غير عضو',
'player' => 'لاعب',
'guest' => 'ضيف',
];
}
/**
* Get the Arabic label for a booker type key.
*/
public static function getBookerTypeLabel(string $type): string
{
$types = self::getBookerTypes();
return $types[$type] ?? $type;
}
/**
* Generate a unique reservation number in the format RES-YYYYMMDD-XXXX.
*/
public static function generateNumber(): string
{
$today = date('Ymd');
$prefix = 'RES-' . $today . '-';
$db = App::getInstance()->db();
$row = $db->selectOne(
"SELECT COUNT(*) as cnt FROM `reservations` WHERE `reservation_number` LIKE ?",
[$prefix . '%']
);
$seq = ((int) ($row['cnt'] ?? 0)) + 1;
return $prefix . str_pad((string) $seq, 4, '0', STR_PAD_LEFT);
}
/**
* Search reservations with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query()
->leftJoin('facilities', '`reservations`.`facility_id` = `facilities`.`id`')
->select('`reservations`.*, `facilities`.`name_ar` as facility_name');
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`reservations`.`reservation_number` LIKE ? OR `reservations`.`booker_name` LIKE ? OR `reservations`.`booker_phone` LIKE ?)',
[$search, $search, $search]
);
}
if (!empty($filters['facility_id'])) {
$query = $query->where('facility_id', '=', (int) $filters['facility_id']);
}
if (!empty($filters['status'])) {
$query = $query->where('status', '=', $filters['status']);
}
if (!empty($filters['date_from'])) {
$query = $query->whereRaw('`reservations`.`reservation_date` >= ?', [$filters['date_from']]);
}
if (!empty($filters['date_to'])) {
$query = $query->whereRaw('`reservations`.`reservation_date` <= ?', [$filters['date_to']]);
}
$query = $query->orderBy('reservation_date', 'DESC')->orderBy('start_time', 'ASC');
return $query->paginate($perPage, $page);
}
/**
* Get all reservations for a facility on a specific date (for conflict detection).
*/
public static function getForFacilityDate(int $facilityId, string $date): array
{
return static::query()
->where('facility_id', '=', $facilityId)
->where('reservation_date', '=', $date)
->whereRaw("`reservations`.`status` NOT IN ('cancelled', 'no_show')")
->orderBy('start_time', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
return [
['GET', '/reservations', 'Reservations\Controllers\ReservationController@index', ['auth'], 'reservation.view'],
['GET', '/reservations/create', 'Reservations\Controllers\ReservationController@create', ['auth'], 'reservation.create'],
['POST', '/reservations', 'Reservations\Controllers\ReservationController@store', ['auth', 'csrf'], 'reservation.create'],
['GET', '/reservations/calendar', 'Reservations\Controllers\ReservationController@calendar', ['auth'], 'reservation.view'],
['GET', '/reservations/{id:\d+}', 'Reservations\Controllers\ReservationController@show', ['auth'], 'reservation.view'],
['POST', '/reservations/{id:\d+}/confirm', 'Reservations\Controllers\ReservationController@confirm', ['auth', 'csrf'], 'reservation.confirm'],
['POST', '/reservations/{id:\d+}/cancel', 'Reservations\Controllers\ReservationController@cancel', ['auth', 'csrf'], 'reservation.cancel'],
['POST', '/reservations/{id:\d+}/checkin', 'Reservations\Controllers\ReservationController@checkIn', ['auth', 'csrf'], 'reservation.view'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Reservations\Services;
use App\Core\App;
use App\Modules\Reservations\Models\Reservation;
use App\Modules\Facilities\Models\FacilityBlackoutDate;
class FacilityConflictDetector
{
/**
* Check for booking conflicts on a facility for a given date and time range.
*
* Returns an empty array when no conflicts exist, or an array of conflict
* descriptions when overlaps are found.
*
* @return array<int, array{type: string, message: string}>
*/
public static function checkConflicts(
int $facilityId,
string $date,
string $startTime,
string $endTime,
?int $excludeReservationId = null
): array {
$conflicts = [];
// 1. Check overlapping reservations
$reservationConflicts = self::checkReservationConflicts(
$facilityId, $date, $startTime, $endTime, $excludeReservationId
);
$conflicts = array_merge($conflicts, $reservationConflicts);
// 2. Check overlapping rental bookings
$rentalConflicts = self::checkRentalConflicts(
$facilityId, $date, $startTime, $endTime
);
$conflicts = array_merge($conflicts, $rentalConflicts);
// 3. Check facility blackout dates
$blackoutConflicts = self::checkBlackoutConflicts(
$facilityId, $date, $startTime, $endTime
);
$conflicts = array_merge($conflicts, $blackoutConflicts);
return $conflicts;
}
/**
* Check for overlapping reservations on the same facility/date.
*/
private static function checkReservationConflicts(
int $facilityId,
string $date,
string $startTime,
string $endTime,
?int $excludeReservationId
): array {
$conflicts = [];
$query = (new \App\Core\QueryBuilder())
->table('reservations')
->where('facility_id', '=', $facilityId)
->where('reservation_date', '=', $date)
->whereRaw("`status` NOT IN ('cancelled', 'no_show')")
->whereRaw('(`start_time` < ? AND `end_time` > ?)', [$endTime, $startTime]);
if ($excludeReservationId !== null) {
$query = $query->whereRaw('`id` != ?', [$excludeReservationId]);
}
$rows = $query->get();
foreach ($rows as $row) {
$conflicts[] = [
'type' => 'reservation',
'message' => sprintf(
'يوجد حجز متعارض (رقم %s) من %s إلى %s',
$row['reservation_number'] ?? '—',
$row['start_time'] ?? '',
$row['end_time'] ?? ''
),
];
}
return $conflicts;
}
/**
* Check for overlapping rental bookings on the same facility/date.
*/
private static function checkRentalConflicts(
int $facilityId,
string $date,
string $startTime,
string $endTime
): array {
$conflicts = [];
$db = App::getInstance()->db();
// Check if rental_bookings table exists; skip gracefully if not
try {
$rows = $db->select(
"SELECT * FROM `rental_bookings`
WHERE `facility_id` = ?
AND `booking_date` = ?
AND `status` NOT IN ('cancelled')
AND (`start_time` < ? AND `end_time` > ?)",
[$facilityId, $date, $endTime, $startTime]
);
} catch (\Throwable) {
// Table may not exist yet; silently skip
return [];
}
foreach ($rows as $row) {
$conflicts[] = [
'type' => 'rental',
'message' => sprintf(
'يوجد حجز تأجير مؤسسي متعارض من %s إلى %s',
$row['start_time'] ?? '',
$row['end_time'] ?? ''
),
];
}
return $conflicts;
}
/**
* Check for blackout dates on the facility.
*/
private static function checkBlackoutConflicts(
int $facilityId,
string $date,
string $startTime,
string $endTime
): array {
$conflicts = [];
$rows = (new \App\Core\QueryBuilder())
->table('facility_blackout_dates')
->noSoftDelete()
->where('facility_id', '=', $facilityId)
->where('blackout_date', '=', $date)
->get();
foreach ($rows as $row) {
$bStart = $row['start_time'] ?? null;
$bEnd = $row['end_time'] ?? null;
// Full-day blackout
if (empty($bStart) && empty($bEnd)) {
$conflicts[] = [
'type' => 'blackout',
'message' => 'المرفق محجوب بالكامل في هذا التاريخ: ' . ($row['reason'] ?? ''),
];
continue;
}
// Time-range blackout
$bStart = $bStart ?? '00:00:00';
$bEnd = $bEnd ?? '23:59:59';
if ($startTime < $bEnd && $endTime > $bStart) {
$conflicts[] = [
'type' => 'blackout',
'message' => sprintf(
'المرفق محجوب من %s إلى %s: %s',
$bStart,
$bEnd,
$row['reason'] ?? ''
),
];
}
}
return $conflicts;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Reservations\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Reservations\Models\Reservation;
use App\Modules\Facilities\Models\Facility;
class ReservationService
{
/**
* Create a new reservation.
*
* Generates a reservation number, calculates duration and total,
* checks for conflicts, and creates the record.
*/
public static function create(array $data): array
{
// Check for conflicts first
$conflicts = FacilityConflictDetector::checkConflicts(
(int) $data['facility_id'],
$data['reservation_date'],
$data['start_time'],
$data['end_time']
);
if (!empty($conflicts)) {
return ['success' => false, 'conflicts' => $conflicts];
}
// Generate reservation number
$data['reservation_number'] = self::generateNumber();
// Calculate duration in hours
$start = strtotime($data['reservation_date'] . ' ' . $data['start_time']);
$end = strtotime($data['reservation_date'] . ' ' . $data['end_time']);
$durationHours = ($end - $start) / 3600;
$data['duration_hours'] = round($durationHours, 2);
// 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';
}
// Calculate unit rate and total amount if facility is set
if (!empty($data['facility_id']) && empty($data['unit_rate'])) {
$facility = Facility::find((int) $data['facility_id']);
if ($facility) {
$isMember = in_array($data['booker_type'] ?? '', ['member', 'player']);
$data['unit_rate'] = $facility->getRate($isMember, $data['time_tier']);
}
}
if (empty($data['total_amount']) && !empty($data['unit_rate'])) {
$data['total_amount'] = bcmul((string) $data['unit_rate'], (string) $data['duration_hours'], 2);
}
// Default status
if (empty($data['status'])) {
$data['status'] = 'pending';
}
$reservation = Reservation::create($data);
EventBus::dispatch('reservation.created', [
'reservation_id' => (int) $reservation->id,
'facility_id' => (int) $data['facility_id'],
'date' => $data['reservation_date'],
]);
return ['success' => true, 'reservation' => $reservation];
}
/**
* Confirm a reservation.
*/
public static function confirm(int $reservationId): bool
{
$reservation = Reservation::find($reservationId);
if (!$reservation) {
return false;
}
$reservation->update([
'status' => 'confirmed',
'confirmed_at' => date('Y-m-d H:i:s'),
]);
EventBus::dispatch('reservation.confirmed', [
'reservation_id' => $reservationId,
]);
return true;
}
/**
* Cancel a reservation with an optional reason.
*/
public static function cancel(int $reservationId, ?string $reason = null): bool
{
$reservation = Reservation::find($reservationId);
if (!$reservation) {
return false;
}
$reservation->update([
'status' => 'cancelled',
'cancelled_at' => date('Y-m-d H:i:s'),
'cancel_reason' => $reason,
]);
EventBus::dispatch('reservation.cancelled', [
'reservation_id' => $reservationId,
]);
return true;
}
/**
* Check in a reservation (mark as checked_in).
*/
public static function checkIn(int $reservationId): bool
{
$reservation = Reservation::find($reservationId);
if (!$reservation) {
return false;
}
$reservation->update([
'status' => 'checked_in',
]);
return true;
}
/**
* Generate a unique reservation number (RES-YYYYMMDD-XXXX).
*/
public static function generateNumber(): string
{
return Reservation::generateNumber();
}
}
<?php
use App\Modules\Reservations\Models\Reservation;
$__template->layout('Layout.main');
$arabicDays = [
6 => 'السبت',
0 => 'الأحد',
1 => 'الاثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
];
// Build the days array from dateFrom to dateTo
$days = [];
$current = strtotime($dateFrom);
$end = strtotime($dateTo);
while ($current <= $end) {
$dateStr = date('Y-m-d', $current);
$dayOfWeek = (int) date('w', $current);
$days[] = [
'date' => $dateStr,
'day_name' => $arabicDays[$dayOfWeek] ?? '',
'display' => date('m/d', $current),
];
$current = strtotime('+1 day', $current);
}
// Group reservations by date
$grouped = [];
foreach ($reservations as $r) {
$d = $r['reservation_date'] ?? '';
if ($d !== '') {
$grouped[$d][] = $r;
}
}
?>
<?php $__template->section('title'); ?>تقويم الحجوزات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/reservations" class="btn btn-outline"><i data-lucide="list" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض القائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Date Range Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/reservations/calendar" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($dateFrom) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($dateTo) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<button type="submit" class="btn btn-primary">
<i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض
</button>
</form>
</div>
<!-- Calendar Grid -->
<?php if (!empty($days)): ?>
<div style="overflow-x:auto;padding-bottom:10px;">
<div style="display:flex;gap:12px;min-width:<?= count($days) * 230 ?>px;">
<?php foreach ($days as $day): ?>
<div style="flex:1;min-width:210px;">
<!-- Day Header -->
<div style="background:#0D7377;color:#fff;padding:12px 15px;border-radius:10px 10px 0 0;text-align:center;">
<div style="font-size:15px;font-weight:700;"><?= e($day['day_name']) ?></div>
<div style="font-size:12px;margin-top:4px;opacity:0.85;"><?= e($day['date']) ?></div>
</div>
<!-- Day Content -->
<div style="background:#F9FAFB;border:1px solid #E5E7EB;border-top:none;border-radius:0 0 10px 10px;padding:10px;min-height:120px;">
<?php if (!empty($grouped[$day['date']])): ?>
<?php foreach ($grouped[$day['date']] as $r):
$rStatus = $r['status'] ?? 'pending';
$rColor = Reservation::getStatusColor($rStatus);
$rLabel = Reservation::getStatusLabel($rStatus);
?>
<div style="background:#fff;border:1px solid #E5E7EB;border-radius:8px;padding:10px;margin-bottom:8px;border-right:3px solid <?= $rColor ?>;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">
<span style="font-size:13px;font-weight:700;color:#1A1A2E;direction:ltr;display:inline-block;">
<?= e(substr($r['start_time'] ?? '', 0, 5)) ?> - <?= e(substr($r['end_time'] ?? '', 0, 5)) ?>
</span>
<span class="badge" style="background:<?= $rColor ?>15;color:<?= $rColor ?>;font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600;">
<?= e($rLabel) ?>
</span>
</div>
<div style="font-size:12px;color:#374151;margin-bottom:3px;">
<i data-lucide="building" style="width:12px;height:12px;vertical-align:middle;color:#9CA3AF;"></i>
<?= e($r['facility_name'] ?? '---') ?>
</div>
<div style="font-size:12px;color:#6B7280;">
<i data-lucide="user" style="width:12px;height:12px;vertical-align:middle;color:#9CA3AF;"></i>
<?= e($r['booker_name'] ?? '---') ?>
</div>
</div>
<?php endforeach; ?>
<?php else: ?>
<div style="text-align:center;padding:20px 10px;color:#9CA3AF;font-size:13px;">
<i data-lucide="calendar-x" style="width:20px;height:20px;margin-bottom:6px;"></i>
<div>لا توجد حجوزات</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="calendar-x" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد أيام لعرضها</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0;">يرجى اختيار نطاق تاريخ صحيح.</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Reservations\Models\Reservation;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>حجز جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/reservations" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="/reservations">
<?= csrf_field() ?>
<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="calendar-plus" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات الحجز</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<!-- Facility -->
<div>
<label class="form-label">المرفق <span style="color:#DC2626;">*</span></label>
<select name="facility_id" class="form-input" required>
<option value="">-- اختر المرفق --</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= old('facility_id') == $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Reservation Date -->
<div>
<label class="form-label">تاريخ الحجز <span style="color:#DC2626;">*</span></label>
<input type="date" name="reservation_date" value="<?= e(old('reservation_date')) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<!-- Start Time -->
<div>
<label class="form-label">وقت البداية <span style="color:#DC2626;">*</span></label>
<input type="time" name="start_time" value="<?= e(old('start_time')) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<!-- End Time -->
<div>
<label class="form-label">وقت النهاية <span style="color:#DC2626;">*</span></label>
<input type="time" name="end_time" value="<?= e(old('end_time')) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<!-- Booker Type -->
<div>
<label class="form-label">نوع الحاجز <span style="color:#DC2626;">*</span></label>
<select name="booker_type" class="form-input" required>
<option value="">-- اختر النوع --</option>
<?php foreach ($bookerTypes as $code => $label): ?>
<option value="<?= e($code) ?>" <?= old('booker_type') === $code ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<!-- Booker Name -->
<div>
<label class="form-label">اسم الحاجز <span style="color:#DC2626;">*</span></label>
<input type="text" name="booker_name" value="<?= e(old('booker_name')) ?>" class="form-input" required placeholder="الاسم بالكامل">
</div>
<!-- Booker Phone -->
<div>
<label class="form-label">هاتف الحاجز</label>
<input type="text" name="booker_phone" value="<?= e(old('booker_phone')) ?>" class="form-input" placeholder="رقم الهاتف" style="direction:ltr;text-align:left;">
</div>
<!-- Player ID -->
<div>
<label class="form-label">رقم اللاعب</label>
<input type="number" name="player_id" value="<?= e(old('player_id')) ?>" class="form-input" placeholder="اختياري" style="direction:ltr;text-align:left;">
</div>
<!-- Member ID -->
<div>
<label class="form-label">رقم العضوية</label>
<input type="number" name="member_id" value="<?= e(old('member_id')) ?>" class="form-input" placeholder="اختياري" style="direction:ltr;text-align:left;">
</div>
<!-- Notes -->
<div style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية..."><?= e(old('notes')) ?></textarea>
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء الحجز
</button>
<a href="/reservations" class="btn btn-outline">إلغاء</a>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Reservations\Models\Reservation;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الحجوزات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/reservations/calendar" class="btn btn-outline"><i data-lucide="calendar" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقويم</a>
<a href="/reservations/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حجز جديد</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$currentStatus = $filters['status'] ?? '';
$allStatuses = Reservation::getStatuses();
?>
<!-- Status Filter Tabs -->
<div class="card" style="margin-bottom:20px;padding:0;">
<div style="display:flex;align-items:center;gap:0;overflow-x:auto;border-bottom:2px solid #E5E7EB;">
<a href="/reservations?<?= http_build_query(array_merge($filters, ['status' => '', 'page' => 1])) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === '' ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === '' ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
الكل
</a>
<?php foreach ($allStatuses as $sKey => $sLabel): ?>
<a href="/reservations?<?= http_build_query(array_merge($filters, ['status' => $sKey, 'page' => 1])) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === $sKey ? '#0D7377' : 'transparent' ?>;color:<?= $currentStatus === $sKey ? '#0D7377' : '#6B7280' ?>;white-space:nowrap;">
<?= e($sLabel) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search & Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/reservations" style="display:flex;flex-wrap:wrap;gap:10px;align-items:end;">
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="رقم الحجز، اسم الحاجز، الهاتف..." class="form-input">
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المرفق</label>
<select name="facility_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= ($filters['facility_id'] ?? '') == $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input">
</div>
<?php if ($currentStatus !== ''): ?>
<input type="hidden" name="status" value="<?= e($currentStatus) ?>">
<?php endif; ?>
<button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<a href="/reservations" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Reservations Table -->
<?php if (!empty($reservations)): ?>
<div class="card" style="padding:0;overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">رقم الحجز</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">المرفق</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">التاريخ</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الوقت</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الحاجز</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">المبلغ</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($reservations as $r):
$statusColor = Reservation::getStatusColor($r['status'] ?? 'pending');
$statusLabel = Reservation::getStatusLabel($r['status'] ?? 'pending');
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;">
<code style="font-size:12px;background:#F0F9FF;color:#0284C7;padding:2px 8px;border-radius:4px;"><?= e($r['reservation_number'] ?? '') ?></code>
</td>
<td style="padding:12px 16px;"><?= e($r['facility_name'] ?? '—') ?></td>
<td style="padding:12px 16px;"><?= e($r['reservation_date'] ?? '') ?></td>
<td style="padding:12px 16px;white-space:nowrap;">
<?= e(substr($r['start_time'] ?? '', 0, 5)) ?> - <?= e(substr($r['end_time'] ?? '', 0, 5)) ?>
</td>
<td style="padding:12px 16px;">
<div><?= e($r['booker_name'] ?? '—') ?></div>
<?php if (!empty($r['booker_phone'])): ?>
<div style="font-size:12px;color:#9CA3AF;"><?= e($r['booker_phone']) ?></div>
<?php endif; ?>
</td>
<td style="padding:12px 16px;font-weight:600;"><?= money($r['total_amount'] ?? 0) ?></td>
<td style="padding:12px 16px;text-align:center;">
<span class="badge" style="background:<?= $statusColor ?>15;color:<?= $statusColor ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($statusLabel) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<a href="/reservations/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline" style="font-size:12px;padding:4px 10px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i> عرض
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php if (isset($pagination) && ($pagination['last_page'] ?? 1) > 1): ?>
<div style="padding:15px;">
<?php $__template->include('Shared.Components.pagination', ['pagination' => $pagination]); ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="calendar-x" style="width:48px;height:48px;color:#D1D5DB;"></i>
</div>
<h3 style="color:#6B7280;margin:0 0 8px;">لا توجد حجوزات</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">
<?php if (!empty($filters['q']) || !empty($filters['facility_id']) || !empty($filters['date_from'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء حجز جديد للمرافق.
<?php endif; ?>
</p>
<a href="/reservations/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حجز جديد</a>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Reservations\Models\Reservation;
$__template->layout('Layout.main');
$status = $reservation->status ?? 'pending';
$statusColor = Reservation::getStatusColor($status);
$statusLabel = Reservation::getStatusLabel($status);
$bookerTypeLabel = Reservation::getBookerTypeLabel($reservation->booker_type ?? '');
?>
<?php $__template->section('title'); ?><?= e($reservation->reservation_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/reservations" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;overflow:hidden;">
<div style="padding:25px;display:flex;align-items:start;gap:20px;">
<div style="width:72px;height:72px;border-radius:16px;background:linear-gradient(135deg, #0D737715, #0D737730);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="calendar-check" style="width:36px;height:36px;color:#0D7377;"></i>
</div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;flex-wrap:wrap;">
<code style="font-size:14px;background:#F0F9FF;color:#0284C7;padding:4px 12px;border-radius:6px;font-weight:700;"><?= e($reservation->reservation_number) ?></code>
<span class="badge" style="background:<?= $statusColor ?>15;color:<?= $statusColor ?>;font-size:12px;padding:4px 12px;border-radius:10px;font-weight:600;">
<?= e($statusLabel) ?>
</span>
</div>
<div style="display:flex;gap:16px;flex-wrap:wrap;font-size:14px;color:#4B5563;margin-top:10px;">
<span style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="building" style="width:15px;height:15px;color:#6B7280;"></i>
<?= e($facility->name_ar ?? '---') ?>
</span>
<span style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="calendar" style="width:15px;height:15px;color:#6B7280;"></i>
<?= e($reservation->reservation_date) ?>
</span>
<span style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="clock" style="width:15px;height:15px;color:#6B7280;"></i>
<?= e(substr($reservation->start_time ?? '', 0, 5)) ?> - <?= e(substr($reservation->end_time ?? '', 0, 5)) ?>
</span>
</div>
</div>
</div>
</div>
<!-- Info Grid -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Booker Info -->
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="user" style="width:18px;height:18px;color:#0D7377;"></i>
<h4 style="margin:0;color:#0D7377;font-size:15px;">بيانات الحاجز</h4>
</div>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:40%;">الاسم</td>
<td style="padding:6px 0;font-weight:600;"><?= e($reservation->booker_name ?? '---') ?></td>
</tr>
<tr>
<td style="padding:6px 0;color:#6B7280;">الهاتف</td>
<td style="padding:6px 0;direction:ltr;text-align:right;"><?= e($reservation->booker_phone ?: '---') ?></td>
</tr>
<tr>
<td style="padding:6px 0;color:#6B7280;">النوع</td>
<td style="padding:6px 0;"><?= e($bookerTypeLabel) ?></td>
</tr>
<?php if ($reservation->member_id): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">رقم العضوية</td>
<td style="padding:6px 0;"><a href="/members/<?= (int) $reservation->member_id ?>" style="color:#0D7377;font-weight:600;">#<?= (int) $reservation->member_id ?></a></td>
</tr>
<?php endif; ?>
<?php if ($reservation->player_id): ?>
<tr>
<td style="padding:6px 0;color:#6B7280;">رقم اللاعب</td>
<td style="padding:6px 0;">#<?= (int) $reservation->player_id ?></td>
</tr>
<?php endif; ?>
</table>
</div>
<!-- Financial Info -->
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="banknote" style="width:18px;height:18px;color:#059669;"></i>
<h4 style="margin:0;color:#059669;font-size:15px;">البيانات المالية</h4>
</div>
<table style="width:100%;font-size:14px;">
<tr>
<td style="padding:6px 0;color:#6B7280;width:40%;">سعر الساعة</td>
<td style="padding:6px 0;font-weight:600;"><?= money($reservation->unit_rate ?? 0) ?></td>
</tr>
<tr>
<td style="padding:6px 0;color:#6B7280;">المدة (ساعات)</td>
<td style="padding:6px 0;font-weight:600;"><?= e($reservation->duration_hours ?? '---') ?></td>
</tr>
<tr>
<td style="padding:8px 0;color:#6B7280;font-size:15px;font-weight:600;">الإجمالي</td>
<td style="padding:8px 0;font-weight:700;font-size:20px;color:#0D7377;"><?= money($reservation->total_amount ?? 0) ?></td>
</tr>
</table>
</div>
</div>
<!-- Dates Info -->
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="history" style="width:18px;height:18px;color:#6B7280;"></i>
<h4 style="margin:0;color:#6B7280;font-size:15px;">التواريخ</h4>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;">
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">تاريخ الإنشاء</div>
<div style="font-size:14px;font-weight:600;color:#374151;"><?= e($reservation->created_at ?? '---') ?></div>
</div>
<?php if ($reservation->confirmed_at): ?>
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">تاريخ التأكيد</div>
<div style="font-size:14px;font-weight:600;color:#059669;"><?= e($reservation->confirmed_at) ?></div>
</div>
<?php endif; ?>
<?php if ($reservation->cancelled_at): ?>
<div>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:4px;">تاريخ الإلغاء</div>
<div style="font-size:14px;font-weight:600;color:#DC2626;"><?= e($reservation->cancelled_at) ?></div>
</div>
<?php endif; ?>
</div>
</div>
<!-- Cancel Reason -->
<?php if (!empty($reservation->cancel_reason)): ?>
<div class="card" style="margin-bottom:20px;padding:20px;background:#FEF2F2;border:1px solid #FECACA;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<i data-lucide="alert-circle" style="width:18px;height:18px;color:#DC2626;"></i>
<h4 style="margin:0;color:#DC2626;font-size:15px;">سبب الإلغاء</h4>
</div>
<p style="margin:0;font-size:14px;color:#374151;line-height:1.7;"><?= nl2br(e($reservation->cancel_reason)) ?></p>
</div>
<?php endif; ?>
<!-- Notes -->
<?php if (!empty($reservation->notes)): ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<i data-lucide="file-text" style="width:18px;height:18px;color:#6B7280;"></i>
<h4 style="margin:0;color:#6B7280;font-size:15px;">ملاحظات</h4>
</div>
<p style="margin:0;font-size:14px;color:#374151;line-height:1.7;"><?= nl2br(e($reservation->notes)) ?></p>
</div>
<?php endif; ?>
<!-- Action Buttons -->
<?php if ($status === 'pending'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#0D7377;"></i>
<h4 style="margin:0;color:#0D7377;font-size:15px;">الإجراءات</h4>
</div>
<div style="display:flex;gap:10px;margin-bottom:20px;">
<form method="POST" action="/reservations/<?= (int) $reservation->id ?>/confirm" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تأكيد هذا الحجز؟')">
<i data-lucide="check-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تأكيد الحجز
</button>
</form>
</div>
<div style="border-top:1px solid #E5E7EB;padding-top:15px;">
<form method="POST" action="/reservations/<?= (int) $reservation->id ?>/cancel">
<?= csrf_field() ?>
<div style="margin-bottom:10px;">
<label class="form-label" style="color:#DC2626;">سبب الإلغاء</label>
<textarea name="cancel_reason" class="form-input" rows="2" placeholder="يرجى توضيح سبب الإلغاء..."></textarea>
</div>
<button type="submit" class="btn btn-danger" onclick="return confirm('هل أنت متأكد من إلغاء هذا الحجز؟')">
<i data-lucide="x-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إلغاء الحجز
</button>
</form>
</div>
</div>
<?php elseif ($status === 'confirmed'): ?>
<div class="card" style="margin-bottom:20px;padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:15px;">
<i data-lucide="settings" style="width:18px;height:18px;color:#0D7377;"></i>
<h4 style="margin:0;color:#0D7377;font-size:15px;">الإجراءات</h4>
</div>
<div style="display:flex;gap:10px;margin-bottom:20px;">
<form method="POST" action="/reservations/<?= (int) $reservation->id ?>/check-in" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تسجيل الدخول؟')">
<i data-lucide="log-in" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تسجيل الدخول
</button>
</form>
</div>
<div style="border-top:1px solid #E5E7EB;padding-top:15px;">
<form method="POST" action="/reservations/<?= (int) $reservation->id ?>/cancel">
<?= csrf_field() ?>
<div style="margin-bottom:10px;">
<label class="form-label" style="color:#DC2626;">سبب الإلغاء</label>
<textarea name="cancel_reason" class="form-input" rows="2" placeholder="يرجى توضيح سبب الإلغاء..."></textarea>
</div>
<button type="submit" class="btn btn-danger" onclick="return confirm('هل أنت متأكد من إلغاء هذا الحجز؟')">
<i data-lucide="x-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إلغاء الحجز
</button>
</form>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
PermissionRegistry::register('reservations', [
'reservation.view' => ['ar' => 'عرض الحجوزات', 'en' => 'View Reservations'],
'reservation.create' => ['ar' => 'إنشاء حجز', 'en' => 'Create Reservation'],
'reservation.confirm' => ['ar' => 'تأكيد الحجوزات', 'en' => 'Confirm Reservations'],
'reservation.cancel' => ['ar' => 'إلغاء الحجوزات', 'en' => 'Cancel Reservations'],
]);
...@@ -33,7 +33,7 @@ $rd = $receiptDesign; ...@@ -33,7 +33,7 @@ $rd = $receiptDesign;
<!-- ============================================ --> <!-- ============================================ -->
<div class="tab-panel active" id="tab-identity"> <div class="tab-panel active" id="tab-identity">
<form method="POST" action="/settings/branding/logo" enctype="multipart/form-data"> <form method="POST" action="/settings/branding/logo" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?= e(CSRF::token()) ?>"> <input type="hidden" name="_csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="branding-grid"> <div class="branding-grid">
<!-- Logo Upload Card --> <!-- Logo Upload Card -->
...@@ -103,7 +103,7 @@ $rd = $receiptDesign; ...@@ -103,7 +103,7 @@ $rd = $receiptDesign;
<!-- ============================================ --> <!-- ============================================ -->
<div class="tab-panel" id="tab-carnet"> <div class="tab-panel" id="tab-carnet">
<form method="POST" action="/settings/branding/carnet"> <form method="POST" action="/settings/branding/carnet">
<input type="hidden" name="csrf_token" value="<?= e(CSRF::token()) ?>"> <input type="hidden" name="_csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="designer-layout"> <div class="designer-layout">
<!-- Settings Panel --> <!-- Settings Panel -->
...@@ -294,7 +294,7 @@ $rd = $receiptDesign; ...@@ -294,7 +294,7 @@ $rd = $receiptDesign;
<!-- ============================================ --> <!-- ============================================ -->
<div class="tab-panel" id="tab-receipt"> <div class="tab-panel" id="tab-receipt">
<form method="POST" action="/settings/branding/receipt"> <form method="POST" action="/settings/branding/receipt">
<input type="hidden" name="csrf_token" value="<?= e(CSRF::token()) ?>"> <input type="hidden" name="_csrf_token" value="<?= e(CSRF::token()) ?>">
<div class="designer-layout"> <div class="designer-layout">
<!-- Settings Panel --> <!-- Settings Panel -->
......
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\Logger;
/**
* Generates monthly activity subscriptions for all active enrollments.
* Runs on the 1st of each month.
*/
class ActivitySubGeneratorJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return (int) date('j') === 1;
}
public function run(): array
{
$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
$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,
p.player_type,
a.discipline_id
FROM academy_enrollments ae
JOIN players p ON p.id = ae.player_id AND p.is_archived = 0
JOIN academies a ON a.id = ae.academy_id AND a.is_active = 1
WHERE ae.status = 'active'
");
foreach ($enrollments as $e) {
$playerId = (int) $e['player_id'];
$enrollmentId = (int) $e['enrollment_id'];
// Check if subscription already exists for this player+month
$existing = $this->db->selectOne(
"SELECT id FROM activity_subscriptions WHERE player_id = ? AND enrollment_id = ? AND subscription_month = ?",
[$playerId, $enrollmentId, $month]
);
if ($existing) {
$skipped++;
continue;
}
// Look up pricing — try academy pricing first, then discipline
$isMember = ($e['player_type'] === 'member');
$rateCol = $isMember ? 'member_rate' : 'nonmember_rate';
$pricing = $this->db->selectOne(
"SELECT {$rateCol} AS rate FROM activity_pricing WHERE pricing_type = 'academy' AND reference_id = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE())",
[(int) $e['academy_id']]
);
if (!$pricing) {
$pricing = $this->db->selectOne(
"SELECT {$rateCol} AS rate FROM activity_pricing WHERE pricing_type = 'discipline' AND reference_id = ? AND is_active = 1 AND effective_from <= CURDATE() AND (effective_to IS NULL OR effective_to >= CURDATE())",
[(int) $e['discipline_id']]
);
}
$baseRate = $pricing ? (string) $pricing['rate'] : '0.00';
// Half-month logic: if enrollment was after the 15th AND this is the first month
$isFirstMonth = $this->isFirstSubscriptionMonth($enrollmentId, $month);
$enrollmentDay = (int) ($e['enrollment_day'] ?? 1);
$isHalfMonth = ($isFirstMonth && $enrollmentDay > 15) ? 1 : 0;
$appliedRate = $isHalfMonth ? bcdiv($baseRate, '2', 2) : $baseRate;
$this->db->insert('activity_subscriptions', [
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'discipline_id' => (int) $e['discipline_id'],
'subscription_month' => $month,
'player_type' => $e['player_type'],
'base_rate' => $baseRate,
'is_half_month' => $isHalfMonth,
'applied_rate' => $appliedRate,
'discount' => '0.00',
'total_amount' => $appliedRate,
'status' => 'pending',
'due_date' => $dueDate,
'created_at' => $ts,
'updated_at' => $ts,
]);
$processed++;
}
Logger::info("Activity subscription generation for {$month}: {$processed} created, {$skipped} skipped");
return ['month' => $month, 'processed' => $processed, 'skipped' => $skipped];
}
private function isFirstSubscriptionMonth(int $enrollmentId, string $currentMonth): bool
{
$prev = $this->db->selectOne(
"SELECT id FROM activity_subscriptions WHERE enrollment_id = ? AND subscription_month < ? LIMIT 1",
[$enrollmentId, $currentMonth]
);
return !$prev;
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\EventBus;
use App\Core\Logger;
/**
* Revokes overdue activity subscriptions and suspends player cards.
* Runs on the 8th of each month (7 days after the due date on the 1st).
*/
class ActivitySubRevokeJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
// Read revoke day from business rules, default to 7
$rule = $this->db->selectOne(
"SELECT current_value_json FROM business_rules WHERE rule_code = 'SPORTS_SUB_REVOKE_DAY' AND is_active = 1 AND branch_id IS NULL"
);
$revokeDay = 7;
if ($rule) {
$decoded = json_decode($rule['current_value_json'], true);
$revokeDay = (int) ($decoded['day'] ?? 7);
}
// Run when current day > due_date(7th) + revoke_day
return (int) date('j') >= (7 + $revokeDay);
}
public function run(): array
{
$ts = date('Y-m-d H:i:s');
$today = date('Y-m-d');
$revoked = 0;
// Find overdue subscriptions: status is pending/overdue AND due_date has passed
$overdue = $this->db->select("
SELECT as2.id, as2.player_id, as2.subscription_month, as2.total_amount
FROM activity_subscriptions as2
WHERE as2.status IN ('pending', 'overdue')
AND as2.due_date IS NOT NULL
AND as2.due_date < ?
", [$today]);
foreach ($overdue as $sub) {
// Revoke the subscription
$this->db->update('activity_subscriptions', [
'status' => 'revoked',
'revoked_at' => $ts,
'updated_at' => $ts,
], 'id = ?', [(int) $sub['id']]);
// Revoke the player's card
$this->db->update('players', [
'card_status' => 'revoked',
'updated_at' => $ts,
], 'id = ? AND card_status != ?', [(int) $sub['player_id'], 'revoked']);
EventBus::dispatch('activity_sub.revoked', [
'subscription_id' => (int) $sub['id'],
'player_id' => (int) $sub['player_id'],
'month' => $sub['subscription_month'],
'amount' => $sub['total_amount'],
]);
EventBus::dispatch('player.card_revoked', [
'player_id' => (int) $sub['player_id'],
'reason' => 'عدم سداد اشتراك شهر ' . $sub['subscription_month'],
]);
$revoked++;
}
Logger::info("Activity subscription revoke: {$revoked} subscriptions revoked");
return ['revoked' => $revoked];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\EventBus;
use App\Core\Logger;
/**
* Sends reminders for expiring medical certificates.
* Runs daily — checks for medical records expiring within 30 days.
*/
class MedicalExpiryReminderJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Runs daily
}
public function run(): array
{
$today = date('Y-m-d');
$thirtyDaysLater = date('Y-m-d', strtotime('+30 days'));
$notified = 0;
$expired = 0;
// Find players with medical certificates expiring within 30 days
$expiring = $this->db->select("
SELECT p.id AS player_id, p.full_name_ar, p.phone, p.medical_expiry_date
FROM players p
WHERE p.is_archived = 0
AND p.medical_expiry_date IS NOT NULL
AND p.medical_expiry_date BETWEEN ? AND ?
AND p.medical_status IN ('fit', 'conditional')
", [$today, $thirtyDaysLater]);
foreach ($expiring as $player) {
EventBus::dispatch('player.medical_expiry', [
'player_id' => (int) $player['player_id'],
'player_name' => $player['full_name_ar'],
'phone' => $player['phone'],
'expiry_date' => $player['medical_expiry_date'],
]);
$notified++;
}
// Update players whose medical has already expired
$expiredPlayers = $this->db->select("
SELECT id FROM players
WHERE is_archived = 0
AND medical_expiry_date IS NOT NULL
AND medical_expiry_date < ?
AND medical_status NOT IN ('expired', 'pending')
", [$today]);
foreach ($expiredPlayers as $ep) {
$this->db->update('players', [
'medical_status' => 'expired',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $ep['id']]);
$expired++;
}
Logger::info("Medical expiry check: {$notified} reminders sent, {$expired} marked expired");
return ['reminders_sent' => $notified, 'marked_expired' => $expired];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
use App\Core\EventBus;
use App\Core\Logger;
/**
* Sends reminders for upcoming facility reservations.
* Runs daily — reminds for reservations happening tomorrow.
*/
class ReservationReminderJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Runs daily
}
public function run(): array
{
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$notified = 0;
// Find confirmed reservations for tomorrow
$reservations = $this->db->select("
SELECT r.id, r.reservation_number, r.reservation_date, r.start_time, r.end_time,
r.booker_name, r.booker_phone, r.player_id, r.member_id,
f.name_ar AS facility_name
FROM reservations r
JOIN facilities f ON f.id = r.facility_id
WHERE r.reservation_date = ?
AND r.status = 'confirmed'
", [$tomorrow]);
foreach ($reservations as $res) {
// Resolve phone for notification
$phone = $res['booker_phone'];
if (!$phone && $res['player_id']) {
$player = $this->db->selectOne("SELECT phone FROM players WHERE id = ?", [(int) $res['player_id']]);
$phone = $player['phone'] ?? null;
}
if (!$phone && $res['member_id']) {
$member = $this->db->selectOne("SELECT phone FROM members WHERE id = ?", [(int) $res['member_id']]);
$phone = $member['phone'] ?? null;
}
EventBus::dispatch('reservation.reminder', [
'reservation_id' => (int) $res['id'],
'reservation_number' => $res['reservation_number'],
'facility_name' => $res['facility_name'],
'date' => $res['reservation_date'],
'start_time' => $res['start_time'],
'end_time' => $res['end_time'],
'phone' => $phone,
]);
$notified++;
}
Logger::info("Reservation reminders: {$notified} sent for {$tomorrow}");
return ['date' => $tomorrow, 'reminders_sent' => $notified];
}
}
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `sport_disciplines` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`category` VARCHAR(50) NOT NULL COMMENT 'individual, team, racket, leisure, emerging',
`icon` VARCHAR(100) NULL,
`description_ar` TEXT NULL,
`config_json` JSON NULL,
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_sport_disciplines_code` (`code`),
INDEX `idx_sport_disciplines_category` (`category`),
INDEX `idx_sport_disciplines_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `sport_disciplines`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `facilities` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`facility_type` VARCHAR(50) NOT NULL COMMENT 'pitch, court, hall, lane, table, device, track, pool',
`location` VARCHAR(50) NOT NULL COMMENT 'indoor, outdoor',
`capacity` INT UNSIGNED NULL,
`hourly_rate_member` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`hourly_rate_nonmember` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`hourly_rate_member_pm` DECIMAL(15,2) NULL,
`hourly_rate_nonmember_pm` DECIMAL(15,2) NULL,
`linked_discipline_id` BIGINT UNSIGNED NULL,
`config_json` JSON NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_facilities_code` (`code`),
INDEX `idx_facilities_type` (`facility_type`),
INDEX `idx_facilities_active` (`is_active`),
INDEX `idx_facilities_discipline` (`linked_discipline_id`),
CONSTRAINT `fk_facilities_discipline` FOREIGN KEY (`linked_discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `facilities`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `facility_time_slots` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`facility_id` BIGINT UNSIGNED NOT NULL,
`day_of_week` TINYINT UNSIGNED NOT NULL COMMENT '0=Sun, 1=Mon, ..., 6=Sat',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`slot_type` VARCHAR(30) NOT NULL DEFAULT 'available' COMMENT 'available, maintenance, academy_reserved, blocked',
`linked_academy_id` BIGINT UNSIGNED NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_fts_facility` (`facility_id`),
INDEX `idx_fts_day_time` (`day_of_week`, `start_time`, `end_time`),
CONSTRAINT `fk_fts_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `facility_time_slots`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `facility_blackout_dates` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`facility_id` BIGINT UNSIGNED NOT NULL,
`blackout_date` DATE NOT NULL,
`start_time` TIME NULL COMMENT 'NULL means entire day',
`end_time` TIME NULL,
`reason` VARCHAR(500) NULL,
`created_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_fbd_facility_date` (`facility_id`, `blackout_date`),
CONSTRAINT `fk_fbd_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `facility_blackout_dates`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `academies` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`discipline_id` BIGINT UNSIGNED NOT NULL,
`academy_type` VARCHAR(50) NOT NULL COMMENT 'football, gymnastics, swimming, combat, racket, general',
`description_ar` TEXT NULL,
`config_json` JSON NULL COMMENT '{\"age_min\",\"age_max\",\"prerequisites\",\"capacity\",\"levels_count\"}',
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_academies_code` (`code`),
INDEX `idx_academies_discipline` (`discipline_id`),
INDEX `idx_academies_type` (`academy_type`),
INDEX `idx_academies_active` (`is_active`),
CONSTRAINT `fk_academies_discipline` FOREIGN KEY (`discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `academies`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `academy_levels` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`academy_id` BIGINT UNSIGNED NOT NULL,
`code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(200) NOT NULL,
`name_en` VARCHAR(200) NULL,
`level_order` INT UNSIGNED NOT NULL DEFAULT 0,
`age_min` INT UNSIGNED NULL,
`age_max` INT UNSIGNED NULL,
`max_capacity` INT UNSIGNED NULL,
`config_json` JSON NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_academy_levels_academy` (`academy_id`),
UNIQUE KEY `uq_academy_levels_code` (`academy_id`, `code`),
CONSTRAINT `fk_academy_levels_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `academy_levels`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `academy_schedules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`academy_id` BIGINT UNSIGNED NOT NULL,
`level_id` BIGINT UNSIGNED NULL,
`facility_id` BIGINT UNSIGNED NULL,
`day_of_week` TINYINT UNSIGNED NOT NULL COMMENT '0=Sun, 1=Mon, ..., 6=Sat',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`coach_name` VARCHAR(200) NULL,
`season` VARCHAR(20) NULL COMMENT 'e.g. 2025-2026',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_academy_schedules_academy` (`academy_id`),
INDEX `idx_academy_schedules_level` (`level_id`),
INDEX `idx_academy_schedules_facility` (`facility_id`),
INDEX `idx_academy_schedules_day` (`day_of_week`, `start_time`),
CONSTRAINT `fk_academy_schedules_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_academy_schedules_level` FOREIGN KEY (`level_id`) REFERENCES `academy_levels`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_academy_schedules_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `academy_schedules`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `players` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_type` VARCHAR(20) NOT NULL COMMENT 'member, non_member',
`member_id` BIGINT UNSIGNED NULL COMMENT 'FK to members for member players',
`activity_id_number` VARCHAR(50) NULL COMMENT 'Sports activity ID card number',
`registration_serial` VARCHAR(50) NULL COMMENT 'Auto-generated serial',
`full_name_ar` VARCHAR(300) NOT NULL,
`full_name_en` VARCHAR(300) NULL,
`national_id` VARCHAR(20) NULL,
`passport_number` VARCHAR(50) NULL,
`date_of_birth` DATE NULL,
`gender` VARCHAR(10) NULL COMMENT 'male, female',
`phone` VARCHAR(30) NULL,
`email` VARCHAR(200) NULL,
`address` TEXT NULL,
`guardian_name` VARCHAR(300) NULL,
`guardian_phone` VARCHAR(30) NULL,
`guardian_national_id` VARCHAR(20) NULL,
`guardian_relationship` VARCHAR(50) NULL COMMENT 'father, mother, brother, sister, other',
`photo_path` VARCHAR(500) NULL,
`medical_status` VARCHAR(30) NULL DEFAULT 'pending' COMMENT 'pending, fit, conditional, unfit, expired',
`medical_expiry_date` DATE NULL,
`school_name` VARCHAR(300) NULL,
`school_grade` VARCHAR(100) NULL,
`registration_fee_paid` TINYINT(1) NOT NULL DEFAULT 0,
`card_status` VARCHAR(20) NOT NULL DEFAULT 'inactive' COMMENT 'inactive, active, suspended, revoked',
`access_window_minutes` INT UNSIGNED NULL COMMENT '30 for non-members',
`notes` TEXT NULL,
`branch_id` BIGINT UNSIGNED NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_players_registration_serial` (`registration_serial`),
INDEX `idx_players_type` (`player_type`),
INDEX `idx_players_member` (`member_id`),
INDEX `idx_players_national_id` (`national_id`),
INDEX `idx_players_card_status` (`card_status`),
INDEX `idx_players_medical_status` (`medical_status`),
INDEX `idx_players_branch` (`branch_id`),
CONSTRAINT `fk_players_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `players`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `player_disciplines` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`discipline_id` BIGINT UNSIGNED NOT NULL,
`age_group` VARCHAR(50) NULL,
`skill_level` VARCHAR(50) NULL,
`season` VARCHAR(20) NULL COMMENT 'e.g. 2025-2026',
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, inactive, transferred',
`registered_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_player_disciplines_player` (`player_id`),
INDEX `idx_player_disciplines_discipline` (`discipline_id`),
INDEX `idx_player_disciplines_season` (`season`),
UNIQUE KEY `uq_player_discipline_season` (`player_id`, `discipline_id`, `season`),
CONSTRAINT `fk_pd_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_pd_discipline` FOREIGN KEY (`discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `player_disciplines`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `academy_enrollments` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`academy_id` BIGINT UNSIGNED NOT NULL,
`level_id` BIGINT UNSIGNED NULL,
`schedule_id` BIGINT UNSIGNED NULL,
`season` VARCHAR(20) NULL,
`enrolled_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`enrollment_day` TINYINT UNSIGNED NULL COMMENT 'Day of month when enrolled (for half-month logic)',
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, suspended, dropped, promoted, completed',
`promoted_from_id` BIGINT UNSIGNED NULL COMMENT 'Previous enrollment ID if promoted',
`dropped_at` TIMESTAMP NULL DEFAULT NULL,
`dropped_reason` VARCHAR(500) NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_ae_player` (`player_id`),
INDEX `idx_ae_academy` (`academy_id`),
INDEX `idx_ae_level` (`level_id`),
INDEX `idx_ae_status` (`status`),
INDEX `idx_ae_season` (`season`),
CONSTRAINT `fk_ae_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_ae_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_ae_level` FOREIGN KEY (`level_id`) REFERENCES `academy_levels`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_ae_schedule` FOREIGN KEY (`schedule_id`) REFERENCES `academy_schedules`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_ae_promoted_from` FOREIGN KEY (`promoted_from_id`) REFERENCES `academy_enrollments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `academy_enrollments`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `player_medical_records` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`record_type` VARCHAR(50) NOT NULL COMMENT 'fitness_cert, medical_exam, blood_test, cardiac_screen, other',
`exam_date` DATE NULL,
`expiry_date` DATE NULL,
`doctor_name` VARCHAR(200) NULL,
`clinic_name` VARCHAR(200) NULL,
`result` VARCHAR(30) NULL COMMENT 'fit, conditional, unfit',
`restrictions` TEXT NULL COMMENT 'Any medical restrictions',
`document_id` BIGINT UNSIGNED NULL COMMENT 'FK to documents table for uploaded file',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_pmr_player` (`player_id`),
INDEX `idx_pmr_expiry` (`expiry_date`),
INDEX `idx_pmr_result` (`result`),
CONSTRAINT `fk_pmr_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_pmr_document` FOREIGN KEY (`document_id`) REFERENCES `documents`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `player_medical_records`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `payments`
MODIFY COLUMN `member_id` BIGINT UNSIGNED NULL,
ADD COLUMN `player_id` BIGINT UNSIGNED NULL AFTER `member_id`,
ADD INDEX `idx_payments_player` (`player_id`),
ADD CONSTRAINT `fk_payments_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE SET NULL",
'down' => "ALTER TABLE `payments`
DROP FOREIGN KEY `fk_payments_player`,
DROP INDEX `idx_payments_player`,
DROP COLUMN `player_id`,
MODIFY COLUMN `member_id` BIGINT UNSIGNED NOT NULL",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `reservations` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`reservation_number` VARCHAR(50) NOT NULL,
`facility_id` BIGINT UNSIGNED NOT NULL,
`booker_type` VARCHAR(20) NOT NULL COMMENT 'member, non_member, player, guest',
`player_id` BIGINT UNSIGNED NULL,
`member_id` BIGINT UNSIGNED NULL,
`booker_name` VARCHAR(300) NULL COMMENT 'For guest bookings',
`booker_phone` VARCHAR(30) NULL,
`reservation_date` DATE NOT NULL,
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`duration_hours` DECIMAL(5,2) NOT NULL DEFAULT 1.00,
`time_tier` VARCHAR(5) NOT NULL DEFAULT 'AM' COMMENT 'AM, PM',
`unit_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`payment_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, confirmed, checked_in, completed, cancelled, no_show',
`confirmed_at` TIMESTAMP NULL DEFAULT NULL,
`cancelled_at` TIMESTAMP NULL DEFAULT NULL,
`cancel_reason` VARCHAR(500) NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_reservations_number` (`reservation_number`),
INDEX `idx_reservations_facility` (`facility_id`),
INDEX `idx_reservations_date` (`reservation_date`),
INDEX `idx_reservations_status` (`status`),
INDEX `idx_reservations_player` (`player_id`),
INDEX `idx_reservations_member` (`member_id`),
INDEX `idx_reservations_facility_date` (`facility_id`, `reservation_date`, `start_time`, `end_time`),
CONSTRAINT `fk_reservations_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_reservations_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_reservations_member` FOREIGN KEY (`member_id`) REFERENCES `members`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_reservations_payment` FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `reservations`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `rental_entities` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`entity_type` VARCHAR(50) NOT NULL COMMENT 'school, university, embassy, federation, private, corporate',
`name_ar` VARCHAR(300) NOT NULL,
`name_en` VARCHAR(300) NULL,
`contact_person` VARCHAR(200) NULL,
`contact_phone` VARCHAR(30) NULL,
`contact_email` VARCHAR(200) NULL,
`address` TEXT NULL,
`tax_number` VARCHAR(50) NULL,
`official_letter_received` TINYINT(1) NOT NULL DEFAULT 0,
`board_approved` TINYINT(1) NOT NULL DEFAULT 0,
`approval_date` DATE NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, approved, active, suspended, blacklisted',
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
INDEX `idx_rental_entities_type` (`entity_type`),
INDEX `idx_rental_entities_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `rental_entities`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `rental_contracts` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`contract_number` VARCHAR(50) NOT NULL,
`entity_id` BIGINT UNSIGNED NOT NULL,
`facility_id` BIGINT UNSIGNED NOT NULL,
`activity_type` VARCHAR(30) NOT NULL DEFAULT 'practice' COMMENT 'practice, competitive, event, training',
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`total_units` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Total hours/sessions',
`unit_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`time_tier` VARCHAR(5) NOT NULL DEFAULT 'AM' COMMENT 'AM, PM',
`subtotal` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`discount_percentage` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '15% for bulk (24+ units, 2+ months)',
`discount_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`deposit_percentage` DECIMAL(5,2) NOT NULL DEFAULT 10.00,
`deposit_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`deposit_status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, collected, partially_refunded, refunded',
`deposit_payment_id` BIGINT UNSIGNED NULL,
`technical_report_submitted` TINYINT(1) NOT NULL DEFAULT 0,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, pending_approval, approved, active, completed, cancelled, terminated',
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL DEFAULT NULL,
`terms_json` JSON NULL COMMENT 'Additional contract terms',
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP NULL DEFAULT NULL,
`archived_by` BIGINT UNSIGNED NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
`updated_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_rental_contracts_number` (`contract_number`),
INDEX `idx_rc_entity` (`entity_id`),
INDEX `idx_rc_facility` (`facility_id`),
INDEX `idx_rc_status` (`status`),
INDEX `idx_rc_dates` (`start_date`, `end_date`),
CONSTRAINT `fk_rc_entity` FOREIGN KEY (`entity_id`) REFERENCES `rental_entities`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_rc_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_rc_deposit_payment` FOREIGN KEY (`deposit_payment_id`) REFERENCES `payments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `rental_contracts`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `rental_bookings` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`contract_id` BIGINT UNSIGNED NOT NULL,
`facility_id` BIGINT UNSIGNED NOT NULL,
`booking_date` DATE NOT NULL,
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'scheduled' COMMENT 'scheduled, completed, cancelled, no_show',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_rb_contract` (`contract_id`),
INDEX `idx_rb_facility` (`facility_id`),
INDEX `idx_rb_date` (`booking_date`),
INDEX `idx_rb_facility_date` (`facility_id`, `booking_date`, `start_time`, `end_time`),
CONSTRAINT `fk_rb_contract` FOREIGN KEY (`contract_id`) REFERENCES `rental_contracts`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_rb_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `rental_bookings`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `activity_subscriptions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`enrollment_id` BIGINT UNSIGNED NULL,
`discipline_id` BIGINT UNSIGNED NULL,
`subscription_month` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM format',
`player_type` VARCHAR(20) NOT NULL COMMENT 'member, non_member',
`base_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`is_half_month` TINYINT(1) NOT NULL DEFAULT 0,
`applied_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT 'Half of base_rate if is_half_month',
`discount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT 'Super admin only can apply discount',
`total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`payment_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, paid, overdue, exempted, revoked',
`due_date` DATE NULL COMMENT '7th of subscription month',
`paid_at` TIMESTAMP NULL DEFAULT NULL,
`revoked_at` TIMESTAMP NULL DEFAULT NULL,
`exempted_by` BIGINT UNSIGNED NULL,
`exemption_reason` VARCHAR(500) NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_as_player` (`player_id`),
INDEX `idx_as_enrollment` (`enrollment_id`),
INDEX `idx_as_discipline` (`discipline_id`),
INDEX `idx_as_month` (`subscription_month`),
INDEX `idx_as_status` (`status`),
INDEX `idx_as_due_date` (`due_date`),
INDEX `idx_as_player_month` (`player_id`, `subscription_month`),
CONSTRAINT `fk_as_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_as_enrollment` FOREIGN KEY (`enrollment_id`) REFERENCES `academy_enrollments`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_as_discipline` FOREIGN KEY (`discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_as_payment` FOREIGN KEY (`payment_id`) REFERENCES `payments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `activity_subscriptions`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `activity_pricing` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`pricing_type` VARCHAR(30) NOT NULL COMMENT 'academy, discipline, facility',
`reference_id` BIGINT UNSIGNED NOT NULL COMMENT 'FK to academy/discipline/facility depending on pricing_type',
`member_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`nonmember_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`member_rate_pm` DECIMAL(15,2) NULL COMMENT 'PM rate for members (facilities)',
`nonmember_rate_pm` DECIMAL(15,2) NULL COMMENT 'PM rate for non-members (facilities)',
`effective_from` DATE NOT NULL,
`effective_to` DATE NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_ap_type_ref` (`pricing_type`, `reference_id`),
INDEX `idx_ap_effective` (`effective_from`, `effective_to`),
INDEX `idx_ap_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `activity_pricing`",
];
<?php
declare(strict_types=1);
return [
'up' => "
CREATE TABLE IF NOT EXISTS `player_attendance` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`enrollment_id` BIGINT UNSIGNED NULL,
`schedule_id` BIGINT UNSIGNED NULL,
`facility_id` BIGINT UNSIGNED NULL,
`attendance_date` DATE NOT NULL,
`check_in_time` TIME NULL,
`check_out_time` TIME NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'present' COMMENT 'present, absent, late, excused',
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
INDEX `idx_pa_player` (`player_id`),
INDEX `idx_pa_enrollment` (`enrollment_id`),
INDEX `idx_pa_schedule` (`schedule_id`),
INDEX `idx_pa_facility` (`facility_id`),
INDEX `idx_pa_date` (`attendance_date`),
INDEX `idx_pa_player_date` (`player_id`, `attendance_date`),
CONSTRAINT `fk_pa_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_pa_enrollment` FOREIGN KEY (`enrollment_id`) REFERENCES `academy_enrollments`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_pa_schedule` FOREIGN KEY (`schedule_id`) REFERENCES `academy_schedules`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_pa_facility` FOREIGN KEY (`facility_id`) REFERENCES `facilities`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
",
'down' => "DROP TABLE IF EXISTS `player_attendance`",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
// ── Reusable config building helpers ──
$fullAgeGroups = [
['code' => 'u6', 'label_ar' => 'تحت 6', 'min_age' => 4, 'max_age' => 5],
['code' => 'u8', 'label_ar' => 'تحت 8', 'min_age' => 6, 'max_age' => 7],
['code' => 'u10', 'label_ar' => 'تحت 10', 'min_age' => 8, 'max_age' => 9],
['code' => 'u12', 'label_ar' => 'تحت 12', 'min_age' => 10, 'max_age' => 11],
['code' => 'u14', 'label_ar' => 'تحت 14', 'min_age' => 12, 'max_age' => 13],
['code' => 'u16', 'label_ar' => 'تحت 16', 'min_age' => 14, 'max_age' => 15],
['code' => 'u18', 'label_ar' => 'تحت 18', 'min_age' => 16, 'max_age' => 17],
['code' => 'senior', 'label_ar' => 'كبار', 'min_age' => 18, 'max_age' => 99],
];
$compactAgeGroups = [
['code' => 'u10', 'label_ar' => 'تحت 10', 'min_age' => 6, 'max_age' => 9],
['code' => 'u14', 'label_ar' => 'تحت 14', 'min_age' => 10, 'max_age' => 13],
['code' => 'u18', 'label_ar' => 'تحت 18', 'min_age' => 14, 'max_age' => 17],
['code' => 'senior', 'label_ar' => 'كبار', 'min_age' => 18, 'max_age' => 99],
];
$openAgeGroup = [
['code' => 'open', 'label_ar' => 'مفتوح', 'min_age' => 0, 'max_age' => 99],
];
$fullSkillLevels = [
['code' => 'beginner', 'label_ar' => 'مبتدئ'],
['code' => 'intermediate', 'label_ar' => 'متوسط'],
['code' => 'advanced', 'label_ar' => 'متقدم'],
['code' => 'elite', 'label_ar' => 'نخبة'],
];
$basicSkillLevels = [
['code' => 'beginner', 'label_ar' => 'مبتدئ'],
['code' => 'intermediate', 'label_ar' => 'متوسط'],
['code' => 'advanced', 'label_ar' => 'متقدم'],
];
$leisureSkillLevels = [
['code' => 'casual', 'label_ar' => 'ترفيهي'],
];
// ── Sport definitions ──
$sports = [
// ── individual ──
[
'code' => 'karate', 'name_ar' => 'كاراتيه', 'name_en' => 'Karate',
'category' => 'individual', 'icon' => 'swords', 'sort_order' => 1,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 20,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'kung_fu', 'name_ar' => 'كونغ فو', 'name_en' => 'Kung Fu',
'category' => 'individual', 'icon' => 'hand-metal', 'sort_order' => 2,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 20,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'gymnastics', 'name_ar' => 'جمباز', 'name_en' => 'Gymnastics',
'category' => 'individual', 'icon' => 'accessibility', 'sort_order' => 3,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 15,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'swimming', 'name_ar' => 'سباحة', 'name_en' => 'Swimming',
'category' => 'individual', 'icon' => 'waves', 'sort_order' => 4,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 25,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'archery', 'name_ar' => 'رماية بالقوس', 'name_en' => 'Archery',
'category' => 'individual', 'icon' => 'target', 'sort_order' => 5,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 12,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'skating', 'name_ar' => 'تزلج', 'name_en' => 'Skating',
'category' => 'individual', 'icon' => 'bike', 'sort_order' => 6,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $basicSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 15,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'chess', 'name_ar' => 'شطرنج', 'name_en' => 'Chess',
'category' => 'individual', 'icon' => 'crown', 'sort_order' => 7,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => false,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 30,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'darts', 'name_ar' => 'سهام', 'name_en' => 'Darts',
'category' => 'individual', 'icon' => 'crosshair', 'sort_order' => 8,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $basicSkillLevels,
'requires_medical_cert' => false,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 16,
], JSON_UNESCAPED_UNICODE),
],
// ── team ──
[
'code' => 'football', 'name_ar' => 'كرة قدم', 'name_en' => 'Football',
'category' => 'team', 'icon' => 'trophy', 'sort_order' => 10,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 22,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'futsal', 'name_ar' => 'كرة صالات', 'name_en' => 'Futsal',
'category' => 'team', 'icon' => 'goal', 'sort_order' => 11,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 10,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'basketball', 'name_ar' => 'كرة سلة', 'name_en' => 'Basketball',
'category' => 'team', 'icon' => 'circle-dot', 'sort_order' => 12,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 12,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'handball', 'name_ar' => 'كرة يد', 'name_en' => 'Handball',
'category' => 'team', 'icon' => 'hand', 'sort_order' => 13,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 14,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'volleyball', 'name_ar' => 'كرة طائرة', 'name_en' => 'Volleyball',
'category' => 'team', 'icon' => 'globe', 'sort_order' => 14,
'config_json' => json_encode([
'age_groups' => $fullAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 12,
], JSON_UNESCAPED_UNICODE),
],
// ── racket ──
[
'code' => 'squash', 'name_ar' => 'اسكواش', 'name_en' => 'Squash',
'category' => 'racket', 'icon' => 'zap', 'sort_order' => 20,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 2,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'table_tennis', 'name_ar' => 'تنس طاولة', 'name_en' => 'Table Tennis',
'category' => 'racket', 'icon' => 'table', 'sort_order' => 21,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 4,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'tennis', 'name_ar' => 'تنس', 'name_en' => 'Tennis',
'category' => 'racket', 'icon' => 'disc', 'sort_order' => 22,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 4,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'padel', 'name_ar' => 'بادل', 'name_en' => 'Padel',
'category' => 'racket', 'icon' => 'rectangle-horizontal', 'sort_order' => 23,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $fullSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 4,
], JSON_UNESCAPED_UNICODE),
],
// ── leisure ──
[
'code' => 'bowling', 'name_ar' => 'بولينج', 'name_en' => 'Bowling',
'category' => 'leisure', 'icon' => 'circle', 'sort_order' => 30,
'config_json' => json_encode([
'age_groups' => $openAgeGroup,
'skill_levels' => $leisureSkillLevels,
'requires_medical_cert' => false,
'requires_guardian_consent_under' => 12,
'max_players_per_session' => 6,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'billiards', 'name_ar' => 'بلياردو', 'name_en' => 'Billiards',
'category' => 'leisure', 'icon' => 'hexagon', 'sort_order' => 31,
'config_json' => json_encode([
'age_groups' => $openAgeGroup,
'skill_levels' => $leisureSkillLevels,
'requires_medical_cert' => false,
'requires_guardian_consent_under' => 12,
'max_players_per_session' => 4,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'playstation', 'name_ar' => 'بلايستيشن', 'name_en' => 'PlayStation',
'category' => 'leisure', 'icon' => 'gamepad-2', 'sort_order' => 32,
'config_json' => json_encode([
'age_groups' => $openAgeGroup,
'skill_levels' => $leisureSkillLevels,
'requires_medical_cert' => false,
'requires_guardian_consent_under' => 12,
'max_players_per_session' => 2,
], JSON_UNESCAPED_UNICODE),
],
// ── emerging ──
[
'code' => 'korffball', 'name_ar' => 'كرة القرف', 'name_en' => 'Korfball',
'category' => 'emerging', 'icon' => 'rocket', 'sort_order' => 40,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $basicSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 16,
], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'badball', 'name_ar' => 'باد بول', 'name_en' => 'Badball',
'category' => 'emerging', 'icon' => 'flame', 'sort_order' => 41,
'config_json' => json_encode([
'age_groups' => $compactAgeGroups,
'skill_levels' => $basicSkillLevels,
'requires_medical_cert' => true,
'requires_guardian_consent_under' => 18,
'max_players_per_session' => 12,
], JSON_UNESCAPED_UNICODE),
],
];
foreach ($sports as $sport) {
$existing = $db->selectOne("SELECT id FROM sport_disciplines WHERE code = ?", [$sport['code']]);
if ($existing) {
continue;
}
$db->insert('sport_disciplines', [
'code' => $sport['code'],
'name_ar' => $sport['name_ar'],
'name_en' => $sport['name_en'],
'category' => $sport['category'],
'icon' => $sport['icon'],
'config_json' => $sport['config_json'],
'sort_order' => $sport['sort_order'],
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
// Helper to resolve discipline id by code
$disciplineId = function (string $code) use ($db): ?int {
$row = $db->selectOne("SELECT id FROM sport_disciplines WHERE code = ?", [$code]);
return $row ? (int) $row['id'] : null;
};
$facilities = [
// ── Football pitches ──
[
'code' => 'football_legal', 'name_ar' => 'ملعب كرة قدم قانوني', 'name_en' => 'Legal Football Pitch',
'facility_type' => 'pitch', 'location' => 'outdoor', 'capacity' => 22,
'hourly_rate_member' => '500.00', 'hourly_rate_nonmember' => '1000.00',
'hourly_rate_member_pm' => '600.00', 'hourly_rate_nonmember_pm' => '1200.00',
'discipline' => 'football', 'config_json' => null,
],
[
'code' => 'football_futsal', 'name_ar' => 'ملعب كرة صالات', 'name_en' => 'Futsal Pitch',
'facility_type' => 'pitch', 'location' => 'indoor', 'capacity' => 10,
'hourly_rate_member' => '300.00', 'hourly_rate_nonmember' => '600.00',
'hourly_rate_member_pm' => '360.00', 'hourly_rate_nonmember_pm' => '720.00',
'discipline' => 'futsal', 'config_json' => null,
],
// ── Multi-purpose courts ──
[
'code' => 'court_multi_1', 'name_ar' => 'ملعب متعدد الأغراض 1', 'name_en' => 'Multi-Purpose Court 1',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 20,
'hourly_rate_member' => '200.00', 'hourly_rate_nonmember' => '400.00',
'hourly_rate_member_pm' => '240.00', 'hourly_rate_nonmember_pm' => '480.00',
'discipline' => null, 'config_json' => null,
],
[
'code' => 'court_multi_2', 'name_ar' => 'ملعب متعدد الأغراض 2', 'name_en' => 'Multi-Purpose Court 2',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 20,
'hourly_rate_member' => '200.00', 'hourly_rate_nonmember' => '400.00',
'hourly_rate_member_pm' => '240.00', 'hourly_rate_nonmember_pm' => '480.00',
'discipline' => null, 'config_json' => null,
],
// ── Tennis courts ──
[
'code' => 'tennis_1', 'name_ar' => 'ملعب تنس 1', 'name_en' => 'Tennis Court 1',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 4,
'hourly_rate_member' => '150.00', 'hourly_rate_nonmember' => '300.00',
'hourly_rate_member_pm' => '180.00', 'hourly_rate_nonmember_pm' => '360.00',
'discipline' => 'tennis', 'config_json' => null,
],
[
'code' => 'tennis_2', 'name_ar' => 'ملعب تنس 2', 'name_en' => 'Tennis Court 2',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 4,
'hourly_rate_member' => '150.00', 'hourly_rate_nonmember' => '300.00',
'hourly_rate_member_pm' => '180.00', 'hourly_rate_nonmember_pm' => '360.00',
'discipline' => 'tennis', 'config_json' => null,
],
// ── Padel courts ──
[
'code' => 'padel_1', 'name_ar' => 'ملعب بادل 1', 'name_en' => 'Padel Court 1',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 4,
'hourly_rate_member' => '200.00', 'hourly_rate_nonmember' => '400.00',
'hourly_rate_member_pm' => '240.00', 'hourly_rate_nonmember_pm' => '480.00',
'discipline' => 'padel', 'config_json' => null,
],
[
'code' => 'padel_2', 'name_ar' => 'ملعب بادل 2', 'name_en' => 'Padel Court 2',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 4,
'hourly_rate_member' => '200.00', 'hourly_rate_nonmember' => '400.00',
'hourly_rate_member_pm' => '240.00', 'hourly_rate_nonmember_pm' => '480.00',
'discipline' => 'padel', 'config_json' => null,
],
[
'code' => 'padel_3', 'name_ar' => 'ملعب بادل 3', 'name_en' => 'Padel Court 3',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 4,
'hourly_rate_member' => '200.00', 'hourly_rate_nonmember' => '400.00',
'hourly_rate_member_pm' => '240.00', 'hourly_rate_nonmember_pm' => '480.00',
'discipline' => 'padel', 'config_json' => null,
],
// ── Squash courts (indoor) ──
[
'code' => 'squash_indoor_1', 'name_ar' => 'ملعب اسكواش داخلي 1', 'name_en' => 'Indoor Squash Court 1',
'facility_type' => 'court', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '100.00', 'hourly_rate_nonmember' => '200.00',
'hourly_rate_member_pm' => '120.00', 'hourly_rate_nonmember_pm' => '240.00',
'discipline' => 'squash', 'config_json' => null,
],
[
'code' => 'squash_indoor_2', 'name_ar' => 'ملعب اسكواش داخلي 2', 'name_en' => 'Indoor Squash Court 2',
'facility_type' => 'court', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '100.00', 'hourly_rate_nonmember' => '200.00',
'hourly_rate_member_pm' => '120.00', 'hourly_rate_nonmember_pm' => '240.00',
'discipline' => 'squash', 'config_json' => null,
],
[
'code' => 'squash_indoor_3', 'name_ar' => 'ملعب اسكواش داخلي 3', 'name_en' => 'Indoor Squash Court 3',
'facility_type' => 'court', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '100.00', 'hourly_rate_nonmember' => '200.00',
'hourly_rate_member_pm' => '120.00', 'hourly_rate_nonmember_pm' => '240.00',
'discipline' => 'squash', 'config_json' => null,
],
[
'code' => 'squash_indoor_4', 'name_ar' => 'ملعب اسكواش داخلي 4', 'name_en' => 'Indoor Squash Court 4',
'facility_type' => 'court', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '100.00', 'hourly_rate_nonmember' => '200.00',
'hourly_rate_member_pm' => '120.00', 'hourly_rate_nonmember_pm' => '240.00',
'discipline' => 'squash', 'config_json' => null,
],
// ── Squash courts (outdoor) ──
[
'code' => 'squash_outdoor_1', 'name_ar' => 'ملعب اسكواش خارجي 1', 'name_en' => 'Outdoor Squash Court 1',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 2,
'hourly_rate_member' => '80.00', 'hourly_rate_nonmember' => '160.00',
'hourly_rate_member_pm' => '96.00', 'hourly_rate_nonmember_pm' => '192.00',
'discipline' => 'squash', 'config_json' => null,
],
[
'code' => 'squash_outdoor_2', 'name_ar' => 'ملعب اسكواش خارجي 2', 'name_en' => 'Outdoor Squash Court 2',
'facility_type' => 'court', 'location' => 'outdoor', 'capacity' => 2,
'hourly_rate_member' => '80.00', 'hourly_rate_nonmember' => '160.00',
'hourly_rate_member_pm' => '96.00', 'hourly_rate_nonmember_pm' => '192.00',
'discipline' => 'squash', 'config_json' => null,
],
// ── Combat halls ──
[
'code' => 'combat_hall_1', 'name_ar' => 'قاعة فنون قتالية 1', 'name_en' => 'Combat Hall 1',
'facility_type' => 'hall', 'location' => 'indoor', 'capacity' => 30,
'hourly_rate_member' => '250.00', 'hourly_rate_nonmember' => '500.00',
'hourly_rate_member_pm' => '300.00', 'hourly_rate_nonmember_pm' => '600.00',
'discipline' => 'karate', 'config_json' => null,
],
[
'code' => 'combat_hall_2', 'name_ar' => 'قاعة فنون قتالية 2', 'name_en' => 'Combat Hall 2',
'facility_type' => 'hall', 'location' => 'indoor', 'capacity' => 30,
'hourly_rate_member' => '250.00', 'hourly_rate_nonmember' => '500.00',
'hourly_rate_member_pm' => '300.00', 'hourly_rate_nonmember_pm' => '600.00',
'discipline' => 'kung_fu', 'config_json' => null,
],
// ── Bowling lanes ──
[
'code' => 'bowling_lane_1', 'name_ar' => 'حارة بولينج 1', 'name_en' => 'Bowling Lane 1',
'facility_type' => 'lane', 'location' => 'indoor', 'capacity' => 6,
'hourly_rate_member' => '50.00', 'hourly_rate_nonmember' => '100.00',
'hourly_rate_member_pm' => '60.00', 'hourly_rate_nonmember_pm' => '120.00',
'discipline' => 'bowling', 'config_json' => json_encode(['kids_only' => true], JSON_UNESCAPED_UNICODE),
],
[
'code' => 'bowling_lane_2', 'name_ar' => 'حارة بولينج 2', 'name_en' => 'Bowling Lane 2',
'facility_type' => 'lane', 'location' => 'indoor', 'capacity' => 6,
'hourly_rate_member' => '50.00', 'hourly_rate_nonmember' => '100.00',
'hourly_rate_member_pm' => '60.00', 'hourly_rate_nonmember_pm' => '120.00',
'discipline' => 'bowling', 'config_json' => null,
],
[
'code' => 'bowling_lane_3', 'name_ar' => 'حارة بولينج 3', 'name_en' => 'Bowling Lane 3',
'facility_type' => 'lane', 'location' => 'indoor', 'capacity' => 6,
'hourly_rate_member' => '50.00', 'hourly_rate_nonmember' => '100.00',
'hourly_rate_member_pm' => '60.00', 'hourly_rate_nonmember_pm' => '120.00',
'discipline' => 'bowling', 'config_json' => null,
],
[
'code' => 'bowling_lane_4', 'name_ar' => 'حارة بولينج 4', 'name_en' => 'Bowling Lane 4',
'facility_type' => 'lane', 'location' => 'indoor', 'capacity' => 6,
'hourly_rate_member' => '50.00', 'hourly_rate_nonmember' => '100.00',
'hourly_rate_member_pm' => '60.00', 'hourly_rate_nonmember_pm' => '120.00',
'discipline' => 'bowling', 'config_json' => null,
],
[
'code' => 'bowling_lane_5', 'name_ar' => 'حارة بولينج 5', 'name_en' => 'Bowling Lane 5',
'facility_type' => 'lane', 'location' => 'indoor', 'capacity' => 6,
'hourly_rate_member' => '50.00', 'hourly_rate_nonmember' => '100.00',
'hourly_rate_member_pm' => '60.00', 'hourly_rate_nonmember_pm' => '120.00',
'discipline' => 'bowling', 'config_json' => null,
],
[
'code' => 'bowling_lane_6', 'name_ar' => 'حارة بولينج 6', 'name_en' => 'Bowling Lane 6',
'facility_type' => 'lane', 'location' => 'indoor', 'capacity' => 6,
'hourly_rate_member' => '50.00', 'hourly_rate_nonmember' => '100.00',
'hourly_rate_member_pm' => '60.00', 'hourly_rate_nonmember_pm' => '120.00',
'discipline' => 'bowling', 'config_json' => null,
],
// ── Table tennis tables ──
[
'code' => 'table_tennis_1', 'name_ar' => 'طاولة تنس طاولة 1', 'name_en' => 'Table Tennis 1',
'facility_type' => 'table', 'location' => 'indoor', 'capacity' => 4,
'hourly_rate_member' => '40.00', 'hourly_rate_nonmember' => '80.00',
'hourly_rate_member_pm' => '48.00', 'hourly_rate_nonmember_pm' => '96.00',
'discipline' => 'table_tennis', 'config_json' => null,
],
[
'code' => 'table_tennis_2', 'name_ar' => 'طاولة تنس طاولة 2', 'name_en' => 'Table Tennis 2',
'facility_type' => 'table', 'location' => 'indoor', 'capacity' => 4,
'hourly_rate_member' => '40.00', 'hourly_rate_nonmember' => '80.00',
'hourly_rate_member_pm' => '48.00', 'hourly_rate_nonmember_pm' => '96.00',
'discipline' => 'table_tennis', 'config_json' => null,
],
// ── Billiards tables ──
[
'code' => 'billiards_1', 'name_ar' => 'طاولة بلياردو 1', 'name_en' => 'Billiards Table 1',
'facility_type' => 'table', 'location' => 'indoor', 'capacity' => 4,
'hourly_rate_member' => '60.00', 'hourly_rate_nonmember' => '120.00',
'hourly_rate_member_pm' => '72.00', 'hourly_rate_nonmember_pm' => '144.00',
'discipline' => 'billiards', 'config_json' => null,
],
[
'code' => 'billiards_2', 'name_ar' => 'طاولة بلياردو 2', 'name_en' => 'Billiards Table 2',
'facility_type' => 'table', 'location' => 'indoor', 'capacity' => 4,
'hourly_rate_member' => '60.00', 'hourly_rate_nonmember' => '120.00',
'hourly_rate_member_pm' => '72.00', 'hourly_rate_nonmember_pm' => '144.00',
'discipline' => 'billiards', 'config_json' => null,
],
// ── PlayStation devices ──
[
'code' => 'ps_1', 'name_ar' => 'جهاز بلايستيشن 1', 'name_en' => 'PlayStation 1',
'facility_type' => 'device', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '30.00', 'hourly_rate_nonmember' => '60.00',
'hourly_rate_member_pm' => '36.00', 'hourly_rate_nonmember_pm' => '72.00',
'discipline' => 'playstation', 'config_json' => null,
],
[
'code' => 'ps_2', 'name_ar' => 'جهاز بلايستيشن 2', 'name_en' => 'PlayStation 2',
'facility_type' => 'device', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '30.00', 'hourly_rate_nonmember' => '60.00',
'hourly_rate_member_pm' => '36.00', 'hourly_rate_nonmember_pm' => '72.00',
'discipline' => 'playstation', 'config_json' => null,
],
[
'code' => 'ps_3', 'name_ar' => 'جهاز بلايستيشن 3', 'name_en' => 'PlayStation 3',
'facility_type' => 'device', 'location' => 'indoor', 'capacity' => 2,
'hourly_rate_member' => '30.00', 'hourly_rate_nonmember' => '60.00',
'hourly_rate_member_pm' => '36.00', 'hourly_rate_nonmember_pm' => '72.00',
'discipline' => 'playstation', 'config_json' => null,
],
// ── Tracks ──
[
'code' => 'running_track', 'name_ar' => 'مضمار جري', 'name_en' => 'Running Track',
'facility_type' => 'track', 'location' => 'outdoor', 'capacity' => 20,
'hourly_rate_member' => '0.00', 'hourly_rate_nonmember' => '50.00',
'hourly_rate_member_pm' => '0.00', 'hourly_rate_nonmember_pm' => '60.00',
'discipline' => null, 'config_json' => null,
],
[
'code' => 'cycling_track', 'name_ar' => 'مسار دراجات/تزلج', 'name_en' => 'Cycling / Skating Track',
'facility_type' => 'track', 'location' => 'outdoor', 'capacity' => 15,
'hourly_rate_member' => '0.00', 'hourly_rate_nonmember' => '50.00',
'hourly_rate_member_pm' => '0.00', 'hourly_rate_nonmember_pm' => '60.00',
'discipline' => 'skating', 'config_json' => null,
],
// ── Pool ──
[
'code' => 'swimming_pool', 'name_ar' => 'حمام سباحة', 'name_en' => 'Swimming Pool',
'facility_type' => 'pool', 'location' => 'outdoor', 'capacity' => 50,
'hourly_rate_member' => '75.00', 'hourly_rate_nonmember' => '150.00',
'hourly_rate_member_pm' => '90.00', 'hourly_rate_nonmember_pm' => '180.00',
'discipline' => 'swimming', 'config_json' => null,
],
];
foreach ($facilities as $f) {
$existing = $db->selectOne("SELECT id FROM facilities WHERE code = ?", [$f['code']]);
if ($existing) {
continue;
}
$linkedId = null;
if ($f['discipline'] !== null) {
$linkedId = $disciplineId($f['discipline']);
}
$db->insert('facilities', [
'code' => $f['code'],
'name_ar' => $f['name_ar'],
'name_en' => $f['name_en'],
'facility_type' => $f['facility_type'],
'location' => $f['location'],
'capacity' => $f['capacity'],
'hourly_rate_member' => $f['hourly_rate_member'],
'hourly_rate_nonmember' => $f['hourly_rate_nonmember'],
'hourly_rate_member_pm' => $f['hourly_rate_member_pm'],
'hourly_rate_nonmember_pm' => $f['hourly_rate_nonmember_pm'],
'linked_discipline_id' => $linkedId,
'config_json' => $f['config_json'],
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$now = date('Y-m-d');
$ts = date('Y-m-d H:i:s');
$rules = [
[
'rule_code' => 'SPORTS_REG_FEE_MEMBER',
'category' => 'sports_activity',
'name_ar' => 'رسوم تسجيل نشاط رياضي - عضو',
'name_en' => 'Sports Registration Fee - Member',
'description_ar' => 'رسوم التسجيل لأول مرة في نشاط رياضي لعضو النادي',
'data_type' => 'amount',
'current_value_json' => '{"amount":"50.00"}',
'parameters_json' => '{"amount":"decimal"}',
],
[
'rule_code' => 'SPORTS_REG_FEE_NONMEMBER',
'category' => 'sports_activity',
'name_ar' => 'رسوم تسجيل نشاط رياضي - غير عضو',
'name_en' => 'Sports Registration Fee - Non-Member',
'description_ar' => 'رسوم التسجيل لأول مرة في نشاط رياضي لغير الأعضاء',
'data_type' => 'amount',
'current_value_json' => '{"amount":"100.00"}',
'parameters_json' => '{"amount":"decimal"}',
],
[
'rule_code' => 'SPORTS_REG_SERIAL_START_MEMBER',
'category' => 'sports_activity',
'name_ar' => 'بداية التسلسل - تسجيل عضو',
'name_en' => 'Registration Serial Start - Member',
'description_ar' => 'رقم بداية التسلسل لإيصالات تسجيل الأعضاء في الأنشطة الرياضية',
'data_type' => 'integer',
'current_value_json' => '{"value":2134}',
'parameters_json' => '{"value":"integer"}',
],
[
'rule_code' => 'SPORTS_REG_SERIAL_START_NONMEMBER',
'category' => 'sports_activity',
'name_ar' => 'بداية التسلسل - تسجيل غير عضو',
'name_en' => 'Registration Serial Start - Non-Member',
'description_ar' => 'رقم بداية التسلسل لإيصالات تسجيل غير الأعضاء في الأنشطة الرياضية',
'data_type' => 'integer',
'current_value_json' => '{"value":682}',
'parameters_json' => '{"value":"integer"}',
],
[
'rule_code' => 'SPORTS_SUB_CUTOFF_DAY',
'category' => 'sports_activity',
'name_ar' => 'يوم قطع الاشتراك الشهري',
'name_en' => 'Monthly Subscription Cutoff Day',
'description_ar' => 'اليوم من الشهر الذي يتم بعده احتساب الاشتراك للشهر التالي',
'data_type' => 'integer',
'current_value_json' => '{"day":15}',
'parameters_json' => '{"day":"integer"}',
],
[
'rule_code' => 'SPORTS_SUB_REVOKE_DAY',
'category' => 'sports_activity',
'name_ar' => 'يوم إلغاء الاشتراك لعدم السداد',
'name_en' => 'Subscription Revoke Day',
'description_ar' => 'عدد الأيام بعد استحقاق الاشتراك التي يتم بعدها الإلغاء التلقائي',
'data_type' => 'integer',
'current_value_json' => '{"day":7}',
'parameters_json' => '{"day":"integer"}',
],
[
'rule_code' => 'SPORTS_NONMEMBER_ACCESS_MINUTES',
'category' => 'sports_activity',
'name_ar' => 'مهلة دخول غير العضو (دقائق)',
'name_en' => 'Non-Member Access Window (Minutes)',
'description_ar' => 'المدة المسموح بها لغير العضو للبقاء في المنشأة قبل بدء المحاسبة',
'data_type' => 'integer',
'current_value_json' => '{"minutes":30}',
'parameters_json' => '{"minutes":"integer"}',
],
[
'rule_code' => 'SPORTS_PM_START_HOUR',
'category' => 'sports_activity',
'name_ar' => 'ساعة بدء الفترة المسائية',
'name_en' => 'PM Rate Start Hour',
'description_ar' => 'الساعة التي يبدأ بعدها تطبيق تسعير الفترة المسائية',
'data_type' => 'integer',
'current_value_json' => '{"hour":17}',
'parameters_json' => '{"hour":"integer"}',
],
[
'rule_code' => 'RENTAL_BULK_MIN_UNITS',
'category' => 'sports_activity',
'name_ar' => 'الحد الأدنى لوحدات التأجير الجماعي',
'name_en' => 'Bulk Rental Minimum Units',
'description_ar' => 'الحد الأدنى لعدد ساعات/وحدات الحجز للاستفادة من خصم التأجير الجماعي',
'data_type' => 'integer',
'current_value_json' => '{"units":24}',
'parameters_json' => '{"units":"integer"}',
],
[
'rule_code' => 'RENTAL_BULK_MIN_MONTHS',
'category' => 'sports_activity',
'name_ar' => 'الحد الأدنى لأشهر التأجير الجماعي',
'name_en' => 'Bulk Rental Minimum Months',
'description_ar' => 'الحد الأدنى لمدة عقد التأجير الجماعي بالأشهر',
'data_type' => 'integer',
'current_value_json' => '{"months":2}',
'parameters_json' => '{"months":"integer"}',
],
[
'rule_code' => 'RENTAL_BULK_DISCOUNT_PCT',
'category' => 'sports_activity',
'name_ar' => 'نسبة خصم التأجير الجماعي',
'name_en' => 'Bulk Rental Discount Percentage',
'description_ar' => 'نسبة الخصم على إجمالي عقد التأجير الجماعي عند استيفاء الحد الأدنى',
'data_type' => 'percentage',
'current_value_json' => '{"percentage":"15.00"}',
'parameters_json' => '{"percentage":"decimal"}',
],
[
'rule_code' => 'RENTAL_DEPOSIT_PCT',
'category' => 'sports_activity',
'name_ar' => 'نسبة تأمين التأجير',
'name_en' => 'Rental Deposit Percentage',
'description_ar' => 'نسبة مبلغ التأمين المطلوب عند توقيع عقد التأجير المؤسسي',
'data_type' => 'percentage',
'current_value_json' => '{"percentage":"10.00"}',
'parameters_json' => '{"percentage":"decimal"}',
],
];
foreach ($rules as $rule) {
$existing = $db->selectOne("SELECT id FROM business_rules WHERE rule_code = ? AND branch_id IS NULL", [$rule['rule_code']]);
if ($existing) {
continue;
}
$db->insert('business_rules', [
'rule_code' => $rule['rule_code'],
'category' => $rule['category'],
'name_ar' => $rule['name_ar'],
'name_en' => $rule['name_en'] ?? null,
'description_ar' => $rule['description_ar'] ?? null,
'data_type' => $rule['data_type'],
'current_value_json' => $rule['current_value_json'],
'parameters_json' => $rule['parameters_json'] ?? null,
'branch_id' => null,
'effective_from' => $now,
'is_active' => 1,
'version' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
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,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
// ── Helper: look up discipline ID by code ──
$disciplineId = function (string $code) use ($db): int {
$row = $db->selectOne("SELECT id FROM sport_disciplines WHERE code = ?", [$code]);
if (!$row) {
throw new \RuntimeException("Sport discipline '{$code}' not found. Run Phase 17 seeds first.");
}
return (int) $row->id;
};
// ── Helper: insert academy idempotently, return its ID ──
$ensureAcademy = function (array $data) use ($db, $ts): int {
$existing = $db->selectOne("SELECT id FROM academies WHERE code = ?", [$data['code']]);
if ($existing) {
return (int) $existing->id;
}
$db->insert('academies', array_merge($data, [
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]));
$inserted = $db->selectOne("SELECT id FROM academies WHERE code = ?", [$data['code']]);
return (int) $inserted->id;
};
// ── Helper: insert level idempotently ──
$ensureLevel = function (int $academyId, array $data) use ($db, $ts): void {
$existing = $db->selectOne(
"SELECT id FROM academy_levels WHERE academy_id = ? AND code = ?",
[$academyId, $data['code']]
);
if ($existing) {
return;
}
$db->insert('academy_levels', array_merge($data, [
'academy_id' => $academyId,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]));
};
// ══════════════════════════════════════════════════════════════
// Football Academies (discipline: football)
// ══════════════════════════════════════════════════════════════
$footballId = $disciplineId('football');
// 1. Anderlecht Academy
$acadId = $ensureAcademy([
'code' => 'acad_anderlecht',
'name_ar' => 'أكاديمية أندرلخت',
'name_en' => 'Anderlecht Academy',
'discipline_id' => $footballId,
'academy_type' => 'football',
'sort_order' => 1,
]);
$ensureLevel($acadId, ['code' => 'u6', 'name_ar' => 'تحت 6', 'name_en' => 'Under 6', 'level_order' => 1, 'age_min' => 4, 'age_max' => 5]);
$ensureLevel($acadId, ['code' => 'u8', 'name_ar' => 'تحت 8', 'name_en' => 'Under 8', 'level_order' => 2, 'age_min' => 6, 'age_max' => 7]);
$ensureLevel($acadId, ['code' => 'u10', 'name_ar' => 'تحت 10', 'name_en' => 'Under 10', 'level_order' => 3, 'age_min' => 8, 'age_max' => 9]);
$ensureLevel($acadId, ['code' => 'u12', 'name_ar' => 'تحت 12', 'name_en' => 'Under 12', 'level_order' => 4, 'age_min' => 10, 'age_max' => 11]);
$ensureLevel($acadId, ['code' => 'u14', 'name_ar' => 'تحت 14', 'name_en' => 'Under 14', 'level_order' => 5, 'age_min' => 12, 'age_max' => 13]);
$ensureLevel($acadId, ['code' => 'u16', 'name_ar' => 'تحت 16', 'name_en' => 'Under 16', 'level_order' => 6, 'age_min' => 14, 'age_max' => 15]);
$ensureLevel($acadId, ['code' => 'u18', 'name_ar' => 'تحت 18', 'name_en' => 'Under 18', 'level_order' => 7, 'age_min' => 16, 'age_max' => 17]);
// 2. Mowahiba Academy
$acadId = $ensureAcademy([
'code' => 'acad_mowahiba',
'name_ar' => 'أكاديمية مواهب',
'name_en' => 'Mowahiba Academy',
'discipline_id' => $footballId,
'academy_type' => 'football',
'sort_order' => 2,
]);
$ensureLevel($acadId, ['code' => 'u8', 'name_ar' => 'تحت 8', 'name_en' => 'Under 8', 'level_order' => 1, 'age_min' => 6, 'age_max' => 7]);
$ensureLevel($acadId, ['code' => 'u10', 'name_ar' => 'تحت 10', 'name_en' => 'Under 10', 'level_order' => 2, 'age_min' => 8, 'age_max' => 9]);
$ensureLevel($acadId, ['code' => 'u12', 'name_ar' => 'تحت 12', 'name_en' => 'Under 12', 'level_order' => 3, 'age_min' => 10, 'age_max' => 11]);
$ensureLevel($acadId, ['code' => 'u14', 'name_ar' => 'تحت 14', 'name_en' => 'Under 14', 'level_order' => 4, 'age_min' => 12, 'age_max' => 13]);
// 3. Al-Rowad Academy
$acadId = $ensureAcademy([
'code' => 'acad_rowad',
'name_ar' => 'أكاديمية الرواد',
'name_en' => 'Al-Rowad Academy',
'discipline_id' => $footballId,
'academy_type' => 'football',
'sort_order' => 3,
]);
$ensureLevel($acadId, ['code' => 'u10', 'name_ar' => 'تحت 10', 'name_en' => 'Under 10', 'level_order' => 1, 'age_min' => 6, 'age_max' => 9]);
$ensureLevel($acadId, ['code' => 'u14', 'name_ar' => 'تحت 14', 'name_en' => 'Under 14', 'level_order' => 2, 'age_min' => 10, 'age_max' => 13]);
$ensureLevel($acadId, ['code' => 'u18', 'name_ar' => 'تحت 18', 'name_en' => 'Under 18', 'level_order' => 3, 'age_min' => 14, 'age_max' => 17]);
// ══════════════════════════════════════════════════════════════
// Gymnastics Academies (discipline: gymnastics)
// ══════════════════════════════════════════════════════════════
$gymnasticsId = $disciplineId('gymnastics');
// 4. Artistic Gymnastics - Men
$acadId = $ensureAcademy([
'code' => 'acad_gym_artistic_m',
'name_ar' => 'جمباز فني - رجال',
'name_en' => 'Artistic Gymnastics - Men',
'discipline_id' => $gymnasticsId,
'academy_type' => 'gymnastics',
'sort_order' => 10,
]);
$ensureLevel($acadId, ['code' => 'beginners', 'name_ar' => 'مبتدئين', 'name_en' => 'Beginners', 'level_order' => 1, 'age_min' => 4, 'age_max' => 7]);
$ensureLevel($acadId, ['code' => 'intermediate', 'name_ar' => 'متوسط', 'name_en' => 'Intermediate', 'level_order' => 2, 'age_min' => 8, 'age_max' => 11]);
$ensureLevel($acadId, ['code' => 'advanced', 'name_ar' => 'متقدم', 'name_en' => 'Advanced', 'level_order' => 3, 'age_min' => 12, 'age_max' => 17]);
// 5. Artistic Gymnastics - Women
$acadId = $ensureAcademy([
'code' => 'acad_gym_artistic_w',
'name_ar' => 'جمباز فني - سيدات',
'name_en' => 'Artistic Gymnastics - Women',
'discipline_id' => $gymnasticsId,
'academy_type' => 'gymnastics',
'sort_order' => 11,
]);
$ensureLevel($acadId, ['code' => 'beginners', 'name_ar' => 'مبتدئين', 'name_en' => 'Beginners', 'level_order' => 1, 'age_min' => 4, 'age_max' => 7]);
$ensureLevel($acadId, ['code' => 'intermediate', 'name_ar' => 'متوسط', 'name_en' => 'Intermediate', 'level_order' => 2, 'age_min' => 8, 'age_max' => 11]);
$ensureLevel($acadId, ['code' => 'advanced', 'name_ar' => 'متقدم', 'name_en' => 'Advanced', 'level_order' => 3, 'age_min' => 12, 'age_max' => 17]);
// 6. Aerobic Gymnastics
$acadId = $ensureAcademy([
'code' => 'acad_gym_aerobic',
'name_ar' => 'جمباز إيروبك',
'name_en' => 'Aerobic Gymnastics',
'discipline_id' => $gymnasticsId,
'academy_type' => 'gymnastics',
'sort_order' => 12,
]);
$ensureLevel($acadId, ['code' => 'general', 'name_ar' => 'عام', 'name_en' => 'General', 'level_order' => 1, 'age_min' => 6, 'age_max' => 99]);
// ══════════════════════════════════════════════════════════════
// Swimming Academies (discipline: swimming)
// ══════════════════════════════════════════════════════════════
$swimmingId = $disciplineId('swimming');
// 7. Swimming Schools
$acadId = $ensureAcademy([
'code' => 'acad_swim_schools',
'name_ar' => 'مدارس السباحة',
'name_en' => 'Swimming Schools',
'discipline_id' => $swimmingId,
'academy_type' => 'swimming',
'sort_order' => 20,
]);
$ensureLevel($acadId, ['code' => 'level_1', 'name_ar' => 'المستوى الأول', 'name_en' => 'Level 1', 'level_order' => 1, 'age_min' => 4, 'age_max' => 6]);
$ensureLevel($acadId, ['code' => 'level_2', 'name_ar' => 'المستوى الثاني', 'name_en' => 'Level 2', 'level_order' => 2, 'age_min' => 6, 'age_max' => 8]);
$ensureLevel($acadId, ['code' => 'level_3', 'name_ar' => 'المستوى الثالث', 'name_en' => 'Level 3', 'level_order' => 3, 'age_min' => 8, 'age_max' => 12]);
// 8. Swimming Practice
$acadId = $ensureAcademy([
'code' => 'acad_swim_practice',
'name_ar' => 'تدريب السباحة',
'name_en' => 'Swimming Practice',
'discipline_id' => $swimmingId,
'academy_type' => 'swimming',
'sort_order' => 21,
]);
$ensureLevel($acadId, ['code' => 'junior', 'name_ar' => 'ناشئين', 'name_en' => 'Junior', 'level_order' => 1, 'age_min' => 8, 'age_max' => 12]);
$ensureLevel($acadId, ['code' => 'intermediate', 'name_ar' => 'متوسط', 'name_en' => 'Intermediate', 'level_order' => 2, 'age_min' => 12, 'age_max' => 15]);
$ensureLevel($acadId, ['code' => 'senior', 'name_ar' => 'كبار', 'name_en' => 'Senior', 'level_order' => 3, 'age_min' => 15, 'age_max' => 99]);
// 9. Advanced Swimming
$acadId = $ensureAcademy([
'code' => 'acad_swim_advanced',
'name_ar' => 'السباحة المتقدمة',
'name_en' => 'Advanced Swimming',
'discipline_id' => $swimmingId,
'academy_type' => 'swimming',
'sort_order' => 22,
]);
$ensureLevel($acadId, ['code' => 'pre_national', 'name_ar' => 'ما قبل المنتخب', 'name_en' => 'Pre-National', 'level_order' => 1, 'age_min' => 12, 'age_max' => 17]);
$ensureLevel($acadId, ['code' => 'national', 'name_ar' => 'منتخب', 'name_en' => 'National', 'level_order' => 2, 'age_min' => 14, 'age_max' => 99]);
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$schemas = [
[
'schema_code' => 'SPORTS_REGISTRATION_MEMBER',
'name_ar' => 'تسجيل لاعب عضو في الأنشطة الرياضية',
'name_en' => 'Sports Registration - Member',
'description_ar' => 'استمارة تسجيل لاعب عضو بالنادي في الأنشطة الرياضية والأكاديميات',
'fee_amount' => '50.00',
'related_service' => 'SVC_SPORTS_REG_MEMBER',
'fields_json' => json_encode([
['key' => 'member_id', 'type' => 'hidden', 'label_ar' => 'رقم العضو', 'required' => true, 'auto_populated' => true, 'order' => 1],
['key' => 'discipline_ids', 'type' => 'multi_select', 'label_ar' => 'الأنشطة الرياضية المطلوبة', 'required' => true, 'data_source' => 'sport_disciplines', 'order' => 2],
['key' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies', 'order' => 3],
['key' => 'level_id', 'type' => 'select', 'label_ar' => 'المستوى', 'required' => false, 'data_source' => 'academy_levels', 'depends_on' => 'academy_id', 'order' => 4],
['key' => 'emergency_contact_name', 'type' => 'text', 'label_ar' => 'اسم شخص للتواصل في حالة الطوارئ', 'required' => true, 'validation' => 'required|string|min:5|max:100', 'order' => 5, 'width' => 'half'],
['key' => 'emergency_contact_phone', 'type' => 'text', 'label_ar' => 'هاتف الطوارئ', 'required' => true, 'validation' => 'required|string|max:20', 'order' => 6, 'width' => 'half'],
['key' => 'medical_cert_document', 'type' => 'file', 'label_ar' => 'شهادة طبية', 'required' => false, 'accepted_types' => 'pdf,jpg,png', 'order' => 7, 'width' => 'half'],
['key' => 'guardian_consent', 'type' => 'checkbox', 'label_ar' => 'موافقة ولي الأمر', 'required' => false, 'conditional' => true, 'show_if' => ['field' => 'age', 'operator' => 'lt', 'value' => 18], 'order' => 8, 'width' => 'half'],
['key' => 'notes', 'type' => 'textarea', 'label_ar' => 'ملاحظات', 'required' => false, 'order' => 9, 'width' => 'full'],
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
],
[
'schema_code' => 'SPORTS_REGISTRATION_NONMEMBER',
'name_ar' => 'تسجيل لاعب غير عضو في الأنشطة الرياضية',
'name_en' => 'Sports Registration - Non-Member',
'description_ar' => 'استمارة تسجيل لاعب من خارج النادي في الأنشطة الرياضية والأكاديميات',
'fee_amount' => '100.00',
'related_service' => 'SVC_SPORTS_REG_NONMEMBER',
'fields_json' => json_encode([
['key' => 'full_name_ar', 'type' => 'text', 'label_ar' => 'الاسم الكامل بالعربي', 'required' => true, 'validation' => 'required|string|min:10|max:200', 'order' => 1, 'width' => 'full'],
['key' => 'full_name_en', 'type' => 'text', 'label_ar' => 'الاسم بالإنجليزي', 'required' => false, 'validation' => 'nullable|string|max:200', 'order' => 2, 'width' => 'half'],
['key' => 'national_id', 'type' => 'text', 'label_ar' => 'الرقم القومي', 'required' => true, 'validation' => 'required|digits:14', 'order' => 3, 'width' => 'half'],
['key' => 'date_of_birth', 'type' => 'date', 'label_ar' => 'تاريخ الميلاد', 'required' => true, 'validation' => 'required|date', 'order' => 4, 'width' => 'half'],
['key' => 'gender', 'type' => 'select', 'label_ar' => 'النوع', 'required' => true, 'validation' => 'required|in:male,female', 'options' => [['value' => 'male', 'label_ar' => 'ذكر'], ['value' => 'female', 'label_ar' => 'أنثى']], 'order' => 5, 'width' => 'half'],
['key' => 'phone', 'type' => 'text', 'label_ar' => 'الهاتف', 'required' => true, 'validation' => 'required|string|max:20', 'order' => 6, 'width' => 'half'],
['key' => 'guardian_name', 'type' => 'text', 'label_ar' => 'اسم ولي الأمر', 'required' => false, 'conditional' => true, 'show_if' => ['field' => 'age', 'operator' => 'lt', 'value' => 18], 'validation' => 'required_if:age_lt_18|string|max:200', 'order' => 7, 'width' => 'half'],
['key' => 'guardian_phone', 'type' => 'text', 'label_ar' => 'هاتف ولي الأمر', 'required' => false, 'conditional' => true, 'show_if' => ['field' => 'age', 'operator' => 'lt', 'value' => 18], 'validation' => 'required_if:age_lt_18|string|max:20', 'order' => 8, 'width' => 'half'],
['key' => 'guardian_national_id', 'type' => 'text', 'label_ar' => 'الرقم القومي لولي الأمر', 'required' => false, 'conditional' => true, 'show_if' => ['field' => 'age', 'operator' => 'lt', 'value' => 18], 'validation' => 'nullable|digits:14', 'order' => 9, 'width' => 'half'],
['key' => 'guardian_relationship', 'type' => 'select', 'label_ar' => 'صلة القرابة', 'required' => false, 'conditional' => true, 'show_if' => ['field' => 'age', 'operator' => 'lt', 'value' => 18], 'options' => [['value' => 'father', 'label_ar' => 'الأب'], ['value' => 'mother', 'label_ar' => 'الأم'], ['value' => 'brother', 'label_ar' => 'الأخ'], ['value' => 'sister', 'label_ar' => 'الأخت'], ['value' => 'other', 'label_ar' => 'أخرى']], 'order' => 10, 'width' => 'half'],
['key' => 'discipline_ids', 'type' => 'multi_select', 'label_ar' => 'الأنشطة الرياضية المطلوبة', 'required' => true, 'data_source' => 'sport_disciplines', 'order' => 11],
['key' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies', 'order' => 12],
['key' => 'medical_cert_document', 'type' => 'file', 'label_ar' => 'شهادة طبية', 'required' => false, 'accepted_types' => 'pdf,jpg,png', 'order' => 13, 'width' => 'half'],
['key' => 'photo', 'type' => 'file', 'label_ar' => 'صورة شخصية', 'required' => false, 'accepted_types' => 'jpg,png', 'order' => 14, 'width' => 'half'],
['key' => 'notes', 'type' => 'textarea', 'label_ar' => 'ملاحظات', 'required' => false, 'order' => 15, 'width' => 'full'],
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
],
];
foreach ($schemas as $s) {
$existing = $db->selectOne("SELECT id FROM form_schemas WHERE schema_code = ?", [$s['schema_code']]);
if ($existing) {
continue;
}
$db->insert('form_schemas', [
'schema_code' => $s['schema_code'],
'name_ar' => $s['name_ar'],
'name_en' => $s['name_en'],
'description_ar' => $s['description_ar'],
'fields_json' => $s['fields_json'],
'validation_json' => null,
'fee_amount' => $s['fee_amount'],
'related_service' => $s['related_service'],
'config_json' => null,
'is_active' => 1,
'version' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$reports = [
[
'report_code' => 'RPT_PLAYER_TOTALS',
'name_ar' => 'إجمالي اللاعبين',
'name_en' => 'Player Totals',
'description_ar' => 'إجمالي اللاعبين حسب النوع (عضو/غير عضو)، الكروت النشطة/الموقوفة، وتوزيع النوع',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'players',
'joins' => [
['table' => 'player_cards', 'on' => 'players.id = player_cards.player_id', 'type' => 'LEFT'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'players.id', 'alias' => 'total_players'],
['function' => 'SUM', 'field' => 'CASE WHEN players.player_type = \'member\' THEN 1 ELSE 0 END', 'alias' => 'member_count'],
['function' => 'SUM', 'field' => 'CASE WHEN players.player_type = \'non_member\' THEN 1 ELSE 0 END', 'alias' => 'non_member_count'],
['function' => 'SUM', 'field' => 'CASE WHEN player_cards.status = \'active\' THEN 1 ELSE 0 END', 'alias' => 'active_cards'],
['function' => 'SUM', 'field' => 'CASE WHEN player_cards.status = \'inactive\' THEN 1 ELSE 0 END', 'alias' => 'inactive_cards'],
['function' => 'SUM', 'field' => 'CASE WHEN players.gender = \'male\' THEN 1 ELSE 0 END', 'alias' => 'male_count'],
['function' => 'SUM', 'field' => 'CASE WHEN players.gender = \'female\' THEN 1 ELSE 0 END', 'alias' => 'female_count'],
],
'group_by' => [],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'summary_cards',
'columns' => ['total_players', 'member_count', 'non_member_count', 'active_cards', 'inactive_cards', 'male_count', 'female_count'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 1,
],
[
'report_code' => 'RPT_PLAYERS_BY_SPORT',
'name_ar' => 'اللاعبين حسب النشاط',
'name_en' => 'Players by Sport',
'description_ar' => 'عدد اللاعبين لكل نشاط رياضي',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'player_disciplines',
'joins' => [
['table' => 'sport_disciplines', 'on' => 'player_disciplines.discipline_id = sport_disciplines.id', 'type' => 'INNER'],
['table' => 'players', 'on' => 'player_disciplines.player_id = players.id', 'type' => 'INNER'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'players.id', 'alias' => 'player_count'],
],
'group_by' => ['sport_disciplines.id', 'sport_disciplines.name_ar'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'discipline_id', 'type' => 'select', 'label_ar' => 'النشاط الرياضي', 'required' => false, 'data_source' => 'sport_disciplines'],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['sport_disciplines.name_ar', 'player_count'],
'chart' => ['type' => 'bar', 'x' => 'sport_disciplines.name_ar', 'y' => 'player_count'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 2,
],
[
'report_code' => 'RPT_PLAYERS_BY_ACADEMY',
'name_ar' => 'اللاعبين حسب الأكاديمية',
'name_en' => 'Players by Academy',
'description_ar' => 'عدد اللاعبين لكل أكاديمية ومستوى',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'enrollments',
'joins' => [
['table' => 'academies', 'on' => 'enrollments.academy_id = academies.id', 'type' => 'INNER'],
['table' => 'academy_levels', 'on' => 'enrollments.level_id = academy_levels.id', 'type' => 'LEFT'],
['table' => 'players', 'on' => 'enrollments.player_id = players.id', 'type' => 'INNER'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'players.id', 'alias' => 'player_count'],
],
'group_by' => ['academies.id', 'academies.name_ar', 'academy_levels.id', 'academy_levels.name_ar'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies'],
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['academies.name_ar', 'academy_levels.name_ar', 'player_count'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 3,
],
[
'report_code' => 'RPT_ENROLLMENT_STATUS',
'name_ar' => 'حالة التسجيلات',
'name_en' => 'Enrollment Status',
'description_ar' => 'حالة التسجيلات (نشط، موقوف، منسحب) لكل أكاديمية',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'enrollments',
'joins' => [
['table' => 'academies', 'on' => 'enrollments.academy_id = academies.id', 'type' => 'INNER'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'enrollments.id', 'alias' => 'total_enrollments'],
['function' => 'SUM', 'field' => 'CASE WHEN enrollments.status = \'active\' THEN 1 ELSE 0 END', 'alias' => 'active_count'],
['function' => 'SUM', 'field' => 'CASE WHEN enrollments.status = \'suspended\' THEN 1 ELSE 0 END', 'alias' => 'suspended_count'],
['function' => 'SUM', 'field' => 'CASE WHEN enrollments.status = \'dropped\' THEN 1 ELSE 0 END', 'alias' => 'dropped_count'],
],
'group_by' => ['academies.id', 'academies.name_ar'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies'],
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['academies.name_ar', 'total_enrollments', 'active_count', 'suspended_count', 'dropped_count'],
'chart' => ['type' => 'stacked_bar', 'x' => 'academies.name_ar', 'y' => ['active_count', 'suspended_count', 'dropped_count']],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 4,
],
[
'report_code' => 'RPT_FACILITY_UTILIZATION',
'name_ar' => 'إشغال الملاعب',
'name_en' => 'Facility Utilization',
'description_ar' => 'نسبة إشغال الملاعب حسب النوع، تغطية الفترات الزمنية، وعدد الحجوزات',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'facilities',
'joins' => [
['table' => 'reservations', 'on' => 'facilities.id = reservations.facility_id', 'type' => 'LEFT'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'reservations.id', 'alias' => 'booking_count'],
['function' => 'SUM', 'field' => 'reservations.duration_hours', 'alias' => 'total_hours_booked'],
['function' => 'AVG', 'field' => 'reservations.duration_hours', 'alias' => 'avg_hours_per_booking'],
],
'group_by' => ['facilities.id', 'facilities.name_ar', 'facilities.facility_type'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'facility_type', 'type' => 'select', 'label_ar' => 'نوع الملعب', 'required' => false],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['facilities.name_ar', 'facilities.facility_type', 'booking_count', 'total_hours_booked', 'avg_hours_per_booking'],
'chart' => ['type' => 'bar', 'x' => 'facilities.name_ar', 'y' => 'booking_count'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 5,
],
[
'report_code' => 'RPT_RESERVATIONS',
'name_ar' => 'الحجوزات',
'name_en' => 'Reservations',
'description_ar' => 'عدد الحجوزات والإيرادات حسب الملعب والفترة الزمنية',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'reservations',
'joins' => [
['table' => 'facilities', 'on' => 'reservations.facility_id = facilities.id', 'type' => 'INNER'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'reservations.id', 'alias' => 'reservation_count'],
['function' => 'SUM', 'field' => 'reservations.total_amount', 'alias' => 'total_revenue'],
],
'group_by' => ['facilities.id', 'facilities.name_ar'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'facility_id', 'type' => 'select', 'label_ar' => 'الملعب', 'required' => false, 'data_source' => 'facilities'],
['name' => 'status', 'type' => 'select', 'label_ar' => 'حالة الحجز', 'required' => false, 'options' => ['confirmed', 'pending', 'cancelled']],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['facilities.name_ar', 'reservation_count', 'total_revenue'],
'chart' => ['type' => 'bar', 'x' => 'facilities.name_ar', 'y' => 'total_revenue'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 6,
],
[
'report_code' => 'RPT_RENTAL_REVENUE',
'name_ar' => 'إيرادات التأجير',
'name_en' => 'Rental Revenue',
'description_ar' => 'إيرادات عقود التأجير حسب الجهة والملعب والفترة الزمنية',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'rental_contracts',
'joins' => [
['table' => 'facilities', 'on' => 'rental_contracts.facility_id = facilities.id', 'type' => 'INNER'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'rental_contracts.id', 'alias' => 'contract_count'],
['function' => 'SUM', 'field' => 'rental_contracts.total_amount', 'alias' => 'total_revenue'],
],
'group_by' => ['rental_contracts.entity_name', 'facilities.id', 'facilities.name_ar'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'facility_id', 'type' => 'select', 'label_ar' => 'الملعب', 'required' => false, 'data_source' => 'facilities'],
['name' => 'entity_name', 'type' => 'text', 'label_ar' => 'اسم الجهة', 'required' => false],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['rental_contracts.entity_name', 'facilities.name_ar', 'contract_count', 'total_revenue'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 7,
],
[
'report_code' => 'RPT_SUB_COLLECTION',
'name_ar' => 'تحصيل الاشتراكات',
'name_en' => 'Subscription Collection',
'description_ar' => 'نسب تحصيل اشتراكات الأنشطة الرياضية: المسدد والمعلق والمتأخر',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'activity_subscriptions',
'joins' => [
['table' => 'players', 'on' => 'activity_subscriptions.player_id = players.id', 'type' => 'INNER'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'activity_subscriptions.id', 'alias' => 'total_subscriptions'],
['function' => 'SUM', 'field' => 'CASE WHEN activity_subscriptions.status = \'paid\' THEN 1 ELSE 0 END', 'alias' => 'paid_count'],
['function' => 'SUM', 'field' => 'CASE WHEN activity_subscriptions.status = \'pending\' THEN 1 ELSE 0 END', 'alias' => 'pending_count'],
['function' => 'SUM', 'field' => 'CASE WHEN activity_subscriptions.status = \'overdue\' THEN 1 ELSE 0 END', 'alias' => 'overdue_count'],
['function' => 'SUM', 'field' => 'activity_subscriptions.amount', 'alias' => 'total_amount'],
['function' => 'SUM', 'field' => 'CASE WHEN activity_subscriptions.status = \'paid\' THEN activity_subscriptions.amount ELSE 0 END', 'alias' => 'collected_amount'],
],
'group_by' => [],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'discipline_id', 'type' => 'select', 'label_ar' => 'النشاط الرياضي', 'required' => false, 'data_source' => 'sport_disciplines'],
['name' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies'],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'summary_cards',
'columns' => ['total_subscriptions', 'paid_count', 'pending_count', 'overdue_count', 'total_amount', 'collected_amount'],
'chart' => ['type' => 'pie', 'labels' => ['paid_count', 'pending_count', 'overdue_count']],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 8,
],
[
'report_code' => 'RPT_SUB_OVERDUE',
'name_ar' => 'المتأخرات',
'name_en' => 'Overdue Subscriptions',
'description_ar' => 'قائمة الاشتراكات المتأخرة مع المبالغ وعدد أيام التأخر',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'activity_subscriptions',
'joins' => [
['table' => 'players', 'on' => 'activity_subscriptions.player_id = players.id', 'type' => 'INNER'],
],
'aggregations' => [],
'group_by' => [],
'where' => [
['field' => 'activity_subscriptions.status', 'operator' => '=', 'value' => 'overdue'],
],
'computed_fields' => [
['alias' => 'days_overdue', 'expression' => 'DATEDIFF(CURDATE(), activity_subscriptions.due_date)'],
],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'discipline_id', 'type' => 'select', 'label_ar' => 'النشاط الرياضي', 'required' => false, 'data_source' => 'sport_disciplines'],
['name' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies'],
['name' => 'min_days_overdue', 'type' => 'number', 'label_ar' => 'الحد الأدنى لأيام التأخر', 'required' => false],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['players.full_name_ar', 'activity_subscriptions.month', 'activity_subscriptions.amount', 'activity_subscriptions.due_date', 'days_overdue'],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 9,
],
[
'report_code' => 'RPT_ATTENDANCE',
'name_ar' => 'الحضور',
'name_en' => 'Attendance',
'description_ar' => 'نسب الحضور لكل أكاديمية ونشاط رياضي حسب الشهر',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'attendance_records',
'joins' => [
['table' => 'players', 'on' => 'attendance_records.player_id = players.id', 'type' => 'INNER'],
['table' => 'academies', 'on' => 'attendance_records.academy_id = academies.id', 'type' => 'LEFT'],
['table' => 'sport_disciplines', 'on' => 'attendance_records.discipline_id = sport_disciplines.id', 'type' => 'LEFT'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'attendance_records.id', 'alias' => 'total_records'],
['function' => 'SUM', 'field' => 'CASE WHEN attendance_records.status = \'present\' THEN 1 ELSE 0 END', 'alias' => 'present_count'],
['function' => 'SUM', 'field' => 'CASE WHEN attendance_records.status = \'absent\' THEN 1 ELSE 0 END', 'alias' => 'absent_count'],
],
'group_by' => ['academies.id', 'academies.name_ar', 'sport_disciplines.id', 'sport_disciplines.name_ar', 'MONTH(attendance_records.attendance_date)'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'date_from', 'type' => 'date', 'label_ar' => 'من تاريخ', 'required' => false],
['name' => 'date_to', 'type' => 'date', 'label_ar' => 'إلى تاريخ', 'required' => false],
['name' => 'discipline_id', 'type' => 'select', 'label_ar' => 'النشاط الرياضي', 'required' => false, 'data_source' => 'sport_disciplines'],
['name' => 'academy_id', 'type' => 'select', 'label_ar' => 'الأكاديمية', 'required' => false, 'data_source' => 'academies'],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'table',
'columns' => ['academies.name_ar', 'sport_disciplines.name_ar', 'month', 'total_records', 'present_count', 'absent_count'],
'chart' => ['type' => 'line', 'x' => 'month', 'y' => ['present_count', 'absent_count']],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 10,
],
[
'report_code' => 'RPT_SPORTS_QUARTERLY',
'name_ar' => 'التقرير الربع سنوي',
'name_en' => 'Sports Quarterly Report',
'description_ar' => 'تقرير ربع سنوي شامل يجمع بيانات اللاعبين والإيرادات والحضور',
'category' => 'sports',
'query_config_json' => json_encode([
'table' => 'players',
'joins' => [
['table' => 'activity_subscriptions', 'on' => 'players.id = activity_subscriptions.player_id', 'type' => 'LEFT'],
['table' => 'attendance_records', 'on' => 'players.id = attendance_records.player_id', 'type' => 'LEFT'],
['table' => 'reservations', 'on' => 'reservations.facility_id IS NOT NULL', 'type' => 'LEFT'],
],
'aggregations' => [
['function' => 'COUNT', 'field' => 'DISTINCT players.id', 'alias' => 'total_players'],
['function' => 'SUM', 'field' => 'activity_subscriptions.amount', 'alias' => 'total_subscription_revenue'],
['function' => 'COUNT', 'field' => 'attendance_records.id', 'alias' => 'total_attendance_records'],
['function' => 'COUNT', 'field' => 'DISTINCT reservations.id', 'alias' => 'total_reservations'],
],
'group_by' => ['QUARTER(players.created_at)', 'YEAR(players.created_at)'],
], JSON_UNESCAPED_UNICODE),
'parameters_json' => json_encode([
['name' => 'year', 'type' => 'number', 'label_ar' => 'السنة', 'required' => true],
['name' => 'quarter', 'type' => 'select', 'label_ar' => 'الربع', 'required' => true, 'options' => [1, 2, 3, 4]],
], JSON_UNESCAPED_UNICODE),
'display_config_json' => json_encode([
'type' => 'composite',
'sections' => [
['title_ar' => 'ملخص اللاعبين', 'type' => 'summary_cards', 'columns' => ['total_players']],
['title_ar' => 'الإيرادات', 'type' => 'summary_cards', 'columns' => ['total_subscription_revenue']],
['title_ar' => 'الحضور', 'type' => 'summary_cards', 'columns' => ['total_attendance_records']],
['title_ar' => 'الحجوزات', 'type' => 'summary_cards', 'columns' => ['total_reservations']],
],
], JSON_UNESCAPED_UNICODE),
'sort_order' => 11,
],
];
foreach ($reports as $r) {
$existing = $db->selectOne("SELECT id FROM report_definitions WHERE report_code = ?", [$r['report_code']]);
if ($existing) {
continue;
}
$db->insert('report_definitions', [
'report_code' => $r['report_code'],
'name_ar' => $r['name_ar'],
'name_en' => $r['name_en'],
'description_ar' => $r['description_ar'],
'category' => $r['category'],
'query_config_json' => $r['query_config_json'],
'parameters_json' => $r['parameters_json'],
'display_config_json' => $r['display_config_json'],
'is_active' => 1,
'sort_order' => $r['sort_order'],
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$templates = [
[
'template_code' => 'SMS_SPORTS_REGISTRATION',
'name_ar' => 'تسجيل في الأنشطة الرياضية',
'event_trigger' => 'player.registered',
'message_template' => 'مرحباً {player_name}، تم تسجيلك بنجاح في الأنشطة الرياضية. رقم التسجيل: {registration_serial}',
'variables_json' => json_encode(['player_name', 'registration_serial'], JSON_UNESCAPED_UNICODE),
],
[
'template_code' => 'SMS_SPORTS_SUB_DUE',
'name_ar' => 'استحقاق اشتراك النشاط الرياضي',
'event_trigger' => 'activity_sub.generated',
'message_template' => 'عزيزي {player_name}، اشتراك شهر {month} بمبلغ {amount} ج.م مستحق. الرجاء السداد قبل {due_date}',
'variables_json' => json_encode(['player_name', 'month', 'amount', 'due_date'], JSON_UNESCAPED_UNICODE),
],
[
'template_code' => 'SMS_SPORTS_SUB_OVERDUE',
'name_ar' => 'تأخر اشتراك النشاط الرياضي',
'event_trigger' => 'activity_sub.overdue',
'message_template' => 'تنبيه: اشتراك {player_name} لشهر {month} متأخر. المبلغ: {amount} ج.م. سيتم إيقاف الكارنيه في حالة عدم السداد',
'variables_json' => json_encode(['player_name', 'month', 'amount'], JSON_UNESCAPED_UNICODE),
],
[
'template_code' => 'SMS_SPORTS_CARD_REVOKED',
'name_ar' => 'إيقاف كارنيه النشاط الرياضي',
'event_trigger' => 'player.card_revoked',
'message_template' => 'تم إيقاف كارنيه النشاط الرياضي للاعب {player_name} بسبب عدم سداد الاشتراك. للاستفسار: اتصل بإدارة النادي',
'variables_json' => json_encode(['player_name'], JSON_UNESCAPED_UNICODE),
],
[
'template_code' => 'SMS_RESERVATION_CONFIRMED',
'name_ar' => 'تأكيد حجز ملعب',
'event_trigger' => 'reservation.confirmed',
'message_template' => 'تم تأكيد حجز {facility_name} بتاريخ {date} من {start_time} إلى {end_time}. رقم الحجز: {reservation_number}',
'variables_json' => json_encode(['facility_name', 'date', 'start_time', 'end_time', 'reservation_number'], JSON_UNESCAPED_UNICODE),
],
[
'template_code' => 'SMS_RESERVATION_REMINDER',
'name_ar' => 'تذكير بحجز ملعب',
'event_trigger' => 'reservation.reminder',
'message_template' => 'تذكير: لديك حجز {facility_name} غداً {date} الساعة {start_time}. رقم الحجز: {reservation_number}',
'variables_json' => json_encode(['facility_name', 'date', 'start_time', 'reservation_number'], JSON_UNESCAPED_UNICODE),
],
[
'template_code' => 'SMS_MEDICAL_EXPIRY',
'name_ar' => 'تنبيه انتهاء الشهادة الطبية',
'event_trigger' => 'player.medical_expiry',
'message_template' => 'تنبيه: شهادتك الطبية ستنتهي بتاريخ {expiry_date}. يرجى تجديدها لاستمرار التسجيل في الأنشطة الرياضية',
'variables_json' => json_encode(['player_name', 'expiry_date'], JSON_UNESCAPED_UNICODE),
],
];
foreach ($templates as $t) {
$existing = $db->selectOne("SELECT id FROM sms_templates WHERE template_code = ?", [$t['template_code']]);
if ($existing) {
continue;
}
$db->insert('sms_templates', [
'template_code' => $t['template_code'],
'name_ar' => $t['name_ar'],
'event_trigger' => $t['event_trigger'],
'message_template' => $t['message_template'],
'variables_json' => $t['variables_json'],
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
}
};
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$ts = date('Y-m-d H:i:s');
$today = date('Y-m-d');
// ── Helper: look up academy ID by code ──
$academyId = function (string $code) use ($db): ?int {
$row = $db->selectOne("SELECT id FROM academies WHERE code = ?", [$code]);
return $row ? (int) ($row->id ?? $row['id']) : null;
};
// ── Helper: look up discipline ID by code ──
$disciplineId = function (string $code) use ($db): ?int {
$row = $db->selectOne("SELECT id FROM sport_disciplines WHERE code = ?", [$code]);
return $row ? (int) ($row->id ?? $row['id']) : null;
};
// ── Helper: idempotent insert into activity_pricing ──
$ensurePricing = function (string $pricingType, ?int $referenceId, float $memberRate, float $nonmemberRate) use ($db, $ts, $today): void {
if ($referenceId === null) {
return;
}
$existing = $db->selectOne(
"SELECT id FROM activity_pricing WHERE pricing_type = ? AND reference_id = ? AND is_active = 1",
[$pricingType, $referenceId]
);
if ($existing) {
return;
}
$db->insert('activity_pricing', [
'pricing_type' => $pricingType,
'reference_id' => $referenceId,
'member_rate' => $memberRate,
'nonmember_rate' => $nonmemberRate,
'effective_from' => $today,
'is_active' => 1,
'created_at' => $ts,
'updated_at' => $ts,
]);
};
// ══════════════════════════════════════════════════════════════
// Academy-level pricing (pricing_type = 'academy')
// ══════════════════════════════════════════════════════════════
// Football academies
$ensurePricing('academy', $academyId('acad_anderlecht'), 400.00, 800.00);
$ensurePricing('academy', $academyId('acad_mowahiba'), 400.00, 800.00);
$ensurePricing('academy', $academyId('acad_rowad'), 350.00, 700.00);
// Gymnastics academies
$ensurePricing('academy', $academyId('acad_gym_artistic_m'), 350.00, 700.00);
$ensurePricing('academy', $academyId('acad_gym_artistic_w'), 350.00, 700.00);
$ensurePricing('academy', $academyId('acad_gym_aerobic'), 350.00, 700.00);
// Swimming academies
$ensurePricing('academy', $academyId('acad_swim_schools'), 300.00, 600.00);
$ensurePricing('academy', $academyId('acad_swim_practice'), 400.00, 800.00);
$ensurePricing('academy', $academyId('acad_swim_advanced'), 400.00, 800.00);
// ══════════════════════════════════════════════════════════════
// Discipline-level pricing for combat sports (pricing_type = 'discipline')
// ══════════════════════════════════════════════════════════════
$ensurePricing('discipline', $disciplineId('karate'), 300.00, 600.00);
$ensurePricing('discipline', $disciplineId('kung_fu'), 300.00, 600.00);
};
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