Commit d01e77d7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

sdgjdhfk

parent 85a4f4fb
<?php
declare(strict_types=1);
namespace App\Modules\AcademyContracts\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\AcademyContracts\Models\AcademyContract;
use App\Modules\AcademyContracts\Models\AcademySettlement;
use App\Modules\AcademyContracts\Services\AcademyContractService;
use App\Modules\Academies\Models\Academy;
class AcademyContractController extends Controller
{
/**
* List all contracts with filters and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => trim((string) $request->get('status', '')),
'academy_id' => trim((string) $request->get('academy_id', '')),
'contract_type' => trim((string) $request->get('contract_type', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = AcademyContract::search($filters, 25, $page);
return $this->view('AcademyContracts.Views.index', [
'contracts' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => AcademyContract::getStatuses(),
'contractTypes' => AcademyContract::getContractTypes(),
'academies' => Academy::allActive(),
]);
}
/**
* Show the create contract form.
*/
public function create(Request $request): Response
{
return $this->view('AcademyContracts.Views.create', [
'contractTypes' => AcademyContract::getContractTypes(),
'academies' => Academy::allActive(),
]);
}
/**
* Validate and store a new contract.
*/
public function store(Request $request): Response
{
$academyId = (int) $request->post('academy_id', 0);
$contractType = trim((string) $request->post('contract_type', 'revenue_share'));
$startDate = trim((string) $request->post('start_date', ''));
$endDate = trim((string) $request->post('end_date', ''));
$minimumRevenueGuarantee = (float) $request->post('minimum_revenue_guarantee', 0);
$clubCommissionPct = (float) $request->post('club_commission_pct', 0);
$academySharePct = (float) $request->post('academy_share_pct', 0);
$fixedMonthlyRent = (float) $request->post('fixed_monthly_rent', 0);
$depositAmount = (float) $request->post('deposit_amount', 0);
$depositStatus = trim((string) $request->post('deposit_status', 'pending'));
$settlementDay = (int) $request->post('settlement_day', 5);
$gracePeriodDays = (int) $request->post('grace_period_days', 7);
$penaltyRatePct = (float) $request->post('penalty_rate_pct', 0);
$autoRenew = (int) $request->post('auto_renew', 0);
$renewalNoticeDays = (int) $request->post('renewal_notice_days', 30);
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
if ($academyId <= 0) {
$errors[] = 'الأكاديمية مطلوبة';
} else {
$academy = Academy::find($academyId);
if (!$academy) {
$errors[] = 'الأكاديمية غير موجودة';
}
}
if ($startDate === '') {
$errors[] = 'تاريخ البداية مطلوب';
}
if ($endDate === '') {
$errors[] = 'تاريخ النهاية مطلوب';
}
if ($startDate !== '' && $endDate !== '' && $startDate >= $endDate) {
$errors[] = 'تاريخ البداية يجب أن يكون قبل تاريخ النهاية';
}
if ($minimumRevenueGuarantee < 0) {
$errors[] = 'الحد الأدنى للإيرادات يجب أن يكون صفر أو أكثر';
}
if (!array_key_exists($contractType, AcademyContract::getContractTypes())) {
$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('/academy-contracts/create');
}
// Auto-generate contract number
$contractNumber = AcademyContractService::generateContractNumber();
$contract = AcademyContract::create([
'contract_number' => $contractNumber,
'academy_id' => $academyId,
'contract_type' => $contractType,
'start_date' => $startDate,
'end_date' => $endDate,
'minimum_revenue_guarantee' => $minimumRevenueGuarantee,
'club_commission_pct' => $clubCommissionPct,
'academy_share_pct' => $academySharePct,
'fixed_monthly_rent' => $fixedMonthlyRent,
'deposit_amount' => $depositAmount,
'deposit_status' => $depositStatus,
'settlement_day' => $settlementDay,
'grace_period_days' => $gracePeriodDays,
'penalty_rate_pct' => $penaltyRatePct,
'auto_renew' => $autoRenew ? 1 : 0,
'renewal_notice_days' => $renewalNoticeDays,
'status' => 'draft',
'notes' => $notes ?: null,
]);
return $this->redirect('/academy-contracts/' . $contract->id)->withSuccess('تم إنشاء العقد بنجاح');
}
/**
* Show contract detail page.
*/
public function show(Request $request, string $id): Response
{
$contract = AcademyContract::find((int) $id);
if (!$contract) {
return $this->redirect('/academy-contracts')->withError('العقد غير موجود');
}
$academy = Academy::find((int) $contract->academy_id);
// Get settlements for this contract
$db = App::getInstance()->db();
$settlements = $db->select(
"SELECT * FROM academy_settlements WHERE contract_id = ? ORDER BY period_month DESC LIMIT 12",
[(int) $id]
);
return $this->view('AcademyContracts.Views.show', [
'contract' => $contract,
'academy' => $academy,
'settlements' => $settlements,
]);
}
/**
* Show edit form for a contract.
*/
public function edit(Request $request, string $id): Response
{
$contract = AcademyContract::find((int) $id);
if (!$contract) {
return $this->redirect('/academy-contracts')->withError('العقد غير موجود');
}
return $this->view('AcademyContracts.Views.edit', [
'contract' => $contract,
'contractTypes' => AcademyContract::getContractTypes(),
'academies' => Academy::allActive(),
]);
}
/**
* Validate and update an existing contract.
*/
public function update(Request $request, string $id): Response
{
$contract = AcademyContract::find((int) $id);
if (!$contract) {
return $this->redirect('/academy-contracts')->withError('العقد غير موجود');
}
$academyId = (int) $request->post('academy_id', 0);
$contractType = trim((string) $request->post('contract_type', 'revenue_share'));
$startDate = trim((string) $request->post('start_date', ''));
$endDate = trim((string) $request->post('end_date', ''));
$minimumRevenueGuarantee = (float) $request->post('minimum_revenue_guarantee', 0);
$clubCommissionPct = (float) $request->post('club_commission_pct', 0);
$academySharePct = (float) $request->post('academy_share_pct', 0);
$fixedMonthlyRent = (float) $request->post('fixed_monthly_rent', 0);
$depositAmount = (float) $request->post('deposit_amount', 0);
$depositStatus = trim((string) $request->post('deposit_status', 'pending'));
$settlementDay = (int) $request->post('settlement_day', 5);
$gracePeriodDays = (int) $request->post('grace_period_days', 7);
$penaltyRatePct = (float) $request->post('penalty_rate_pct', 0);
$autoRenew = (int) $request->post('auto_renew', 0);
$renewalNoticeDays = (int) $request->post('renewal_notice_days', 30);
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
if ($academyId <= 0) {
$errors[] = 'الأكاديمية مطلوبة';
} else {
$academy = Academy::find($academyId);
if (!$academy) {
$errors[] = 'الأكاديمية غير موجودة';
}
}
if ($startDate === '') {
$errors[] = 'تاريخ البداية مطلوب';
}
if ($endDate === '') {
$errors[] = 'تاريخ النهاية مطلوب';
}
if ($startDate !== '' && $endDate !== '' && $startDate >= $endDate) {
$errors[] = 'تاريخ البداية يجب أن يكون قبل تاريخ النهاية';
}
if ($minimumRevenueGuarantee < 0) {
$errors[] = 'الحد الأدنى للإيرادات يجب أن يكون صفر أو أكثر';
}
if (!array_key_exists($contractType, AcademyContract::getContractTypes())) {
$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('/academy-contracts/' . $id . '/edit');
}
$contract->update([
'academy_id' => $academyId,
'contract_type' => $contractType,
'start_date' => $startDate,
'end_date' => $endDate,
'minimum_revenue_guarantee' => $minimumRevenueGuarantee,
'club_commission_pct' => $clubCommissionPct,
'academy_share_pct' => $academySharePct,
'fixed_monthly_rent' => $fixedMonthlyRent,
'deposit_amount' => $depositAmount,
'deposit_status' => $depositStatus,
'settlement_day' => $settlementDay,
'grace_period_days' => $gracePeriodDays,
'penalty_rate_pct' => $penaltyRatePct,
'auto_renew' => $autoRenew ? 1 : 0,
'renewal_notice_days' => $renewalNoticeDays,
'notes' => $notes ?: null,
]);
return $this->redirect('/academy-contracts/' . $id)->withSuccess('تم تحديث العقد بنجاح');
}
/**
* Activate a contract.
*/
public function activate(Request $request, string $id): Response
{
$contract = AcademyContract::find((int) $id);
if (!$contract) {
return $this->redirect('/academy-contracts')->withError('العقد غير موجود');
}
$session = App::getInstance()->session();
$currentUser = $session->get('employee_id', 0);
AcademyContractService::activate((int) $id, (int) $currentUser);
return $this->redirect('/academy-contracts/' . $id)->withSuccess('تم تفعيل العقد بنجاح');
}
/**
* Terminate a contract.
*/
public function terminate(Request $request, string $id): Response
{
$contract = AcademyContract::find((int) $id);
if (!$contract) {
return $this->redirect('/academy-contracts')->withError('العقد غير موجود');
}
$reason = trim((string) $request->post('termination_reason', ''));
if ($reason === '') {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'سبب الإنهاء مطلوب']]);
return $this->redirect('/academy-contracts/' . $id);
}
$session = App::getInstance()->session();
$currentUser = $session->get('employee_id', 0);
AcademyContractService::terminate((int) $id, (int) $currentUser, $reason);
return $this->redirect('/academy-contracts/' . $id)->withSuccess('تم إنهاء العقد بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\AcademyContracts\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\AcademyContracts\Models\AcademySettlement;
use App\Modules\AcademyContracts\Services\SettlementService;
use App\Modules\Academies\Models\Academy;
class SettlementController extends Controller
{
/**
* List all settlements with filters and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'status' => trim((string) $request->get('status', '')),
'academy_id' => trim((string) $request->get('academy_id', '')),
'period_month' => trim((string) $request->get('period_month', '')),
'direction' => trim((string) $request->get('direction', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = AcademySettlement::search($filters, 25, $page);
return $this->view('AcademyContracts.Views.settlement_index', [
'settlements' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'statuses' => AcademySettlement::getStatuses(),
'directions' => AcademySettlement::getDirections(),
'academies' => Academy::allActive(),
]);
}
/**
* Show settlement detail page.
*/
public function show(Request $request, string $id): Response
{
$settlement = AcademySettlement::find((int) $id);
if (!$settlement) {
return $this->redirect('/academy-settlements')->withError('التسوية غير موجودة');
}
$academy = Academy::find((int) $settlement->academy_id);
$db = App::getInstance()->db();
$contract = $db->selectOne(
"SELECT * FROM academy_contracts WHERE id = ?",
[(int) $settlement->contract_id]
);
return $this->view('AcademyContracts.Views.settlement_show', [
'settlement' => $settlement,
'academy' => $academy,
'contract' => $contract,
]);
}
/**
* Generate settlements for a given month (POST with month param).
*/
public function generate(Request $request): Response
{
$month = trim((string) $request->post('period_month', ''));
if ($month === '' || !preg_match('/^\d{4}-\d{2}$/', $month)) {
$session = App::getInstance()->session();
$session->flash('_alerts', [['type' => 'error', 'message' => 'الشهر مطلوب بالصيغة YYYY-MM']]);
return $this->redirect('/academy-settlements');
}
$count = SettlementService::generateMonthlySettlements($month);
if ($count > 0) {
return $this->redirect('/academy-settlements?period_month=' . $month)
->withSuccess("تم إنشاء {$count} تسوية لشهر {$month}");
}
return $this->redirect('/academy-settlements')
->withWarning('لا توجد عقود نشطة تحتاج لتسوية أو تم إنشاء التسويات مسبقاً');
}
/**
* Approve a settlement.
*/
public function approve(Request $request, string $id): Response
{
$settlement = AcademySettlement::find((int) $id);
if (!$settlement) {
return $this->redirect('/academy-settlements')->withError('التسوية غير موجودة');
}
$session = App::getInstance()->session();
$currentUser = $session->get('employee_id', 0);
SettlementService::approveSettlement((int) $id, (int) $currentUser);
return $this->redirect('/academy-settlements/' . $id)->withSuccess('تم اعتماد التسوية بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\AcademyContracts\Models;
use App\Core\Model;
use App\Core\App;
class AcademyContract extends Model
{
protected static string $table = 'academy_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',
'academy_id',
'contract_type',
'start_date',
'end_date',
'minimum_revenue_guarantee',
'club_commission_pct',
'academy_share_pct',
'fixed_monthly_rent',
'deposit_amount',
'deposit_status',
'settlement_day',
'grace_period_days',
'penalty_rate_pct',
'auto_renew',
'renewal_notice_days',
'status',
'approved_by',
'approved_at',
'terminated_by',
'terminated_at',
'termination_reason',
'terms_json',
'notes',
];
/**
* Get all contract types with Arabic labels.
*/
public static function getContractTypes(): array
{
return [
'revenue_share' => 'مشاركة إيرادات',
'fixed_rent' => 'إيجار ثابت',
'hybrid' => 'نظام مختلط',
];
}
/**
* Get all statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'pending_approval' => 'في انتظار الموافقة',
'active' => 'نشط',
'suspended' => 'موقوف',
'expired' => 'منتهي',
'terminated' => 'ملغى',
];
}
/**
* 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 = [
'draft' => '#9CA3AF',
'pending_approval' => '#D97706',
'active' => '#059669',
'suspended' => '#DC2626',
'expired' => '#6B7280',
'terminated' => '#991B1B',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Get the Arabic label for a contract type.
*/
public static function getContractTypeLabel(string $type): string
{
$types = self::getContractTypes();
return $types[$type] ?? $type;
}
/**
* Search contracts with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query()
->leftJoin('academies', '`academy_contracts`.`academy_id` = `academies`.`id`')
->select('`academy_contracts`.*, `academies`.`name_ar` as academy_name');
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`academy_contracts`.`contract_number` LIKE ? OR `academies`.`name_ar` LIKE ?)',
[$search, $search]
);
}
if (!empty($filters['status'])) {
$query = $query->where('academy_contracts.status', '=', $filters['status']);
}
if (!empty($filters['academy_id'])) {
$query = $query->where('academy_contracts.academy_id', '=', (int) $filters['academy_id']);
}
if (!empty($filters['contract_type'])) {
$query = $query->where('academy_contracts.contract_type', '=', $filters['contract_type']);
}
$query = $query->orderBy('academy_contracts.created_at', 'DESC');
return $query->paginate($perPage, $page);
}
/**
* Get the active contract for a specific academy.
*/
public static function getActiveForAcademy(int $academyId): ?array
{
$result = static::query()
->where('academy_id', '=', $academyId)
->where('status', '=', 'active')
->orderBy('start_date', 'DESC')
->first();
return $result ?: null;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\AcademyContracts\Models;
use App\Core\Model;
use App\Core\App;
class AcademySettlement extends Model
{
protected static string $table = 'academy_settlements';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'settlement_number',
'academy_id',
'contract_id',
'period_month',
'total_revenue',
'minimum_guarantee',
'revenue_surplus',
'revenue_shortfall',
'club_commission',
'academy_share',
'net_amount',
'penalty_amount',
'direction',
'status',
'approved_by',
'approved_at',
'payment_id',
'paid_at',
'dispute_reason',
'notes',
];
/**
* Get all statuses with Arabic labels.
*/
public static function getStatuses(): array
{
return [
'draft' => 'مسودة',
'pending_approval' => 'في انتظار الموافقة',
'approved' => 'معتمدة',
'paid' => 'مدفوعة',
'disputed' => 'متنازع عليها',
];
}
/**
* Get all directions with Arabic labels.
*/
public static function getDirections(): array
{
return [
'club_pays_academy' => 'النادي يدفع للأكاديمية',
'academy_pays_club' => 'الأكاديمية تدفع للنادي',
'balanced' => 'متوازن',
];
}
/**
* 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 = [
'draft' => '#9CA3AF',
'pending_approval' => '#D97706',
'approved' => '#059669',
'paid' => '#2563EB',
'disputed' => '#DC2626',
];
return $colors[$status] ?? '#6B7280';
}
/**
* Get the Arabic label for a direction.
*/
public static function getDirectionLabel(string $direction): string
{
$directions = self::getDirections();
return $directions[$direction] ?? $direction;
}
/**
* Get the color for a direction.
*/
public static function getDirectionColor(string $direction): string
{
$colors = [
'club_pays_academy' => '#059669',
'academy_pays_club' => '#DC2626',
'balanced' => '#6B7280',
];
return $colors[$direction] ?? '#6B7280';
}
/**
* Search settlements with filters and pagination.
*/
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query()
->leftJoin('academies', '`academy_settlements`.`academy_id` = `academies`.`id`')
->leftJoin('academy_contracts', '`academy_settlements`.`contract_id` = `academy_contracts`.`id`')
->select('`academy_settlements`.*, `academies`.`name_ar` as academy_name, `academy_contracts`.`contract_number`');
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$query = $query->whereRaw(
'(`academy_settlements`.`settlement_number` LIKE ? OR `academies`.`name_ar` LIKE ?)',
[$search, $search]
);
}
if (!empty($filters['status'])) {
$query = $query->where('academy_settlements.status', '=', $filters['status']);
}
if (!empty($filters['academy_id'])) {
$query = $query->where('academy_settlements.academy_id', '=', (int) $filters['academy_id']);
}
if (!empty($filters['period_month'])) {
$query = $query->where('academy_settlements.period_month', '=', $filters['period_month']);
}
if (!empty($filters['direction'])) {
$query = $query->where('academy_settlements.direction', '=', $filters['direction']);
}
$query = $query->orderBy('academy_settlements.period_month', 'DESC')
->orderBy('academy_settlements.created_at', 'DESC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
return [
['GET', '/academy-contracts', 'AcademyContracts\Controllers\AcademyContractController@index', ['auth'], 'academy_contract.view'],
['GET', '/academy-contracts/create', 'AcademyContracts\Controllers\AcademyContractController@create', ['auth'], 'academy_contract.manage'],
['POST', '/academy-contracts', 'AcademyContracts\Controllers\AcademyContractController@store', ['auth', 'csrf'], 'academy_contract.manage'],
['GET', '/academy-contracts/{id:\d+}', 'AcademyContracts\Controllers\AcademyContractController@show', ['auth'], 'academy_contract.view'],
['GET', '/academy-contracts/{id:\d+}/edit', 'AcademyContracts\Controllers\AcademyContractController@edit', ['auth'], 'academy_contract.manage'],
['POST', '/academy-contracts/{id:\d+}', 'AcademyContracts\Controllers\AcademyContractController@update', ['auth', 'csrf'], 'academy_contract.manage'],
['POST', '/academy-contracts/{id:\d+}/activate', 'AcademyContracts\Controllers\AcademyContractController@activate', ['auth', 'csrf'], 'academy_contract.manage'],
['POST', '/academy-contracts/{id:\d+}/terminate', 'AcademyContracts\Controllers\AcademyContractController@terminate', ['auth', 'csrf'], 'academy_contract.manage'],
['GET', '/academy-settlements', 'AcademyContracts\Controllers\SettlementController@index', ['auth'], 'academy_contract.settlement'],
['GET', '/academy-settlements/{id:\d+}', 'AcademyContracts\Controllers\SettlementController@show', ['auth'], 'academy_contract.settlement'],
['POST', '/academy-settlements/generate', 'AcademyContracts\Controllers\SettlementController@generate', ['auth', 'csrf'], 'academy_contract.settlement'],
['POST', '/academy-settlements/{id:\d+}/approve', 'AcademyContracts\Controllers\SettlementController@approve', ['auth', 'csrf'], 'academy_contract.settlement'],
];
<?php
declare(strict_types=1);
namespace App\Modules\AcademyContracts\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\AcademyContracts\Models\AcademyContract;
final class AcademyContractService
{
/**
* Activate a contract (set status to 'active', record approver).
*/
public static function activate(int $contractId, int $approvedBy): void
{
$contract = AcademyContract::findOrFail($contractId);
$contract->update([
'status' => 'active',
'approved_by' => $approvedBy,
'approved_at' => date('Y-m-d H:i:s'),
]);
EventBus::dispatch('academy.contract.activated', [
'contract_id' => $contractId,
'approved_by' => $approvedBy,
]);
}
/**
* Terminate a contract.
*/
public static function terminate(int $contractId, int $terminatedBy, string $reason): void
{
$contract = AcademyContract::findOrFail($contractId);
$contract->update([
'status' => 'terminated',
'terminated_by' => $terminatedBy,
'terminated_at' => date('Y-m-d H:i:s'),
'termination_reason' => $reason,
]);
EventBus::dispatch('academy.contract.terminated', [
'contract_id' => $contractId,
'terminated_by' => $terminatedBy,
'reason' => $reason,
]);
}
/**
* Generate a unique contract number in the format AC-YYYY-XXXX.
*/
public static function generateContractNumber(): string
{
$db = App::getInstance()->db();
$year = date('Y');
$prefix = 'AC-' . $year . '-';
$lastRow = $db->selectOne(
"SELECT contract_number FROM academy_contracts
WHERE contract_number LIKE ?
ORDER BY contract_number DESC LIMIT 1",
[$prefix . '%']
);
if ($lastRow) {
$lastSeq = (int) substr($lastRow['contract_number'], strlen($prefix));
$nextSeq = $lastSeq + 1;
} else {
$nextSeq = 1;
}
return $prefix . str_pad((string) $nextSeq, 4, '0', STR_PAD_LEFT);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\AcademyContracts\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\AcademyContracts\Models\AcademyContract;
use App\Modules\AcademyContracts\Models\AcademySettlement;
final class SettlementService
{
/**
* Calculate and create a settlement for a contract for a given month.
*
* @param int $contractId
* @param string $month YYYY-MM format
* @return array The settlement data
*/
public static function calculateSettlement(int $contractId, string $month): array
{
$db = App::getInstance()->db();
$contract = AcademyContract::findOrFail($contractId);
$academyId = (int) $contract->academy_id;
// Query activity_subscriptions where the enrollment links to this academy and the month matches
$subscriptionRevenue = $db->selectOne(
"SELECT COALESCE(SUM(asub.total_amount), 0) as total
FROM activity_subscriptions asub
INNER JOIN academy_enrollments ae ON ae.id = asub.enrollment_id
WHERE ae.academy_id = ?
AND asub.subscription_month = ?
AND asub.status IN ('paid', 'pending')
AND asub.is_archived = 0",
[$academyId, $month]
);
$totalRevenue = (float) ($subscriptionRevenue['total'] ?? 0);
$minimumGuarantee = (float) $contract->minimum_revenue_guarantee;
$clubCommissionPct = (float) $contract->club_commission_pct;
$academySharePct = (float) $contract->academy_share_pct;
$penaltyRatePct = (float) $contract->penalty_rate_pct;
$revenueSurplus = 0.0;
$revenueShortfall = 0.0;
$clubCommission = 0.0;
$academyShare = 0.0;
$netAmount = 0.0;
$penaltyAmount = 0.0;
$direction = 'balanced';
if ($totalRevenue >= $minimumGuarantee) {
// Revenue meets or exceeds minimum guarantee
$revenueSurplus = $totalRevenue - $minimumGuarantee;
$academyShare = $revenueSurplus * ($academySharePct / 100);
$clubCommission = $revenueSurplus * ($clubCommissionPct / 100);
$direction = 'club_pays_academy';
$netAmount = $academyShare;
} else {
// Revenue below minimum guarantee
$revenueShortfall = $minimumGuarantee - $totalRevenue;
$direction = 'academy_pays_club';
$netAmount = $revenueShortfall;
// Apply penalty if configured
if ($penaltyRatePct > 0) {
$penaltyAmount = $revenueShortfall * ($penaltyRatePct / 100);
}
}
// If revenue equals minimum exactly and no surplus
if ($totalRevenue == $minimumGuarantee) {
$direction = 'balanced';
$netAmount = 0.0;
}
// Count enrolled and active players for the revenue record
$playerCounts = $db->selectOne(
"SELECT
COUNT(*) as enrolled_count,
SUM(CASE WHEN ae.status = 'active' THEN 1 ELSE 0 END) as active_count
FROM academy_enrollments ae
WHERE ae.academy_id = ?",
[$academyId]
);
// Create or update academy_revenue_records
$existingRecord = $db->selectOne(
"SELECT id FROM academy_revenue_records
WHERE academy_id = ? AND contract_id = ? AND period_month = ?",
[$academyId, $contractId, $month]
);
$revenueData = [
'academy_id' => $academyId,
'contract_id' => $contractId,
'period_month' => $month,
'subscription_revenue' => $totalRevenue,
'registration_revenue' => 0.00,
'other_revenue' => 0.00,
'total_revenue' => $totalRevenue,
'enrolled_count' => (int) ($playerCounts['enrolled_count'] ?? 0),
'active_players_count' => (int) ($playerCounts['active_count'] ?? 0),
'is_finalized' => 1,
'calculated_at' => date('Y-m-d H:i:s'),
];
if ($existingRecord) {
$db->update('academy_revenue_records', $revenueData, 'id = ?', [(int) $existingRecord['id']]);
} else {
$db->insert('academy_revenue_records', $revenueData);
}
// Generate settlement number
$settlementNumber = self::generateSettlementNumber();
// Create academy_settlements record
$settlementData = [
'settlement_number' => $settlementNumber,
'academy_id' => $academyId,
'contract_id' => $contractId,
'period_month' => $month,
'total_revenue' => $totalRevenue,
'minimum_guarantee' => $minimumGuarantee,
'revenue_surplus' => $revenueSurplus,
'revenue_shortfall' => $revenueShortfall,
'club_commission' => $clubCommission,
'academy_share' => $academyShare,
'net_amount' => $netAmount,
'penalty_amount' => $penaltyAmount,
'direction' => $direction,
'status' => 'pending_approval',
];
$settlement = AcademySettlement::create($settlementData);
// Dispatch event
EventBus::dispatch('academy.settlement.created', [
'settlement_id' => $settlement->id,
'contract_id' => $contractId,
'academy_id' => $academyId,
'month' => $month,
'direction' => $direction,
'net_amount' => $netAmount,
]);
return array_merge($settlementData, ['id' => $settlement->id]);
}
/**
* Generate monthly settlements for all active contracts.
*
* @param string $month YYYY-MM format
* @return int Number of settlements generated
*/
public static function generateMonthlySettlements(string $month): int
{
$db = App::getInstance()->db();
// Get all active contracts
$contracts = $db->select(
"SELECT id FROM academy_contracts WHERE status = 'active' AND is_archived = 0"
);
$count = 0;
foreach ($contracts as $contract) {
// Check if settlement already exists for this contract and month
$existing = $db->selectOne(
"SELECT id FROM academy_settlements WHERE contract_id = ? AND period_month = ?",
[(int) $contract['id'], $month]
);
if (!$existing) {
self::calculateSettlement((int) $contract['id'], $month);
$count++;
}
}
return $count;
}
/**
* Approve a settlement.
*/
public static function approveSettlement(int $settlementId, int $approvedBy): void
{
$settlement = AcademySettlement::findOrFail($settlementId);
$settlement->update([
'status' => 'approved',
'approved_by' => $approvedBy,
'approved_at' => date('Y-m-d H:i:s'),
]);
EventBus::dispatch('academy.settlement.approved', [
'settlement_id' => $settlementId,
'approved_by' => $approvedBy,
'contract_id' => (int) $settlement->contract_id,
'academy_id' => (int) $settlement->academy_id,
'direction' => $settlement->direction,
'net_amount' => (float) $settlement->net_amount,
]);
}
/**
* Generate a unique settlement number in the format STL-YYYY-XXXX.
*/
private static function generateSettlementNumber(): string
{
$db = App::getInstance()->db();
$year = date('Y');
$prefix = 'STL-' . $year . '-';
$lastRow = $db->selectOne(
"SELECT settlement_number FROM academy_settlements
WHERE settlement_number LIKE ?
ORDER BY settlement_number DESC LIMIT 1",
[$prefix . '%']
);
if ($lastRow) {
$lastSeq = (int) substr($lastRow['settlement_number'], strlen($prefix));
$nextSeq = $lastSeq + 1;
} else {
$nextSeq = 1;
}
return $prefix . str_pad((string) $nextSeq, 4, '0', STR_PAD_LEFT);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إنشاء عقد أكاديمية جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academy-contracts" 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="/academy-contracts">
<?= 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>
<select name="academy_id" class="form-input" required>
<option value="">-- اختر الأكاديمية --</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= old('academy_id') == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع العقد <span style="color:#DC2626;">*</span></label>
<select name="contract_type" class="form-input" required>
<?php foreach ($contractTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('contract_type', 'revenue_share') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<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 style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<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 style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Financial Terms -->
<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:#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="minimum_revenue_guarantee" value="<?= e(old('minimum_revenue_guarantee', '0')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نسبة عمولة النادي %</label>
<input type="number" name="club_commission_pct" value="<?= e(old('club_commission_pct', '0')) ?>" class="form-input" min="0" max="100" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نسبة حصة الأكاديمية %</label>
<input type="number" name="academy_share_pct" value="<?= e(old('academy_share_pct', '0')) ?>" class="form-input" min="0" max="100" step="0.01" 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">الإيجار الشهري الثابت (ج.م)</label>
<input type="number" name="fixed_monthly_rent" value="<?= e(old('fixed_monthly_rent', '0')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">مبلغ التأمين (ج.م)</label>
<input type="number" name="deposit_amount" value="<?= e(old('deposit_amount', '0')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">حالة التأمين</label>
<select name="deposit_status" class="form-input">
<option value="pending" <?= old('deposit_status', 'pending') === 'pending' ? 'selected' : '' ?>>معلق</option>
<option value="paid" <?= old('deposit_status') === 'paid' ? 'selected' : '' ?>>مدفوع</option>
<option value="returned" <?= old('deposit_status') === 'returned' ? 'selected' : '' ?>>مسترد</option>
<option value="forfeited" <?= old('deposit_status') === 'forfeited' ? 'selected' : '' ?>>مصادر</option>
</select>
</div>
</div>
</div>
</div>
<!-- Settlement & Penalty 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:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">إعدادات التسوية والجزاءات</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">يوم التسوية</label>
<input type="number" name="settlement_day" value="<?= e(old('settlement_day', '5')) ?>" class="form-input" min="1" max="28" style="direction:ltr;text-align:left;">
<small style="color:#9CA3AF;font-size:11px;">يوم من الشهر (1-28)</small>
</div>
<div class="form-group">
<label class="form-label">فترة السماح (أيام)</label>
<input type="number" name="grace_period_days" value="<?= e(old('grace_period_days', '7')) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نسبة الغرامة %</label>
<input type="number" name="penalty_rate_pct" value="<?= e(old('penalty_rate_pct', '0')) ?>" class="form-input" min="0" max="100" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">مهلة التجديد (أيام)</label>
<input type="number" name="renewal_notice_days" value="<?= e(old('renewal_notice_days', '30')) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="margin-top:15px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" name="auto_renew" value="1" <?= old('auto_renew') ? 'checked' : '' ?> style="width:18px;height:18px;">
<span class="form-label" style="margin:0;">تجديد تلقائي عند انتهاء العقد</span>
</label>
</div>
</div>
</div>
<!-- Notes -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="message-square" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">ملاحظات</h3>
</div>
<div style="padding:20px;">
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية..."><?= e(old('notes')) ?></textarea>
</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="/academy-contracts" 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($contract->contract_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academy-contracts/<?= (int) $contract->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="/academy-contracts/<?= (int) $contract->id ?>">
<?= 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">رقم العقد</label>
<input type="text" value="<?= e($contract->contract_number) ?>" class="form-input" disabled style="direction:ltr;text-align:left;background:#F9FAFB;">
</div>
<div class="form-group">
<label class="form-label">الأكاديمية <span style="color:#DC2626;">*</span></label>
<select name="academy_id" class="form-input" required>
<option value="">-- اختر الأكاديمية --</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= (old('academy_id', (string) $contract->academy_id)) == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">نوع العقد <span style="color:#DC2626;">*</span></label>
<select name="contract_type" class="form-input" required>
<?php foreach ($contractTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('contract_type', $contract->contract_type) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:15px;">
<div class="form-group">
<label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" value="<?= e(old('start_date', $contract->start_date)) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ النهاية <span style="color:#DC2626;">*</span></label>
<input type="date" name="end_date" value="<?= e(old('end_date', $contract->end_date)) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Financial Terms -->
<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:#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="minimum_revenue_guarantee" value="<?= e(old('minimum_revenue_guarantee', (string) $contract->minimum_revenue_guarantee)) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نسبة عمولة النادي %</label>
<input type="number" name="club_commission_pct" value="<?= e(old('club_commission_pct', (string) $contract->club_commission_pct)) ?>" class="form-input" min="0" max="100" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نسبة حصة الأكاديمية %</label>
<input type="number" name="academy_share_pct" value="<?= e(old('academy_share_pct', (string) $contract->academy_share_pct)) ?>" class="form-input" min="0" max="100" step="0.01" 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">الإيجار الشهري الثابت (ج.م)</label>
<input type="number" name="fixed_monthly_rent" value="<?= e(old('fixed_monthly_rent', (string) $contract->fixed_monthly_rent)) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">مبلغ التأمين (ج.م)</label>
<input type="number" name="deposit_amount" value="<?= e(old('deposit_amount', (string) $contract->deposit_amount)) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">حالة التأمين</label>
<select name="deposit_status" class="form-input">
<option value="pending" <?= old('deposit_status', $contract->deposit_status) === 'pending' ? 'selected' : '' ?>>معلق</option>
<option value="paid" <?= old('deposit_status', $contract->deposit_status) === 'paid' ? 'selected' : '' ?>>مدفوع</option>
<option value="returned" <?= old('deposit_status', $contract->deposit_status) === 'returned' ? 'selected' : '' ?>>مسترد</option>
<option value="forfeited" <?= old('deposit_status', $contract->deposit_status) === 'forfeited' ? 'selected' : '' ?>>مصادر</option>
</select>
</div>
</div>
</div>
</div>
<!-- Settlement & Penalty 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:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;font-size:15px;">إعدادات التسوية والجزاءات</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">يوم التسوية</label>
<input type="number" name="settlement_day" value="<?= e(old('settlement_day', (string) $contract->settlement_day)) ?>" class="form-input" min="1" max="28" style="direction:ltr;text-align:left;">
<small style="color:#9CA3AF;font-size:11px;">يوم من الشهر (1-28)</small>
</div>
<div class="form-group">
<label class="form-label">فترة السماح (أيام)</label>
<input type="number" name="grace_period_days" value="<?= e(old('grace_period_days', (string) $contract->grace_period_days)) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نسبة الغرامة %</label>
<input type="number" name="penalty_rate_pct" value="<?= e(old('penalty_rate_pct', (string) $contract->penalty_rate_pct)) ?>" class="form-input" min="0" max="100" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">مهلة التجديد (أيام)</label>
<input type="number" name="renewal_notice_days" value="<?= e(old('renewal_notice_days', (string) $contract->renewal_notice_days)) ?>" class="form-input" min="0" style="direction:ltr;text-align:left;">
</div>
</div>
<div style="margin-top:15px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="checkbox" name="auto_renew" value="1" <?= old('auto_renew', (string) $contract->auto_renew) ? 'checked' : '' ?> style="width:18px;height:18px;">
<span class="form-label" style="margin:0;">تجديد تلقائي عند انتهاء العقد</span>
</label>
</div>
</div>
</div>
<!-- Notes -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="message-square" style="width:18px;height:18px;color:#6B7280;"></i>
<h3 style="margin:0;color:#6B7280;font-size:15px;">ملاحظات</h3>
</div>
<div style="padding:20px;">
<textarea name="notes" class="form-input" rows="3" placeholder="ملاحظات إضافية..."><?= e(old('notes', $contract->notes ?? '')) ?></textarea>
</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="/academy-contracts/<?= (int) $contract->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\AcademyContracts\Models\AcademyContract;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>عقود الأكاديميات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academy-settlements" class="btn btn-outline"><i data-lucide="calculator" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> التسويات</a>
<a href="/academy-contracts/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'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/academy-contracts" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:180px;">
<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:150px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-input">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">الأكاديمية</label>
<select name="academy_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= ($filters['academy_id'] ?? '') == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">نوع العقد</label>
<select name="contract_type" class="form-input">
<option value="">الكل</option>
<?php foreach ($contractTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['contract_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</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="/academy-contracts" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Contracts Table -->
<?php if (!empty($contracts)): ?>
<div class="card" style="overflow:hidden;">
<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:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 15px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($contracts as $c):
$statusColor = AcademyContract::getStatusColor($c['status'] ?? '');
$statusLabel = AcademyContract::getStatusLabel($c['status'] ?? '');
$typeLabel = AcademyContract::getContractTypeLabel($c['contract_type'] ?? '');
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 15px;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($c['contract_number'] ?? '') ?></code>
</td>
<td style="padding:12px 15px;font-weight:500;"><?= e($c['academy_name'] ?? '') ?></td>
<td style="padding:12px 15px;"><?= e($typeLabel) ?></td>
<td style="padding:12px 15px;font-size:12px;color:#6B7280;">
<?= e($c['start_date'] ?? '') ?> - <?= e($c['end_date'] ?? '') ?>
</td>
<td style="padding:12px 15px;direction:ltr;text-align:right;">
<?= number_format((float) ($c['minimum_revenue_guarantee'] ?? 0), 2) ?> ج.م
</td>
<td style="padding:12px 15px;text-align:center;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $statusColor ?>15;color:<?= $statusColor ?>;">
<?= e($statusLabel) ?>
</span>
</td>
<td style="padding:12px 15px;text-align:center;">
<div style="display:flex;gap:6px;justify-content:center;">
<a href="/academy-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>
<a href="/academy-contracts/<?= (int) $c['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>
</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['academy_id'])): ?>
لا توجد نتائج مطابقة لبحثك. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء عقد جديد لأكاديمية.
<?php endif; ?>
</p>
<a href="/academy-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
use App\Modules\AcademyContracts\Models\AcademySettlement;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تسويات الأكاديميات<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/academy-contracts" class="btn btn-outline"><i data-lucide="file-text" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العقود</a>
<button type="button" class="btn btn-primary" onclick="document.getElementById('generateModal').style.display='flex'">
<i data-lucide="play" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء تسويات شهرية
</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/academy-settlements" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:180px;">
<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:140px;">
<label class="form-label" style="font-size:12px;">الشهر</label>
<input type="month" name="period_month" value="<?= e($filters['period_month'] ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الأكاديمية</label>
<select name="academy_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= ($filters['academy_id'] ?? '') == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:120px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-input">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:140px;">
<label class="form-label" style="font-size:12px;">الاتجاه</label>
<select name="direction" class="form-input">
<option value="">الكل</option>
<?php foreach ($directions as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['direction'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</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="/academy-settlements" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Settlements Table -->
<?php if (!empty($settlements)): ?>
<div class="card" style="overflow:hidden;">
<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:center;font-weight:600;color:#374151;">الاتجاه</th>
<th style="padding:12px 15px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:12px 15px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($settlements as $s):
$statusColor = AcademySettlement::getStatusColor($s['status'] ?? '');
$statusLabel = AcademySettlement::getStatusLabel($s['status'] ?? '');
$dirColor = AcademySettlement::getDirectionColor($s['direction'] ?? '');
$dirLabel = AcademySettlement::getDirectionLabel($s['direction'] ?? '');
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 15px;">
<code style="font-size:12px;background:#F3F4F6;padding:2px 8px;border-radius:4px;"><?= e($s['settlement_number'] ?? '') ?></code>
</td>
<td style="padding:12px 15px;font-weight:500;"><?= e($s['academy_name'] ?? '') ?></td>
<td style="padding:12px 15px;font-size:13px;"><?= e($s['period_month'] ?? '') ?></td>
<td style="padding:12px 15px;direction:ltr;text-align:right;"><?= number_format((float) ($s['total_revenue'] ?? 0), 2) ?> ج.م</td>
<td style="padding:12px 15px;direction:ltr;text-align:right;font-weight:600;"><?= number_format((float) ($s['net_amount'] ?? 0), 2) ?> ج.م</td>
<td style="padding:12px 15px;text-align:center;">
<span style="font-size:11px;font-weight:600;color:<?= $dirColor ?>;"><?= e($dirLabel) ?></span>
</td>
<td style="padding:12px 15px;text-align:center;">
<span style="display:inline-block;padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $statusColor ?>15;color:<?= $statusColor ?>;">
<?= e($statusLabel) ?>
</span>
</td>
<td style="padding:12px 15px;text-align:center;">
<a href="/academy-settlements/<?= (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>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</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="calculator" 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['academy_id']) || !empty($filters['period_month'])): ?>
لا توجد نتائج مطابقة لبحثك.
<?php else: ?>
لم يتم إنشاء أي تسويات بعد. قم بإنشاء تسويات شهرية للعقود النشطة.
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<!-- Generate Modal -->
<div id="generateModal" style="display:none;position:fixed;inset:0;z-index:9999;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);">
<div class="card" style="width:100%;max-width:450px;margin:20px;">
<div style="padding:20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;font-size:16px;"><i data-lucide="calculator" style="width:18px;height:18px;vertical-align:middle;margin-left:4px;"></i> إنشاء تسويات شهرية</h3>
</div>
<form method="POST" action="/academy-settlements/generate">
<?= csrf_field() ?>
<div style="padding:20px;">
<p style="color:#6B7280;font-size:14px;margin:0 0 15px;">سيتم حساب التسويات لجميع العقود النشطة للشهر المحدد.</p>
<div class="form-group">
<label class="form-label">الشهر <span style="color:#DC2626;">*</span></label>
<input type="month" name="period_month" class="form-input" required style="direction:ltr;text-align:left;" value="<?= date('Y-m') ?>">
</div>
</div>
<div style="padding:15px 20px;border-top:1px solid #E5E7EB;display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('generateModal').style.display='none'">إلغاء</button>
<button type="submit" class="btn btn-primary">إنشاء التسويات</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\AcademyContracts\Models\AcademySettlement;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تسوية <?= e($settlement->settlement_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($settlement->status === 'pending_approval'): ?>
<form method="POST" action="/academy-settlements/<?= (int) $settlement->id ?>/approve" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من اعتماد هذه التسوية؟')">
<i data-lucide="check-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> اعتماد التسوية
</button>
</form>
<?php endif; ?>
<a href="/academy-settlements" 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
$statusColor = AcademySettlement::getStatusColor($settlement->status ?? '');
$statusLabel = AcademySettlement::getStatusLabel($settlement->status ?? '');
$dirColor = AcademySettlement::getDirectionColor($settlement->direction ?? '');
$dirLabel = AcademySettlement::getDirectionLabel($settlement->direction ?? '');
?>
<!-- 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, <?= $dirColor ?>15, <?= $dirColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="calculator" style="width:36px;height:36px;color:<?= $dirColor ?>;"></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($settlement->settlement_number) ?></h2>
<span style="display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;background:<?= $statusColor ?>15;color:<?= $statusColor ?>;">
<?= e($statusLabel) ?>
</span>
</div>
<div style="display:flex;gap:15px;flex-wrap:wrap;font-size:13px;color:#6B7280;margin-top:8px;">
<?php if ($academy): ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="graduation-cap" style="width:14px;height:14px;"></i>
<?= e($academy->name_ar) ?>
</span>
<?php endif; ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="calendar" style="width:14px;height:14px;"></i>
<?= e($settlement->period_month) ?>
</span>
<?php if ($contract): ?>
<a href="/academy-contracts/<?= (int) $contract['id'] ?>" style="display:inline-flex;align-items:center;gap:4px;color:#0D7377;text-decoration:none;">
<i data-lucide="file-text" style="width:14px;height:14px;"></i>
<?= e($contract['contract_number'] ?? '') ?>
</a>
<?php endif; ?>
</div>
</div>
</div>
</div>
<!-- Direction Banner -->
<div class="card" style="margin-bottom:20px;background:<?= $dirColor ?>08;border:1px solid <?= $dirColor ?>30;">
<div style="padding:20px;text-align:center;">
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;">اتجاه التسوية</div>
<div style="font-size:20px;font-weight:700;color:<?= $dirColor ?>;"><?= e($dirLabel) ?></div>
<div style="font-size:28px;font-weight:800;color:<?= $dirColor ?>;margin-top:8px;direction:ltr;">
<?= number_format((float) $settlement->net_amount, 2) ?> ج.م
</div>
</div>
</div>
<!-- Financial Breakdown -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Revenue Summary -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="trending-up" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#059669;"></i> ملخص الإيرادات</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:13px;">
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">إجمالي الإيرادات</td>
<td style="padding:10px 0;font-weight:700;text-align:left;direction:ltr;font-size:15px;color:#1A1A2E;"><?= number_format((float) $settlement->total_revenue, 2) ?> ج.م</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">الحد الأدنى المضمون</td>
<td style="padding:10px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $settlement->minimum_guarantee, 2) ?> ج.م</td>
</tr>
<?php if ((float) $settlement->revenue_surplus > 0): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#059669;">فائض الإيرادات</td>
<td style="padding:10px 0;font-weight:600;text-align:left;direction:ltr;color:#059669;">+<?= number_format((float) $settlement->revenue_surplus, 2) ?> ج.م</td>
</tr>
<?php endif; ?>
<?php if ((float) $settlement->revenue_shortfall > 0): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#DC2626;">عجز الإيرادات</td>
<td style="padding:10px 0;font-weight:600;text-align:left;direction:ltr;color:#DC2626;">-<?= number_format((float) $settlement->revenue_shortfall, 2) ?> ج.م</td>
</tr>
<?php endif; ?>
</table>
</div>
</div>
<!-- Settlement Calculation -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="receipt" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#D97706;"></i> تفاصيل الحساب</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:13px;">
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">عمولة النادي</td>
<td style="padding:10px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $settlement->club_commission, 2) ?> ج.م</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#6B7280;">حصة الأكاديمية</td>
<td style="padding:10px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $settlement->academy_share, 2) ?> ج.م</td>
</tr>
<?php if ((float) $settlement->penalty_amount > 0): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 0;color:#DC2626;">غرامة</td>
<td style="padding:10px 0;font-weight:600;text-align:left;direction:ltr;color:#DC2626;"><?= number_format((float) $settlement->penalty_amount, 2) ?> ج.م</td>
</tr>
<?php endif; ?>
<tr style="background:#F9FAFB;">
<td style="padding:12px 0;font-weight:700;color:#1A1A2E;">صافي المبلغ</td>
<td style="padding:12px 0;font-weight:800;text-align:left;direction:ltr;font-size:16px;color:<?= $dirColor ?>;"><?= number_format((float) $settlement->net_amount, 2) ?> ج.م</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Approval Info -->
<?php if ($settlement->approved_by): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;display:flex;align-items:center;gap:8px;">
<i data-lucide="shield-check" style="width:16px;height:16px;color:#059669;"></i>
<span style="font-size:13px;color:#6B7280;">تم الاعتماد بواسطة الموظف #<?= (int) $settlement->approved_by ?> في <?= e($settlement->approved_at ?? '') ?></span>
</div>
</div>
<?php endif; ?>
<!-- Notes -->
<?php if (!empty($settlement->notes)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="message-square" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#6B7280;"></i> ملاحظات</h3>
</div>
<div style="padding:20px;font-size:14px;color:#4B5563;line-height:1.7;">
<?= e($settlement->notes) ?>
</div>
</div>
<?php endif; ?>
<!-- Dispute Info -->
<?php if (!empty($settlement->dispute_reason)): ?>
<div class="card" style="margin-bottom:20px;border:1px solid #FCA5A5;">
<div style="padding:15px 20px;border-bottom:1px solid #FCA5A5;background:#FEF2F2;">
<h3 style="margin:0;font-size:14px;color:#991B1B;"><i data-lucide="alert-triangle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> سبب النزاع</h3>
</div>
<div style="padding:20px;font-size:14px;color:#991B1B;line-height:1.7;">
<?= e($settlement->dispute_reason) ?>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\AcademyContracts\Models\AcademyContract;
use App\Modules\AcademyContracts\Models\AcademySettlement;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>عقد <?= e($contract->contract_number) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if (in_array($contract->status, ['draft', 'pending_approval'])): ?>
<a href="/academy-contracts/<?= (int) $contract->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<?php endif; ?>
<a href="/academy-contracts" 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
$statusColor = AcademyContract::getStatusColor($contract->status ?? '');
$statusLabel = AcademyContract::getStatusLabel($contract->status ?? '');
$typeLabel = AcademyContract::getContractTypeLabel($contract->contract_type ?? '');
?>
<!-- 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="file-signature" 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;">
<h2 style="margin:0;font-size:22px;font-weight:700;color:#1A1A2E;"><?= e($contract->contract_number) ?></h2>
<span style="display:inline-block;padding:4px 12px;border-radius:12px;font-size:12px;font-weight:600;background:<?= $statusColor ?>15;color:<?= $statusColor ?>;">
<?= e($statusLabel) ?>
</span>
</div>
<div style="display:flex;gap:15px;flex-wrap:wrap;font-size:13px;color:#6B7280;margin-top:8px;">
<?php if ($academy): ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="graduation-cap" style="width:14px;height:14px;"></i>
<?= e($academy->name_ar) ?>
</span>
<?php endif; ?>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="briefcase" style="width:14px;height:14px;"></i>
<?= e($typeLabel) ?>
</span>
<span style="display:inline-flex;align-items:center;gap:4px;">
<i data-lucide="calendar" style="width:14px;height:14px;"></i>
<?= e($contract->start_date) ?> - <?= e($contract->end_date) ?>
</span>
</div>
</div>
<div style="flex-shrink:0;display:flex;gap:8px;">
<?php if ($contract->status === 'draft' || $contract->status === 'pending_approval'): ?>
<form method="POST" action="/academy-contracts/<?= (int) $contract->id ?>/activate" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary" onclick="return confirm('هل أنت متأكد من تفعيل هذا العقد؟')">
<i data-lucide="check-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تفعيل
</button>
</form>
<?php endif; ?>
<?php if ($contract->status === 'active'): ?>
<button type="button" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="document.getElementById('terminateModal').style.display='flex'">
<i data-lucide="x-circle" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إنهاء العقد
</button>
<?php endif; ?>
</div>
</div>
</div>
<!-- Financial Details -->
<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;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="banknote" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#D97706;"></i> الشروط المالية</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:13px;">
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">الحد الأدنى المضمون</td>
<td style="padding:8px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $contract->minimum_revenue_guarantee, 2) ?> ج.م</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">نسبة عمولة النادي</td>
<td style="padding:8px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $contract->club_commission_pct, 2) ?>%</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">نسبة حصة الأكاديمية</td>
<td style="padding:8px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $contract->academy_share_pct, 2) ?>%</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">الإيجار الشهري</td>
<td style="padding:8px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $contract->fixed_monthly_rent, 2) ?> ج.م</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">مبلغ التأمين</td>
<td style="padding:8px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $contract->deposit_amount, 2) ?> ج.م</td>
</tr>
<tr>
<td style="padding:8px 0;color:#6B7280;">حالة التأمين</td>
<td style="padding:8px 0;font-weight:600;"><?= e($contract->deposit_status ?? '-') ?></td>
</tr>
</table>
</div>
</div>
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="settings" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#7C3AED;"></i> إعدادات التسوية</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:13px;">
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">يوم التسوية</td>
<td style="padding:8px 0;font-weight:600;"><?= (int) $contract->settlement_day ?> من كل شهر</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">فترة السماح</td>
<td style="padding:8px 0;font-weight:600;"><?= (int) $contract->grace_period_days ?> يوم</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">نسبة الغرامة</td>
<td style="padding:8px 0;font-weight:600;text-align:left;direction:ltr;"><?= number_format((float) $contract->penalty_rate_pct, 2) ?>%</td>
</tr>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 0;color:#6B7280;">تجديد تلقائي</td>
<td style="padding:8px 0;font-weight:600;"><?= (int) $contract->auto_renew ? 'نعم' : 'لا' ?></td>
</tr>
<tr>
<td style="padding:8px 0;color:#6B7280;">مهلة التجديد</td>
<td style="padding:8px 0;font-weight:600;"><?= (int) $contract->renewal_notice_days ?> يوم</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Notes -->
<?php if (!empty($contract->notes)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="message-square" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#6B7280;"></i> ملاحظات</h3>
</div>
<div style="padding:20px;font-size:14px;color:#4B5563;line-height:1.7;">
<?= e($contract->notes) ?>
</div>
</div>
<?php endif; ?>
<!-- Settlement History -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;font-size:14px;color:#374151;"><i data-lucide="calculator" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;color:#059669;"></i> سجل التسويات</h3>
<a href="/academy-settlements?academy_id=<?= (int) $contract->academy_id ?>" class="btn btn-sm btn-outline" style="font-size:12px;">عرض الكل</a>
</div>
<?php if (!empty($settlements)): ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">الشهر</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">الإيرادات</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#6B7280;">صافي المبلغ</th>
<th style="padding:10px 15px;text-align:center;font-weight:600;color:#6B7280;">الاتجاه</th>
<th style="padding:10px 15px;text-align:center;font-weight:600;color:#6B7280;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($settlements as $s):
$sStatusColor = AcademySettlement::getStatusColor($s['status'] ?? '');
$sStatusLabel = AcademySettlement::getStatusLabel($s['status'] ?? '');
$dirColor = AcademySettlement::getDirectionColor($s['direction'] ?? '');
$dirLabel = AcademySettlement::getDirectionLabel($s['direction'] ?? '');
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 15px;">
<a href="/academy-settlements/<?= (int) $s['id'] ?>" style="color:#0D7377;text-decoration:none;font-weight:500;"><?= e($s['period_month']) ?></a>
</td>
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= number_format((float) ($s['total_revenue'] ?? 0), 2) ?> ج.م</td>
<td style="padding:10px 15px;direction:ltr;text-align:right;font-weight:600;"><?= number_format((float) ($s['net_amount'] ?? 0), 2) ?> ج.م</td>
<td style="padding:10px 15px;text-align:center;">
<span style="font-size:11px;font-weight:600;color:<?= $dirColor ?>;"><?= e($dirLabel) ?></span>
</td>
<td style="padding:10px 15px;text-align:center;">
<span style="display:inline-block;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;background:<?= $sStatusColor ?>15;color:<?= $sStatusColor ?>;">
<?= e($sStatusLabel) ?>
</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<div style="padding:40px 20px;text-align:center;color:#9CA3AF;font-size:14px;">
لا توجد تسويات لهذا العقد بعد
</div>
<?php endif; ?>
</div>
<!-- Terminate Modal -->
<?php if ($contract->status === 'active'): ?>
<div id="terminateModal" style="display:none;position:fixed;inset:0;z-index:9999;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);">
<div class="card" style="width:100%;max-width:500px;margin:20px;">
<div style="padding:20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#991B1B;font-size:16px;"><i data-lucide="alert-triangle" style="width:18px;height:18px;vertical-align:middle;margin-left:4px;"></i> إنهاء العقد</h3>
</div>
<form method="POST" action="/academy-contracts/<?= (int) $contract->id ?>/terminate">
<?= csrf_field() ?>
<div style="padding:20px;">
<p style="color:#6B7280;font-size:14px;margin:0 0 15px;">هل أنت متأكد من إنهاء هذا العقد؟ هذا الإجراء لا يمكن التراجع عنه.</p>
<div class="form-group">
<label class="form-label">سبب الإنهاء <span style="color:#DC2626;">*</span></label>
<textarea name="termination_reason" class="form-input" rows="3" required placeholder="اذكر سبب إنهاء العقد..."></textarea>
</div>
</div>
<div style="padding:15px 20px;border-top:1px solid #E5E7EB;display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('terminateModal').style.display='none'">إلغاء</button>
<button type="submit" class="btn btn-primary" style="background:#991B1B;border-color:#991B1B;">تأكيد الإنهاء</button>
</div>
</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;
use App\Core\Registries\MenuRegistry;
// ────────────────────────────────────────────────────────────
// Academy Contracts — Permissions & Sidebar Menu
// ────────────────────────────────────────────────────────────
PermissionRegistry::register('academy_contracts', [
'academy_contract.view' => ['ar' => 'عرض عقود الأكاديميات', 'en' => 'View Academy Contracts'],
'academy_contract.manage' => ['ar' => 'إدارة عقود الأكاديميات', 'en' => 'Manage Academy Contracts'],
'academy_contract.settlement' => ['ar' => 'تسويات عقود الأكاديميات', 'en' => 'Academy Contract Settlements'],
]);
MenuRegistry::register('academy_contracts', [
'label_ar' => 'عقود الأكاديميات',
'label_en' => 'Academy Contracts',
'icon' => 'file-signature',
'route' => '/academy-contracts',
'permission' => 'academy_contract.view',
'order' => 398,
'parent' => 'sports_activities',
]);
<?php
declare(strict_types=1);
namespace App\Modules\Alerts\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Alerts\Models\AlertRule;
use App\Modules\Alerts\Models\AlertLog;
class AlertController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('alert.view');
$rules = AlertRule::query()
->orderBy('category', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
$categories = AlertRule::getCategories();
$triggerTypes = AlertRule::getTriggerTypes();
return $this->view('Alerts.Views.index', [
'rules' => $rules,
'categories' => $categories,
'triggerTypes' => $triggerTypes,
]);
}
public function log(Request $request): Response
{
$this->authorize('alert.view');
$db = App::getInstance()->db();
$logs = $db->select(
"SELECT al.*, ar.name_ar AS rule_name, ar.category
FROM alert_log al
LEFT JOIN alert_rules ar ON ar.id = al.alert_rule_id
ORDER BY al.triggered_at DESC
LIMIT 100"
);
return $this->view('Alerts.Views.log', [
'logs' => $logs,
'categories' => AlertRule::getCategories(),
]);
}
public function toggle(Request $request, int $id): Response
{
$this->authorize('alert.manage');
$rule = AlertRule::find($id);
if (!$rule) {
return $this->redirect('/alerts')->withError('القاعدة غير موجودة');
}
$db = App::getInstance()->db();
$newStatus = ((int) $rule->is_active === 1) ? 0 : 1;
$db->update('alert_rules', ['is_active' => $newStatus], 'id = ?', [$id]);
$msg = $newStatus === 1 ? 'تم تفعيل التنبيه' : 'تم إيقاف التنبيه';
return $this->redirect('/alerts')->withSuccess($msg);
}
public function acknowledge(Request $request, int $id): Response
{
$this->authorize('alert.view');
$db = App::getInstance()->db();
$log = $db->selectOne("SELECT * FROM alert_log WHERE id = ?", [$id]);
if (!$log) {
return $this->redirect('/alerts/log')->withError('السجل غير موجود');
}
$session = App::getInstance()->session();
$db->update('alert_log', [
'status' => 'acknowledged',
'acknowledged_by' => $session->get('employee_id'),
'acknowledged_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$id]);
return $this->redirect('/alerts/log')->withSuccess('تم تأكيد الإطلاع على التنبيه');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Alerts\Models;
use App\Core\Model;
class AlertLog extends Model
{
protected static string $table = 'alert_log';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static array $fillable = [
'alert_rule_id',
'triggered_at',
'context_json',
'recipients_json',
'status',
'acknowledged_by',
'acknowledged_at',
];
}
<?php
declare(strict_types=1);
namespace App\Modules\Alerts\Models;
use App\Core\Model;
class AlertRule extends Model
{
protected static string $table = 'alert_rules';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static array $fillable = [
'code',
'name_ar',
'category',
'trigger_type',
'trigger_config_json',
'notification_channels',
'recipient_roles_json',
'is_active',
'last_triggered_at',
];
public static function getCategories(): array
{
return [
'medical' => 'طبي',
'contracts' => 'عقود',
'financial' => 'مالي',
'attendance' => 'حضور',
'capacity' => 'سعة',
'subscriptions' => 'اشتراكات',
'safety' => 'سلامة',
];
}
public static function getTriggerTypes(): array
{
return [
'threshold' => 'عتبة / حد',
'expiry' => 'انتهاء صلاحية',
'schedule' => 'مجدول',
];
}
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('category', 'ASC')
->orderBy('name_ar', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
return [
['GET', '/alerts', 'Alerts\Controllers\AlertController@index', ['auth'], 'alert.view'],
['GET', '/alerts/log', 'Alerts\Controllers\AlertController@log', ['auth'], 'alert.view'],
['POST', '/alerts/{id:\d+}/toggle', 'Alerts\Controllers\AlertController@toggle', ['auth', 'csrf'], 'alert.manage'],
['POST', '/alerts/log/{id:\d+}/acknowledge', 'Alerts\Controllers\AlertController@acknowledge', ['auth', 'csrf'], 'alert.view'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Alerts\Services;
use App\Core\App;
use App\Modules\Alerts\Models\AlertRule;
final class AlertProcessorService
{
/**
* Process all active alert rules and return triggered alerts.
*/
public static function processAll(): array
{
$rules = AlertRule::allActive();
$triggered = [];
foreach ($rules as $rule) {
$results = [];
switch ($rule['category']) {
case 'medical':
$results = self::checkMedicalCertExpiry($rule);
break;
case 'contracts':
$results = self::checkContractExpiry($rule);
break;
case 'attendance':
$results = self::checkAttendanceDropping($rule);
break;
case 'capacity':
$results = self::checkGroupNearCapacity($rule);
break;
}
if (!empty($results)) {
foreach ($results as $context) {
self::logAlert((int) $rule['id'], $context);
}
$triggered[] = [
'rule' => $rule,
'count' => count($results),
];
// Update last_triggered_at
$db = App::getInstance()->db();
$db->update('alert_rules', [
'last_triggered_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $rule['id']]);
}
}
return $triggered;
}
/**
* Check for player medical records expiring within configured days.
*/
public static function checkMedicalCertExpiry(array $rule): array
{
$db = App::getInstance()->db();
$config = json_decode($rule['trigger_config_json'] ?? '{}', true);
$days = (int) ($config['days_before'] ?? 30);
$rows = $db->select(
"SELECT pmr.id, pmr.player_id, pmr.expiry_date, p.full_name_ar AS player_name
FROM player_medical_records pmr
LEFT JOIN players p ON p.id = pmr.player_id
WHERE pmr.expiry_date IS NOT NULL
AND pmr.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND pmr.is_current = 1",
[$days]
);
$results = [];
foreach ($rows as $row) {
$results[] = [
'type' => 'medical_expiry',
'player_id' => $row['player_id'],
'player_name' => $row['player_name'],
'expiry_date' => $row['expiry_date'],
'message' => "الشهادة الطبية للاعب {$row['player_name']} تنتهي في {$row['expiry_date']}",
];
}
return $results;
}
/**
* Check for academy contracts expiring soon.
*/
public static function checkContractExpiry(array $rule): array
{
$db = App::getInstance()->db();
$config = json_decode($rule['trigger_config_json'] ?? '{}', true);
$days = (int) ($config['days_before'] ?? 30);
$rows = $db->select(
"SELECT ac.id, ac.academy_id, ac.end_date, a.name_ar AS academy_name
FROM academy_contracts ac
LEFT JOIN academies a ON a.id = ac.academy_id
WHERE ac.end_date IS NOT NULL
AND ac.end_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ? DAY)
AND ac.status = 'active'",
[$days]
);
$results = [];
foreach ($rows as $row) {
$results[] = [
'type' => 'contract_expiry',
'academy_id' => $row['academy_id'],
'academy_name' => $row['academy_name'],
'end_date' => $row['end_date'],
'message' => "عقد أكاديمية {$row['academy_name']} ينتهي في {$row['end_date']}",
];
}
return $results;
}
/**
* Check for players with 3+ consecutive absences.
*/
public static function checkAttendanceDropping(array $rule): array
{
$db = App::getInstance()->db();
$config = json_decode($rule['trigger_config_json'] ?? '{}', true);
$threshold = (int) ($config['consecutive_absences'] ?? 3);
$rows = $db->select(
"SELECT sa.player_id, p.full_name_ar AS player_name, COUNT(*) AS absence_count
FROM session_attendance sa
LEFT JOIN players p ON p.id = sa.player_id
WHERE sa.status = 'absent'
AND sa.session_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY sa.player_id
HAVING absence_count >= ?
ORDER BY absence_count DESC",
[$threshold]
);
$results = [];
foreach ($rows as $row) {
$results[] = [
'type' => 'attendance_dropping',
'player_id' => $row['player_id'],
'player_name' => $row['player_name'],
'absence_count' => $row['absence_count'],
'message' => "اللاعب {$row['player_name']} لديه {$row['absence_count']} غيابات متتالية",
];
}
return $results;
}
/**
* Check for training groups at 90%+ capacity.
*/
public static function checkGroupNearCapacity(array $rule): array
{
$db = App::getInstance()->db();
$config = json_decode($rule['trigger_config_json'] ?? '{}', true);
$threshold = (int) ($config['capacity_percent'] ?? 90);
$rows = $db->select(
"SELECT tg.id, tg.name_ar, tg.max_capacity,
COUNT(tgp.id) AS current_count
FROM training_groups tg
LEFT JOIN training_group_players tgp ON tgp.group_id = tg.id AND tgp.is_active = 1
WHERE tg.is_active = 1 AND tg.max_capacity > 0
GROUP BY tg.id
HAVING (current_count / tg.max_capacity * 100) >= ?",
[$threshold]
);
$results = [];
foreach ($rows as $row) {
$pct = $row['max_capacity'] > 0
? round(($row['current_count'] / $row['max_capacity']) * 100)
: 0;
$results[] = [
'type' => 'capacity_near_full',
'group_id' => $row['id'],
'group_name' => $row['name_ar'],
'current_count' => $row['current_count'],
'max_capacity' => $row['max_capacity'],
'percent' => $pct,
'message' => "المجموعة {$row['name_ar']} وصلت {$pct}% من السعة ({$row['current_count']}/{$row['max_capacity']})",
];
}
return $results;
}
/**
* Log a triggered alert to the alert_log table.
*/
public static function logAlert(int $ruleId, array $context): void
{
$db = App::getInstance()->db();
$db->insert('alert_log', [
'alert_rule_id' => $ruleId,
'triggered_at' => date('Y-m-d H:i:s'),
'context_json' => json_encode($context, JSON_UNESCAPED_UNICODE),
'recipients_json' => null,
'status' => 'sent',
]);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>التنبيهات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="margin:0;color:#0D7377;">قواعد التنبيهات</h2>
<a href="/alerts/log" class="btn btn-outline" style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="list" style="width:16px;height:16px;"></i>
سجل التنبيهات
</a>
</div>
<?php if (empty($rules)): ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<i data-lucide="bell-off" style="width:48px;height:48px;margin-bottom:12px;opacity:0.5;"></i>
<p>لا توجد قواعد تنبيهات مسجلة بعد.</p>
</div>
<?php else: ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>اسم التنبيه</th>
<th>التصنيف</th>
<th>نوع المحفز</th>
<th>قنوات الإشعار</th>
<th>الحالة</th>
<th>آخر تشغيل</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($rules as $rule): ?>
<tr>
<td style="font-weight:600;"><?= e($rule['name_ar']) ?></td>
<td>
<span style="background:#E0F2FE;color:#0369A1;padding:3px 10px;border-radius:12px;font-size:12px;">
<?= e($categories[$rule['category']] ?? $rule['category']) ?>
</span>
</td>
<td>
<span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;">
<?= e($triggerTypes[$rule['trigger_type']] ?? $rule['trigger_type']) ?>
</span>
</td>
<td style="font-size:13px;"><?= e($rule['notification_channels']) ?></td>
<td>
<?php if ((int)$rule['is_active'] === 1): ?>
<span style="background:#D1FAE5;color:#065F46;padding:3px 10px;border-radius:12px;font-size:12px;">مفعّل</span>
<?php else: ?>
<span style="background:#FEE2E2;color:#991B1B;padding:3px 10px;border-radius:12px;font-size:12px;">متوقف</span>
<?php endif; ?>
</td>
<td style="font-size:13px;color:#6B7280;">
<?= $rule['last_triggered_at'] ? e($rule['last_triggered_at']) : '—' ?>
</td>
<td>
<form method="POST" action="/alerts/<?= (int)$rule['id'] ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm <?= (int)$rule['is_active'] === 1 ? 'btn-outline-danger' : 'btn-outline-success' ?>" style="font-size:12px;">
<?= (int)$rule['is_active'] === 1 ? 'إيقاف' : 'تفعيل' ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>سجل التنبيهات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="margin:0;color:#0D7377;">سجل التنبيهات</h2>
<a href="/alerts" class="btn btn-outline" style="display:inline-flex;align-items:center;gap:6px;">
<i data-lucide="settings" style="width:16px;height:16px;"></i>
قواعد التنبيهات
</a>
</div>
<?php if (empty($logs)): ?>
<div class="card" style="padding:40px;text-align:center;color:#6B7280;">
<i data-lucide="inbox" style="width:48px;height:48px;margin-bottom:12px;opacity:0.5;"></i>
<p>لا توجد تنبيهات مسجلة بعد.</p>
</div>
<?php else: ?>
<div class="card">
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>التنبيه</th>
<th>التصنيف</th>
<th>تاريخ التشغيل</th>
<th>التفاصيل</th>
<th>الحالة</th>
<th>إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<?php
$context = json_decode($log['context_json'] ?? '{}', true);
$statusColors = [
'sent' => 'background:#FEF3C7;color:#92400E;',
'failed' => 'background:#FEE2E2;color:#991B1B;',
'acknowledged' => 'background:#D1FAE5;color:#065F46;',
];
$statusLabels = [
'sent' => 'مرسل',
'failed' => 'فشل',
'acknowledged' => 'تم الإطلاع',
];
?>
<tr>
<td style="font-weight:600;"><?= e($log['rule_name'] ?? '—') ?></td>
<td>
<span style="background:#E0F2FE;color:#0369A1;padding:3px 10px;border-radius:12px;font-size:12px;">
<?= e($categories[$log['category'] ?? ''] ?? ($log['category'] ?? '—')) ?>
</span>
</td>
<td style="font-size:13px;"><?= e($log['triggered_at']) ?></td>
<td style="font-size:13px;max-width:300px;overflow:hidden;text-overflow:ellipsis;">
<?= e($context['message'] ?? '—') ?>
</td>
<td>
<span style="<?= $statusColors[$log['status']] ?? '' ?>padding:3px 10px;border-radius:12px;font-size:12px;">
<?= e($statusLabels[$log['status']] ?? $log['status']) ?>
</span>
</td>
<td>
<?php if ($log['status'] === 'sent'): ?>
<form method="POST" action="/alerts/log/<?= (int)$log['id'] ?>/acknowledge" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline" style="font-size:12px;">
تأكيد الإطلاع
</button>
</form>
<?php elseif ($log['status'] === 'acknowledged'): ?>
<span style="color:#6B7280;font-size:12px;">✓ تم</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('alerts', [
'alert.view' => ['ar' => 'عرض التنبيهات', 'en' => 'View Alerts'],
'alert.manage' => ['ar' => 'إدارة التنبيهات', 'en' => 'Manage Alerts'],
]);
MenuRegistry::register('alerts', [
'label_ar' => 'التنبيهات',
'icon' => 'bell',
'route' => '/alerts',
'permission' => 'alert.view',
'order' => 950,
'parent' => 'settings',
]);
<?php
declare(strict_types=1);
namespace App\Modules\Coaches\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Coaches\Models\Coach;
use App\Modules\Coaches\Services\CoachService;
use App\Modules\Coaches\Services\CoachSchedulingService;
class CoachController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'employment_type' => trim((string) $request->get('employment_type', '')),
'payment_model' => trim((string) $request->get('payment_model', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = Coach::search($filters, 25, $page);
return $this->view('Coaches.Views.index', [
'coaches' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'employmentTypes' => Coach::getEmploymentTypes(),
'paymentModels' => Coach::getPaymentModels(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$disciplines = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('Coaches.Views.create', [
'disciplines' => $disciplines,
'employmentTypes' => Coach::getEmploymentTypes(),
'paymentModels' => Coach::getPaymentModels(),
]);
}
public function store(Request $request): Response
{
$data = [
'full_name_ar' => trim((string) $request->post('full_name_ar', '')),
'full_name_en' => trim((string) $request->post('full_name_en', '')) ?: null,
'code' => trim((string) $request->post('code', '')),
'national_id' => trim((string) $request->post('national_id', '')) ?: null,
'phone' => trim((string) $request->post('phone', '')) ?: null,
'email' => trim((string) $request->post('email', '')) ?: null,
'date_of_birth' => trim((string) $request->post('date_of_birth', '')) ?: null,
'gender' => trim((string) $request->post('gender', '')) ?: null,
'bio_ar' => trim((string) $request->post('bio_ar', '')) ?: null,
'employment_type' => trim((string) $request->post('employment_type', 'contract')),
'max_players_default'=> (int) $request->post('max_players_default', 20),
'hourly_rate' => $request->post('hourly_rate', '') !== '' ? (float) $request->post('hourly_rate') : null,
'session_rate' => $request->post('session_rate', '') !== '' ? (float) $request->post('session_rate') : null,
'monthly_rate' => $request->post('monthly_rate', '') !== '' ? (float) $request->post('monthly_rate') : null,
'payment_model' => trim((string) $request->post('payment_model', 'per_session')),
'is_active' => 1,
];
$errors = [];
if ($data['full_name_ar'] === '' || mb_strlen($data['full_name_ar']) < 3) {
$errors[] = 'اسم المدرب بالعربي مطلوب (3 أحرف على الأقل)';
}
if (!array_key_exists($data['employment_type'], Coach::getEmploymentTypes())) {
$errors[] = 'نوع التوظيف غير صالح';
}
if (!array_key_exists($data['payment_model'], Coach::getPaymentModels())) {
$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('/coaches/create');
}
$photoPath = $this->handlePhotoUpload($request);
if ($photoPath) {
$data['photo_path'] = $photoPath;
}
$certs = $request->post('certifications', []);
if (!empty($certs) && is_array($certs)) {
$data['certifications_json'] = json_encode(array_filter($certs), JSON_UNESCAPED_UNICODE);
}
$disciplineIds = $request->post('discipline_ids', []);
if (!is_array($disciplineIds)) {
$disciplineIds = [];
}
$coach = CoachService::createCoach($data, $disciplineIds);
return $this->redirect('/coaches/' . $coach->id)->withSuccess('تم إضافة المدرب بنجاح');
}
public function show(Request $request, string $id): Response
{
$coach = Coach::find((int) $id);
if (!$coach) {
return $this->redirect('/coaches')->withError('المدرب غير موجود');
}
$db = App::getInstance()->db();
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
$levels = $db->select(
"SELECT id, academy_id, name_ar FROM academy_levels WHERE is_active = 1 ORDER BY level_order ASC"
);
$utilization = CoachSchedulingService::getUtilization((int) $id, date('Y-m-01'), date('Y-m-t'));
return $this->view('Coaches.Views.show', [
'coach' => $coach,
'disciplines' => $coach->getDisciplines(),
'assignments' => $coach->getAcademyAssignments(),
'availability' => $coach->getAvailability(),
'utilization' => $utilization,
'academies' => $academies,
'levels' => $levels,
'roles' => Coach::getRoles(),
'dayNames' => self::getDayNames(),
]);
}
public function edit(Request $request, string $id): Response
{
$coach = Coach::find((int) $id);
if (!$coach) {
return $this->redirect('/coaches')->withError('المدرب غير موجود');
}
$db = App::getInstance()->db();
$disciplines = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
$coachDisciplineIds = array_column($coach->getDisciplines(), 'discipline_id');
return $this->view('Coaches.Views.edit', [
'coach' => $coach,
'disciplines' => $disciplines,
'coachDisciplineIds' => $coachDisciplineIds,
'employmentTypes' => Coach::getEmploymentTypes(),
'paymentModels' => Coach::getPaymentModels(),
]);
}
public function update(Request $request, string $id): Response
{
$coach = Coach::find((int) $id);
if (!$coach) {
return $this->redirect('/coaches')->withError('المدرب غير موجود');
}
$data = [
'full_name_ar' => trim((string) $request->post('full_name_ar', '')),
'full_name_en' => trim((string) $request->post('full_name_en', '')) ?: null,
'code' => trim((string) $request->post('code', '')),
'national_id' => trim((string) $request->post('national_id', '')) ?: null,
'phone' => trim((string) $request->post('phone', '')) ?: null,
'email' => trim((string) $request->post('email', '')) ?: null,
'date_of_birth' => trim((string) $request->post('date_of_birth', '')) ?: null,
'gender' => trim((string) $request->post('gender', '')) ?: null,
'bio_ar' => trim((string) $request->post('bio_ar', '')) ?: null,
'employment_type' => trim((string) $request->post('employment_type', 'contract')),
'max_players_default'=> (int) $request->post('max_players_default', 20),
'hourly_rate' => $request->post('hourly_rate', '') !== '' ? (float) $request->post('hourly_rate') : null,
'session_rate' => $request->post('session_rate', '') !== '' ? (float) $request->post('session_rate') : null,
'monthly_rate' => $request->post('monthly_rate', '') !== '' ? (float) $request->post('monthly_rate') : null,
'payment_model' => trim((string) $request->post('payment_model', 'per_session')),
];
if ($data['code'] === '') {
$base = $data['full_name_en'] ?? $data['full_name_ar'] ?? 'COACH';
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '_', $base));
$data['code'] = 'C_' . substr($code, 0, 20) . '_' . time() % 10000;
}
$data['code'] = strtoupper($data['code']);
$errors = [];
if ($data['full_name_ar'] === '' || mb_strlen($data['full_name_ar']) < 3) {
$errors[] = 'اسم المدرب بالعربي مطلوب (3 أحرف على الأقل)';
}
$existing = Coach::query()
->where('code', '=', $data['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('/coaches/' . $id . '/edit');
}
$photoPath = $this->handlePhotoUpload($request);
if ($photoPath) {
$data['photo_path'] = $photoPath;
}
$certs = $request->post('certifications', []);
if (is_array($certs)) {
$data['certifications_json'] = json_encode(array_filter($certs), JSON_UNESCAPED_UNICODE);
}
$coach->update($data);
$disciplineIds = $request->post('discipline_ids', []);
if (is_array($disciplineIds)) {
CoachService::syncDisciplines((int) $id, $disciplineIds);
}
return $this->redirect('/coaches/' . $id)->withSuccess('تم تحديث بيانات المدرب بنجاح');
}
public function toggle(Request $request, string $id): Response
{
$coach = Coach::find((int) $id);
if (!$coach) {
return $this->redirect('/coaches')->withError('المدرب غير موجود');
}
$newStatus = $coach->is_active ? 0 : 1;
$coach->update(['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل المدرب' : 'تم إيقاف المدرب';
return $this->redirect('/coaches/' . $id)->withSuccess($message);
}
public function assign(Request $request, string $id): Response
{
$coach = Coach::find((int) $id);
if (!$coach) {
return $this->redirect('/coaches')->withError('المدرب غير موجود');
}
$academyId = (int) $request->post('academy_id', 0);
$levelId = $request->post('level_id', '') !== '' ? (int) $request->post('level_id') : null;
$role = trim((string) $request->post('role', 'coach'));
$maxPlayers = $request->post('max_players', '') !== '' ? (int) $request->post('max_players') : null;
$from = trim((string) $request->post('assigned_from', date('Y-m-d')));
if ($academyId <= 0) {
return $this->redirect('/coaches/' . $id)->withError('يجب اختيار أكاديمية');
}
CoachService::assignToAcademy((int) $id, $academyId, $levelId, $role, $maxPlayers, $from);
return $this->redirect('/coaches/' . $id)->withSuccess('تم تعيين المدرب في الأكاديمية');
}
public function unassign(Request $request, string $id): Response
{
$assignmentId = (int) $request->post('assignment_id', 0);
if ($assignmentId <= 0) {
return $this->redirect('/coaches/' . $id)->withError('بيانات غير صالحة');
}
CoachService::unassignFromAcademy($assignmentId);
return $this->redirect('/coaches/' . $id)->withSuccess('تم إلغاء تعيين المدرب');
}
public function saveAvailability(Request $request, string $id): Response
{
$coach = Coach::find((int) $id);
if (!$coach) {
return $this->redirect('/coaches')->withError('المدرب غير موجود');
}
$slots = [];
$days = $request->post('avail_day', []);
$starts = $request->post('avail_start', []);
$ends = $request->post('avail_end', []);
if (is_array($days)) {
for ($i = 0; $i < count($days); $i++) {
if (isset($starts[$i], $ends[$i]) && $starts[$i] !== '' && $ends[$i] !== '') {
$slots[] = [
'day_of_week' => (int) $days[$i],
'start_time' => $starts[$i],
'end_time' => $ends[$i],
];
}
}
}
CoachService::setAvailability((int) $id, $slots);
return $this->redirect('/coaches/' . $id)->withSuccess('تم حفظ مواعيد المدرب');
}
private function handlePhotoUpload(Request $request): ?string
{
if (!isset($_FILES['photo']) || $_FILES['photo']['error'] !== UPLOAD_ERR_OK) {
return null;
}
$file = $_FILES['photo'];
$maxSize = 5 * 1024 * 1024;
if ($file['size'] > $maxSize) {
return null;
}
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedTypes, true)) {
return null;
}
$ext = match ($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
default => 'jpg',
};
$dir = 'uploads/coaches';
$fullDir = __DIR__ . '/../../../public/' . $dir;
if (!is_dir($fullDir)) {
mkdir($fullDir, 0755, true);
}
$filename = 'coach_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$dest = $fullDir . '/' . $filename;
if (move_uploaded_file($file['tmp_name'], $dest)) {
return '/' . $dir . '/' . $filename;
}
return null;
}
private static function getDayNames(): array
{
return [
0 => 'الأحد',
1 => 'الإثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
6 => 'السبت',
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Coaches\Models;
use App\Core\Model;
use App\Core\App;
class Coach extends Model
{
protected static string $table = 'coaches';
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',
'full_name_ar',
'full_name_en',
'national_id',
'phone',
'email',
'date_of_birth',
'gender',
'photo_path',
'bio_ar',
'certifications_json',
'employment_type',
'employee_id',
'max_players_default',
'hourly_rate',
'session_rate',
'monthly_rate',
'payment_model',
'is_active',
];
public static function getEmploymentTypes(): array
{
return [
'staff' => 'موظف (عقد دائم)',
'contract' => 'متعاقد',
'freelance' => 'حر (فريلانس)',
];
}
public static function getPaymentModels(): array
{
return [
'per_session' => 'بالحصة',
'per_player' => 'بعدد اللاعبين',
'monthly_fixed' => 'راتب شهري ثابت',
'hybrid' => 'مختلط (ثابت + حوافز)',
];
}
public static function getRoles(): array
{
return [
'head_coach' => 'مدرب أول',
'coach' => 'مدرب',
'assistant' => 'مدرب مساعد',
];
}
public static function getSessionTypes(): array
{
return [
'private' => 'خاص (فردي)',
'small_group' => 'مجموعة صغيرة (2-5)',
'group' => 'مجموعة (6-15)',
'team' => 'فريق (16+)',
];
}
public function getCertifications(): array
{
$raw = $this->certifications_json;
if (empty($raw)) {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
public function getDisciplines(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT cd.*, sd.name_ar AS discipline_name, sd.icon AS discipline_icon
FROM coach_disciplines cd
LEFT JOIN sport_disciplines sd ON sd.id = cd.discipline_id
WHERE cd.coach_id = ?
ORDER BY cd.is_primary DESC, sd.name_ar ASC",
[(int) $this->id]
);
}
public function getAcademyAssignments(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT caa.*, a.name_ar AS academy_name, al.name_ar AS level_name
FROM coach_academy_assignments caa
LEFT JOIN academies a ON a.id = caa.academy_id
LEFT JOIN academy_levels al ON al.id = caa.level_id
WHERE caa.coach_id = ? AND caa.is_active = 1
ORDER BY caa.assigned_from DESC",
[(int) $this->id]
);
}
public function getAvailability(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM coach_availability
WHERE coach_id = ? AND is_available = 1
AND (effective_from IS NULL OR effective_from <= CURDATE())
AND (effective_to IS NULL OR effective_to >= CURDATE())
ORDER BY day_of_week ASC, start_time ASC",
[(int) $this->id]
);
}
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('full_name_ar', 'ASC')
->get();
}
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(
'(`full_name_ar` LIKE ? OR `full_name_en` LIKE ? OR `code` LIKE ? OR `phone` LIKE ?)',
[$search, $search, $search, $search]
);
}
if (!empty($filters['employment_type'])) {
$query = $query->where('employment_type', '=', $filters['employment_type']);
}
if (!empty($filters['payment_model'])) {
$query = $query->where('payment_model', '=', $filters['payment_model']);
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$query = $query->where('is_active', '=', (int) $filters['is_active']);
}
$query = $query->orderBy('full_name_ar', 'ASC');
return $query->paginate($perPage, $page);
}
public static function getByDiscipline(int $disciplineId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT c.* FROM coaches c
INNER JOIN coach_disciplines cd ON cd.coach_id = c.id
WHERE cd.discipline_id = ? AND c.is_active = 1 AND c.is_archived = 0
ORDER BY c.full_name_ar ASC",
[$disciplineId]
);
}
}
<?php
declare(strict_types=1);
return [
['GET', '/coaches', 'Coaches\Controllers\CoachController@index', ['auth'], 'coach.view'],
['GET', '/coaches/create', 'Coaches\Controllers\CoachController@create', ['auth'], 'coach.manage'],
['POST', '/coaches', 'Coaches\Controllers\CoachController@store', ['auth', 'csrf'], 'coach.manage'],
['GET', '/coaches/{id:\d+}', 'Coaches\Controllers\CoachController@show', ['auth'], 'coach.view'],
['GET', '/coaches/{id:\d+}/edit', 'Coaches\Controllers\CoachController@edit', ['auth'], 'coach.manage'],
['POST', '/coaches/{id:\d+}', 'Coaches\Controllers\CoachController@update', ['auth', 'csrf'], 'coach.manage'],
['POST', '/coaches/{id:\d+}/toggle', 'Coaches\Controllers\CoachController@toggle', ['auth', 'csrf'], 'coach.manage'],
['POST', '/coaches/{id:\d+}/assign', 'Coaches\Controllers\CoachController@assign', ['auth', 'csrf'], 'coach.manage'],
['POST', '/coaches/{id:\d+}/unassign', 'Coaches\Controllers\CoachController@unassign', ['auth', 'csrf'], 'coach.manage'],
['POST', '/coaches/{id:\d+}/availability', 'Coaches\Controllers\CoachController@saveAvailability', ['auth', 'csrf'], 'coach.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Coaches\Services;
use App\Core\App;
use App\Core\EventBus;
final class CoachPaymentService
{
/**
* Calculate payment for a specific coach and period.
*
* @param int $coachId Coach ID
* @param string $period YYYY-MM format
* @return array Payment calculation details
*/
public static function calculatePayment(int $coachId, string $period): array
{
$db = App::getInstance()->db();
// Get coach details
$coach = $db->selectOne("SELECT * FROM coaches WHERE id = ? AND is_archived = 0", [$coachId]);
if (!$coach) {
return [];
}
$paymentModel = $coach['payment_model'] ?? 'per_session';
$sessionRate = (float) ($coach['session_rate'] ?? 0);
$monthlyRate = (float) ($coach['monthly_rate'] ?? 0);
$hourlyRate = (float) ($coach['hourly_rate'] ?? 0);
// Get sessions for the period
$periodStart = $period . '-01';
$periodEnd = date('Y-m-t', strtotime($periodStart));
$sessions = $db->select(
"SELECT ts.id, ts.session_date, ts.status,
(SELECT COUNT(*) FROM session_attendance sa WHERE sa.session_id = ts.id AND sa.status = 'present') AS players_attended
FROM training_sessions ts
WHERE ts.coach_id = ?
AND ts.session_date BETWEEN ? AND ?
AND ts.status = 'completed'
ORDER BY ts.session_date ASC",
[$coachId, $periodStart, $periodEnd]
);
$sessionsConducted = count($sessions);
$totalPlayersServed = 0;
foreach ($sessions as $s) {
$totalPlayersServed += (int) $s['players_attended'];
}
$baseAmount = 0.0;
$bonus = 0.0;
$deductions = 0.0;
$calculationDetails = [];
switch ($paymentModel) {
case 'per_session':
$baseAmount = $sessionsConducted * $sessionRate;
$calculationDetails = [
'model' => 'per_session',
'sessions' => $sessionsConducted,
'rate_per_session' => $sessionRate,
'formula' => "{$sessionsConducted} sessions x {$sessionRate} EGP",
];
break;
case 'per_player':
$perPlayerRate = $hourlyRate > 0 ? $hourlyRate : $sessionRate;
$baseAmount = $totalPlayersServed * $perPlayerRate;
$calculationDetails = [
'model' => 'per_player',
'total_players' => $totalPlayersServed,
'rate_per_player' => $perPlayerRate,
'formula' => "{$totalPlayersServed} players x {$perPlayerRate} EGP",
];
break;
case 'monthly_fixed':
$baseAmount = $monthlyRate;
$calculationDetails = [
'model' => 'monthly_fixed',
'monthly_rate' => $monthlyRate,
'formula' => "Fixed monthly: {$monthlyRate} EGP",
];
break;
case 'hybrid':
$baseAmount = $monthlyRate;
// Bonus for sessions over a threshold (e.g., 20 sessions/month)
$sessionThreshold = 20;
$extraSessions = max(0, $sessionsConducted - $sessionThreshold);
$bonus = $extraSessions * $sessionRate;
$calculationDetails = [
'model' => 'hybrid',
'base_monthly' => $monthlyRate,
'session_threshold' => $sessionThreshold,
'extra_sessions' => $extraSessions,
'bonus_rate' => $sessionRate,
'formula' => "Base {$monthlyRate} + ({$extraSessions} extra sessions x {$sessionRate})",
];
break;
}
$netAmount = $baseAmount + $bonus - $deductions;
return [
'sessions_conducted' => $sessionsConducted,
'total_players_served' => $totalPlayersServed,
'base_amount' => round($baseAmount, 2),
'bonus' => round($bonus, 2),
'deductions' => round($deductions, 2),
'net_amount' => round($netAmount, 2),
'calculation_json' => $calculationDetails,
];
}
/**
* Generate monthly payments for all active coaches.
*
* @param string $period YYYY-MM format
* @return int Number of payments generated
*/
public static function generateMonthlyPayments(string $period): int
{
$db = App::getInstance()->db();
$coaches = $db->select(
"SELECT id, payment_model FROM coaches WHERE is_active = 1 AND is_archived = 0"
);
$count = 0;
foreach ($coaches as $coach) {
$coachId = (int) $coach['id'];
// Skip if already generated for this period
$existing = $db->selectOne(
"SELECT id FROM coach_payments WHERE coach_id = ? AND payment_period = ?",
[$coachId, $period]
);
if ($existing) {
continue;
}
$calc = self::calculatePayment($coachId, $period);
if (empty($calc)) {
continue;
}
$db->insert('coach_payments', [
'coach_id' => $coachId,
'payment_period' => $period,
'payment_model' => $coach['payment_model'] ?? 'per_session',
'sessions_conducted' => $calc['sessions_conducted'],
'total_players_served' => $calc['total_players_served'],
'base_amount' => $calc['base_amount'],
'bonus' => $calc['bonus'],
'deductions' => $calc['deductions'],
'net_amount' => $calc['net_amount'],
'calculation_json' => json_encode($calc['calculation_json'], JSON_UNESCAPED_UNICODE),
'status' => 'draft',
]);
$count++;
}
return $count;
}
/**
* Approve a coach payment.
*/
public static function approvePayment(int $paymentId, int $approvedBy): void
{
$db = App::getInstance()->db();
$payment = $db->selectOne("SELECT * FROM coach_payments WHERE id = ?", [$paymentId]);
if (!$payment || $payment['status'] !== 'draft') {
return;
}
$db->update('coach_payments', [
'status' => 'approved',
'approved_by' => $approvedBy,
'approved_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$paymentId]);
EventBus::dispatch('coach.payment.approved', [
'payment_id' => $paymentId,
'coach_id' => (int) $payment['coach_id'],
'period' => $payment['payment_period'],
'net_amount' => (float) $payment['net_amount'],
'approved_by' => $approvedBy,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Coaches\Services;
use App\Core\App;
final class CoachSchedulingService
{
public static function isAvailable(int $coachId, int $dayOfWeek, string $startTime, string $endTime, ?string $date = null): bool
{
$db = App::getInstance()->db();
$availability = $db->selectOne(
"SELECT id FROM coach_availability
WHERE coach_id = ? AND day_of_week = ? AND is_available = 1
AND start_time <= ? AND end_time >= ?
AND (effective_from IS NULL OR effective_from <= COALESCE(?, CURDATE()))
AND (effective_to IS NULL OR effective_to >= COALESCE(?, CURDATE()))",
[$coachId, $dayOfWeek, $startTime, $endTime, $date, $date]
);
if (!$availability) {
return false;
}
$conflicts = self::getConflicts($coachId, $dayOfWeek, $startTime, $endTime);
return empty($conflicts);
}
public static function findAvailableCoaches(int $dayOfWeek, string $startTime, string $endTime, ?int $disciplineId = null): array
{
$db = App::getInstance()->db();
$sql = "SELECT DISTINCT c.id, c.full_name_ar, c.code, c.phone, c.max_players_default
FROM coaches c
INNER JOIN coach_availability ca ON ca.coach_id = c.id
WHERE c.is_active = 1 AND c.is_archived = 0
AND ca.day_of_week = ? AND ca.is_available = 1
AND ca.start_time <= ? AND ca.end_time >= ?
AND (ca.effective_from IS NULL OR ca.effective_from <= CURDATE())
AND (ca.effective_to IS NULL OR ca.effective_to >= CURDATE())";
$params = [$dayOfWeek, $startTime, $endTime];
if ($disciplineId !== null) {
$sql .= " AND EXISTS (
SELECT 1 FROM coach_disciplines cd WHERE cd.coach_id = c.id AND cd.discipline_id = ?
)";
$params[] = $disciplineId;
}
$sql .= " ORDER BY c.full_name_ar ASC";
$coaches = $db->select($sql, $params);
return array_filter($coaches, function ($coach) use ($dayOfWeek, $startTime, $endTime) {
$conflicts = self::getConflicts((int) $coach['id'], $dayOfWeek, $startTime, $endTime);
return empty($conflicts);
});
}
public static function getConflicts(int $coachId, int $dayOfWeek, string $startTime, string $endTime, ?int $excludeScheduleId = null): array
{
$db = App::getInstance()->db();
$sql = "SELECT asched.id, asched.start_time, asched.end_time, a.name_ar AS academy_name
FROM academy_schedules asched
LEFT JOIN academies a ON a.id = asched.academy_id
WHERE asched.coach_id = ? AND asched.day_of_week = ? AND asched.is_active = 1
AND asched.start_time < ? AND asched.end_time > ?";
$params = [$coachId, $dayOfWeek, $endTime, $startTime];
if ($excludeScheduleId !== null) {
$sql .= " AND asched.id != ?";
$params[] = $excludeScheduleId;
}
return $db->select($sql, $params);
}
public static function getUtilization(int $coachId, string $monthStart, string $monthEnd): array
{
$db = App::getInstance()->db();
$schedules = $db->selectOne(
"SELECT COUNT(*) AS total_schedules,
SUM(TIMESTAMPDIFF(MINUTE, start_time, end_time)) AS total_minutes
FROM academy_schedules
WHERE coach_id = ? AND is_active = 1",
[$coachId]
);
$assignments = $db->selectOne(
"SELECT COUNT(*) AS total_assignments
FROM coach_academy_assignments
WHERE coach_id = ? AND is_active = 1",
[$coachId]
);
return [
'total_schedules' => (int) ($schedules['total_schedules'] ?? 0),
'total_minutes' => (int) ($schedules['total_minutes'] ?? 0),
'total_hours' => round(((int) ($schedules['total_minutes'] ?? 0)) / 60, 1),
'total_assignments' => (int) ($assignments['total_assignments'] ?? 0),
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Coaches\Services;
use App\Core\App;
use App\Modules\Coaches\Models\Coach;
final class CoachService
{
public static function createCoach(array $data, array $disciplineIds = []): Coach
{
if (empty($data['code'])) {
$base = $data['full_name_en'] ?? $data['full_name_ar'] ?? 'COACH';
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '_', $base));
$data['code'] = 'C_' . substr($code, 0, 20) . '_' . time() % 10000;
}
$data['code'] = strtoupper($data['code']);
$existing = Coach::query()->where('code', '=', $data['code'])->first();
if ($existing) {
$data['code'] = $data['code'] . '_' . bin2hex(random_bytes(2));
}
$coach = Coach::create($data);
if (!empty($disciplineIds)) {
self::syncDisciplines((int) $coach->id, $disciplineIds);
}
return $coach;
}
public static function syncDisciplines(int $coachId, array $disciplineIds, ?int $primaryId = null): void
{
$db = App::getInstance()->db();
$db->delete('coach_disciplines', 'coach_id = ?', [$coachId]);
foreach ($disciplineIds as $discId) {
$db->insert('coach_disciplines', [
'coach_id' => $coachId,
'discipline_id' => (int) $discId,
'is_primary' => ($primaryId !== null && (int) $discId === $primaryId) ? 1 : 0,
'specialization_level' => 'general',
]);
}
}
public static function assignToAcademy(int $coachId, int $academyId, ?int $levelId, string $role, ?int $maxPlayers, string $from, ?string $to = null): void
{
$db = App::getInstance()->db();
$db->insert('coach_academy_assignments', [
'coach_id' => $coachId,
'academy_id' => $academyId,
'level_id' => $levelId,
'role' => $role,
'max_players' => $maxPlayers,
'assigned_from' => $from,
'assigned_to' => $to,
'is_active' => 1,
]);
}
public static function unassignFromAcademy(int $assignmentId): void
{
$db = App::getInstance()->db();
$db->update('coach_academy_assignments', [
'is_active' => 0,
'assigned_to' => date('Y-m-d'),
], 'id = ?', [$assignmentId]);
}
public static function setAvailability(int $coachId, array $slots): void
{
$db = App::getInstance()->db();
$db->delete('coach_availability', 'coach_id = ?', [$coachId]);
foreach ($slots as $slot) {
$db->insert('coach_availability', [
'coach_id' => $coachId,
'day_of_week' => (int) $slot['day_of_week'],
'start_time' => $slot['start_time'],
'end_time' => $slot['end_time'],
'is_available' => 1,
'effective_from' => $slot['effective_from'] ?? null,
'effective_to' => $slot['effective_to'] ?? null,
]);
}
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة مدرب جديد<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/coaches" 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="/coaches" enctype="multipart/form-data" id="coachForm">
<?= 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="user" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">البيانات الأساسية</h3>
</div>
<div style="padding:20px;">
<!-- Photo Upload -->
<div style="display:flex;align-items:center;gap:20px;margin-bottom:20px;">
<label style="cursor:pointer;">
<div id="photoPreview" style="width:80px;height:80px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;overflow:hidden;border:3px dashed #D1D5DB;">
<i data-lucide="camera" style="width:24px;height:24px;color:#9CA3AF;"></i>
</div>
<input type="file" name="photo" accept="image/jpeg,image/png,image/webp" style="display:none;" id="photoInput">
</label>
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">صورة المدرب</div>
<div style="font-size:11px;color:#9CA3AF;">JPG, PNG أو WebP — حد أقصى 5MB</div>
</div>
</div>
<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="full_name_ar" value="<?= e(old('full_name_ar') ?? '') ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" value="<?= e(old('full_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:#9CA3AF;font-size:11px;">(اختياري)</span></label>
<input type="text" name="code" value="<?= e(old('code') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;text-transform:uppercase;" placeholder="يُولّد تلقائياً">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?? '') ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;" id="nidInput">
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e(old('phone') ?? '') ?>" 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">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e(old('email') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" id="dobInput">
</div>
<div class="form-group">
<label class="form-label">النوع</label>
<select name="gender" class="form-select">
<option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= old('gender') === 'female' ? 'selected' : '' ?>>أنثى</option>
</select>
</div>
</div>
</div>
</div>
<!-- Employment & Payment -->
<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="briefcase" 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">نوع التوظيف <span style="color:#DC2626;">*</span></label>
<select name="employment_type" class="form-select" required>
<?php foreach ($employmentTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('employment_type') ?? 'contract') === $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="payment_model" class="form-select" required>
<?php foreach ($paymentModels as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('payment_model') ?? 'per_session') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للاعبين</label>
<input type="number" name="max_players_default" value="<?= e(old('max_players_default') ?? '20') ?>" class="form-input" min="1" max="100" 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">سعر الساعة (ج.م)</label>
<input type="number" name="hourly_rate" value="<?= e(old('hourly_rate') ?? '') ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سعر الحصة (ج.م)</label>
<input type="number" name="session_rate" value="<?= e(old('session_rate') ?? '') ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الراتب الشهري (ج.م)</label>
<input type="number" name="monthly_rate" value="<?= e(old('monthly_rate') ?? '') ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Disciplines -->
<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:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">التخصصات الرياضية</h3>
</div>
<div style="padding:20px;">
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<?php foreach ($disciplines as $disc): ?>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:1px solid #E5E7EB;border-radius:8px;cursor:pointer;transition:all 0.2s;">
<input type="checkbox" name="discipline_ids[]" value="<?= (int) $disc['id'] ?>" style="width:16px;height:16px;accent-color:#0D7377;">
<span style="font-size:13px;"><?= e($disc['name_ar']) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php if (empty($disciplines)): ?>
<p style="color:#9CA3AF;font-size:13px;margin:0;">لا توجد أنشطة رياضية. أضف الأنشطة أولاً من صفحة الأنشطة الرياضية.</p>
<?php endif; ?>
</div>
</div>
<!-- Bio -->
<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:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">نبذة وشهادات</h3>
</div>
<div style="padding:20px;">
<div class="form-group">
<label class="form-label">نبذة عن المدرب</label>
<textarea name="bio_ar" class="form-input" rows="3"><?= e(old('bio_ar') ?? '') ?></textarea>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">الشهادات والتراخيص</label>
<div id="certsContainer">
<div style="display:flex;gap:8px;margin-bottom:8px;">
<input type="text" name="certifications[]" class="form-input" placeholder="مثال: رخصة تدريب AFC-B" style="flex:1;">
<button type="button" onclick="addCertRow()" class="btn btn-outline" style="padding:8px 12px;"><i data-lucide="plus" style="width:14px;height:14px;"></i></button>
</div>
</div>
</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="/coaches" 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 photoInput = document.getElementById('photoInput');
if (photoInput) {
photoInput.addEventListener('change', function() {
var file = this.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById('photoPreview').innerHTML = '<img src="' + e.target.result + '" style="width:80px;height:80px;object-fit:cover;border-radius:50%;">';
};
reader.readAsDataURL(file);
});
}
var nidInput = document.getElementById('nidInput');
if (nidInput) {
nidInput.addEventListener('input', function() {
var v = this.value.replace(/\D/g, '');
if (v.length === 14) {
var century = v[0] === '2' ? '19' : '20';
var year = century + v.substring(1,3);
var month = v.substring(3,5);
var day = v.substring(5,7);
var dob = year + '-' + month + '-' + day;
var dobInput = document.getElementById('dobInput');
if (dobInput) dobInput.value = dob;
var genderDigit = parseInt(v[12]);
var genderSelect = document.querySelector('[name=gender]');
if (genderSelect) genderSelect.value = (genderDigit % 2 === 1) ? 'male' : 'female';
}
});
}
});
function addCertRow() {
var container = document.getElementById('certsContainer');
var div = document.createElement('div');
div.style.cssText = 'display:flex;gap:8px;margin-bottom:8px;';
div.innerHTML = '<input type="text" name="certifications[]" class="form-input" placeholder="شهادة إضافية..." style="flex:1;"><button type="button" onclick="this.parentElement.remove()" class="btn btn-outline" style="padding:8px 12px;color:#DC2626;"><i data-lucide="x" style="width:14px;height:14px;"></i></button>';
container.appendChild(div);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل: <?= e($coach->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/coaches/<?= (int) $coach->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض</a>
<a href="/coaches" 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 $certs = $coach->getCertifications(); ?>
<form method="POST" action="/coaches/<?= (int) $coach->id ?>" enctype="multipart/form-data" id="coachForm">
<?= 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="user" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">البيانات الأساسية</h3>
</div>
<div style="padding:20px;">
<!-- Photo Upload -->
<div style="display:flex;align-items:center;gap:20px;margin-bottom:20px;">
<label style="cursor:pointer;">
<div id="photoPreview" style="width:80px;height:80px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;overflow:hidden;border:3px dashed #D1D5DB;">
<?php if (!empty($coach->photo_path)): ?>
<img src="<?= e($coach->photo_path) ?>" style="width:80px;height:80px;object-fit:cover;border-radius:50%;">
<?php else: ?>
<i data-lucide="camera" style="width:24px;height:24px;color:#9CA3AF;"></i>
<?php endif; ?>
</div>
<input type="file" name="photo" accept="image/jpeg,image/png,image/webp" style="display:none;" id="photoInput">
</label>
<div>
<div style="font-size:13px;font-weight:600;color:#1A1A2E;">صورة المدرب</div>
<div style="font-size:11px;color:#9CA3AF;">JPG, PNG أو WebP — حد أقصى 5MB</div>
</div>
</div>
<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="full_name_ar" value="<?= e(old('full_name_ar') ?: $coach->full_name_ar) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" value="<?= e(old('full_name_en') ?: ($coach->full_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:#9CA3AF;font-size:11px;">(اختياري)</span></label>
<input type="text" name="code" value="<?= e(old('code') ?: $coach->code) ?>" class="form-input" style="direction:ltr;text-align:left;text-transform:uppercase;">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?: ($coach->national_id ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e(old('phone') ?: ($coach->phone ?? '')) ?>" 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">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e(old('email') ?: ($coach->email ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" value="<?= e(old('date_of_birth') ?: ($coach->date_of_birth ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">النوع</label>
<select name="gender" class="form-select">
<option value="">-- اختر --</option>
<option value="male" <?= (old('gender') ?: ($coach->gender ?? '')) === 'male' ? 'selected' : '' ?>>ذكر</option>
<option value="female" <?= (old('gender') ?: ($coach->gender ?? '')) === 'female' ? 'selected' : '' ?>>أنثى</option>
</select>
</div>
</div>
</div>
</div>
<!-- Employment & Payment -->
<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="briefcase" 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">نوع التوظيف <span style="color:#DC2626;">*</span></label>
<select name="employment_type" class="form-select" required>
<?php foreach ($employmentTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('employment_type') ?: $coach->employment_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="payment_model" class="form-select" required>
<?php foreach ($paymentModels as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('payment_model') ?: $coach->payment_model) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للاعبين</label>
<input type="number" name="max_players_default" value="<?= e(old('max_players_default') ?: (string)($coach->max_players_default ?? 20)) ?>" class="form-input" min="1" max="100" 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">سعر الساعة (ج.م)</label>
<input type="number" name="hourly_rate" value="<?= e(old('hourly_rate') ?: (string)($coach->hourly_rate ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">سعر الحصة (ج.م)</label>
<input type="number" name="session_rate" value="<?= e(old('session_rate') ?: (string)($coach->session_rate ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الراتب الشهري (ج.م)</label>
<input type="number" name="monthly_rate" value="<?= e(old('monthly_rate') ?: (string)($coach->monthly_rate ?? '')) ?>" class="form-input" min="0" step="0.01" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Disciplines -->
<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:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">التخصصات الرياضية</h3>
</div>
<div style="padding:20px;">
<div style="display:flex;flex-wrap:wrap;gap:10px;">
<?php foreach ($disciplines as $disc): ?>
<label style="display:flex;align-items:center;gap:6px;padding:8px 14px;border:1px solid #E5E7EB;border-radius:8px;cursor:pointer;transition:all 0.2s;">
<input type="checkbox" name="discipline_ids[]" value="<?= (int) $disc['id'] ?>" <?= in_array((int) $disc['id'], $coachDisciplineIds) ? 'checked' : '' ?> style="width:16px;height:16px;accent-color:#0D7377;">
<span style="font-size:13px;"><?= e($disc['name_ar']) ?></span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Bio & Certifications -->
<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:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">نبذة وشهادات</h3>
</div>
<div style="padding:20px;">
<div class="form-group">
<label class="form-label">نبذة عن المدرب</label>
<textarea name="bio_ar" class="form-input" rows="3"><?= e(old('bio_ar') ?: ($coach->bio_ar ?? '')) ?></textarea>
</div>
<div class="form-group" style="margin-top:15px;">
<label class="form-label">الشهادات والتراخيص</label>
<div id="certsContainer">
<?php if (!empty($certs)): ?>
<?php foreach ($certs as $i => $cert): ?>
<div style="display:flex;gap:8px;margin-bottom:8px;">
<input type="text" name="certifications[]" value="<?= e($cert) ?>" class="form-input" style="flex:1;">
<?php if ($i === 0): ?>
<button type="button" onclick="addCertRow()" class="btn btn-outline" style="padding:8px 12px;"><i data-lucide="plus" style="width:14px;height:14px;"></i></button>
<?php else: ?>
<button type="button" onclick="this.parentElement.remove()" class="btn btn-outline" style="padding:8px 12px;color:#DC2626;"><i data-lucide="x" style="width:14px;height:14px;"></i></button>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<div style="display:flex;gap:8px;margin-bottom:8px;">
<input type="text" name="certifications[]" class="form-input" placeholder="مثال: رخصة تدريب AFC-B" style="flex:1;">
<button type="button" onclick="addCertRow()" class="btn btn-outline" style="padding:8px 12px;"><i data-lucide="plus" style="width:14px;height:14px;"></i></button>
</div>
<?php endif; ?>
</div>
</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="/coaches/<?= (int) $coach->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();
var photoInput = document.getElementById('photoInput');
if (photoInput) {
photoInput.addEventListener('change', function() {
var file = this.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(e) {
document.getElementById('photoPreview').innerHTML = '<img src="' + e.target.result + '" style="width:80px;height:80px;object-fit:cover;border-radius:50%;">';
};
reader.readAsDataURL(file);
});
}
});
function addCertRow() {
var container = document.getElementById('certsContainer');
var div = document.createElement('div');
div.style.cssText = 'display:flex;gap:8px;margin-bottom:8px;';
div.innerHTML = '<input type="text" name="certifications[]" class="form-input" placeholder="شهادة إضافية..." style="flex:1;"><button type="button" onclick="this.parentElement.remove()" class="btn btn-outline" style="padding:8px 12px;color:#DC2626;"><i data-lucide="x" style="width:14px;height:14px;"></i></button>';
container.appendChild(div);
if (typeof lucide !== 'undefined') lucide.createIcons();
}
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Coaches\Models\Coach;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>المدربين<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/coaches/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['employment_type'] ?? ''; ?>
<!-- Employment 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="/coaches"
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 ($employmentTypes as $typeKey => $typeLabel): ?>
<a href="/coaches?employment_type=<?= e($typeKey) ?><?= ($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 -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/coaches" 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:160px;">
<label class="form-label" style="font-size:12px;">نموذج الدفع</label>
<select name="payment_model" class="form-input">
<option value="">الكل</option>
<?php foreach ($paymentModels as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['payment_model'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentType !== ''): ?>
<input type="hidden" name="employment_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="/coaches" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Coaches Grid -->
<?php if (!empty($coaches)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:20px;margin-bottom:20px;">
<?php foreach ($coaches as $c):
$isActive = (int) ($c['is_active'] ?? 0);
$empType = $c['employment_type'] ?? 'contract';
$empLabel = $employmentTypes[$empType] ?? $empType;
$payModel = $c['payment_model'] ?? 'per_session';
$payLabel = $paymentModels[$payModel] ?? $payModel;
$empColors = ['staff' => '#059669', 'contract' => '#2563EB', 'freelance' => '#D97706'];
$empColor = $empColors[$empType] ?? '#6B7280';
?>
<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="/coaches/<?= (int) $c['id'] ?>" style="text-decoration:none;color:inherit;display:block;">
<div style="padding:20px 20px 15px;display:flex;align-items:start;gap:15px;">
<div style="width:52px;height:52px;border-radius:50%;background:linear-gradient(135deg, <?= $empColor ?>15, <?= $empColor ?>30);display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;">
<?php if (!empty($c['photo_path'])): ?>
<img src="<?= e($c['photo_path']) ?>" style="width:52px;height:52px;object-fit:cover;border-radius:50%;" alt="">
<?php else: ?>
<i data-lucide="user" style="width:26px;height:26px;color:<?= $empColor ?>;"></i>
<?php endif; ?>
</div>
<div style="flex:1;min-width:0;">
<h3 style="margin:0 0 4px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($c['full_name_ar']) ?></h3>
<?php if (!empty($c['full_name_en'])): ?>
<div style="font-size:12px;color:#9CA3AF;margin-bottom:6px;"><?= e($c['full_name_en']) ?></div>
<?php endif; ?>
<span style="display:inline-block;padding:2px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $empColor ?>15;color:<?= $empColor ?>;"><?= e($empLabel) ?></span>
</div>
</div>
<div style="padding:0 20px 15px;display:flex;gap:15px;font-size:12px;color:#6B7280;">
<?php if (!empty($c['phone'])): ?>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="phone" style="width:13px;height:13px;"></i>
<?= e($c['phone']) ?>
</span>
<?php endif; ?>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="banknote" style="width:13px;height:13px;"></i>
<?= e($payLabel) ?>
</span>
<span style="display:flex;align-items:center;gap:4px;">
<i data-lucide="users" style="width:13px;height:13px;"></i>
حد أقصى: <?= (int) ($c['max_players_default'] ?? 20) ?>
</span>
</div>
</a>
<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($c['code'] ?? '') ?></code>
<div style="display:flex;gap:6px;">
<a href="/coaches/<?= (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>
<a href="/coaches/<?= (int) $c['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="user" 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['employment_type']) || !empty($filters['payment_model'])): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإضافة مدرب جديد.
<?php endif; ?>
</p>
<a href="/coaches/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\Coaches\Models\Coach;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($coach->full_name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/coaches/<?= (int) $coach->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<form method="POST" action="/coaches/<?= (int) $coach->id ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:<?= $coach->is_active ? '#DC2626' : '#059669' ?>;">
<i data-lucide="<?= $coach->is_active ? 'x-circle' : 'check-circle' ?>" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i>
<?= $coach->is_active ? 'إيقاف' : 'تفعيل' ?>
</button>
</form>
<a href="/coaches" 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
$empTypes = Coach::getEmploymentTypes();
$payModels = Coach::getPaymentModels();
$empColor = ['staff' => '#059669', 'contract' => '#2563EB', 'freelance' => '#D97706'][$coach->employment_type] ?? '#6B7280';
$certs = $coach->getCertifications();
?>
<!-- Profile Header -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:25px;display:flex;align-items:center;gap:20px;">
<div style="width:90px;height:90px;border-radius:50%;background:#F3F4F6;display:flex;align-items:center;justify-content:center;overflow:hidden;border:3px solid <?= $empColor ?>;flex-shrink:0;">
<?php if (!empty($coach->photo_path)): ?>
<img src="<?= e($coach->photo_path) ?>" style="width:90px;height:90px;object-fit:cover;border-radius:50%;">
<?php else: ?>
<i data-lucide="user" style="width:40px;height:40px;color:#9CA3AF;"></i>
<?php endif; ?>
</div>
<div style="flex:1;">
<h2 style="margin:0 0 4px;font-size:22px;color:#1A1A2E;"><?= e($coach->full_name_ar) ?></h2>
<?php if (!empty($coach->full_name_en)): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:8px;"><?= e($coach->full_name_en) ?></div>
<?php endif; ?>
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<span style="padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $empColor ?>15;color:<?= $empColor ?>;"><?= e($empTypes[$coach->employment_type] ?? '') ?></span>
<span style="padding:4px 12px;border-radius:10px;font-size:12px;background:#F3F4F6;color:#6B7280;"><?= e($payModels[$coach->payment_model] ?? '') ?></span>
<?php if (!$coach->is_active): ?>
<span style="padding:4px 12px;border-radius:10px;font-size:12px;background:#FEE2E2;color:#DC2626;">معطّل</span>
<?php endif; ?>
</div>
</div>
<div style="text-align:left;direction:ltr;">
<code style="font-size:12px;color:#9CA3AF;background:#F9FAFB;padding:4px 8px;border-radius:4px;"><?= e($coach->code) ?></code>
</div>
</div>
</div>
<!-- Stats Cards -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#0D7377;"><?= $utilization['total_schedules'] ?></div>
<div style="font-size:12px;color:#6B7280;">جدول أسبوعي</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#2563EB;"><?= $utilization['total_hours'] ?></div>
<div style="font-size:12px;color:#6B7280;">ساعة/أسبوع</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#D97706;"><?= $utilization['total_assignments'] ?></div>
<div style="font-size:12px;color:#6B7280;">أكاديمية</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#059669;"><?= (int) ($coach->max_players_default ?? 20) ?></div>
<div style="font-size:12px;color:#6B7280;">حد اللاعبين</div>
</div>
</div>
<!-- Info Sections -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<!-- Contact Info -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="contact" style="width:16px;height:16px;color:#0D7377;"></i>
<h3 style="margin:0;font-size:14px;color:#0D7377;">بيانات التواصل</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<?php if (!empty($coach->phone)): ?>
<tr><td style="padding:6px 0;color:#6B7280;width:120px;">الهاتف</td><td style="direction:ltr;text-align:right;"><?= e($coach->phone) ?></td></tr>
<?php endif; ?>
<?php if (!empty($coach->email)): ?>
<tr><td style="padding:6px 0;color:#6B7280;">البريد</td><td style="direction:ltr;text-align:right;"><?= e($coach->email) ?></td></tr>
<?php endif; ?>
<?php if (!empty($coach->national_id)): ?>
<tr><td style="padding:6px 0;color:#6B7280;">الرقم القومي</td><td style="direction:ltr;text-align:right;"><?= e($coach->national_id) ?></td></tr>
<?php endif; ?>
<?php if (!empty($coach->date_of_birth)): ?>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الميلاد</td><td><?= e($coach->date_of_birth) ?></td></tr>
<?php endif; ?>
<?php if (!empty($coach->gender)): ?>
<tr><td style="padding:6px 0;color:#6B7280;">النوع</td><td><?= $coach->gender === 'male' ? 'ذكر' : 'أنثى' ?></td></tr>
<?php endif; ?>
</table>
</div>
</div>
<!-- Financial Info -->
<div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="banknote" style="width:16px;height:16px;color:#059669;"></i>
<h3 style="margin:0;font-size:14px;color:#059669;">البيانات المالية</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:120px;">نموذج الدفع</td><td><?= e($payModels[$coach->payment_model] ?? '') ?></td></tr>
<?php if ($coach->hourly_rate): ?>
<tr><td style="padding:6px 0;color:#6B7280;">سعر الساعة</td><td><?= number_format((float) $coach->hourly_rate, 2) ?> ج.م</td></tr>
<?php endif; ?>
<?php if ($coach->session_rate): ?>
<tr><td style="padding:6px 0;color:#6B7280;">سعر الحصة</td><td><?= number_format((float) $coach->session_rate, 2) ?> ج.م</td></tr>
<?php endif; ?>
<?php if ($coach->monthly_rate): ?>
<tr><td style="padding:6px 0;color:#6B7280;">الراتب الشهري</td><td><?= number_format((float) $coach->monthly_rate, 2) ?> ج.م</td></tr>
<?php endif; ?>
</table>
</div>
</div>
</div>
<!-- Disciplines -->
<?php if (!empty($disciplines)): ?>
<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:16px;height:16px;color:#2563EB;"></i>
<h3 style="margin:0;font-size:14px;color:#2563EB;">التخصصات الرياضية</h3>
</div>
<div style="padding:15px 20px;display:flex;flex-wrap:wrap;gap:10px;">
<?php foreach ($disciplines as $disc): ?>
<span style="padding:6px 14px;border-radius:8px;font-size:13px;background:#EFF6FF;color:#2563EB;display:flex;align-items:center;gap:6px;">
<i data-lucide="<?= e($disc['discipline_icon'] ?? 'activity') ?>" style="width:14px;height:14px;"></i>
<?= e($disc['discipline_name'] ?? '') ?>
<?php if ($disc['is_primary']): ?>
<span style="font-size:10px;background:#2563EB;color:white;padding:1px 6px;border-radius:4px;">رئيسي</span>
<?php endif; ?>
</span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Certifications -->
<?php if (!empty($certs)): ?>
<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="award" style="width:16px;height:16px;color:#D97706;"></i>
<h3 style="margin:0;font-size:14px;color:#D97706;">الشهادات والتراخيص</h3>
</div>
<div style="padding:15px 20px;">
<ul style="margin:0;padding:0 15px;font-size:13px;color:#374151;">
<?php foreach ($certs as $cert): ?>
<li style="padding:4px 0;"><?= e($cert) ?></li>
<?php endforeach; ?>
</ul>
</div>
</div>
<?php endif; ?>
<!-- Academy Assignments -->
<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="graduation-cap" style="width:16px;height:16px;color:#7C3AED;"></i>
<h3 style="margin:0;font-size:14px;color:#7C3AED;">الأكاديميات المعيّن بها</h3>
</div>
</div>
<div style="padding:15px 20px;">
<?php if (!empty($assignments)): ?>
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<thead>
<tr style="background:#F9FAFB;color:#6B7280;font-size:12px;">
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الأكاديمية</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">المستوى</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الدور</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">من</th>
<th style="padding:8px;text-align:center;border-bottom:1px solid #E5E7EB;">إجراء</th>
</tr>
</thead>
<tbody>
<?php foreach ($assignments as $asgn): ?>
<tr>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($asgn['academy_name'] ?? '—') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($asgn['level_name'] ?? 'الكل') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($roles[$asgn['role']] ?? $asgn['role']) ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($asgn['assigned_from'] ?? '') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;text-align:center;">
<form method="POST" action="/coaches/<?= (int) $coach->id ?>/unassign" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="assignment_id" value="<?= (int) $asgn['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline" style="color:#DC2626;font-size:11px;padding:3px 8px;">إلغاء</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p style="color:#9CA3AF;font-size:13px;margin:0;">لم يتم تعيين المدرب في أي أكاديمية بعد.</p>
<?php endif; ?>
<!-- Add Assignment Form -->
<div style="margin-top:15px;padding-top:15px;border-top:1px solid #E5E7EB;">
<form method="POST" action="/coaches/<?= (int) $coach->id ?>/assign" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div style="min-width:150px;">
<label class="form-label" style="font-size:11px;">الأكاديمية</label>
<select name="academy_id" class="form-input" style="font-size:12px;" required>
<option value="">-- اختر --</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>"><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:130px;">
<label class="form-label" style="font-size:11px;">المستوى</label>
<select name="level_id" class="form-input" style="font-size:12px;">
<option value="">الكل</option>
<?php foreach ($levels as $lvl): ?>
<option value="<?= (int) $lvl['id'] ?>"><?= e($lvl['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:110px;">
<label class="form-label" style="font-size:11px;">الدور</label>
<select name="role" class="form-input" style="font-size:12px;">
<?php foreach ($roles as $rKey => $rLabel): ?>
<option value="<?= e($rKey) ?>"><?= e($rLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:120px;">
<label class="form-label" style="font-size:11px;">من تاريخ</label>
<input type="date" name="assigned_from" value="<?= date('Y-m-d') ?>" class="form-input" style="font-size:12px;direction:ltr;">
</div>
<button type="submit" class="btn btn-primary" style="font-size:12px;padding:8px 14px;">
<i data-lucide="plus" style="width:13px;height:13px;vertical-align:middle;"></i> تعيين
</button>
</form>
</div>
</div>
</div>
<!-- Availability -->
<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="clock" style="width:16px;height:16px;color:#059669;"></i>
<h3 style="margin:0;font-size:14px;color:#059669;">مواعيد العمل</h3>
</div>
<div style="padding:15px 20px;">
<?php if (!empty($availability)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:10px;">
<?php foreach ($availability as $slot): ?>
<div style="padding:10px;background:#F0FDF4;border-radius:8px;border:1px solid #BBF7D0;font-size:13px;">
<strong style="color:#059669;"><?= e($dayNames[(int) $slot['day_of_week']] ?? '') ?></strong><br>
<span style="direction:ltr;display:inline-block;"><?= e($slot['start_time']) ?><?= e($slot['end_time']) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="color:#9CA3AF;font-size:13px;margin:0 0 10px;">لم يتم تحديد مواعيد العمل.</p>
<?php endif; ?>
<!-- Availability Form -->
<details style="margin-top:15px;">
<summary style="cursor:pointer;font-size:13px;color:#0D7377;font-weight:600;">تعديل المواعيد</summary>
<form method="POST" action="/coaches/<?= (int) $coach->id ?>/availability" style="margin-top:10px;">
<?= csrf_field() ?>
<div id="availContainer">
<?php if (!empty($availability)): ?>
<?php foreach ($availability as $slot): ?>
<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center;">
<select name="avail_day[]" class="form-input" style="width:120px;font-size:12px;">
<?php foreach ($dayNames as $dKey => $dLabel): ?>
<option value="<?= $dKey ?>" <?= (int) $slot['day_of_week'] === $dKey ? 'selected' : '' ?>><?= e($dLabel) ?></option>
<?php endforeach; ?>
</select>
<input type="time" name="avail_start[]" value="<?= e($slot['start_time']) ?>" class="form-input" style="width:110px;font-size:12px;direction:ltr;">
<input type="time" name="avail_end[]" value="<?= e($slot['end_time']) ?>" class="form-input" style="width:110px;font-size:12px;direction:ltr;">
<button type="button" onclick="this.parentElement.remove()" style="border:none;background:none;color:#DC2626;cursor:pointer;font-size:18px;">&times;</button>
</div>
<?php endforeach; ?>
<?php else: ?>
<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center;">
<select name="avail_day[]" class="form-input" style="width:120px;font-size:12px;">
<?php foreach ($dayNames as $dKey => $dLabel): ?>
<option value="<?= $dKey ?>"><?= e($dLabel) ?></option>
<?php endforeach; ?>
</select>
<input type="time" name="avail_start[]" value="08:00" class="form-input" style="width:110px;font-size:12px;direction:ltr;">
<input type="time" name="avail_end[]" value="16:00" class="form-input" style="width:110px;font-size:12px;direction:ltr;">
<button type="button" onclick="this.parentElement.remove()" style="border:none;background:none;color:#DC2626;cursor:pointer;font-size:18px;">&times;</button>
</div>
<?php endif; ?>
</div>
<div style="display:flex;gap:8px;margin-top:8px;">
<button type="button" onclick="addAvailRow()" class="btn btn-outline" style="font-size:12px;padding:6px 12px;">+ إضافة يوم</button>
<button type="submit" class="btn btn-primary" style="font-size:12px;padding:6px 14px;">حفظ المواعيد</button>
</div>
</form>
</details>
</div>
</div>
<!-- Bio -->
<?php if (!empty($coach->bio_ar)): ?>
<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:16px;height:16px;color:#6B7280;"></i>
<h3 style="margin:0;font-size:14px;color:#6B7280;">نبذة</h3>
</div>
<div style="padding:15px 20px;font-size:14px;color:#374151;line-height:1.8;">
<?= nl2br(e($coach->bio_ar)) ?>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
function addAvailRow() {
var container = document.getElementById('availContainer');
var dayOptions = <?= json_encode($dayNames) ?>;
var opts = '';
for (var k in dayOptions) {
opts += '<option value="' + k + '">' + dayOptions[k] + '</option>';
}
var div = document.createElement('div');
div.style.cssText = 'display:flex;gap:8px;margin-bottom:8px;align-items:center;';
div.innerHTML = '<select name="avail_day[]" class="form-input" style="width:120px;font-size:12px;">' + opts + '</select>' +
'<input type="time" name="avail_start[]" value="08:00" class="form-input" style="width:110px;font-size:12px;direction:ltr;">' +
'<input type="time" name="avail_end[]" value="16:00" class="form-input" style="width:110px;font-size:12px;direction:ltr;">' +
'<button type="button" onclick="this.parentElement.remove()" style="border:none;background:none;color:#DC2626;cursor:pointer;font-size:18px;">&times;</button>';
container.appendChild(div);
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('coaches', [
'coach.view' => ['ar' => 'عرض المدربين', 'en' => 'View Coaches'],
'coach.manage' => ['ar' => 'إدارة المدربين', 'en' => 'Manage Coaches'],
]);
MenuRegistry::register('coaches', [
'label_ar' => 'المدربين',
'icon' => 'users',
'route' => '/coaches',
'permission' => 'coach.view',
'order' => 397,
'parent' => 'sports_activities',
]);
...@@ -102,6 +102,56 @@ class SportsDashboardController extends Controller ...@@ -102,6 +102,56 @@ class SportsDashboardController extends Controller
"SELECT COUNT(*) AS total FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0" "SELECT COUNT(*) AS total FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0"
)['total'] ?? 0; )['total'] ?? 0;
// Coaches stats
$coachStats = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) AS active
FROM coaches WHERE is_archived = 0
") ?: ['total' => 0, 'active' => 0];
// Training sessions this week
$weekStart = date('Y-m-d', strtotime('monday this week'));
$weekEnd = date('Y-m-d', strtotime('sunday this week'));
$sessionStats = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled,
SUM(CASE WHEN status = 'scheduled' THEN 1 ELSE 0 END) AS scheduled
FROM training_sessions
WHERE session_date BETWEEN ? AND ?
", [$weekStart, $weekEnd]) ?: ['total' => 0, 'completed' => 0, 'cancelled' => 0, 'scheduled' => 0];
// Active contracts
$contractStats = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active,
SUM(CASE WHEN status = 'active' AND end_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) AS expiring_soon
FROM academy_contracts
") ?: ['total' => 0, 'active' => 0, 'expiring_soon' => 0];
// Training groups capacity
$groupStats = $db->selectOne("
SELECT
COUNT(*) AS total,
SUM(CASE WHEN is_full = 1 THEN 1 ELSE 0 END) AS full_groups,
COALESCE(SUM(current_count), 0) AS total_players
FROM training_groups
WHERE status = 'active'
") ?: ['total' => 0, 'full_groups' => 0, 'total_players' => 0];
// Pool utilization today
$poolStats = $db->selectOne("
SELECT
COUNT(*) AS total_bookings,
SUM(CASE WHEN status = 'confirmed' THEN 1 ELSE 0 END) AS confirmed,
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress
FROM pool_bookings
WHERE booking_date = CURDATE()
") ?: ['total_bookings' => 0, 'confirmed' => 0, 'in_progress' => 0];
return $this->view('Disciplines.Views.sports_dashboard', [ return $this->view('Disciplines.Views.sports_dashboard', [
'playerStats' => $playerStats, 'playerStats' => $playerStats,
'enrollmentCount' => $enrollmentCount, 'enrollmentCount' => $enrollmentCount,
...@@ -114,6 +164,11 @@ class SportsDashboardController extends Controller ...@@ -114,6 +164,11 @@ class SportsDashboardController extends Controller
'academyCount' => $academyCount, 'academyCount' => $academyCount,
'disciplineCount' => $disciplineCount, 'disciplineCount' => $disciplineCount,
'currentMonth' => $currentMonth, 'currentMonth' => $currentMonth,
'coachStats' => $coachStats,
'sessionStats' => $sessionStats,
'contractStats' => $contractStats,
'groupStats' => $groupStats,
'poolStats' => $poolStats,
]); ]);
} }
} }
...@@ -76,6 +76,87 @@ $monthLabel = date('Y/m', strtotime($currentMonth . '-01')); ...@@ -76,6 +76,87 @@ $monthLabel = date('Y/m', strtotime($currentMonth . '-01'));
</div> </div>
</div> </div>
<!-- Operations Row: Coaches, Sessions, Groups, Pool -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;border-right:4px solid #7C3AED;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المدربين</div>
<div style="font-size:28px;font-weight:700;color:#1A1A2E;"><?= (int) $coachStats['active'] ?></div>
<div style="font-size:11px;color:#6B7280;margin-top:4px;">من <?= (int) $coachStats['total'] ?> مسجل</div>
</div>
<div style="width:42px;height:42px;border-radius:10px;background:#7C3AED15;display:flex;align-items:center;justify-content:center;">
<i data-lucide="user-check" style="width:22px;height:22px;color:#7C3AED;"></i>
</div>
</div>
</div>
<div class="card" style="padding:20px;border-right:4px solid #2563EB;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">حصص الأسبوع</div>
<div style="font-size:28px;font-weight:700;color:#1A1A2E;"><?= (int) $sessionStats['total'] ?></div>
<div style="font-size:11px;margin-top:4px;">
<span style="color:#10B981;"><?= (int) $sessionStats['completed'] ?> مكتملة</span>
<?php if ((int) $sessionStats['cancelled'] > 0): ?>
&middot; <span style="color:#EF4444;"><?= (int) $sessionStats['cancelled'] ?> ملغاة</span>
<?php endif; ?>
</div>
</div>
<div style="width:42px;height:42px;border-radius:10px;background:#2563EB15;display:flex;align-items:center;justify-content:center;">
<i data-lucide="calendar-days" style="width:22px;height:22px;color:#2563EB;"></i>
</div>
</div>
</div>
<div class="card" style="padding:20px;border-right:4px solid #0891B2;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">المجموعات التدريبية</div>
<div style="font-size:28px;font-weight:700;color:#1A1A2E;"><?= (int) $groupStats['total'] ?></div>
<div style="font-size:11px;margin-top:4px;">
<span style="color:#059669;"><?= (int) $groupStats['total_players'] ?> لاعب</span>
<?php if ((int) $groupStats['full_groups'] > 0): ?>
&middot; <span style="color:#D97706;"><?= (int) $groupStats['full_groups'] ?> ممتلئة</span>
<?php endif; ?>
</div>
</div>
<div style="width:42px;height:42px;border-radius:10px;background:#0891B215;display:flex;align-items:center;justify-content:center;">
<i data-lucide="users" style="width:22px;height:22px;color:#0891B2;"></i>
</div>
</div>
</div>
<div class="card" style="padding:20px;border-right:4px solid #0369A1;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<div>
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">حمام السباحة اليوم</div>
<div style="font-size:28px;font-weight:700;color:#1A1A2E;"><?= (int) $poolStats['total_bookings'] ?></div>
<div style="font-size:11px;margin-top:4px;">
<?php if ((int) $poolStats['in_progress'] > 0): ?>
<span style="color:#F59E0B;"><?= (int) $poolStats['in_progress'] ?> جاري</span> &middot;
<?php endif; ?>
<span style="color:#3B82F6;"><?= (int) $poolStats['confirmed'] ?> مؤكد</span>
</div>
</div>
<div style="width:42px;height:42px;border-radius:10px;background:#0369A115;display:flex;align-items:center;justify-content:center;">
<i data-lucide="waves" style="width:22px;height:22px;color:#0369A1;"></i>
</div>
</div>
</div>
</div>
<!-- Contracts Alert -->
<?php if ((int) $contractStats['expiring_soon'] > 0): ?>
<div style="background:#FEF3C7;border:1px solid #F59E0B;border-radius:8px;padding:12px 18px;margin-bottom:20px;display:flex;align-items:center;gap:10px;">
<i data-lucide="alert-triangle" style="width:18px;height:18px;color:#D97706;flex-shrink:0;"></i>
<span style="font-size:13px;color:#92400E;">
<strong><?= (int) $contractStats['expiring_soon'] ?></strong> عقد أكاديمية ينتهي خلال 30 يوم —
<a href="/academy-contracts" style="color:#D97706;font-weight:600;">مراجعة العقود</a>
</span>
</div>
<?php endif; ?>
<!-- Facility Stats + Collection Summary --> <!-- Facility Stats + Collection Summary -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card" style="padding:20px;"> <div class="card" style="padding:20px;">
...@@ -269,14 +350,20 @@ $monthLabel = date('Y/m', strtotime($currentMonth . '-01')); ...@@ -269,14 +350,20 @@ $monthLabel = date('Y/m', strtotime($currentMonth . '-01'));
<a href="/reservations/create" class="btn btn-outline" style="font-size:13px;"> <a href="/reservations/create" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="calendar-plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حجز ملعب <i data-lucide="calendar-plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حجز ملعب
</a> </a>
<a href="/sessions" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="calendar-days" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الحصص التدريبية
</a>
<a href="/activity-subscriptions" class="btn btn-outline" style="font-size:13px;"> <a href="/activity-subscriptions" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="credit-card" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إدارة الاشتراكات <i data-lucide="credit-card" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إدارة الاشتراكات
</a> </a>
<a href="/academies" class="btn btn-outline" style="font-size:13px;"> <a href="/academy-contracts/settlements" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="school" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الأكاديميات <i data-lucide="receipt" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> التسويات المالية
</a>
<a href="/mirror" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="monitor" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> المراية
</a> </a>
<a href="/disciplines" class="btn btn-outline" style="font-size:13px;"> <a href="/pool" class="btn btn-outline" style="font-size:13px;">
<i data-lucide="activity" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الأنشطة الرياضية <i data-lucide="waves" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حمام السباحة
</a> </a>
</div> </div>
</div> </div>
......
...@@ -21,13 +21,20 @@ MenuRegistry::register('sports_activities', [ ...@@ -21,13 +21,20 @@ MenuRegistry::register('sports_activities', [
['label_ar' => 'لوحة التحكم', 'label_en' => 'Sports Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 0], ['label_ar' => 'لوحة التحكم', 'label_en' => 'Sports Dashboard', 'route' => '/sports-dashboard', 'permission' => 'discipline.view', 'order' => 0],
['label_ar' => 'الأنشطة الرياضية', 'label_en' => 'Disciplines', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 1], ['label_ar' => 'الأنشطة الرياضية', 'label_en' => 'Disciplines', 'route' => '/disciplines', 'permission' => 'discipline.view', 'order' => 1],
['label_ar' => 'الملاعب والمرافق', 'label_en' => 'Facilities', 'route' => '/facilities', 'permission' => 'facility.view', 'order' => 2], ['label_ar' => 'الملاعب والمرافق', 'label_en' => 'Facilities', 'route' => '/facilities', 'permission' => 'facility.view', 'order' => 2],
['label_ar' => 'المراية', 'label_en' => 'Mirror Display', 'route' => '/mirror', 'permission' => 'facility.mirror', 'order' => 2.5],
['label_ar' => 'الأكاديميات', 'label_en' => 'Academies', 'route' => '/academies', 'permission' => 'academy.view', 'order' => 3], ['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' => 'Academy Contracts', 'route' => '/academy-contracts', 'permission' => 'academy_contract.view', 'order' => 3.5],
['label_ar' => 'الحضور والغياب', 'label_en' => 'Attendance', 'route' => '/attendance', 'permission' => 'player.view', 'order' => 4.5], ['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/coaches', 'permission' => 'coach.view', 'order' => 4],
['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 5], ['label_ar' => 'المجموعات التدريبية','label_en' => 'Training Groups', 'route' => '/training-groups', 'permission' => 'training_group.view', 'order' => 4.2],
['label_ar' => 'التأجير المؤسسي', 'label_en' => 'Corporate Rentals', 'route' => '/rentals', 'permission' => 'rental.view', 'order' => 6], ['label_ar' => 'شئون اللاعبين', 'label_en' => 'Players', 'route' => '/players', 'permission' => 'player.view', 'order' => 4.5],
['label_ar' => 'اشتراكات الأنشطة', 'label_en' => 'Activity Subscriptions','route' => '/activity-subscriptions', 'permission' => 'activity_sub.view', 'order' => 7], ['label_ar' => 'الحصص التدريبية', 'label_en' => 'Training Sessions', 'route' => '/sessions', 'permission' => 'session.view', 'order' => 4.7],
['label_ar' => 'تسعير الأنشطة', 'label_en' => 'Activity Pricing', 'route' => '/activity-subscriptions/pricing', 'permission' => 'activity_sub.manage_pricing', 'order' => 8], ['label_ar' => 'الحضور والغياب', 'label_en' => 'Attendance', 'route' => '/attendance', 'permission' => 'player.view', 'order' => 5],
['label_ar' => 'الحجوزات', 'label_en' => 'Reservations', 'route' => '/reservations', 'permission' => 'reservation.view', 'order' => 6],
['label_ar' => 'حمام السباحة', 'label_en' => 'Pool Management', 'route' => '/pool', 'permission' => 'pool.view', 'order' => 6.5],
['label_ar' => 'التأجير المؤسسي', 'label_en' => 'Corporate Rentals', 'route' => '/rentals', 'permission' => 'rental.view', 'order' => 7],
['label_ar' => 'اشتراكات الأنشطة', 'label_en' => 'Activity Subscriptions','route' => '/activity-subscriptions', 'permission' => 'activity_sub.view', 'order' => 8],
['label_ar' => 'تسعير الأنشطة', 'label_en' => 'Activity Pricing', 'route' => '/activity-subscriptions/pricing', 'permission' => 'activity_sub.manage_pricing', 'order' => 9],
['label_ar' => 'التسويات المالية', 'label_en' => 'Settlements', 'route' => '/academy-contracts/settlements', 'permission' => 'academy_contract.view', 'order' => 10],
], ],
]); ]);
......
<?php
declare(strict_types=1);
namespace App\Modules\FacilityDashboards\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class FacilityDashboardController extends Controller
{
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$facility = $db->selectOne("SELECT * FROM facilities WHERE id = ? AND is_archived = 0", [(int) $id]);
if (!$facility) {
return $this->redirect('/facilities')->withError('المرفق غير موجود');
}
$today = date('Y-m-d');
$monthStart = date('Y-m-01');
$weekStart = date('Y-m-d', strtotime('monday this week'));
$todayBookings = $db->select(
"SELECT * FROM reservations WHERE facility_id = ? AND reservation_date = ? AND status NOT IN ('cancelled') ORDER BY start_time ASC",
[(int) $id, $today]
);
$todayRevenue = $db->selectOne(
"SELECT COALESCE(SUM(final_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date = ? AND payment_status = 'paid'",
[(int) $id, $today]
);
$weekRevenue = $db->selectOne(
"SELECT COALESCE(SUM(final_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND payment_status = 'paid'",
[(int) $id, $weekStart]
);
$monthRevenue = $db->selectOne(
"SELECT COALESCE(SUM(final_amount), 0) AS total FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND payment_status = 'paid'",
[(int) $id, $monthStart]
);
$monthBookingCount = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND status NOT IN ('cancelled')",
[(int) $id, $monthStart]
);
$upcomingBookings = $db->select(
"SELECT * FROM reservations WHERE facility_id = ? AND reservation_date >= ? AND status = 'confirmed' ORDER BY reservation_date ASC, start_time ASC LIMIT 5",
[(int) $id, $today]
);
return $this->view('FacilityDashboards.Views.dashboard', [
'facility' => $facility,
'todayBookings' => $todayBookings,
'todayRevenue' => (float) ($todayRevenue['total'] ?? 0),
'weekRevenue' => (float) ($weekRevenue['total'] ?? 0),
'monthRevenue' => (float) ($monthRevenue['total'] ?? 0),
'monthBookingCount' => (int) ($monthBookingCount['cnt'] ?? 0),
'upcomingBookings' => $upcomingBookings,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\FacilityDashboards\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\FacilityDashboards\Services\MirrorDisplayService;
class MirrorDisplayController extends Controller
{
public function index(Request $request): Response
{
$facilityType = trim((string) $request->get('type', ''));
$disciplineId = $request->get('discipline_id', '') !== '' ? (int) $request->get('discipline_id') : null;
$states = MirrorDisplayService::getFacilityStates(
$facilityType ?: null,
$disciplineId
);
$db = App::getInstance()->db();
$facilityTypes = $db->select(
"SELECT DISTINCT facility_type FROM facilities WHERE is_active = 1 AND is_archived = 0 ORDER BY facility_type"
);
$disciplines = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar"
);
return $this->view('FacilityDashboards.Views.mirror', [
'states' => $states,
'facilityTypes' => $facilityTypes,
'disciplines' => $disciplines,
'currentType' => $facilityType,
'currentDisc' => $disciplineId,
]);
}
public function apiState(Request $request): Response
{
$facilityType = trim((string) $request->get('type', ''));
$disciplineId = $request->get('discipline_id', '') !== '' ? (int) $request->get('discipline_id') : null;
$states = MirrorDisplayService::getFacilityStates(
$facilityType ?: null,
$disciplineId
);
return $this->json([
'states' => $states,
'timestamp' => date('Y-m-d H:i:s'),
'time' => date('H:i'),
]);
}
}
<?php
declare(strict_types=1);
return [
['GET', '/mirror', 'FacilityDashboards\Controllers\MirrorDisplayController@index', ['auth'], 'facility.mirror'],
['GET', '/api/mirror/state', 'FacilityDashboards\Controllers\MirrorDisplayController@apiState', ['auth'], 'facility.mirror'],
['GET', '/facilities/{id:\d+}/dashboard', 'FacilityDashboards\Controllers\FacilityDashboardController@show', ['auth'], 'facility.dashboard'],
];
<?php
declare(strict_types=1);
namespace App\Modules\FacilityDashboards\Services;
use App\Core\App;
final class MirrorDisplayService
{
public static function getFacilityStates(?string $facilityType = null, ?int $disciplineId = null): array
{
$db = App::getInstance()->db();
$now = date('H:i:s');
$today = date('Y-m-d');
$sql = "SELECT f.id, f.name_ar, f.name_en, f.facility_type, f.capacity, f.location,
f.linked_discipline_id, f.is_active
FROM facilities f
WHERE f.is_active = 1 AND f.is_archived = 0";
$params = [];
if ($facilityType) {
$sql .= " AND f.facility_type = ?";
$params[] = $facilityType;
}
if ($disciplineId) {
$sql .= " AND f.linked_discipline_id = ?";
$params[] = $disciplineId;
}
$sql .= " ORDER BY f.facility_type ASC, f.name_ar ASC";
$facilities = $db->select($sql, $params);
$states = [];
foreach ($facilities as $f) {
$facilityId = (int) $f['id'];
$currentBooking = $db->selectOne(
"SELECT id, member_id, purpose, start_time, end_time, status
FROM reservations
WHERE facility_id = ? AND reservation_date = ? AND status IN ('confirmed', 'checked_in')
AND start_time <= ? AND end_time > ?
ORDER BY start_time ASC LIMIT 1",
[$facilityId, $today, $now, $now]
);
$nextBooking = $db->selectOne(
"SELECT id, purpose, start_time, end_time
FROM reservations
WHERE facility_id = ? AND reservation_date = ? AND status = 'confirmed'
AND start_time > ?
ORDER BY start_time ASC LIMIT 1",
[$facilityId, $today, $now]
);
$todayBookings = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM reservations
WHERE facility_id = ? AND reservation_date = ? AND status NOT IN ('cancelled')
",
[$facilityId, $today]
);
if ($currentBooking) {
$status = $currentBooking['status'] === 'checked_in' ? 'in_progress' : 'booked';
} else {
$status = 'available';
}
$timeUntilNext = null;
if ($nextBooking) {
$diff = strtotime($nextBooking['start_time']) - strtotime($now);
if ($diff > 0) {
$mins = (int) ($diff / 60);
if ($mins < 60) {
$timeUntilNext = $mins . ' دقيقة';
} else {
$hours = (int) ($mins / 60);
$remaining = $mins % 60;
$timeUntilNext = $hours . ' ساعة' . ($remaining > 0 ? ' و ' . $remaining . ' دقيقة' : '');
}
}
}
$states[] = [
'facility_id' => $facilityId,
'name_ar' => $f['name_ar'],
'type' => $f['facility_type'],
'location' => $f['location'] ?? '',
'capacity' => (int) ($f['capacity'] ?? 0),
'current_status' => $status,
'current_booking' => $currentBooking ? [
'purpose' => $currentBooking['purpose'] ?? '',
'start_time' => $currentBooking['start_time'],
'end_time' => $currentBooking['end_time'],
] : null,
'next_booking' => $nextBooking ? [
'purpose' => $nextBooking['purpose'] ?? '',
'start_time' => $nextBooking['start_time'],
'end_time' => $nextBooking['end_time'],
] : null,
'time_until_next' => $timeUntilNext,
'today_count' => (int) ($todayBookings['cnt'] ?? 0),
];
}
return $states;
}
}
<?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="info" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> بيانات المرفق</a>
<a href="/reservations/create?facility_id=<?= (int) $facility['id'] ?>" class="btn btn-primary"><i data-lucide="plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حجز سريع</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- KPI Cards -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #0D7377;">
<div style="font-size:28px;font-weight:700;color:#0D7377;"><?= count($todayBookings) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">حجوزات اليوم</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #059669;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= number_format($todayRevenue, 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">إيرادات اليوم (ج.م)</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #2563EB;">
<div style="font-size:28px;font-weight:700;color:#2563EB;"><?= number_format($weekRevenue, 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">إيرادات الأسبوع (ج.م)</div>
</div>
<div class="card" style="padding:20px;text-align:center;border-top:4px solid #D97706;">
<div style="font-size:28px;font-weight:700;color:#D97706;"><?= number_format($monthRevenue, 0) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:4px;">إيرادات الشهر (ج.م)</div>
</div>
</div>
<!-- Today's Timeline -->
<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="clock" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">جدول اليوم</h3>
<span style="font-size:12px;color:#9CA3AF;margin-right:8px;"><?= date('Y-m-d') ?></span>
</div>
<div style="padding:20px;">
<?php if (!empty($todayBookings)): ?>
<div style="position:relative;padding-right:20px;">
<?php foreach ($todayBookings as $i => $b):
$statusColors = ['confirmed' => '#3B82F6', 'checked_in' => '#F59E0B', 'completed' => '#10B981'];
$color = $statusColors[$b['status']] ?? '#6B7280';
$now = date('H:i:s');
$isCurrent = ($b['start_time'] <= $now && $b['end_time'] > $now);
?>
<div style="display:flex;gap:15px;align-items:center;padding:10px 0;<?= $i < count($todayBookings)-1 ? 'border-bottom:1px solid #F3F4F6;' : '' ?><?= $isCurrent ? 'background:#FFFBEB;margin:-5px -10px;padding:10px;border-radius:8px;' : '' ?>">
<div style="width:8px;height:8px;border-radius:50%;background:<?= $color ?>;flex-shrink:0;"></div>
<div style="min-width:100px;direction:ltr;text-align:left;font-size:13px;font-weight:600;color:<?= $color ?>;">
<?= e(substr($b['start_time'], 0, 5)) ?><?= e(substr($b['end_time'], 0, 5)) ?>
</div>
<div style="flex:1;font-size:13px;color:#1A1A2E;">
<?= e($b['purpose'] ?? 'حجز') ?>
</div>
<div>
<span style="font-size:11px;padding:2px 8px;border-radius:8px;background:<?= $color ?>15;color:<?= $color ?>;">
<?= ['confirmed'=>'مؤكد','checked_in'=>'جاري','completed'=>'مكتمل'][$b['status']] ?? $b['status'] ?>
</span>
</div>
<?php if ($b['final_amount'] > 0): ?>
<div style="font-size:12px;color:#059669;font-weight:600;"><?= number_format((float) $b['final_amount'], 0) ?> ج.م</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p style="color:#9CA3AF;text-align:center;font-size:14px;padding:20px 0;">لا توجد حجوزات اليوم</p>
<?php endif; ?>
</div>
</div>
<!-- Upcoming Bookings -->
<?php if (!empty($upcomingBookings)): ?>
<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" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">الحجوزات القادمة</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<thead>
<tr style="color:#6B7280;font-size:12px;">
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">التاريخ</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الوقت</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الغرض</th>
<th style="padding:8px;text-align:left;border-bottom:1px solid #E5E7EB;">المبلغ</th>
</tr>
</thead>
<tbody>
<?php foreach ($upcomingBookings as $b): ?>
<tr>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($b['reservation_date']) ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;direction:ltr;text-align:right;"><?= e(substr($b['start_time'],0,5)) ?><?= e(substr($b['end_time'],0,5)) ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e($b['purpose'] ?? '—') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;text-align:left;"><?= $b['final_amount'] > 0 ? number_format((float)$b['final_amount'], 0) . ' ج.م' : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.mirror'); ?>
<?php $__template->section('title'); ?>المراية — عرض مباشر<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusConfig = [
'available' => ['label' => 'متاح', 'color' => '#10B981', 'bg' => '#ECFDF5', 'icon' => 'check-circle'],
'booked' => ['label' => 'محجوز', 'color' => '#3B82F6', 'bg' => '#EFF6FF', 'icon' => 'calendar'],
'in_progress' => ['label' => 'جاري', 'color' => '#F59E0B', 'bg' => '#FFFBEB', 'icon' => 'play-circle'],
'maintenance' => ['label' => 'صيانة', 'color' => '#EF4444', 'bg' => '#FEF2F2', 'icon' => 'wrench'],
'closed' => ['label' => 'مغلق', 'color' => '#6B7280', 'bg' => '#F9FAFB', 'icon' => 'x-circle'],
];
$typeLabels = [
'court' => 'ملعب',
'pool' => 'حمام سباحة',
'gym' => 'صالة رياضية',
'track' => 'مضمار',
'hall' => 'قاعة',
'field' => 'ملعب كبير',
'other' => 'أخرى',
];
?>
<!-- Header Bar -->
<div style="position:fixed;top:0;right:0;left:0;z-index:100;background:linear-gradient(135deg, #0D7377, #14919B);padding:15px 30px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 4px 20px rgba(0,0,0,0.15);">
<div style="display:flex;align-items:center;gap:12px;">
<i data-lucide="monitor" style="width:28px;height:28px;color:white;"></i>
<h1 style="margin:0;font-size:22px;color:white;font-weight:700;">المراية — عرض مباشر</h1>
</div>
<div style="display:flex;align-items:center;gap:20px;">
<!-- Filters -->
<select id="filterType" style="padding:6px 12px;border-radius:6px;border:none;font-size:13px;background:rgba(255,255,255,0.9);" onchange="applyFilters()">
<option value="">كل الأنواع</option>
<?php foreach ($facilityTypes as $ft): ?>
<option value="<?= e($ft['facility_type']) ?>" <?= $currentType === $ft['facility_type'] ? 'selected' : '' ?>><?= e($typeLabels[$ft['facility_type']] ?? $ft['facility_type']) ?></option>
<?php endforeach; ?>
</select>
<!-- Clock -->
<div id="liveClock" style="font-size:28px;font-weight:700;color:white;font-family:monospace;direction:ltr;"></div>
<!-- Status Indicator -->
<div style="display:flex;align-items:center;gap:6px;">
<span id="syncDot" style="width:10px;height:10px;border-radius:50%;background:#10B981;animation:pulse 2s infinite;"></span>
<span style="font-size:11px;color:rgba(255,255,255,0.8);">مباشر</span>
</div>
</div>
</div>
<!-- Legend -->
<div style="position:fixed;top:65px;right:0;left:0;z-index:99;background:white;padding:10px 30px;border-bottom:1px solid #E5E7EB;display:flex;gap:20px;align-items:center;">
<?php foreach ($statusConfig as $st => $cfg): ?>
<span style="display:flex;align-items:center;gap:6px;font-size:13px;">
<span style="width:16px;height:16px;border-radius:4px;background:<?= $cfg['color'] ?>;display:inline-block;"></span>
<?= $cfg['label'] ?>
</span>
<?php endforeach; ?>
<span style="margin-right:auto;font-size:12px;color:#9CA3AF;" id="lastUpdate">آخر تحديث: <?= date('H:i:s') ?></span>
</div>
<!-- Facility Grid -->
<div id="facilityGrid" style="padding:120px 30px 30px;display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:20px;min-height:100vh;">
<?php foreach ($states as $s):
$cfg = $statusConfig[$s['current_status']] ?? $statusConfig['closed'];
?>
<div class="mirror-card" data-facility-id="<?= $s['facility_id'] ?>" style="
border-radius:16px;
border:3px solid <?= $cfg['color'] ?>40;
background:<?= $cfg['bg'] ?>;
padding:20px;
transition:all 0.3s;
position:relative;
overflow:hidden;
">
<!-- Status indicator stripe -->
<div style="position:absolute;top:0;right:0;left:0;height:5px;background:<?= $cfg['color'] ?>;"></div>
<!-- Header -->
<div style="display:flex;align-items:start;justify-content:space-between;margin-bottom:12px;">
<div>
<h3 style="margin:0 0 4px;font-size:18px;font-weight:700;color:#1A1A2E;"><?= e($s['name_ar']) ?></h3>
<span style="font-size:12px;color:#6B7280;"><?= e($typeLabels[$s['type']] ?? $s['type']) ?></span>
</div>
<div style="width:44px;height:44px;border-radius:12px;background:<?= $cfg['color'] ?>20;display:flex;align-items:center;justify-content:center;">
<i data-lucide="<?= $cfg['icon'] ?>" style="width:22px;height:22px;color:<?= $cfg['color'] ?>;"></i>
</div>
</div>
<!-- Status Badge -->
<div style="margin-bottom:12px;">
<span style="padding:5px 14px;border-radius:20px;font-size:14px;font-weight:700;background:<?= $cfg['color'] ?>20;color:<?= $cfg['color'] ?>;">
<?= $cfg['label'] ?>
</span>
</div>
<!-- Current/Next Info -->
<?php if ($s['current_booking']): ?>
<div style="padding:10px;background:white;border-radius:8px;margin-bottom:8px;font-size:13px;">
<div style="color:#6B7280;font-size:11px;margin-bottom:2px;">الآن:</div>
<div style="color:#1A1A2E;font-weight:600;"><?= e($s['current_booking']['purpose'] ?: 'حجز') ?></div>
<div style="color:#6B7280;direction:ltr;text-align:right;font-size:12px;"><?= e($s['current_booking']['start_time']) ?><?= e($s['current_booking']['end_time']) ?></div>
</div>
<?php endif; ?>
<?php if ($s['next_booking']): ?>
<div style="padding:8px 10px;background:white;border-radius:8px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="color:#6B7280;">التالي: <?= e($s['next_booking']['purpose'] ?: 'حجز') ?></span>
<?php if ($s['time_until_next']): ?>
<span style="font-weight:600;color:#D97706;">بعد <?= e($s['time_until_next']) ?></span>
<?php endif; ?>
</div>
<?php elseif ($s['current_status'] === 'available'): ?>
<div style="padding:8px 10px;background:white;border-radius:8px;font-size:13px;text-align:center;color:#059669;font-weight:600;">
لا يوجد حجوزات قادمة
</div>
<?php endif; ?>
<!-- Footer -->
<div style="margin-top:12px;display:flex;justify-content:space-between;font-size:11px;color:#9CA3AF;">
<span>حجوزات اليوم: <?= $s['today_count'] ?></span>
<?php if ($s['capacity']): ?>
<span>سعة: <?= $s['capacity'] ?></span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php if (empty($states)): ?>
<div style="padding:120px 30px;text-align:center;">
<i data-lucide="monitor-off" style="width:64px;height:64px;color:#D1D5DB;margin-bottom:20px;"></i>
<h2 style="color:#6B7280;">لا توجد مرافق نشطة</h2>
</div>
<?php endif; ?>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
body { overflow-x: hidden; }
.mirror-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.1); }
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
updateClock();
setInterval(updateClock, 1000);
setInterval(refreshMirror, 30000);
});
function updateClock() {
var now = new Date();
var h = String(now.getHours()).padStart(2, '0');
var m = String(now.getMinutes()).padStart(2, '0');
var s = String(now.getSeconds()).padStart(2, '0');
document.getElementById('liveClock').textContent = h + ':' + m + ':' + s;
}
function refreshMirror() {
var type = document.getElementById('filterType').value;
var url = '/api/mirror/state?type=' + encodeURIComponent(type);
fetch(url)
.then(r => r.json())
.then(data => {
document.getElementById('lastUpdate').textContent = 'آخر تحديث: ' + data.time;
updateCards(data.states);
})
.catch(() => {
document.getElementById('syncDot').style.background = '#EF4444';
});
}
function updateCards(states) {
var statusConfig = {
available: {label:'متاح', color:'#10B981', bg:'#ECFDF5'},
booked: {label:'محجوز', color:'#3B82F6', bg:'#EFF6FF'},
in_progress: {label:'جاري', color:'#F59E0B', bg:'#FFFBEB'},
maintenance: {label:'صيانة', color:'#EF4444', bg:'#FEF2F2'},
closed: {label:'مغلق', color:'#6B7280', bg:'#F9FAFB'},
};
states.forEach(function(s) {
var card = document.querySelector('[data-facility-id="' + s.facility_id + '"]');
if (!card) return;
var cfg = statusConfig[s.current_status] || statusConfig.closed;
card.style.borderColor = cfg.color + '40';
card.style.background = cfg.bg;
var stripe = card.querySelector('div');
if (stripe) stripe.style.background = cfg.color;
});
document.getElementById('syncDot').style.background = '#10B981';
}
function applyFilters() {
var type = document.getElementById('filterType').value;
window.location.href = '/mirror' + (type ? '?type=' + encodeURIComponent(type) : '');
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('facility_dashboards', [
'facility.dashboard' => ['ar' => 'لوحة المرفق', 'en' => 'Facility Dashboard'],
'facility.mirror' => ['ar' => 'المراية', 'en' => 'Mirror Display'],
]);
MenuRegistry::register('facility_mirror', [
'label_ar' => 'المراية (عرض مباشر)',
'icon' => 'monitor',
'route' => '/mirror',
'permission' => 'facility.mirror',
'order' => 396,
'parent' => 'sports_activities',
]);
<?php
declare(strict_types=1);
namespace App\Modules\PlayerAffairs\Services;
use App\Core\App;
use App\Core\EventBus;
final class PlayerProgressionService
{
public static function getProgressionTypes(): array
{
return [
'promotion' => 'ترقية',
'demotion' => 'تراجع',
'lateral' => 'نقل جانبي',
];
}
public static function getCertificateTypes(): array
{
return [
'recreational' => ['label' => 'ممارسين', 'validity_months' => 12, 'color' => '#059669'],
'academy' => ['label' => 'أكاديمية', 'validity_months' => 6, 'color' => '#2563EB'],
'international' => ['label' => 'دولية', 'validity_months' => 3, 'color' => '#7C3AED'],
];
}
public static function recordProgression(
int $playerId,
int $disciplineId,
?int $fromLevelId,
int $toLevelId,
string $type,
?float $score,
?int $assessedBy,
?string $notes,
?string $effectiveDate = null
): int {
$db = App::getInstance()->db();
$id = $db->insert('player_progressions', [
'player_id' => $playerId,
'discipline_id' => $disciplineId,
'from_level_id' => $fromLevelId,
'to_level_id' => $toLevelId,
'progression_type' => $type,
'assessment_score' => $score,
'assessed_by' => $assessedBy,
'assessment_notes' => $notes,
'effective_date' => $effectiveDate ?? date('Y-m-d'),
]);
$db->update('players', [
'current_level_id' => $toLevelId,
'overall_rating' => $score,
], 'id = ?', [$playerId]);
EventBus::dispatch('player.progression.recorded', [
'player_id' => $playerId,
'type' => $type,
'to_level_id' => $toLevelId,
'score' => $score,
]);
return $id;
}
public static function getPlayerProgressions(int $playerId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT pp.*, sd.name_ar AS discipline_name,
al_from.name_ar AS from_level_name,
al_to.name_ar AS to_level_name
FROM player_progressions pp
LEFT JOIN sport_disciplines sd ON sd.id = pp.discipline_id
LEFT JOIN academy_levels al_from ON al_from.id = pp.from_level_id
LEFT JOIN academy_levels al_to ON al_to.id = pp.to_level_id
WHERE pp.player_id = ?
ORDER BY pp.effective_date DESC, pp.created_at DESC",
[$playerId]
);
}
public static function calculateExpiryDate(string $certificateType, string $issueDate): string
{
$types = self::getCertificateTypes();
$months = $types[$certificateType]['validity_months'] ?? 12;
return date('Y-m-d', strtotime($issueDate . ' +' . $months . ' months'));
}
public static function getExpiringCertificates(int $daysAhead = 30): array
{
$db = App::getInstance()->db();
$targetDate = date('Y-m-d', strtotime('+' . $daysAhead . ' days'));
return $db->select(
"SELECT pmr.*, p.full_name AS player_name, p.phone AS player_phone
FROM player_medical_records pmr
LEFT JOIN players p ON p.id = pmr.player_id
WHERE pmr.expiry_date IS NOT NULL
AND pmr.expiry_date <= ?
AND pmr.expiry_date >= CURDATE()
ORDER BY pmr.expiry_date ASC",
[$targetDate]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
use App\Modules\PoolManagement\Models\PoolBooking;
use App\Modules\PoolManagement\Services\PoolGridService;
use App\Modules\PoolManagement\Services\PoolOccupancyOptimizer;
class PoolBookingController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'pool_config_id' => trim((string) $request->get('pool_config_id', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
'status' => trim((string) $request->get('status', '')),
'booking_type' => trim((string) $request->get('booking_type', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = PoolBooking::search($filters, 25, $page);
return $this->view('PoolManagement.Views.bookings', [
'bookings' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'pools' => PoolConfiguration::allActive(),
'bookingTypes' => PoolConfiguration::getBookingTypes(),
'statuses' => PoolBooking::getStatuses(),
]);
}
public function create(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$date = trim((string) $request->get('date', date('Y-m-d')));
$lanes = $pool->getLanes();
return $this->view('PoolManagement.Views.book', [
'pool' => $pool,
'lanes' => $lanes,
'date' => $date,
'bookingTypes' => PoolConfiguration::getBookingTypes(),
'bookerTypes' => PoolBooking::getBookerTypes(),
]);
}
public function store(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$date = trim((string) $request->post('booking_date', date('Y-m-d')));
$startTime = trim((string) $request->post('start_time', ''));
$endTime = trim((string) $request->post('end_time', ''));
$bookingType = trim((string) $request->post('booking_type', ''));
$selectedLanes = $request->post('lanes', []);
$bookerType = trim((string) $request->post('booker_type', 'member'));
$bookerName = trim((string) $request->post('booker_name', ''));
$swimmers = max(1, (int) $request->post('expected_swimmers', 1));
if (!is_array($selectedLanes)) {
$selectedLanes = [];
}
$selectedLanes = array_map('intval', $selectedLanes);
$errors = [];
if (empty($startTime)) $errors[] = 'وقت البداية مطلوب';
if (empty($endTime)) $errors[] = 'وقت النهاية مطلوب';
if ($startTime >= $endTime) $errors[] = 'وقت البداية يجب أن يكون قبل النهاية';
if (empty($bookingType)) $errors[] = 'نوع الحجز مطلوب';
if (empty($selectedLanes)) $errors[] = 'يجب اختيار حارة واحدة على الأقل';
if ($bookerName === '') $errors[] = 'اسم الحاجز مطلوب';
if (empty($errors)) {
$availability = PoolGridService::checkLaneAvailability((int) $id, $date, $startTime, $endTime, $selectedLanes);
if (!$availability['available']) {
$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('/pool/' . $id . '/book?date=' . $date);
}
$selectionMode = count($selectedLanes) === count($pool->getLanes()) ? 'full_pool' : (count($selectedLanes) === 1 ? 'lane' : 'multi_lane');
$bookingCode = 'PB-' . date('ymd') . '-' . strtoupper(bin2hex(random_bytes(3)));
$booking = PoolBooking::create([
'booking_code' => $bookingCode,
'pool_config_id' => (int) $id,
'booking_date' => $date,
'start_time' => $startTime,
'end_time' => $endTime,
'booking_type' => $bookingType,
'selection_mode' => $selectionMode,
'lanes_json' => json_encode($selectedLanes),
'booker_type' => $bookerType,
'booker_name' => $bookerName,
'expected_swimmers'=> $swimmers,
'unit_rate' => 0,
'total_amount' => 0,
'payment_status' => 'pending',
'status' => 'confirmed',
]);
return $this->redirect('/pool/' . $id . '/grid?date=' . $date)->withSuccess('تم الحجز بنجاح — كود: ' . $bookingCode);
}
public function show(Request $request, string $id): Response
{
$booking = PoolBooking::find((int) $id);
if (!$booking) {
return $this->redirect('/pool/bookings')->withError('الحجز غير موجود');
}
$pool = PoolConfiguration::find((int) $booking->pool_config_id);
return $this->view('PoolManagement.Views.booking_show', [
'booking' => $booking->toArray(),
'poolName' => $pool ? $pool->name_ar : '',
'bookingTypes' => PoolConfiguration::getBookingTypes(),
'statuses' => PoolBooking::getStatuses(),
'bookerTypes' => PoolBooking::getBookerTypes(),
]);
}
public function cancel(Request $request, string $id): Response
{
$booking = PoolBooking::find((int) $id);
if (!$booking) {
return $this->redirect('/pool/bookings')->withError('الحجز غير موجود');
}
$employee = App::getInstance()->currentEmployee();
$booking->update([
'status' => 'cancelled',
'cancelled_by' => $employee ? ($employee->id ?? $employee['id'] ?? null) : null,
'cancelled_at' => date('Y-m-d H:i:s'),
'cancel_reason'=> trim((string) $request->post('cancel_reason', '')),
]);
return $this->redirect('/pool/bookings')->withSuccess('تم إلغاء الحجز');
}
public function checkin(Request $request, string $id): Response
{
$booking = PoolBooking::find((int) $id);
if (!$booking) {
return $this->redirect('/pool/bookings')->withError('الحجز غير موجود');
}
$actualSwimmers = (int) $request->post('actual_swimmers', $booking->expected_swimmers);
$booking->update([
'status' => 'checked_in',
'actual_swimmers' => $actualSwimmers,
]);
return $this->redirect('/pool/' . $booking->pool_config_id . '/grid?date=' . $booking->booking_date)
->withSuccess('تم تسجيل الدخول');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
class PoolConfigController extends Controller
{
public function index(Request $request): Response
{
$pools = PoolConfiguration::allActive();
$db = App::getInstance()->db();
$facilities = $db->select(
"SELECT id, name_ar FROM facilities WHERE facility_type = 'pool' AND is_active = 1 AND is_archived = 0 ORDER BY name_ar"
);
return $this->view('PoolManagement.Views.index', [
'pools' => $pools,
'facilities' => $facilities,
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$facilities = $db->select(
"SELECT id, name_ar FROM facilities WHERE facility_type = 'pool' AND is_active = 1 AND is_archived = 0 ORDER BY name_ar"
);
return $this->view('PoolManagement.Views.config_create', [
'facilities' => $facilities,
]);
}
public function store(Request $request): Response
{
$facilityId = (int) $request->post('facility_id', 0);
$nameAr = trim((string) $request->post('name_ar', ''));
$length = (float) $request->post('length_meters', 0);
$width = (float) $request->post('width_meters', 0);
$totalLanesLength = (int) $request->post('total_lanes_lengthwise', 6);
$errors = [];
if ($facilityId <= 0) $errors[] = 'يجب اختيار المرفق';
if ($nameAr === '') $errors[] = 'اسم الحمام مطلوب';
if ($length <= 0) $errors[] = 'الطول مطلوب';
if ($width <= 0) $errors[] = 'العرض مطلوب';
if ($totalLanesLength < 1) $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('/pool/config/create');
}
$laneWidth = round($width / $totalLanesLength, 2);
$laneWidths = array_fill(0, $totalLanesLength, $laneWidth);
$safetyRatios = [
'lap_swimming' => (int) $request->post('safety_lap', 6),
'lessons' => (int) $request->post('safety_lessons', 4),
'free_swim' => (int) $request->post('safety_free', 8),
'academy_session' => (int) $request->post('safety_academy', 5),
'competition' => (int) $request->post('safety_competition', 4),
'maintenance' => 0,
];
$operatingHours = [
'start' => trim((string) $request->post('op_start', '06:00')),
'end' => trim((string) $request->post('op_end', '22:00')),
'slot_minutes' => (int) $request->post('slot_minutes', 60),
];
$pool = PoolConfiguration::create([
'facility_id' => $facilityId,
'name_ar' => $nameAr,
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'length_meters' => $length,
'width_meters' => $width,
'depth_shallow' => $request->post('depth_shallow', '') !== '' ? (float) $request->post('depth_shallow') : null,
'depth_deep' => $request->post('depth_deep', '') !== '' ? (float) $request->post('depth_deep') : null,
'total_lanes_lengthwise' => $totalLanesLength,
'total_lanes_widthwise' => (int) $request->post('total_lanes_widthwise', 0),
'lane_widths_json' => json_encode($laneWidths),
'max_total_swimmers' => (int) $request->post('max_total_swimmers', 50),
'max_per_lane' => (int) $request->post('max_per_lane', 8),
'safety_ratio_json' => json_encode($safetyRatios),
'operating_hours_json' => json_encode($operatingHours),
'is_active' => 1,
]);
$db = App::getInstance()->db();
for ($i = 1; $i <= $totalLanesLength; $i++) {
$db->insert('pool_lanes', [
'pool_config_id' => (int) $pool->id,
'lane_number' => $i,
'direction' => 'lengthwise',
'width_meters' => $laneWidth,
'label_ar' => 'حارة ' . $i,
'max_swimmers' => (int) $request->post('max_per_lane', 8),
'is_active' => 1,
'sort_order' => $i,
]);
}
return $this->redirect('/pool/' . $pool->id . '/grid')->withSuccess('تم إنشاء تكوين حمام السباحة بنجاح');
}
public function edit(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('التكوين غير موجود');
}
$db = App::getInstance()->db();
$facilities = $db->select(
"SELECT id, name_ar FROM facilities WHERE facility_type = 'pool' AND is_active = 1 AND is_archived = 0 ORDER BY name_ar"
);
return $this->view('PoolManagement.Views.config_edit', [
'pool' => $pool,
'facilities' => $facilities,
'safety' => $pool->getSafetyRatios(),
'hours' => $pool->getOperatingHours(),
]);
}
public function update(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('التكوين غير موجود');
}
$pool->update([
'name_ar' => trim((string) $request->post('name_ar', $pool->name_ar)),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'depth_shallow' => $request->post('depth_shallow', '') !== '' ? (float) $request->post('depth_shallow') : null,
'depth_deep' => $request->post('depth_deep', '') !== '' ? (float) $request->post('depth_deep') : null,
'max_total_swimmers' => (int) $request->post('max_total_swimmers', $pool->max_total_swimmers),
'max_per_lane' => (int) $request->post('max_per_lane', $pool->max_per_lane),
'safety_ratio_json' => json_encode([
'lap_swimming' => (int) $request->post('safety_lap', 6),
'lessons' => (int) $request->post('safety_lessons', 4),
'free_swim' => (int) $request->post('safety_free', 8),
'academy_session' => (int) $request->post('safety_academy', 5),
'competition' => (int) $request->post('safety_competition', 4),
'maintenance' => 0,
]),
'operating_hours_json' => json_encode([
'start' => trim((string) $request->post('op_start', '06:00')),
'end' => trim((string) $request->post('op_end', '22:00')),
'slot_minutes' => (int) $request->post('slot_minutes', 60),
]),
]);
return $this->redirect('/pool/' . $id . '/grid')->withSuccess('تم تحديث التكوين');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\PoolManagement\Models\PoolConfiguration;
use App\Modules\PoolManagement\Services\PoolGridService;
use App\Modules\PoolManagement\Services\PoolOccupancyOptimizer;
class PoolGridController extends Controller
{
public function grid(Request $request, string $id): Response
{
$pool = PoolConfiguration::find((int) $id);
if (!$pool) {
return $this->redirect('/pool')->withError('حمام السباحة غير موجود');
}
$date = trim((string) $request->get('date', date('Y-m-d')));
$gridState = PoolGridService::getGridState((int) $id, $date);
$utilization = PoolOccupancyOptimizer::getUtilizationMetrics((int) $id, $date, $date);
return $this->view('PoolManagement.Views.grid', [
'pool' => $pool,
'gridState' => $gridState,
'utilization' => $utilization,
'date' => $date,
'bookingTypes'=> PoolConfiguration::getBookingTypes(),
]);
}
public function apiState(Request $request, string $id): Response
{
$date = trim((string) $request->get('date', date('Y-m-d')));
$gridState = PoolGridService::getGridState((int) $id, $date);
return $this->json($gridState);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Models;
use App\Core\Model;
class PoolBooking extends Model
{
protected static string $table = 'pool_bookings';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'booking_code',
'pool_config_id',
'booking_date',
'start_time',
'end_time',
'booking_type',
'selection_mode',
'lanes_json',
'booker_type',
'booker_id',
'booker_name',
'expected_swimmers',
'actual_swimmers',
'unit_rate',
'total_amount',
'payment_status',
'payment_id',
'status',
'cancelled_by',
'cancelled_at',
'cancel_reason',
'notes',
];
public function getLanes(): array
{
$raw = $this->lanes_json;
if (empty($raw)) return [];
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
public static function getStatuses(): array
{
return [
'confirmed' => 'مؤكد',
'checked_in' => 'تم الدخول',
'completed' => 'مكتمل',
'cancelled' => 'ملغي',
'no_show' => 'لم يحضر',
];
}
public static function getBookerTypes(): array
{
return [
'member' => 'عضو',
'non_member' => 'غير عضو',
'academy' => 'أكاديمية',
'staff' => 'موظف',
];
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$query = static::query();
if (!empty($filters['pool_config_id'])) {
$query = $query->where('pool_config_id', '=', (int) $filters['pool_config_id']);
}
if (!empty($filters['date_from'])) {
$query = $query->where('booking_date', '>=', $filters['date_from']);
}
if (!empty($filters['date_to'])) {
$query = $query->where('booking_date', '<=', $filters['date_to']);
}
if (!empty($filters['booking_date'])) {
$query = $query->where('booking_date', '=', $filters['booking_date']);
}
if (!empty($filters['status'])) {
$query = $query->where('status', '=', $filters['status']);
}
if (!empty($filters['booking_type'])) {
$query = $query->where('booking_type', '=', $filters['booking_type']);
}
$query = $query->orderBy('booking_date', 'DESC')->orderBy('start_time', 'ASC');
return $query->paginate($perPage, $page);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Models;
use App\Core\Model;
use App\Core\App;
class PoolConfiguration extends Model
{
protected static string $table = 'pool_configurations';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'facility_id',
'name_ar',
'name_en',
'length_meters',
'width_meters',
'depth_shallow',
'depth_deep',
'total_lanes_lengthwise',
'total_lanes_widthwise',
'lane_widths_json',
'max_total_swimmers',
'max_per_lane',
'safety_ratio_json',
'operating_hours_json',
'is_active',
];
public function getLaneWidths(): array
{
$raw = $this->lane_widths_json;
if (empty($raw)) return [];
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
public function getSafetyRatios(): array
{
$raw = $this->safety_ratio_json;
if (empty($raw)) {
return ['lap_swimming' => 6, 'lessons' => 4, 'free_swim' => 8, 'academy_session' => 5, 'competition' => 4, 'maintenance' => 0];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
public function getOperatingHours(): array
{
$raw = $this->operating_hours_json;
if (empty($raw)) {
return ['start' => '06:00', 'end' => '22:00', 'slot_minutes' => 60];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
public function getLanes(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM pool_lanes WHERE pool_config_id = ? AND is_active = 1 ORDER BY sort_order ASC, lane_number ASC",
[(int) $this->id]
);
}
public static function getBookingTypes(): array
{
return [
'lap_swimming' => 'سباحة حرة (لفات)',
'lessons' => 'دروس سباحة',
'free_swim' => 'سباحة ترفيهية',
'academy_session' => 'حصة أكاديمية',
'competition' => 'مسابقة / تدريب فريق',
'maintenance' => 'صيانة',
];
}
public static function allActive(): array
{
return static::query()
->where('is_active', '=', 1)
->get();
}
}
<?php
declare(strict_types=1);
return [
['GET', '/pool', 'PoolManagement\Controllers\PoolConfigController@index', ['auth'], 'pool.view'],
['GET', '/pool/config/create', 'PoolManagement\Controllers\PoolConfigController@create', ['auth'], 'pool.manage'],
['POST', '/pool/config', 'PoolManagement\Controllers\PoolConfigController@store', ['auth', 'csrf'], 'pool.manage'],
['GET', '/pool/config/{id:\d+}/edit', 'PoolManagement\Controllers\PoolConfigController@edit', ['auth'], 'pool.manage'],
['POST', '/pool/config/{id:\d+}', 'PoolManagement\Controllers\PoolConfigController@update', ['auth', 'csrf'], 'pool.manage'],
['GET', '/pool/{id:\d+}/grid', 'PoolManagement\Controllers\PoolGridController@grid', ['auth'], 'pool.view'],
['GET', '/api/pool/{id:\d+}/state', 'PoolManagement\Controllers\PoolGridController@apiState', ['auth'], 'pool.view'],
['GET', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@create', ['auth'], 'pool.book'],
['POST', '/pool/{id:\d+}/book', 'PoolManagement\Controllers\PoolBookingController@store', ['auth', 'csrf'], 'pool.book'],
['GET', '/pool/bookings', 'PoolManagement\Controllers\PoolBookingController@index', ['auth'], 'pool.view'],
['GET', '/pool/bookings/{id:\d+}', 'PoolManagement\Controllers\PoolBookingController@show', ['auth'], 'pool.view'],
['POST', '/pool/bookings/{id:\d+}/cancel','PoolManagement\Controllers\PoolBookingController@cancel', ['auth', 'csrf'], 'pool.manage'],
['POST', '/pool/bookings/{id:\d+}/checkin','PoolManagement\Controllers\PoolBookingController@checkin', ['auth', 'csrf'], 'pool.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Services;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
final class PoolGridService
{
public static function getGridState(int $poolConfigId, string $date, ?string $currentTime = null): array
{
$config = PoolConfiguration::find($poolConfigId);
if (!$config) {
return ['error' => 'Pool not found'];
}
$lanes = $config->getLanes();
$hours = $config->getOperatingHours();
$slotMinutes = (int) ($hours['slot_minutes'] ?? 60);
$startHour = $hours['start'] ?? '06:00';
$endHour = $hours['end'] ?? '22:00';
$timeSlots = self::generateTimeSlots($startHour, $endHour, $slotMinutes);
$db = App::getInstance()->db();
$bookings = $db->select(
"SELECT * FROM pool_bookings
WHERE pool_config_id = ? AND booking_date = ? AND status NOT IN ('cancelled')
ORDER BY start_time ASC",
[$poolConfigId, $date]
);
$cells = [];
foreach ($lanes as $lane) {
$laneNum = (int) $lane['lane_number'];
$cells[$laneNum] = [];
foreach ($timeSlots as $slot) {
$slotStart = $slot['start'];
$slotEnd = $slot['end'];
$cellStatus = 'available';
$cellBooking = null;
foreach ($bookings as $booking) {
$bookedLanes = json_decode($booking['lanes_json'] ?? '[]', true);
if (!is_array($bookedLanes) || !in_array($laneNum, $bookedLanes)) {
continue;
}
if ($booking['start_time'] < $slotEnd && $booking['end_time'] > $slotStart) {
if ($booking['booking_type'] === 'maintenance') {
$cellStatus = 'maintenance';
} elseif ($booking['status'] === 'checked_in') {
$cellStatus = 'in_progress';
} else {
$cellStatus = 'booked';
}
$cellBooking = [
'id' => (int) $booking['id'],
'type' => $booking['booking_type'],
'booker' => $booking['booker_name'] ?? '',
'swimmers' => (int) $booking['expected_swimmers'],
'start' => $booking['start_time'],
'end' => $booking['end_time'],
'status' => $booking['status'],
];
break;
}
}
$cells[$laneNum][$slotStart] = [
'status' => $cellStatus,
'booking' => $cellBooking,
];
}
}
return [
'pool' => [
'id' => (int) $config->id,
'name' => $config->name_ar,
'length' => (float) $config->length_meters,
'width' => (float) $config->width_meters,
],
'lanes' => $lanes,
'time_slots' => $timeSlots,
'cells' => $cells,
'date' => $date,
];
}
public static function checkLaneAvailability(int $poolConfigId, string $date, string $startTime, string $endTime, array $laneNumbers): array
{
$db = App::getInstance()->db();
$bookings = $db->select(
"SELECT id, lanes_json, booking_type, booker_name, start_time, end_time
FROM pool_bookings
WHERE pool_config_id = ? AND booking_date = ? AND status NOT IN ('cancelled')
AND start_time < ? AND end_time > ?",
[$poolConfigId, $date, $endTime, $startTime]
);
$conflicts = [];
foreach ($bookings as $booking) {
$bookedLanes = json_decode($booking['lanes_json'] ?? '[]', true);
$overlap = array_intersect($laneNumbers, is_array($bookedLanes) ? $bookedLanes : []);
if (!empty($overlap)) {
$conflicts[] = [
'booking_id' => (int) $booking['id'],
'lanes' => array_values($overlap),
'type' => $booking['booking_type'],
'booker' => $booking['booker_name'] ?? '',
'time' => $booking['start_time'] . ' - ' . $booking['end_time'],
];
}
}
return [
'available' => empty($conflicts),
'conflicts' => $conflicts,
];
}
private static function generateTimeSlots(string $start, string $end, int $slotMinutes): array
{
$slots = [];
$current = strtotime($start);
$endTs = strtotime($end);
while ($current < $endTs) {
$slotStart = date('H:i', $current);
$next = $current + ($slotMinutes * 60);
$slotEnd = date('H:i', $next);
$slots[] = ['start' => $slotStart, 'end' => $slotEnd];
$current = $next;
}
return $slots;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\PoolManagement\Services;
use App\Core\App;
use App\Modules\PoolManagement\Models\PoolConfiguration;
final class PoolOccupancyOptimizer
{
public static function suggestLanes(
int $poolConfigId,
string $date,
string $startTime,
string $endTime,
string $bookingType,
int $swimmerCount
): array {
$config = PoolConfiguration::find($poolConfigId);
if (!$config) {
return [];
}
$lanes = $config->getLanes();
$safetyRatios = $config->getSafetyRatios();
$maxPerLane = $safetyRatios[$bookingType] ?? (int) $config->max_per_lane;
$lanesNeeded = (int) ceil($swimmerCount / max(1, $maxPerLane));
$availability = PoolGridService::checkLaneAvailability(
$poolConfigId, $date, $startTime, $endTime,
array_column($lanes, 'lane_number')
);
$bookedLaneNumbers = [];
foreach ($availability['conflicts'] as $conflict) {
$bookedLaneNumbers = array_merge($bookedLaneNumbers, $conflict['lanes']);
}
$bookedLaneNumbers = array_unique($bookedLaneNumbers);
$availableLanes = array_filter($lanes, function ($lane) use ($bookedLaneNumbers) {
return !in_array((int) $lane['lane_number'], $bookedLaneNumbers);
});
if (count($availableLanes) < $lanesNeeded) {
return [
'success' => false,
'message' => 'لا تتوفر حارات كافية للعدد المطلوب',
'available' => count($availableLanes),
'needed' => $lanesNeeded,
'suggestions' => [],
];
}
$suggestions = self::rankLaneCombinations(
array_values($availableLanes),
$lanesNeeded,
$bookingType,
$poolConfigId,
$date,
$startTime,
$endTime
);
return [
'success' => true,
'lanes_needed' => $lanesNeeded,
'max_per_lane' => $maxPerLane,
'suggestions' => $suggestions,
];
}
public static function getUtilizationMetrics(int $poolConfigId, string $dateFrom, string $dateTo): array
{
$db = App::getInstance()->db();
$config = PoolConfiguration::find($poolConfigId);
if (!$config) {
return [];
}
$totalLanes = count($config->getLanes());
$hours = $config->getOperatingHours();
$dailySlots = self::countDailySlots($hours['start'] ?? '06:00', $hours['end'] ?? '22:00', (int) ($hours['slot_minutes'] ?? 60));
$totalDays = max(1, (int) ((strtotime($dateTo) - strtotime($dateFrom)) / 86400) + 1);
$totalCapacity = $totalLanes * $dailySlots * $totalDays;
$bookings = $db->selectOne(
"SELECT COUNT(*) AS total_bookings,
SUM(JSON_LENGTH(lanes_json)) AS total_lane_slots,
SUM(expected_swimmers) AS total_swimmers,
SUM(total_amount) AS total_revenue
FROM pool_bookings
WHERE pool_config_id = ? AND booking_date BETWEEN ? AND ?
AND status NOT IN ('cancelled', 'no_show')",
[$poolConfigId, $dateFrom, $dateTo]
);
$occupancyPct = $totalCapacity > 0
? round(((int) ($bookings['total_lane_slots'] ?? 0)) / $totalCapacity * 100, 1)
: 0;
return [
'total_bookings' => (int) ($bookings['total_bookings'] ?? 0),
'total_lane_slots' => (int) ($bookings['total_lane_slots'] ?? 0),
'total_swimmers' => (int) ($bookings['total_swimmers'] ?? 0),
'total_revenue' => (float) ($bookings['total_revenue'] ?? 0),
'total_capacity' => $totalCapacity,
'occupancy_pct' => $occupancyPct,
'avg_swimmers_per_booking' => ($bookings['total_bookings'] ?? 0) > 0
? round((int) ($bookings['total_swimmers'] ?? 0) / (int) $bookings['total_bookings'], 1)
: 0,
];
}
private static function rankLaneCombinations(array $availableLanes, int $needed, string $bookingType, int $poolConfigId, string $date, string $startTime, string $endTime): array
{
if ($needed === 1) {
$suggestions = [];
foreach ($availableLanes as $lane) {
$suggestions[] = [
'lanes' => [(int) $lane['lane_number']],
'score' => self::scoreLane($lane, $bookingType),
'label' => $lane['label_ar'] ?? ('حارة ' . $lane['lane_number']),
];
}
usort($suggestions, fn($a, $b) => $b['score'] <=> $a['score']);
return array_slice($suggestions, 0, 3);
}
$combinations = self::getAdjacentCombinations($availableLanes, $needed);
$scored = [];
foreach ($combinations as $combo) {
$laneNums = array_map(fn($l) => (int) $l['lane_number'], $combo);
$score = 0;
$isAdjacent = true;
sort($laneNums);
for ($i = 1; $i < count($laneNums); $i++) {
if ($laneNums[$i] - $laneNums[$i-1] !== 1) {
$isAdjacent = false;
break;
}
}
if ($isAdjacent) $score += 20;
foreach ($combo as $lane) {
$score += self::scoreLane($lane, $bookingType);
}
$labels = array_map(fn($l) => $l['label_ar'] ?? ('حارة ' . $l['lane_number']), $combo);
$scored[] = [
'lanes' => $laneNums,
'score' => $score,
'label' => implode(' + ', $labels),
'adjacent' => $isAdjacent,
];
}
usort($scored, fn($a, $b) => $b['score'] <=> $a['score']);
return array_slice($scored, 0, 3);
}
private static function scoreLane(array $lane, string $bookingType): int
{
$score = 10;
$width = (float) ($lane['width_meters'] ?? 2.5);
if ($bookingType === 'lessons' && $width >= 2.5) $score += 5;
if ($bookingType === 'competition' && $width >= 2.5) $score += 5;
if ($bookingType === 'lap_swimming') $score += 3;
return $score;
}
private static function getAdjacentCombinations(array $lanes, int $size): array
{
$n = count($lanes);
if ($size > $n) return [];
$combos = [];
for ($i = 0; $i <= $n - $size; $i++) {
$combos[] = array_slice($lanes, $i, $size);
}
return $combos;
}
private static function countDailySlots(string $start, string $end, int $slotMinutes): int
{
$startTs = strtotime($start);
$endTs = strtotime($end);
$diff = $endTs - $startTs;
return max(1, (int) ($diff / ($slotMinutes * 60)));
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجز جديد: <?= e($pool->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= e($date) ?>" class="btn btn-outline"><i data-lucide="grid-3x3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الشبكة</a>
<a href="/pool" 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
$preselectedLane = (int) ($_GET['lane'] ?? 0);
$preselectedStart = $_GET['start'] ?? '';
$preselectedEnd = $_GET['end'] ?? '';
?>
<form method="POST" action="/pool/<?= (int) $pool->id ?>/book" id="bookingForm">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:20px;">
<!-- Left: Lane Selection -->
<div>
<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="grid-3x3" 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="margin-bottom:15px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin-bottom:10px;">
<input type="checkbox" id="selectAll" style="width:18px;height:18px;accent-color:#0D7377;">
<span style="font-size:13px;font-weight:600;">تحديد الحمام بالكامل</span>
</label>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<?php foreach ($lanes as $lane): ?>
<label style="display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 16px;border:2px solid #E5E7EB;border-radius:10px;cursor:pointer;transition:all 0.2s;min-width:70px;" class="lane-option">
<input type="checkbox" name="lanes[]" value="<?= (int) $lane['lane_number'] ?>"
<?= $preselectedLane === (int) $lane['lane_number'] ? 'checked' : '' ?>
style="width:18px;height:18px;accent-color:#0D7377;" class="lane-checkbox">
<span style="font-size:13px;font-weight:600;color:#1A1A2E;"><?= e($lane['label_ar'] ?? ('حارة ' . $lane['lane_number'])) ?></span>
<span style="font-size:10px;color:#9CA3AF;"><?= e($lane['width_meters']) ?>م</span>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<!-- Right: Booking Details -->
<div>
<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" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">تفاصيل الحجز</h3>
</div>
<div style="padding:20px;">
<div class="form-group" style="margin-bottom:12px;">
<label class="form-label">التاريخ <span style="color:#DC2626;">*</span></label>
<input type="date" name="booking_date" value="<?= e($date) ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px;">
<div class="form-group">
<label class="form-label">من <span style="color:#DC2626;">*</span></label>
<input type="time" name="start_time" value="<?= e($preselectedStart ?: old('start_time') ?: '') ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">إلى <span style="color:#DC2626;">*</span></label>
<input type="time" name="end_time" value="<?= e($preselectedEnd ?: old('end_time') ?: '') ?>" class="form-input" required style="direction:ltr;text-align:left;">
</div>
</div>
<div class="form-group" style="margin-bottom:12px;">
<label class="form-label">نوع الحجز <span style="color:#DC2626;">*</span></label>
<select name="booking_type" class="form-select" required>
<option value="">-- اختر --</option>
<?php foreach ($bookingTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('booking_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom:12px;">
<label class="form-label">نوع الحاجز</label>
<select name="booker_type" class="form-select">
<?php foreach ($bookerTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= old('booker_type') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom:12px;">
<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>
</div>
<div class="form-group" style="margin-bottom:12px;">
<label class="form-label">عدد السبّاحين المتوقع</label>
<input type="number" name="expected_swimmers" value="<?= e(old('expected_swimmers') ?? '1') ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-input" rows="2"><?= e(old('notes') ?? '') ?></textarea>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:14px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تأكيد الحجز
</button>
</div>
</div>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var selectAll = document.getElementById('selectAll');
var checkboxes = document.querySelectorAll('.lane-checkbox');
selectAll.addEventListener('change', function() {
checkboxes.forEach(function(cb) { cb.checked = selectAll.checked; updateLaneStyle(cb); });
});
checkboxes.forEach(function(cb) {
cb.addEventListener('change', function() { updateLaneStyle(cb); });
updateLaneStyle(cb);
});
});
function updateLaneStyle(cb) {
var label = cb.closest('.lane-option');
if (cb.checked) {
label.style.borderColor = '#0D7377';
label.style.background = '#F0FDFA';
} else {
label.style.borderColor = '#E5E7EB';
label.style.background = '';
}
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجز: <?= e($booking['booking_code']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<?php if ($booking['status'] === 'confirmed'): ?>
<form method="POST" action="/pool/bookings/<?= (int) $booking['id'] ?>/checkin" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-primary"><i data-lucide="log-in" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تسجيل دخول</button>
</form>
<?php endif; ?>
<?php if (in_array($booking['status'], ['confirmed', 'in_progress'])): ?>
<form method="POST" action="/pool/bookings/<?= (int) $booking['id'] ?>/cancel" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل أنت متأكد من إلغاء الحجز؟')">
<i data-lucide="x" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إلغاء الحجز
</button>
</form>
<?php endif; ?>
<a href="/pool/bookings" 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
$statusColors = ['confirmed' => '#3B82F6', 'in_progress' => '#F59E0B', 'completed' => '#10B981', 'cancelled' => '#EF4444'];
$statusLabels = ['confirmed' => 'مؤكد', 'in_progress' => 'جاري', 'completed' => 'مكتمل', 'cancelled' => 'ملغى'];
$typeLabels = ['lap_swimming' => 'سباحة حرة', 'lessons' => 'دروس', 'free_swim' => 'ترفيهي', 'academy_session' => 'أكاديمية', 'competition' => 'مسابقة', 'maintenance' => 'صيانة'];
$sc = $statusColors[$booking['status']] ?? '#6B7280';
$sl = $statusLabels[$booking['status']] ?? $booking['status'];
$lanes = json_decode($booking['lanes_json'] ?? '[]', true);
?>
<!-- Header Card -->
<div class="card" style="margin-bottom:20px;border-top:4px solid <?= $sc ?>;">
<div style="padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<div>
<h2 style="margin:0;font-size:20px;color:#1A1A2E;"><?= e($booking['booking_code']) ?></h2>
<div style="font-size:13px;color:#6B7280;margin-top:4px;"><?= e($poolName ?? '') ?></div>
</div>
<span style="font-size:13px;padding:4px 14px;border-radius:12px;background:<?= $sc ?>15;color:<?= $sc ?>;font-weight:600;"><?= $sl ?></span>
</div>
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;">
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">التاريخ</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-top:4px;"><?= e($booking['booking_date']) ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">الوقت</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-top:4px;direction:ltr;text-align:right;">
<?= e(substr($booking['start_time'], 0, 5)) ?><?= e(substr($booking['end_time'], 0, 5)) ?>
</div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">نوع الحجز</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-top:4px;"><?= $typeLabels[$booking['booking_type']] ?? $booking['booking_type'] ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;">وضع الاختيار</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-top:4px;">
<?php
$modeLabels = ['full_pool' => 'الحمام كاملاً', 'lane' => 'حارة', 'multi_lane' => 'حارات متعددة', 'cell' => 'خلية'];
echo $modeLabels[$booking['selection_mode']] ?? $booking['selection_mode'];
?>
</div>
</div>
</div>
</div>
</div>
<!-- Lanes & Swimmers -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<i data-lucide="waves" style="width:18px;height:18px;color:#0369A1;"></i>
<h3 style="margin:0;color:#0369A1;font-size:15px;">الحارات المحجوزة</h3>
</div>
<?php if (!empty($lanes)): ?>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<?php foreach ($lanes as $lane): ?>
<div style="width:42px;height:42px;border-radius:8px;background:#DBEAFE;border:2px solid #3B82F6;display:flex;align-items:center;justify-content:center;font-weight:700;color:#1E40AF;font-size:14px;">
<?= (int) $lane ?>
</div>
<?php endforeach; ?>
</div>
<div style="font-size:12px;color:#6B7280;margin-top:8px;"><?= count($lanes) ?> حارة محجوزة</div>
<?php else: ?>
<div style="color:#9CA3AF;font-size:13px;">الحمام بالكامل</div>
<?php endif; ?>
</div>
<div class="card" style="padding:20px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<i data-lucide="users" style="width:18px;height:18px;color:#059669;"></i>
<h3 style="margin:0;color:#059669;font-size:15px;">معلومات إضافية</h3>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
<div>
<div style="font-size:11px;color:#6B7280;">عدد السبّاحين المتوقع</div>
<div style="font-size:16px;font-weight:600;color:#1A1A2E;margin-top:2px;"><?= (int) ($booking['expected_swimmers'] ?? 0) ?></div>
</div>
<div>
<div style="font-size:11px;color:#6B7280;">نوع الحاجز</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;margin-top:2px;">
<?php
$bookerLabels = ['player' => 'لاعب', 'academy' => 'أكاديمية', 'corporate' => 'مؤسسة', 'walkin' => 'حضور مباشر'];
echo $bookerLabels[$booking['booker_type']] ?? $booking['booker_type'];
?>
</div>
</div>
<?php if (!empty($booking['rate_per_lane'])): ?>
<div>
<div style="font-size:11px;color:#6B7280;">السعر/حارة</div>
<div style="font-size:14px;font-weight:600;color:#059669;margin-top:2px;"><?= number_format((float) $booking['rate_per_lane'], 0) ?> ج.م</div>
</div>
<?php endif; ?>
<?php if (!empty($booking['total_amount'])): ?>
<div>
<div style="font-size:11px;color:#6B7280;">الإجمالي</div>
<div style="font-size:14px;font-weight:600;color:#059669;margin-top:2px;"><?= number_format((float) $booking['total_amount'], 0) ?> ج.م</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php if (!empty($booking['notes'])): ?>
<div class="card" style="margin-bottom:20px;padding:15px 20px;">
<div style="font-size:12px;color:#6B7280;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:13px;color:#1A1A2E;"><?= e($booking['notes']) ?></div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حجوزات حمام السباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool" 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'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px 20px;">
<form method="GET" action="/pool/bookings" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">من تاريخ</label>
<input type="date" name="date_from" value="<?= e($filters['date_from'] ?? '') ?>" class="form-input" style="width:150px;direction:ltr;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">إلى تاريخ</label>
<input type="date" name="date_to" value="<?= e($filters['date_to'] ?? '') ?>" class="form-input" style="width:150px;direction:ltr;">
</div>
<div class="form-group" style="margin:0;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-select" style="width:130px;">
<option value="">الكل</option>
<option value="confirmed" <?= ($filters['status'] ?? '') === 'confirmed' ? 'selected' : '' ?>>مؤكد</option>
<option value="in_progress" <?= ($filters['status'] ?? '') === 'in_progress' ? 'selected' : '' ?>>جاري</option>
<option value="completed" <?= ($filters['status'] ?? '') === 'completed' ? 'selected' : '' ?>>مكتمل</option>
<option value="cancelled" <?= ($filters['status'] ?? '') === 'cancelled' ? 'selected' : '' ?>>ملغى</option>
</select>
</div>
<button type="submit" class="btn btn-primary" style="height:38px;font-size:13px;">
<i data-lucide="search" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> بحث
</button>
</form>
</div>
<!-- Bookings Table -->
<div class="card">
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#374151;">الكود</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#374151;">التاريخ</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#374151;">الوقت</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#374151;">النوع</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#374151;">الحارات</th>
<th style="padding:10px 15px;text-align:right;font-weight:600;color:#374151;">السبّاحين</th>
<th style="padding:10px 15px;text-align:center;font-weight:600;color:#374151;">الحالة</th>
<th style="padding:10px 15px;text-align:center;font-weight:600;color:#374151;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php if (empty($bookings)): ?>
<tr><td colspan="8" style="padding:30px;text-align:center;color:#9CA3AF;">لا توجد حجوزات</td></tr>
<?php else: ?>
<?php
$statusColors = ['confirmed' => '#3B82F6', 'in_progress' => '#F59E0B', 'completed' => '#10B981', 'cancelled' => '#EF4444'];
$statusLabels = ['confirmed' => 'مؤكد', 'in_progress' => 'جاري', 'completed' => 'مكتمل', 'cancelled' => 'ملغى'];
$typeLabels = ['lap_swimming' => 'سباحة حرة', 'lessons' => 'دروس', 'free_swim' => 'ترفيهي', 'academy_session' => 'أكاديمية', 'competition' => 'مسابقة', 'maintenance' => 'صيانة'];
?>
<?php foreach ($bookings as $b):
$sc = $statusColors[$b['status']] ?? '#6B7280';
$sl = $statusLabels[$b['status']] ?? $b['status'];
$lanes = json_decode($b['lanes_json'] ?? '[]', true);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 15px;font-weight:600;color:#0D7377;">
<a href="/pool/bookings/<?= (int) $b['id'] ?>" style="color:#0D7377;text-decoration:none;"><?= e($b['booking_code']) ?></a>
</td>
<td style="padding:10px 15px;"><?= e($b['booking_date']) ?></td>
<td style="padding:10px 15px;direction:ltr;text-align:right;"><?= e(substr($b['start_time'], 0, 5)) ?><?= e(substr($b['end_time'], 0, 5)) ?></td>
<td style="padding:10px 15px;"><?= $typeLabels[$b['booking_type']] ?? $b['booking_type'] ?></td>
<td style="padding:10px 15px;">
<?php if (!empty($lanes)): ?>
<span style="font-size:11px;background:#E0F2FE;color:#0369A1;padding:2px 6px;border-radius:4px;"><?= implode(', ', $lanes) ?></span>
<?php else: ?>
<span style="color:#9CA3AF;"></span>
<?php endif; ?>
</td>
<td style="padding:10px 15px;text-align:center;"><?= (int) ($b['expected_swimmers'] ?? 0) ?></td>
<td style="padding:10px 15px;text-align:center;">
<span style="font-size:11px;padding:2px 8px;border-radius:8px;background:<?= $sc ?>15;color:<?= $sc ?>;font-weight:600;"><?= $sl ?></span>
</td>
<td style="padding:10px 15px;text-align:center;">
<a href="/pool/bookings/<?= (int) $b['id'] ?>" style="color:#0D7377;font-size:12px;">عرض</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php if (!empty($pagination) && $pagination['last_page'] > 1): ?>
<div style="padding:15px 20px;border-top:1px solid #E5E7EB;display:flex;justify-content:center;gap:5px;">
<?php for ($p = 1; $p <= $pagination['last_page']; $p++): ?>
<a href="?page=<?= $p ?>&<?= http_build_query(array_filter($filters)) ?>"
style="padding:6px 12px;border-radius:6px;font-size:12px;text-decoration:none;
<?= $p === $pagination['current_page'] ? 'background:#0D7377;color:white;' : 'background:#F3F4F6;color:#374151;' ?>">
<?= $p ?>
</a>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إضافة حمام سباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool" 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="/pool/config" id="poolConfigForm">
<?= csrf_field() ?>
<!-- Basic Info -->
<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="waves" 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>
<select name="facility_id" class="form-select" 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>
<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" style="direction:ltr;text-align:left;" placeholder="Olympic Pool">
</div>
</div>
</div>
</div>
<!-- Dimensions -->
<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="ruler" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">الأبعاد والحارات</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">الطول (متر) <span style="color:#DC2626;">*</span></label>
<input type="number" name="length_meters" value="<?= e(old('length_meters') ?? '50') ?>" class="form-input" min="5" max="100" step="0.5" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العرض (متر) <span style="color:#DC2626;">*</span></label>
<input type="number" name="width_meters" value="<?= e(old('width_meters') ?? '25') ?>" class="form-input" min="3" max="50" step="0.5" required style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العمق الضحل (متر)</label>
<input type="number" name="depth_shallow" value="<?= e(old('depth_shallow') ?? '1.2') ?>" class="form-input" min="0.5" max="5" step="0.1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العمق العميق (متر)</label>
<input type="number" name="depth_deep" value="<?= e(old('depth_deep') ?? '2.0') ?>" class="form-input" min="0.5" max="10" step="0.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>
<input type="number" name="total_lanes_lengthwise" value="<?= e(old('total_lanes_lengthwise') ?? '6') ?>" class="form-input" min="1" max="12" required style="direction:ltr;text-align:left;" id="laneCount">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى الإجمالي</label>
<input type="number" name="max_total_swimmers" value="<?= e(old('max_total_swimmers') ?? '50') ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">حد أقصى لكل حارة</label>
<input type="number" name="max_per_lane" value="<?= e(old('max_per_lane') ?? '8') ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
</div>
<!-- Visual Preview -->
<div style="margin-top:20px;padding:15px;background:#F0FDFA;border-radius:8px;border:1px solid #CCFBF1;">
<div style="font-size:12px;color:#059669;margin-bottom:8px;font-weight:600;">معاينة تقريبية:</div>
<div id="lanePreview" style="display:flex;gap:3px;height:50px;"></div>
</div>
</div>
</div>
<!-- Safety Ratios -->
<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:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">حدود الأمان (أقصى عدد سبّاحين/حارة)</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:15px;">
<div class="form-group">
<label class="form-label" style="font-size:12px;">سباحة حرة</label>
<input type="number" name="safety_lap" value="6" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">دروس</label>
<input type="number" name="safety_lessons" value="4" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">ترفيهي</label>
<input type="number" name="safety_free" value="8" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">أكاديمية</label>
<input type="number" name="safety_academy" value="5" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">مسابقة</label>
<input type="number" name="safety_competition" value="4" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Operating Hours -->
<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="clock" 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="time" name="op_start" value="06:00" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">وقت الإغلاق</label>
<input type="time" name="op_end" value="22:00" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">مدة الفترة (دقيقة)</label>
<select name="slot_minutes" class="form-select">
<option value="30">30 دقيقة</option>
<option value="60" selected>60 دقيقة (ساعة)</option>
<option value="90">90 دقيقة</option>
<option value="120">120 دقيقة (ساعتين)</option>
</select>
</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;">
<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="/pool" 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();
updateLanePreview();
document.getElementById('laneCount').addEventListener('input', updateLanePreview);
});
function updateLanePreview() {
var count = parseInt(document.getElementById('laneCount').value) || 6;
var container = document.getElementById('lanePreview');
container.innerHTML = '';
for (var i = 1; i <= count; i++) {
var div = document.createElement('div');
div.style.cssText = 'flex:1;background:#CCFBF1;border:1px dashed #0D737740;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;color:#0D7377;';
div.textContent = i;
container.appendChild(div);
}
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل حمام السباحة: <?= e($pool->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/<?= (int) $pool->id ?>/grid" class="btn btn-outline"><i data-lucide="grid-3x3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الشبكة</a>
<a href="/pool" 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="/pool/config/<?= (int) $pool->id ?>">
<?= csrf_field() ?>
<!-- Basic Info -->
<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="waves" 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">المرفق</label>
<select name="facility_id" class="form-select" disabled>
<?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= (int) $pool->facility_id === (int) $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
<?php endforeach; ?>
</select>
<div style="font-size:11px;color:#9CA3AF;margin-top:4px;">لا يمكن تغيير المرفق بعد الإنشاء</div>
</div>
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="name_ar" value="<?= e($pool->name_ar) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e($pool->name_en ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Dimensions (read-only info + editable parts) -->
<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="ruler" style="width:18px;height:18px;color:#2563EB;"></i>
<h3 style="margin:0;color:#2563EB;font-size:15px;">الأبعاد</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:20px;">
<div class="form-group">
<label class="form-label">الطول (متر)</label>
<input type="text" value="<?= e((string) $pool->length_meters) ?>" class="form-input" disabled style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العرض (متر)</label>
<input type="text" value="<?= e((string) $pool->width_meters) ?>" class="form-input" disabled style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العمق الضحل (متر)</label>
<input type="number" name="depth_shallow" value="<?= e((string) ($pool->depth_shallow ?? '')) ?>" class="form-input" min="0.5" max="5" step="0.1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">العمق العميق (متر)</label>
<input type="number" name="depth_deep" value="<?= e((string) ($pool->depth_deep ?? '')) ?>" class="form-input" min="0.5" max="10" step="0.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">عدد الحارات</label>
<input type="text" value="<?= (int) $pool->total_lanes_lengthwise ?>" class="form-input" disabled style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى الإجمالي</label>
<input type="number" name="max_total_swimmers" value="<?= (int) $pool->max_total_swimmers ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">حد أقصى لكل حارة</label>
<input type="number" name="max_per_lane" value="<?= (int) $pool->max_per_lane ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Safety Ratios -->
<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:#DC2626;"></i>
<h3 style="margin:0;color:#DC2626;font-size:15px;">حدود الأمان (أقصى عدد سبّاحين/حارة)</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:repeat(5, 1fr);gap:15px;">
<div class="form-group">
<label class="form-label" style="font-size:12px;">سباحة حرة</label>
<input type="number" name="safety_lap" value="<?= (int) ($safety['lap_swimming'] ?? 6) ?>" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">دروس</label>
<input type="number" name="safety_lessons" value="<?= (int) ($safety['lessons'] ?? 4) ?>" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">ترفيهي</label>
<input type="number" name="safety_free" value="<?= (int) ($safety['free_swim'] ?? 8) ?>" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">أكاديمية</label>
<input type="number" name="safety_academy" value="<?= (int) ($safety['academy_session'] ?? 5) ?>" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">مسابقة</label>
<input type="number" name="safety_competition" value="<?= (int) ($safety['competition'] ?? 4) ?>" class="form-input" min="1" max="20" style="direction:ltr;text-align:left;">
</div>
</div>
</div>
</div>
<!-- Operating Hours -->
<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="clock" 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="time" name="op_start" value="<?= e($hours['start'] ?? '06:00') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">وقت الإغلاق</label>
<input type="time" name="op_end" value="<?= e($hours['end'] ?? '22:00') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">مدة الفترة (دقيقة)</label>
<select name="slot_minutes" class="form-select">
<option value="30" <?= (int) ($hours['slot_minutes'] ?? 60) === 30 ? 'selected' : '' ?>>30 دقيقة</option>
<option value="60" <?= (int) ($hours['slot_minutes'] ?? 60) === 60 ? 'selected' : '' ?>>60 دقيقة (ساعة)</option>
<option value="90" <?= (int) ($hours['slot_minutes'] ?? 60) === 90 ? 'selected' : '' ?>>90 دقيقة</option>
<option value="120" <?= (int) ($hours['slot_minutes'] ?? 60) === 120 ? 'selected' : '' ?>>120 دقيقة (ساعتين)</option>
</select>
</div>
</div>
</div>
</div>
<!-- Submit -->
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:15px;">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ التعديلات
</button>
<a href="/pool/<?= (int) $pool->id ?>/grid" 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($pool->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/<?= (int) $pool->id ?>/book?date=<?= e($date) ?>" class="btn btn-primary"><i data-lucide="plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> حجز جديد</a>
<a href="/pool/config/<?= (int) $pool->id ?>/edit" class="btn btn-outline"><i data-lucide="settings" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> الإعدادات</a>
<a href="/pool" 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
$lanes = $gridState['lanes'] ?? [];
$timeSlots = $gridState['time_slots'] ?? [];
$cells = $gridState['cells'] ?? [];
$statusColors = [
'available' => '#10B981',
'booked' => '#3B82F6',
'in_progress' => '#F59E0B',
'maintenance' => '#EF4444',
];
?>
<!-- Date Selector + Stats -->
<div style="display:flex;gap:15px;margin-bottom:20px;align-items:stretch;">
<!-- Date Navigation -->
<div class="card" style="padding:15px;display:flex;align-items:center;gap:10px;flex:0 0 auto;">
<?php
$prevDate = date('Y-m-d', strtotime($date . ' -1 day'));
$nextDate = date('Y-m-d', strtotime($date . ' +1 day'));
?>
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= $prevDate ?>" class="btn btn-sm btn-outline">&rarr;</a>
<form method="GET" action="/pool/<?= (int) $pool->id ?>/grid" style="display:flex;gap:8px;">
<input type="date" name="date" value="<?= e($date) ?>" class="form-input" style="direction:ltr;font-size:13px;" onchange="this.form.submit()">
</form>
<a href="/pool/<?= (int) $pool->id ?>/grid?date=<?= $nextDate ?>" class="btn btn-sm btn-outline">&larr;</a>
<a href="/pool/<?= (int) $pool->id ?>/grid" class="btn btn-sm btn-outline">اليوم</a>
</div>
<!-- Stats -->
<div class="card" style="padding:15px;flex:1;display:flex;gap:25px;align-items:center;">
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#0D7377;"><?= $utilization['occupancy_pct'] ?? 0 ?>%</div>
<div style="font-size:11px;color:#6B7280;">الإشغال</div>
</div>
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#2563EB;"><?= $utilization['total_bookings'] ?? 0 ?></div>
<div style="font-size:11px;color:#6B7280;">حجز</div>
</div>
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#D97706;"><?= $utilization['total_swimmers'] ?? 0 ?></div>
<div style="font-size:11px;color:#6B7280;">سبّاح</div>
</div>
<div style="text-align:center;">
<div style="font-size:20px;font-weight:700;color:#059669;"><?= number_format($utilization['total_revenue'] ?? 0, 0) ?></div>
<div style="font-size:11px;color:#6B7280;">إيرادات (ج.م)</div>
</div>
</div>
<!-- Legend -->
<div class="card" style="padding:15px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;">
<?php foreach ($statusColors as $st => $color): ?>
<span style="display:flex;align-items:center;gap:4px;font-size:11px;">
<span style="width:14px;height:14px;border-radius:3px;background:<?= $color ?>;display:inline-block;"></span>
<?= ['available'=>'متاح','booked'=>'محجوز','in_progress'=>'جاري','maintenance'=>'صيانة'][$st] ?>
</span>
<?php endforeach; ?>
</div>
</div>
<!-- Pool Grid -->
<div class="card" style="padding:0;overflow:auto;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="grid-3x3" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">شبكة الحارات × الأوقات</h3>
<span style="font-size:12px;color:#9CA3AF;margin-right:auto;"><?= e($pool->length_meters) ?>م × <?= e($pool->width_meters) ?>م — <?= count($lanes) ?> حارة</span>
</div>
<div style="overflow-x:auto;padding:10px;">
<table style="width:100%;border-collapse:collapse;min-width:<?= count($timeSlots) * 70 + 100 ?>px;" id="poolGrid">
<thead>
<tr>
<th style="padding:8px;font-size:12px;color:#6B7280;text-align:right;position:sticky;right:0;background:#fff;z-index:2;border-bottom:2px solid #E5E7EB;min-width:90px;">
الحارة
</th>
<?php foreach ($timeSlots as $slot): ?>
<th style="padding:6px 4px;font-size:11px;color:#6B7280;text-align:center;border-bottom:2px solid #E5E7EB;min-width:65px;direction:ltr;">
<?= e($slot['start']) ?>
</th>
<?php endforeach; ?>
</tr>
</thead>
<tbody>
<?php foreach ($lanes as $lane):
$laneNum = (int) $lane['lane_number'];
?>
<tr>
<td style="padding:8px;font-size:12px;font-weight:600;color:#1A1A2E;position:sticky;right:0;background:#fff;z-index:1;border-bottom:1px solid #F3F4F6;">
<?= e($lane['label_ar'] ?? ('حارة ' . $laneNum)) ?>
<div style="font-size:10px;color:#9CA3AF;font-weight:normal;"><?= e($lane['width_meters']) ?>م</div>
</td>
<?php foreach ($timeSlots as $slot):
$slotStart = $slot['start'];
$cell = $cells[$laneNum][$slotStart] ?? ['status' => 'available', 'booking' => null];
$color = $statusColors[$cell['status']] ?? '#D1D5DB';
$booking = $cell['booking'];
?>
<td style="padding:3px;border-bottom:1px solid #F3F4F6;"
data-lane="<?= $laneNum ?>" data-slot="<?= e($slotStart) ?>" data-status="<?= e($cell['status']) ?>">
<div class="grid-cell" style="
height:40px;
border-radius:6px;
background:<?= $color ?>15;
border:2px solid <?= $color ?>40;
cursor:<?= $cell['status'] === 'available' ? 'pointer' : 'default' ?>;
display:flex;
align-items:center;
justify-content:center;
font-size:10px;
color:<?= $color ?>;
font-weight:600;
position:relative;
transition:all 0.15s;
"
<?php if ($booking): ?>
title="<?= e(($bookingTypes[$booking['type']] ?? '') . ' — ' . ($booking['booker'] ?? '') . ' (' . $booking['swimmers'] . ' سبّاح)') ?>"
<?php else: ?>
title="متاح — اضغط للحجز"
onclick="quickBook(<?= $laneNum ?>, '<?= e($slotStart) ?>', '<?= e($slot['end']) ?>')"
<?php endif; ?>
>
<?php if ($booking): ?>
<?php if ($cell['status'] === 'in_progress'): ?>
<i data-lucide="activity" style="width:12px;height:12px;"></i>
<?php elseif ($cell['status'] === 'maintenance'): ?>
<i data-lucide="wrench" style="width:12px;height:12px;"></i>
<?php else: ?>
<?= $booking['swimmers'] ?>
<?php endif; ?>
<?php endif; ?>
</div>
</td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- Pool Dimensions Visual -->
<div class="card" style="margin-top:20px;padding:20px;">
<h4 style="margin:0 0 10px;font-size:14px;color:#6B7280;">المخطط التقريبي</h4>
<div style="position:relative;width:100%;max-width:600px;margin:0 auto;">
<div style="border:3px solid #0D7377;border-radius:12px;padding:10px;background:#F0FDFA;">
<div style="display:flex;gap:2px;">
<?php foreach ($lanes as $lane): ?>
<div style="flex:1;height:60px;background:#CCFBF1;border:1px dashed #0D737740;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;color:#0D7377;">
<?= (int) $lane['lane_number'] ?>
</div>
<?php endforeach; ?>
</div>
<div style="text-align:center;margin-top:8px;font-size:11px;color:#6B7280;">
<?= e($pool->length_meters) ?>م (طول) × <?= e($pool->width_meters) ?>م (عرض)
<?php if ($pool->depth_shallow || $pool->depth_deep): ?>
— العمق: <?= e($pool->depth_shallow ?? '?') ?>م إلى <?= e($pool->depth_deep ?? '?') ?>م
<?php endif; ?>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
// Auto-refresh every 30 seconds
setInterval(function() {
fetch('/api/pool/<?= (int) $pool->id ?>/state?date=<?= e($date) ?>')
.then(r => r.json())
.then(data => updateGrid(data))
.catch(() => {});
}, 30000);
});
function updateGrid(data) {
var cells = data.cells || {};
var statusColors = {available:'#10B981', booked:'#3B82F6', in_progress:'#F59E0B', maintenance:'#EF4444'};
document.querySelectorAll('#poolGrid td[data-lane]').forEach(function(td) {
var lane = parseInt(td.getAttribute('data-lane'));
var slot = td.getAttribute('data-slot');
if (cells[lane] && cells[lane][slot]) {
var cell = cells[lane][slot];
var color = statusColors[cell.status] || '#D1D5DB';
var div = td.querySelector('.grid-cell');
if (div) {
div.style.background = color + '15';
div.style.borderColor = color + '40';
div.style.color = color;
td.setAttribute('data-status', cell.status);
}
}
});
}
function quickBook(lane, startTime, endTime) {
window.location.href = '/pool/<?= (int) $pool->id ?>/book?date=<?= e($date) ?>&lane=' + lane + '&start=' + startTime + '&end=' + endTime;
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>حمام السباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/pool/config/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إضافة حمام سباحة</a>
<a href="/pool/bookings" class="btn btn-outline"><i data-lucide="calendar" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> جميع الحجوزات</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php if (!empty($pools)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(340px, 1fr));gap:20px;">
<?php foreach ($pools as $p): ?>
<div class="card" style="overflow:hidden;">
<a href="/pool/<?= (int) $p['id'] ?>/grid" style="text-decoration:none;color:inherit;display:block;">
<div style="padding:20px;display:flex;align-items:start;gap:15px;">
<div style="width:56px;height:56px;border-radius:12px;background:linear-gradient(135deg, #0D737715, #0D737730);display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="waves" style="width:28px;height:28px;color:#0D7377;"></i>
</div>
<div style="flex:1;">
<h3 style="margin:0 0 6px;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($p['name_ar']) ?></h3>
<div style="display:flex;gap:12px;font-size:12px;color:#6B7280;">
<span><?= e($p['length_meters']) ?>م × <?= e($p['width_meters']) ?>م</span>
<span><?= (int) $p['total_lanes_lengthwise'] ?> حارة</span>
<span>حد أقصى: <?= (int) $p['max_total_swimmers'] ?> سبّاح</span>
</div>
</div>
</div>
<div style="padding:0 20px 15px;display:flex;gap:10px;">
<span class="btn btn-sm btn-primary" style="font-size:12px;padding:5px 12px;">
<i data-lucide="grid-3x3" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> فتح الشبكة
</span>
<a href="/pool/<?= (int) $p['id'] ?>/book" class="btn btn-sm btn-outline" style="font-size:12px;padding:5px 12px;">
<i data-lucide="plus" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> حجز
</a>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<i data-lucide="waves" style="width:48px;height:48px;color:#D1D5DB;margin-bottom:15px;"></i>
<h3 style="color:#6B7280;margin:0 0 8px;">لا يوجد حمامات سباحة</h3>
<p style="color:#9CA3AF;font-size:14px;margin:0 0 20px;">أضف مرفق من نوع "حمام سباحة" أولاً، ثم قم بتكوينه هنا.</p>
<a href="/pool/config/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;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('pool_management', [
'pool.view' => ['ar' => 'عرض حمام السباحة', 'en' => 'View Pool'],
'pool.manage' => ['ar' => 'إدارة حمام السباحة', 'en' => 'Manage Pool'],
'pool.book' => ['ar' => 'حجز حمام السباحة', 'en' => 'Book Pool'],
]);
MenuRegistry::register('pool_management', [
'label_ar' => 'حمام السباحة',
'icon' => 'waves',
'route' => '/pool',
'permission' => 'pool.view',
'order' => 401,
'parent' => 'sports_activities',
]);
<?php
declare(strict_types=1);
namespace App\Modules\Reports\Services;
use App\Core\App;
final class AcademyRevenueReportService
{
/**
* Generate academy revenue report: actual vs minimum guarantee, surplus/shortfall, trends.
*
* @param string $periodFrom Start date (YYYY-MM-DD)
* @param string $periodTo End date (YYYY-MM-DD)
* @param int|null $academyId Filter by specific academy (null = all)
* @return array Report data
*/
public static function generate(string $periodFrom, string $periodTo, ?int $academyId = null): array
{
$db = App::getInstance()->db();
// Get academies with their contract details
$params = [$periodFrom, $periodTo];
$academyFilter = '';
if ($academyId !== null) {
$academyFilter = ' AND a.id = ?';
$params[] = $academyId;
}
$academies = $db->select(
"SELECT a.id, a.name_ar, a.code,
ac.minimum_guarantee, ac.revenue_share_percent, ac.start_date, ac.end_date
FROM academies a
LEFT JOIN academy_contracts ac ON ac.academy_id = a.id AND ac.status = 'active'
WHERE a.is_archived = 0{$academyFilter}
ORDER BY a.name_ar ASC",
$academyId !== null ? [$academyId] : []
);
$results = [];
$totals = [
'total_revenue' => 0,
'total_guarantee' => 0,
'total_surplus' => 0,
'total_shortfall' => 0,
'academy_count' => 0,
];
foreach ($academies as $academy) {
$aId = (int) $academy['id'];
// Calculate actual revenue from payments
$revenue = $db->selectOne(
"SELECT COALESCE(SUM(p.amount), 0) AS total_revenue
FROM payments p
WHERE p.payable_type = 'academy'
AND p.payable_id = ?
AND p.payment_date BETWEEN ? AND ?
AND p.status = 'completed'",
[$aId, $periodFrom, $periodTo]
);
$actualRevenue = (float) ($revenue['total_revenue'] ?? 0);
$minimumGuarantee = (float) ($academy['minimum_guarantee'] ?? 0);
// Calculate months in period for prorating guarantee
$monthsInPeriod = self::monthsBetween($periodFrom, $periodTo);
$proratedGuarantee = $minimumGuarantee * $monthsInPeriod;
$surplus = max(0, $actualRevenue - $proratedGuarantee);
$shortfall = max(0, $proratedGuarantee - $actualRevenue);
// Monthly trend
$monthlyRevenue = $db->select(
"SELECT DATE_FORMAT(p.payment_date, '%Y-%m') AS month,
COALESCE(SUM(p.amount), 0) AS revenue
FROM payments p
WHERE p.payable_type = 'academy'
AND p.payable_id = ?
AND p.payment_date BETWEEN ? AND ?
AND p.status = 'completed'
GROUP BY DATE_FORMAT(p.payment_date, '%Y-%m')
ORDER BY month ASC",
[$aId, $periodFrom, $periodTo]
);
$results[] = [
'academy_id' => $aId,
'academy_name' => $academy['name_ar'],
'academy_code' => $academy['code'],
'actual_revenue' => $actualRevenue,
'minimum_guarantee' => $proratedGuarantee,
'surplus' => $surplus,
'shortfall' => $shortfall,
'revenue_share_percent' => (float) ($academy['revenue_share_percent'] ?? 0),
'monthly_trend' => $monthlyRevenue,
'contract_start' => $academy['start_date'],
'contract_end' => $academy['end_date'],
];
$totals['total_revenue'] += $actualRevenue;
$totals['total_guarantee'] += $proratedGuarantee;
$totals['total_surplus'] += $surplus;
$totals['total_shortfall'] += $shortfall;
$totals['academy_count']++;
}
return [
'period_from' => $periodFrom,
'period_to' => $periodTo,
'academies' => $results,
'totals' => $totals,
];
}
/**
* Calculate approximate months between two dates.
*/
private static function monthsBetween(string $from, string $to): float
{
$d1 = new \DateTime($from);
$d2 = new \DateTime($to);
$diff = $d1->diff($d2);
return $diff->m + ($diff->y * 12) + ($diff->d / 30);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Reports\Services;
use App\Core\App;
final class CoachUtilizationReportService
{
/**
* Generate coach utilization report: sessions, players, hours, revenue.
*
* @param string $periodFrom Start date (YYYY-MM-DD)
* @param string $periodTo End date (YYYY-MM-DD)
* @param int|null $coachId Filter by specific coach (null = all)
* @return array Report data
*/
public static function generate(string $periodFrom, string $periodTo, ?int $coachId = null): array
{
$db = App::getInstance()->db();
$params = [$periodFrom, $periodTo];
$coachFilter = '';
if ($coachId !== null) {
$coachFilter = ' AND c.id = ?';
$params[] = $coachId;
}
$coaches = $db->select(
"SELECT c.id, c.full_name_ar, c.code, c.payment_model, c.session_rate, c.hourly_rate, c.monthly_rate
FROM coaches c
WHERE c.is_active = 1 AND c.is_archived = 0{$coachFilter}
ORDER BY c.full_name_ar ASC",
$coachId !== null ? [$coachId] : []
);
$results = [];
$totals = [
'total_sessions' => 0,
'total_players_served' => 0,
'total_hours' => 0,
'total_revenue' => 0,
'coach_count' => 0,
];
foreach ($coaches as $coach) {
$cId = (int) $coach['id'];
// Sessions conducted
$sessionData = $db->selectOne(
"SELECT COUNT(*) AS session_count,
COALESCE(SUM(TIMESTAMPDIFF(MINUTE, ts.start_time, ts.end_time)), 0) AS total_minutes
FROM training_sessions ts
WHERE ts.coach_id = ?
AND ts.session_date BETWEEN ? AND ?
AND ts.status = 'completed'",
[$cId, $periodFrom, $periodTo]
);
$sessionsConducted = (int) ($sessionData['session_count'] ?? 0);
$totalMinutes = (int) ($sessionData['total_minutes'] ?? 0);
$totalHours = round($totalMinutes / 60, 1);
// Players served
$playerData = $db->selectOne(
"SELECT COALESCE(SUM(sa_count.cnt), 0) AS total_players,
COUNT(DISTINCT sa_count.player_id) AS unique_players
FROM (
SELECT sa.player_id, COUNT(*) AS cnt
FROM session_attendance sa
INNER JOIN training_sessions ts ON ts.id = sa.session_id
WHERE ts.coach_id = ?
AND ts.session_date BETWEEN ? AND ?
AND ts.status = 'completed'
AND sa.status = 'present'
GROUP BY sa.player_id
) sa_count",
[$cId, $periodFrom, $periodTo]
);
$totalPlayersServed = (int) ($playerData['total_players'] ?? 0);
$uniquePlayers = (int) ($playerData['unique_players'] ?? 0);
// Revenue attributed (from coach_payments if exists)
$revenueData = $db->selectOne(
"SELECT COALESCE(SUM(cp.net_amount), 0) AS total_paid
FROM coach_payments cp
WHERE cp.coach_id = ?
AND cp.payment_period >= ?
AND cp.payment_period <= ?
AND cp.status IN ('approved', 'paid')",
[$cId, substr($periodFrom, 0, 7), substr($periodTo, 0, 7)]
);
$revenueAttributed = (float) ($revenueData['total_paid'] ?? 0);
// Utilization rate (sessions per available day)
$workingDays = self::workingDaysBetween($periodFrom, $periodTo);
$utilizationRate = $workingDays > 0 ? round(($sessionsConducted / $workingDays) * 100, 1) : 0;
$results[] = [
'coach_id' => $cId,
'coach_name' => $coach['full_name_ar'],
'coach_code' => $coach['code'],
'payment_model' => $coach['payment_model'],
'sessions_conducted' => $sessionsConducted,
'total_players_served' => $totalPlayersServed,
'unique_players' => $uniquePlayers,
'total_hours' => $totalHours,
'revenue_attributed' => $revenueAttributed,
'utilization_rate' => $utilizationRate,
];
$totals['total_sessions'] += $sessionsConducted;
$totals['total_players_served'] += $totalPlayersServed;
$totals['total_hours'] += $totalHours;
$totals['total_revenue'] += $revenueAttributed;
$totals['coach_count']++;
}
return [
'period_from' => $periodFrom,
'period_to' => $periodTo,
'coaches' => $results,
'totals' => $totals,
];
}
/**
* Approximate working days between two dates (excluding Fridays).
*/
private static function workingDaysBetween(string $from, string $to): int
{
$start = new \DateTime($from);
$end = new \DateTime($to);
$days = 0;
while ($start <= $end) {
// Friday = 5 in PHP (0=Sunday)
if ((int) $start->format('w') !== 5) {
$days++;
}
$start->modify('+1 day');
}
return $days;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Reports\Services;
use App\Core\App;
final class PlayerRetentionReportService
{
/**
* Generate player retention report: enrollment counts, churn rate, active vs dropped.
*
* @param string $periodFrom Start date (YYYY-MM-DD)
* @param string $periodTo End date (YYYY-MM-DD)
* @return array Report data
*/
public static function generate(string $periodFrom, string $periodTo): array
{
$db = App::getInstance()->db();
// Total active players at start of period
$activeAtStart = $db->selectOne(
"SELECT COUNT(DISTINCT tgp.player_id) AS cnt
FROM training_group_players tgp
WHERE tgp.joined_at < ?
AND (tgp.left_at IS NULL OR tgp.left_at >= ?)",
[$periodFrom, $periodFrom]
);
$startCount = (int) ($activeAtStart['cnt'] ?? 0);
// New enrollments during period
$newEnrollments = $db->selectOne(
"SELECT COUNT(DISTINCT tgp.player_id) AS cnt
FROM training_group_players tgp
WHERE tgp.joined_at BETWEEN ? AND ?",
[$periodFrom, $periodTo]
);
$newCount = (int) ($newEnrollments['cnt'] ?? 0);
// Dropped during period (left_at within range)
$dropped = $db->selectOne(
"SELECT COUNT(DISTINCT tgp.player_id) AS cnt
FROM training_group_players tgp
WHERE tgp.left_at BETWEEN ? AND ?",
[$periodFrom, $periodTo]
);
$droppedCount = (int) ($dropped['cnt'] ?? 0);
// Active at end of period
$activeAtEnd = $db->selectOne(
"SELECT COUNT(DISTINCT tgp.player_id) AS cnt
FROM training_group_players tgp
WHERE tgp.joined_at <= ?
AND (tgp.left_at IS NULL OR tgp.left_at > ?)",
[$periodTo, $periodTo]
);
$endCount = (int) ($activeAtEnd['cnt'] ?? 0);
// Churn rate
$churnRate = $startCount > 0 ? round(($droppedCount / $startCount) * 100, 2) : 0;
// Retention rate
$retentionRate = $startCount > 0 ? round((($startCount - $droppedCount) / $startCount) * 100, 2) : 0;
// Net growth
$netGrowth = $newCount - $droppedCount;
// Monthly breakdown
$monthlyBreakdown = $db->select(
"SELECT
DATE_FORMAT(dates.month_date, '%Y-%m') AS month,
COALESCE(new_t.cnt, 0) AS new_enrollments,
COALESCE(drop_t.cnt, 0) AS dropped
FROM (
SELECT DISTINCT DATE_FORMAT(joined_at, '%Y-%m-01') AS month_date
FROM training_group_players
WHERE joined_at BETWEEN ? AND ?
UNION
SELECT DISTINCT DATE_FORMAT(left_at, '%Y-%m-01') AS month_date
FROM training_group_players
WHERE left_at BETWEEN ? AND ?
) dates
LEFT JOIN (
SELECT DATE_FORMAT(joined_at, '%Y-%m-01') AS m, COUNT(DISTINCT player_id) AS cnt
FROM training_group_players
WHERE joined_at BETWEEN ? AND ?
GROUP BY m
) new_t ON new_t.m = dates.month_date
LEFT JOIN (
SELECT DATE_FORMAT(left_at, '%Y-%m-01') AS m, COUNT(DISTINCT player_id) AS cnt
FROM training_group_players
WHERE left_at BETWEEN ? AND ?
GROUP BY m
) drop_t ON drop_t.m = dates.month_date
ORDER BY dates.month_date ASC",
[$periodFrom, $periodTo, $periodFrom, $periodTo, $periodFrom, $periodTo, $periodFrom, $periodTo]
);
// By academy breakdown
$byAcademy = $db->select(
"SELECT a.id, a.name_ar,
COUNT(DISTINCT CASE WHEN tgp.joined_at BETWEEN ? AND ? THEN tgp.player_id END) AS new_enrollments,
COUNT(DISTINCT CASE WHEN tgp.left_at BETWEEN ? AND ? THEN tgp.player_id END) AS dropped,
COUNT(DISTINCT CASE WHEN (tgp.left_at IS NULL OR tgp.left_at > ?) AND tgp.joined_at <= ? THEN tgp.player_id END) AS active_now
FROM training_groups tg
LEFT JOIN academies a ON a.id = tg.academy_id
LEFT JOIN training_group_players tgp ON tgp.group_id = tg.id
WHERE a.id IS NOT NULL
GROUP BY a.id, a.name_ar
HAVING new_enrollments > 0 OR dropped > 0 OR active_now > 0
ORDER BY a.name_ar ASC",
[$periodFrom, $periodTo, $periodFrom, $periodTo, $periodTo, $periodTo]
);
return [
'period_from' => $periodFrom,
'period_to' => $periodTo,
'summary' => [
'active_at_start' => $startCount,
'new_enrollments' => $newCount,
'dropped' => $droppedCount,
'active_at_end' => $endCount,
'churn_rate' => $churnRate,
'retention_rate' => $retentionRate,
'net_growth' => $netGrowth,
],
'monthly_breakdown' => $monthlyBreakdown,
'by_academy' => $byAcademy,
];
}
}
...@@ -22,7 +22,8 @@ class FacilityConflictDetector ...@@ -22,7 +22,8 @@ class FacilityConflictDetector
string $date, string $date,
string $startTime, string $startTime,
string $endTime, string $endTime,
?int $excludeReservationId = null ?int $excludeReservationId = null,
?int $coachId = null
): array { ): array {
$conflicts = []; $conflicts = [];
...@@ -44,6 +45,20 @@ class FacilityConflictDetector ...@@ -44,6 +45,20 @@ class FacilityConflictDetector
); );
$conflicts = array_merge($conflicts, $blackoutConflicts); $conflicts = array_merge($conflicts, $blackoutConflicts);
// 4. Check training session conflicts on this facility
$sessionConflicts = self::checkSessionConflicts(
$facilityId, $date, $startTime, $endTime
);
$conflicts = array_merge($conflicts, $sessionConflicts);
// 5. Check coach scheduling conflicts
if ($coachId !== null) {
$coachConflicts = self::checkCoachConflicts(
$coachId, $date, $startTime, $endTime
);
$conflicts = array_merge($conflicts, $coachConflicts);
}
return $conflicts; return $conflicts;
} }
...@@ -128,6 +143,132 @@ class FacilityConflictDetector ...@@ -128,6 +143,132 @@ class FacilityConflictDetector
return $conflicts; return $conflicts;
} }
/**
* Check for training session conflicts on this facility.
*/
private static function checkSessionConflicts(
int $facilityId,
string $date,
string $startTime,
string $endTime
): array {
$conflicts = [];
$db = App::getInstance()->db();
try {
$rows = $db->select(
"SELECT ts.*, tg.name_ar AS group_name, c.full_name_ar AS coach_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN coaches c ON c.id = ts.coach_id
WHERE ts.facility_id = ?
AND ts.session_date = ?
AND ts.status NOT IN ('cancelled')
AND (ts.start_time < ? AND ts.end_time > ?)",
[$facilityId, $date, $endTime, $startTime]
);
} catch (\Throwable) {
return [];
}
foreach ($rows as $row) {
$conflicts[] = [
'type' => 'session',
'message' => sprintf(
'يوجد حصة تدريبية متعارضة (%s) من %s إلى %s — المدرب: %s',
$row['group_name'] ?? 'مجموعة',
$row['start_time'] ?? '',
$row['end_time'] ?? '',
$row['coach_name'] ?? '—'
),
];
}
return $conflicts;
}
/**
* Check for coach scheduling conflicts across all facilities.
*/
private static function checkCoachConflicts(
int $coachId,
string $date,
string $startTime,
string $endTime
): array {
$conflicts = [];
$db = App::getInstance()->db();
try {
$rows = $db->select(
"SELECT ts.*, tg.name_ar AS group_name, f.name_ar AS facility_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN facilities f ON f.id = ts.facility_id
WHERE ts.coach_id = ?
AND ts.session_date = ?
AND ts.status NOT IN ('cancelled')
AND (ts.start_time < ? AND ts.end_time > ?)",
[$coachId, $date, $endTime, $startTime]
);
} catch (\Throwable) {
return [];
}
foreach ($rows as $row) {
$conflicts[] = [
'type' => 'coach',
'message' => sprintf(
'المدرب لديه حصة أخرى في %s (%s) من %s إلى %s',
$row['facility_name'] ?? '—',
$row['group_name'] ?? '—',
$row['start_time'] ?? '',
$row['end_time'] ?? ''
),
];
}
return $conflicts;
}
/**
* Suggest alternative time slots when a conflict is detected.
*/
public static function suggestAlternatives(
int $facilityId,
string $date,
string $startTime,
string $endTime,
?int $coachId = null
): array {
$duration = (strtotime($endTime) - strtotime($startTime)) / 3600;
$alternatives = [];
$slots = ['07:00', '08:00', '09:00', '10:00', '11:00', '12:00',
'13:00', '14:00', '15:00', '16:00', '17:00', '18:00',
'19:00', '20:00', '21:00'];
foreach ($slots as $slotStart) {
$slotEnd = date('H:i', strtotime($slotStart) + (int)($duration * 3600));
if ($slotStart === $startTime) {
continue;
}
$conflicts = self::checkConflicts($facilityId, $date, $slotStart . ':00', $slotEnd . ':00', null, $coachId);
if (empty($conflicts)) {
$alternatives[] = [
'date' => $date,
'start_time' => $slotStart,
'end_time' => $slotEnd,
];
}
if (count($alternatives) >= 5) {
break;
}
}
return $alternatives;
}
/** /**
* Check for blackout dates on the facility. * Check for blackout dates on the facility.
*/ */
......
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Sessions\Models\SessionAttendance;
use App\Modules\Sessions\Services\SessionManagementService;
class SessionAttendanceController extends Controller
{
public function form(Request $request, string $id): Response
{
$this->authorize('session.attendance');
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT ts.*,
tg.name_ar AS group_name,
a.name_ar AS academy_name,
c.full_name_ar AS coach_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN academies a ON a.id = ts.academy_id
LEFT JOIN coaches c ON c.id = ts.coach_id
WHERE ts.id = ?",
[(int) $id]
);
if (!$session) {
return $this->redirect('/sessions')->withError('الحصة غير موجودة');
}
if ($session['status'] === 'cancelled') {
return $this->redirect('/sessions/' . $id)->withError('لا يمكن تسجيل حضور لحصة ملغاة');
}
// Get players from group memberships
$players = $db->select(
"SELECT gm.player_id, gm.enrollment_id, p.full_name_ar AS player_name, p.phone
FROM group_memberships gm
LEFT JOIN players p ON p.id = gm.player_id
WHERE gm.group_id = ? AND gm.status = 'active'
ORDER BY p.full_name_ar ASC",
[(int) $session['group_id']]
);
// Get existing attendance records for this session
$existingAttendance = $db->select(
"SELECT player_id, status, check_in_time FROM session_attendance WHERE session_id = ?",
[(int) $id]
);
$attendanceMap = [];
foreach ($existingAttendance as $att) {
$attendanceMap[(int) $att['player_id']] = $att;
}
return $this->view('Sessions.Views.attendance', [
'session' => $session,
'players' => $players,
'attendanceMap' => $attendanceMap,
'statuses' => SessionAttendance::getStatuses(),
]);
}
public function save(Request $request, string $id): Response
{
$this->authorize('session.attendance');
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT id, status, group_id FROM training_sessions WHERE id = ?",
[(int) $id]
);
if (!$session) {
return $this->redirect('/sessions')->withError('الحصة غير موجودة');
}
if ($session['status'] === 'cancelled') {
return $this->redirect('/sessions/' . $id)->withError('لا يمكن تسجيل حضور لحصة ملغاة');
}
// Update session status to in_progress if it was scheduled
if ($session['status'] === 'scheduled') {
$db->update('training_sessions', [
'status' => 'in_progress',
], 'id = ?', [(int) $id]);
}
// Build attendance data from POST
$playerIds = $request->post('player_ids', []);
$statuses = $request->post('statuses', []);
$checkInTimes = $request->post('check_in_times', []);
$enrollmentIds = $request->post('enrollment_ids', []);
if (!is_array($playerIds)) {
return $this->redirect('/sessions/' . $id . '/attendance')->withError('بيانات غير صالحة');
}
$attendanceData = [];
foreach ($playerIds as $index => $playerId) {
$attendanceData[] = [
'player_id' => (int) $playerId,
'status' => $statuses[$index] ?? 'present',
'check_in_time' => !empty($checkInTimes[$index]) ? $checkInTimes[$index] : null,
'enrollment_id' => !empty($enrollmentIds[$index]) ? (int) $enrollmentIds[$index] : null,
];
}
SessionManagementService::recordAttendance((int) $id, $attendanceData);
return $this->redirect('/sessions/' . $id)->withSuccess('تم تسجيل الحضور بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\Sessions\Models\TrainingSession;
use App\Modules\Sessions\Services\SessionManagementService;
class SessionController extends Controller
{
public function index(Request $request): Response
{
$this->authorize('session.view');
$filters = [
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
'group_id' => trim((string) $request->get('group_id', '')),
'academy_id' => trim((string) $request->get('academy_id', '')),
'coach_id' => trim((string) $request->get('coach_id', '')),
'status' => trim((string) $request->get('status', '')),
'session_type' => trim((string) $request->get('session_type', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = TrainingSession::search($filters, 25, $page);
$db = App::getInstance()->db();
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
$groups = $db->select(
"SELECT id, name_ar FROM training_groups WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('Sessions.Views.index', [
'sessions' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'academies' => $academies,
'groups' => $groups,
'statuses' => TrainingSession::getStatuses(),
'sessionTypes' => TrainingSession::getSessionTypes(),
]);
}
public function calendar(Request $request): Response
{
$this->authorize('session.view');
$year = (int) $request->get('year', (int) date('Y'));
$month = (int) $request->get('month', (int) date('m'));
$academyId = $request->get('academy_id', '') !== '' ? (int) $request->get('academy_id') : null;
// Clamp month
if ($month < 1) { $month = 12; $year--; }
if ($month > 12) { $month = 1; $year++; }
$sessions = TrainingSession::getForCalendar($year, $month, $academyId);
$db = App::getInstance()->db();
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('Sessions.Views.calendar', [
'sessions' => $sessions,
'year' => $year,
'month' => $month,
'academyId' => $academyId,
'academies' => $academies,
]);
}
public function show(Request $request, string $id): Response
{
$this->authorize('session.view');
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT ts.*,
tg.name_ar AS group_name,
a.name_ar AS academy_name,
c.full_name_ar AS coach_name,
f.name_ar AS facility_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN academies a ON a.id = ts.academy_id
LEFT JOIN coaches c ON c.id = ts.coach_id
LEFT JOIN facilities f ON f.id = ts.facility_id
WHERE ts.id = ?",
[(int) $id]
);
if (!$session) {
return $this->redirect('/sessions')->withError('الحصة غير موجودة');
}
// Get attendance records if session is in_progress or completed
$attendance = [];
if (in_array($session['status'], ['in_progress', 'completed'], true)) {
$attendance = $db->select(
"SELECT sa.*, p.full_name_ar AS player_name, p.phone AS player_phone
FROM session_attendance sa
LEFT JOIN players p ON p.id = sa.player_id
WHERE sa.session_id = ?
ORDER BY p.full_name_ar ASC",
[(int) $id]
);
}
return $this->view('Sessions.Views.show', [
'session' => $session,
'attendance' => $attendance,
'statuses' => TrainingSession::getStatuses(),
'types' => TrainingSession::getSessionTypes(),
]);
}
public function cancel(Request $request, string $id): Response
{
$this->authorize('session.manage');
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT id, status FROM training_sessions WHERE id = ?",
[(int) $id]
);
if (!$session) {
return $this->redirect('/sessions')->withError('الحصة غير موجودة');
}
if ($session['status'] === 'cancelled') {
return $this->redirect('/sessions/' . $id)->withError('الحصة ملغاة بالفعل');
}
if ($session['status'] === 'completed') {
return $this->redirect('/sessions/' . $id)->withError('لا يمكن إلغاء حصة مكتملة');
}
$reason = trim((string) $request->post('cancellation_reason', ''));
if ($reason === '') {
$session_obj = App::getInstance()->session();
$session_obj->flash('_alerts', [['type' => 'error', 'message' => 'يجب إدخال سبب الإلغاء']]);
return $this->redirect('/sessions/' . $id);
}
$cancelledBy = (int) ($_SESSION['employee_id'] ?? 0);
SessionManagementService::cancelSession((int) $id, $reason, $cancelledBy);
return $this->redirect('/sessions/' . $id)->withSuccess('تم إلغاء الحصة وإصدار رصيد تعويضي للاعبين');
}
public function complete(Request $request, string $id): Response
{
$this->authorize('session.manage');
$db = App::getInstance()->db();
$session = $db->selectOne(
"SELECT id, status FROM training_sessions WHERE id = ?",
[(int) $id]
);
if (!$session) {
return $this->redirect('/sessions')->withError('الحصة غير موجودة');
}
if ($session['status'] === 'completed') {
return $this->redirect('/sessions/' . $id)->withError('الحصة مكتملة بالفعل');
}
if ($session['status'] === 'cancelled') {
return $this->redirect('/sessions/' . $id)->withError('لا يمكن إكمال حصة ملغاة');
}
SessionManagementService::completeSession((int) $id);
return $this->redirect('/sessions/' . $id)->withSuccess('تم تحديث حالة الحصة إلى مكتملة');
}
public function generate(Request $request): Response
{
$this->authorize('session.manage');
$weekStart = trim((string) $request->post('week_start', ''));
if ($weekStart === '') {
// Default to next Sunday
$nextSunday = date('Y-m-d', strtotime('next sunday'));
$weekStart = $nextSunday;
}
$count = SessionManagementService::generateWeeklySessions($weekStart);
if ($count > 0) {
return $this->redirect('/sessions')->withSuccess("تم إنشاء {$count} حصة تدريبية للأسبوع بدءًا من {$weekStart}");
}
return $this->redirect('/sessions')->withError('لم يتم إنشاء أي حصص جديدة. قد تكون الحصص موجودة بالفعل أو لا توجد مجموعات بجدول محدد.');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Models;
use App\Core\Model;
use App\Core\App;
class MakeupCredit extends Model
{
protected static string $table = 'makeup_credits';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'player_id',
'enrollment_id',
'missed_session_id',
'used_session_id',
'status',
'expires_at',
];
public static function getStatuses(): array
{
return [
'available' => 'متاح',
'used' => 'مستخدم',
'expired' => 'منتهي',
];
}
public static function getAvailableForPlayer(int $playerId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT mc.*, ts.session_date AS missed_date, tg.name_ar AS group_name
FROM makeup_credits mc
LEFT JOIN training_sessions ts ON ts.id = mc.missed_session_id
LEFT JOIN training_groups tg ON tg.id = ts.group_id
WHERE mc.player_id = ?
AND mc.status = 'available'
AND (mc.expires_at IS NULL OR mc.expires_at >= CURDATE())
ORDER BY mc.created_at DESC",
[$playerId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Models;
use App\Core\Model;
use App\Core\App;
class SessionAttendance extends Model
{
protected static string $table = 'session_attendance';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'session_id',
'player_id',
'enrollment_id',
'status',
'check_in_time',
'check_out_time',
'is_makeup',
'makeup_for_session_id',
'notes',
];
public static function getStatuses(): array
{
return [
'present' => 'حاضر',
'absent' => 'غائب',
'late' => 'متأخر',
'excused' => 'معتذر',
'makeup' => 'تعويضي',
];
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'present' => '#10B981',
'absent' => '#EF4444',
'late' => '#F59E0B',
'excused' => '#6B7280',
'makeup' => '#7C3AED',
default => '#6B7280',
};
}
public static function getForSession(int $sessionId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT sa.*, p.full_name_ar AS player_name, p.phone AS player_phone
FROM session_attendance sa
LEFT JOIN players p ON p.id = sa.player_id
WHERE sa.session_id = ?
ORDER BY p.full_name_ar ASC",
[$sessionId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Models;
use App\Core\Model;
use App\Core\App;
class TrainingSession extends Model
{
protected static string $table = 'training_sessions';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'group_id',
'academy_id',
'coach_id',
'facility_id',
'session_date',
'start_time',
'end_time',
'session_type',
'status',
'cancellation_reason',
'cancelled_by',
'players_expected',
'players_attended',
'notes',
];
public static function getSessionTypes(): array
{
return [
'regular' => 'عادية',
'makeup' => 'تعويضية',
'extra' => 'إضافية',
'assessment' => 'تقييم',
];
}
public static function getStatuses(): array
{
return [
'scheduled' => 'مجدولة',
'in_progress' => 'جارية',
'completed' => 'مكتملة',
'cancelled' => 'ملغاة',
];
}
public static function getStatusColor(string $status): string
{
return match ($status) {
'scheduled' => '#3B82F6',
'in_progress' => '#F59E0B',
'completed' => '#10B981',
'cancelled' => '#EF4444',
default => '#6B7280',
};
}
public static function getSessionTypeColor(string $type): string
{
return match ($type) {
'regular' => '#6B7280',
'makeup' => '#7C3AED',
'extra' => '#D97706',
'assessment' => '#2563EB',
default => '#6B7280',
};
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = ['1=1'];
$params = [];
if (!empty($filters['date_from'])) {
$where[] = 'ts.session_date >= ?';
$params[] = $filters['date_from'];
}
if (!empty($filters['date_to'])) {
$where[] = 'ts.session_date <= ?';
$params[] = $filters['date_to'];
}
if (!empty($filters['group_id'])) {
$where[] = 'ts.group_id = ?';
$params[] = (int) $filters['group_id'];
}
if (!empty($filters['academy_id'])) {
$where[] = 'ts.academy_id = ?';
$params[] = (int) $filters['academy_id'];
}
if (!empty($filters['coach_id'])) {
$where[] = 'ts.coach_id = ?';
$params[] = (int) $filters['coach_id'];
}
if (!empty($filters['status'])) {
$where[] = 'ts.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['session_type'])) {
$where[] = 'ts.session_type = ?';
$params[] = $filters['session_type'];
}
$whereClause = implode(' AND ', $where);
$countSql = "SELECT COUNT(*) AS total FROM training_sessions ts WHERE {$whereClause}";
$countResult = $db->selectOne($countSql, $params);
$total = (int) ($countResult['total'] ?? 0);
$lastPage = max(1, (int) ceil($total / $perPage));
$page = min($page, $lastPage);
$offset = ($page - 1) * $perPage;
$sql = "SELECT ts.*,
tg.name_ar AS group_name,
a.name_ar AS academy_name,
c.full_name_ar AS coach_name,
f.name_ar AS facility_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
LEFT JOIN academies a ON a.id = ts.academy_id
LEFT JOIN coaches c ON c.id = ts.coach_id
LEFT JOIN facilities f ON f.id = ts.facility_id
WHERE {$whereClause}
ORDER BY ts.session_date DESC, ts.start_time ASC
LIMIT {$perPage} OFFSET {$offset}";
$data = $db->select($sql, $params);
return [
'data' => $data,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => $lastPage,
],
];
}
public static function getForCalendar(int $year, int $month, ?int $academyId = null): array
{
$db = App::getInstance()->db();
$startDate = sprintf('%04d-%02d-01', $year, $month);
$endDate = date('Y-m-t', strtotime($startDate));
$params = [$startDate, $endDate];
$extraWhere = '';
if ($academyId) {
$extraWhere = ' AND ts.academy_id = ?';
$params[] = $academyId;
}
$sql = "SELECT ts.*, tg.name_ar AS group_name
FROM training_sessions ts
LEFT JOIN training_groups tg ON tg.id = ts.group_id
WHERE ts.session_date >= ? AND ts.session_date <= ?{$extraWhere}
ORDER BY ts.session_date ASC, ts.start_time ASC";
return $db->select($sql, $params);
}
}
<?php
declare(strict_types=1);
return [
['GET', '/sessions', 'Sessions\Controllers\SessionController@index', ['auth'], 'session.view'],
['GET', '/sessions/calendar', 'Sessions\Controllers\SessionController@calendar', ['auth'], 'session.view'],
['GET', '/sessions/{id:\d+}', 'Sessions\Controllers\SessionController@show', ['auth'], 'session.view'],
['GET', '/sessions/{id:\d+}/attendance', 'Sessions\Controllers\SessionAttendanceController@form', ['auth'], 'session.attendance'],
['POST', '/sessions/{id:\d+}/attendance', 'Sessions\Controllers\SessionAttendanceController@save', ['auth', 'csrf'], 'session.attendance'],
['POST', '/sessions/{id:\d+}/cancel', 'Sessions\Controllers\SessionController@cancel', ['auth', 'csrf'], 'session.manage'],
['POST', '/sessions/{id:\d+}/complete', 'Sessions\Controllers\SessionController@complete', ['auth', 'csrf'], 'session.manage'],
['POST', '/sessions/generate', 'Sessions\Controllers\SessionController@generate', ['auth', 'csrf'], 'session.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Services;
use App\Core\App;
final class MakeupService
{
/**
* Use a makeup credit for a given session.
* Validates the credit is available and not expired.
* Returns true on success, false otherwise.
*/
public static function useMakeupCredit(int $creditId, int $sessionId): bool
{
$db = App::getInstance()->db();
$credit = $db->selectOne(
"SELECT id, status, expires_at FROM makeup_credits WHERE id = ?",
[$creditId]
);
if (!$credit) {
return false;
}
if ($credit['status'] !== 'available') {
return false;
}
// Check expiration
if ($credit['expires_at'] !== null && $credit['expires_at'] < date('Y-m-d')) {
return false;
}
$db->update('makeup_credits', [
'status' => 'used',
'used_session_id' => $sessionId,
], 'id = ?', [$creditId]);
return true;
}
/**
* Expire all credits that are past their expiration date.
* Returns count of expired credits.
*/
public static function expireOldCredits(): int
{
$db = App::getInstance()->db();
$stmt = $db->query(
"UPDATE makeup_credits
SET status = 'expired'
WHERE status = 'available'
AND expires_at < CURDATE()"
);
return $stmt->rowCount();
}
/**
* Get available credits for a player with session/group info.
*/
public static function getPlayerCredits(int $playerId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT mc.*,
ts.session_date AS missed_date,
ts.start_time AS missed_start_time,
tg.name_ar AS group_name
FROM makeup_credits mc
LEFT JOIN training_sessions ts ON ts.id = mc.missed_session_id
LEFT JOIN training_groups tg ON tg.id = ts.group_id
WHERE mc.player_id = ?
AND mc.status = 'available'
AND (mc.expires_at IS NULL OR mc.expires_at >= CURDATE())
ORDER BY mc.expires_at ASC",
[$playerId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Services;
use App\Core\App;
final class ScheduleConflictResolver
{
public static function suggestAlternatives(
int $facilityId,
?int $coachId,
string $dayOfWeek,
string $startTime,
string $endTime
): array {
$db = App::getInstance()->db();
$duration = (strtotime($endTime) - strtotime($startTime)) / 3600;
$alternatives = [];
$slots = ['07:00:00', '08:00:00', '09:00:00', '10:00:00', '11:00:00',
'12:00:00', '13:00:00', '14:00:00', '15:00:00', '16:00:00',
'17:00:00', '18:00:00', '19:00:00', '20:00:00', '21:00:00'];
foreach ($slots as $slotStart) {
if ($slotStart === $startTime) {
continue;
}
$slotEnd = date('H:i:s', strtotime($slotStart) + (int)($duration * 3600));
$facilityConflict = $db->selectOne("
SELECT id FROM training_groups
WHERE facility_id = ? AND day_of_week = ? AND status = 'active'
AND (start_time < ? AND end_time > ?)
", [$facilityId, $dayOfWeek, $slotEnd, $slotStart]);
if ($facilityConflict) {
continue;
}
if ($coachId !== null) {
$coachConflict = $db->selectOne("
SELECT id FROM training_groups
WHERE coach_id = ? AND day_of_week = ? AND status = 'active'
AND (start_time < ? AND end_time > ?)
", [$coachId, $dayOfWeek, $slotEnd, $slotStart]);
if ($coachConflict) {
continue;
}
}
$alternatives[] = [
'day_of_week' => $dayOfWeek,
'start_time' => $slotStart,
'end_time' => $slotEnd,
];
if (count($alternatives) >= 5) {
break;
}
}
return $alternatives;
}
public static function generateSeasonSchedule(
int $groupId,
string $seasonStart,
string $seasonEnd,
array $excludeDates = []
): int {
$db = App::getInstance()->db();
$group = $db->selectOne("SELECT * FROM training_groups WHERE id = ?", [$groupId]);
if (!$group) {
return 0;
}
$dayMap = [
'sunday' => 0, 'monday' => 1, 'tuesday' => 2, 'wednesday' => 3,
'thursday' => 4, 'friday' => 5, 'saturday' => 6,
];
$targetDay = $dayMap[$group['day_of_week']] ?? null;
if ($targetDay === null) {
return 0;
}
$enrolledCount = $db->selectOne(
"SELECT COUNT(*) AS cnt FROM group_memberships WHERE group_id = ? AND status = 'active'",
[$groupId]
)['cnt'] ?? 0;
$created = 0;
$current = strtotime($seasonStart);
$end = strtotime($seasonEnd);
while ($current <= $end) {
if ((int) date('w', $current) === $targetDay) {
$dateStr = date('Y-m-d', $current);
if (in_array($dateStr, $excludeDates, true)) {
$current += 86400;
continue;
}
$exists = $db->selectOne(
"SELECT id FROM training_sessions WHERE group_id = ? AND session_date = ?",
[$groupId, $dateStr]
);
if (!$exists) {
$db->insert('training_sessions', [
'group_id' => $groupId,
'academy_id' => $group['academy_id'] ? (int) $group['academy_id'] : null,
'coach_id' => $group['coach_id'] ? (int) $group['coach_id'] : null,
'facility_id' => $group['facility_id'] ? (int) $group['facility_id'] : null,
'session_date' => $dateStr,
'start_time' => $group['start_time'],
'end_time' => $group['end_time'],
'session_type' => 'regular',
'status' => 'scheduled',
'players_expected' => (int) $enrolledCount,
]);
$created++;
}
}
$current += 86400;
}
return $created;
}
public static function applyHolidayOverrides(string $startDate, string $endDate, string $reason = 'إجازة رسمية'): int
{
$db = App::getInstance()->db();
$result = $db->query(
"UPDATE training_sessions
SET status = 'cancelled', cancellation_reason = ?
WHERE session_date BETWEEN ? AND ?
AND status = 'scheduled'",
[$reason, $startDate, $endDate]
);
return $result->rowCount();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\Sessions\Services;
use App\Core\App;
use App\Core\EventBus;
final class SessionManagementService
{
/**
* Generate sessions for a given week from training_groups schedules.
* Returns the count of sessions created.
*/
public static function generateWeeklySessions(string $weekStartDate): int
{
$db = App::getInstance()->db();
$count = 0;
// Get all active training groups that have schedule info
$groups = $db->select(
"SELECT tg.id, tg.academy_id, tg.coach_id, tg.facility_id,
tg.day_of_week, tg.start_time, tg.end_time, tg.current_count
FROM training_groups tg
WHERE tg.is_active = 1
AND tg.is_archived = 0
AND tg.day_of_week IS NOT NULL
AND tg.start_time IS NOT NULL
AND tg.end_time IS NOT NULL"
);
$weekStart = new \DateTimeImmutable($weekStartDate);
foreach ($groups as $group) {
$dayOfWeek = (int) $group['day_of_week'];
// Calculate the actual date for this day_of_week in the given week
// weekStartDate is assumed to be a Sunday (day 0)
$daysToAdd = $dayOfWeek;
$sessionDate = $weekStart->modify("+{$daysToAdd} days")->format('Y-m-d');
// Check if session already exists for this group + date
$existing = $db->selectOne(
"SELECT id FROM training_sessions
WHERE group_id = ? AND session_date = ? AND status != 'cancelled'",
[(int) $group['id'], $sessionDate]
);
if ($existing) {
continue;
}
// Create the session
$db->insert('training_sessions', [
'group_id' => (int) $group['id'],
'academy_id' => (int) $group['academy_id'],
'coach_id' => $group['coach_id'] ? (int) $group['coach_id'] : null,
'facility_id' => $group['facility_id'] ? (int) $group['facility_id'] : null,
'session_date' => $sessionDate,
'start_time' => $group['start_time'],
'end_time' => $group['end_time'],
'session_type' => 'regular',
'status' => 'scheduled',
'players_expected' => (int) $group['current_count'],
]);
$count++;
}
return $count;
}
/**
* Cancel a session and issue makeup credits to active group members.
*/
public static function cancelSession(int $sessionId, string $reason, int $cancelledBy): void
{
$db = App::getInstance()->db();
// Update session status
$db->update('training_sessions', [
'status' => 'cancelled',
'cancellation_reason' => $reason,
'cancelled_by' => $cancelledBy,
], 'id = ?', [$sessionId]);
// Get the session details for group_id
$session = $db->selectOne(
"SELECT group_id, session_date FROM training_sessions WHERE id = ?",
[$sessionId]
);
if (!$session) {
return;
}
// Get all active memberships for this group
$members = $db->select(
"SELECT gm.player_id, gm.enrollment_id
FROM group_memberships gm
WHERE gm.group_id = ? AND gm.status = 'active'",
[(int) $session['group_id']]
);
// Create makeup credits for each member
$expiresAt = date('Y-m-d', strtotime('+30 days'));
foreach ($members as $member) {
$db->insert('makeup_credits', [
'player_id' => (int) $member['player_id'],
'enrollment_id' => (int) $member['enrollment_id'],
'missed_session_id' => $sessionId,
'status' => 'available',
'expires_at' => $expiresAt,
]);
}
EventBus::dispatch('session.cancelled', [
'session_id' => $sessionId,
'reason' => $reason,
'group_id' => (int) $session['group_id'],
]);
}
/**
* Record attendance for a session.
* $attendanceData: array of ['player_id' => int, 'status' => string, 'check_in_time' => ?string, 'enrollment_id' => ?int]
*/
public static function recordAttendance(int $sessionId, array $attendanceData): void
{
$db = App::getInstance()->db();
$attendedCount = 0;
$session = $db->selectOne(
"SELECT group_id FROM training_sessions WHERE id = ?",
[$sessionId]
);
foreach ($attendanceData as $record) {
$playerId = (int) $record['player_id'];
$status = $record['status'] ?? 'present';
$checkInTime = !empty($record['check_in_time']) ? $record['check_in_time'] : null;
$enrollmentId = !empty($record['enrollment_id']) ? (int) $record['enrollment_id'] : null;
// Check if attendance record already exists
$existing = $db->selectOne(
"SELECT id FROM session_attendance WHERE session_id = ? AND player_id = ?",
[$sessionId, $playerId]
);
$data = [
'session_id' => $sessionId,
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'status' => $status,
'check_in_time' => $checkInTime,
];
if ($existing) {
$db->update('session_attendance', $data, 'id = ?', [(int) $existing['id']]);
} else {
$db->insert('session_attendance', $data);
}
// Count attended (present, late, makeup all count)
if (in_array($status, ['present', 'late', 'makeup'], true)) {
$attendedCount++;
}
// Create makeup credit for absent players
if ($status === 'absent' && $enrollmentId && $session) {
// Check if credit already exists for this session+player
$existingCredit = $db->selectOne(
"SELECT id FROM makeup_credits WHERE missed_session_id = ? AND player_id = ?",
[$sessionId, $playerId]
);
if (!$existingCredit) {
$db->insert('makeup_credits', [
'player_id' => $playerId,
'enrollment_id' => $enrollmentId,
'missed_session_id' => $sessionId,
'status' => 'available',
'expires_at' => date('Y-m-d', strtotime('+30 days')),
]);
}
}
}
// Update session players_attended count
$db->update('training_sessions', [
'players_attended' => $attendedCount,
], 'id = ?', [$sessionId]);
EventBus::dispatch('session.attendance.recorded', [
'session_id' => $sessionId,
'players_attended' => $attendedCount,
]);
}
/**
* Mark a session as completed.
*/
public static function completeSession(int $sessionId): void
{
$db = App::getInstance()->db();
$db->update('training_sessions', [
'status' => 'completed',
], 'id = ?', [$sessionId]);
EventBus::dispatch('session.completed', [
'session_id' => $sessionId,
]);
}
/**
* Use a makeup credit for a session.
*/
public static function useMakeupCredit(int $creditId, int $sessionId): void
{
$db = App::getInstance()->db();
$db->update('makeup_credits', [
'status' => 'used',
'used_session_id' => $sessionId,
], 'id = ?', [$creditId]);
}
}
<?php
use App\Modules\Sessions\Models\SessionAttendance;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تسجيل الحضور<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sessions/<?= (int) $session['id'] ?>" 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
$statusColors = [
'scheduled' => '#3B82F6',
'in_progress' => '#F59E0B',
'completed' => '#10B981',
'cancelled' => '#EF4444',
];
$sStatus = $session['status'] ?? 'scheduled';
$sColor = $statusColors[$sStatus] ?? '#6B7280';
?>
<!-- Session Info Header -->
<div class="card" style="margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<div style="width:40px;height:40px;border-radius:10px;background:<?= $sColor ?>15;display:flex;align-items:center;justify-content:center;">
<i data-lucide="clipboard-check" style="width:20px;height:20px;color:<?= $sColor ?>;"></i>
</div>
<div>
<h2 style="margin:0;font-size:18px;font-weight:700;color:#1A1A2E;">
تسجيل حضور: <?= e($session['group_name'] ?? '—') ?>
</h2>
<p style="margin:4px 0 0;font-size:13px;color:#6B7280;">
<?= e($session['session_date']) ?> | <?= e(substr($session['start_time'], 0, 5)) ?> - <?= e(substr($session['end_time'], 0, 5)) ?>
<?php if (!empty($session['coach_name'])): ?>
| المدرب: <?= e($session['coach_name']) ?>
<?php endif; ?>
</p>
</div>
</div>
</div>
<!-- Attendance Form -->
<?php if (!empty($players)): ?>
<form method="POST" action="/sessions/<?= (int) $session['id'] ?>/attendance">
<?= csrf_field() ?>
<!-- Quick Actions -->
<div class="card" style="margin-bottom:16px;padding:12px 16px;">
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
<span style="font-size:13px;font-weight:600;color:#374151;">تحديد سريع:</span>
<button type="button" class="btn btn-sm btn-outline" onclick="setAllStatus('present')" 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" onclick="setAllStatus('absent')" style="font-size:12px;color:#EF4444;border-color:#EF4444;">
<i data-lucide="x" style="width:13px;height:13px;vertical-align:middle;margin-left:3px;"></i> غياب الجميع
</button>
</div>
</div>
<div class="card" style="padding:0;overflow:hidden;">
<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;width:40px;">#</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;width:160px;">الحالة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;width:130px;">وقت الحضور</th>
</tr>
</thead>
<tbody>
<?php foreach ($players as $i => $player):
$playerId = (int) $player['player_id'];
$existingStatus = $attendanceMap[$playerId]['status'] ?? 'present';
$existingTime = $attendanceMap[$playerId]['check_in_time'] ?? '';
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 16px;color:#6B7280;">
<?= $i + 1 ?>
<input type="hidden" name="player_ids[<?= $i ?>]" value="<?= $playerId ?>">
<input type="hidden" name="enrollment_ids[<?= $i ?>]" value="<?= (int) ($player['enrollment_id'] ?? 0) ?>">
</td>
<td style="padding:10px 16px;">
<div style="font-weight:500;color:#1A1A2E;"><?= e($player['player_name'] ?? '—') ?></div>
<?php if (!empty($player['phone'])): ?>
<div style="font-size:11px;color:#9CA3AF;"><?= e($player['phone']) ?></div>
<?php endif; ?>
</td>
<td style="padding:10px 16px;text-align:center;">
<select name="statuses[<?= $i ?>]" class="form-input status-select" style="font-size:13px;padding:6px 10px;min-width:120px;" data-row="<?= $i ?>">
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= $existingStatus === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</td>
<td style="padding:10px 16px;text-align:center;">
<input type="time" name="check_in_times[<?= $i ?>]" value="<?= e($existingTime ? substr($existingTime, 0, 5) : '') ?>" class="form-input" style="font-size:13px;padding:6px 10px;width:110px;">
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div style="margin-top:20px;display:flex;gap:10px;justify-content:end;">
<a href="/sessions/<?= (int) $session['id'] ?>" class="btn btn-outline">إلغاء</a>
<button type="submit" class="btn btn-primary">
<i data-lucide="save" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> حفظ الحضور
</button>
</div>
</form>
<?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;">
يجب إضافة لاعبين إلى المجموعة التدريبية أولاً قبل تسجيل الحضور.
</p>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
function setAllStatus(status) {
var selects = document.querySelectorAll('.status-select');
selects.forEach(function(select) {
select.value = status;
});
}
</script>
<?php $__template->endSection(); ?>
<?php
declare(strict_types=1);
use App\Modules\Sessions\Models\TrainingSession;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تقويم الحصص<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sessions" class="btn btn-outline"><i data-lucide="list" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض القائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
// Group sessions by date
$sessionsByDate = [];
foreach ($sessions as $s) {
$sessionsByDate[$s['session_date']][] = $s;
}
// Calendar calculations
$firstDay = mktime(0, 0, 0, $month, 1, $year);
$daysInMonth = (int) date('t', $firstDay);
$startDayOfWeek = (int) date('w', $firstDay); // 0=Sunday
$prevMonth = $month - 1;
$prevYear = $year;
if ($prevMonth < 1) { $prevMonth = 12; $prevYear--; }
$nextMonth = $month + 1;
$nextYear = $year;
if ($nextMonth > 12) { $nextMonth = 1; $nextYear++; }
$arabicMonths = [
1 => 'يناير', 2 => 'فبراير', 3 => 'مارس', 4 => 'أبريل',
5 => 'مايو', 6 => 'يونيو', 7 => 'يوليو', 8 => 'أغسطس',
9 => 'سبتمبر', 10 => 'أكتوبر', 11 => 'نوفمبر', 12 => 'ديسمبر',
];
$arabicDays = ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'];
?>
<!-- Month Navigation & Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;">
<div style="display:flex;align-items:center;gap:12px;">
<a href="/sessions/calendar?year=<?= $prevYear ?>&month=<?= $prevMonth ?><?= $academyId ? '&academy_id=' . $academyId : '' ?>"
class="btn btn-sm btn-outline" style="padding:6px 10px;">
<i data-lucide="chevron-right" style="width:16px;height:16px;"></i>
</a>
<h2 style="margin:0;font-size:20px;font-weight:700;color:#1F2937;">
<?= e($arabicMonths[$month]) ?> <?= $year ?>
</h2>
<a href="/sessions/calendar?year=<?= $nextYear ?>&month=<?= $nextMonth ?><?= $academyId ? '&academy_id=' . $academyId : '' ?>"
class="btn btn-sm btn-outline" style="padding:6px 10px;">
<i data-lucide="chevron-left" style="width:16px;height:16px;"></i>
</a>
</div>
<form method="GET" action="/sessions/calendar" style="display:flex;gap:8px;align-items:center;">
<input type="hidden" name="year" value="<?= $year ?>">
<input type="hidden" name="month" value="<?= $month ?>">
<select name="academy_id" class="form-input" style="min-width:160px;" onchange="this.form.submit()">
<option value="">كل الأكاديميات</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= $academyId == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</form>
</div>
</div>
<!-- Legend -->
<div style="margin-bottom:15px;display:flex;gap:15px;flex-wrap:wrap;font-size:12px;">
<span style="display:inline-flex;align-items:center;gap:4px;">
<span style="width:12px;height:12px;border-radius:3px;background:#3B82F6;display:inline-block;"></span> مجدولة
</span>
<span style="display:inline-flex;align-items:center;gap:4px;">
<span style="width:12px;height:12px;border-radius:3px;background:#F59E0B;display:inline-block;"></span> جارية
</span>
<span style="display:inline-flex;align-items:center;gap:4px;">
<span style="width:12px;height:12px;border-radius:3px;background:#10B981;display:inline-block;"></span> مكتملة
</span>
<span style="display:inline-flex;align-items:center;gap:4px;">
<span style="width:12px;height:12px;border-radius:3px;background:#EF4444;display:inline-block;"></span> ملغاة
</span>
</div>
<!-- Calendar Grid -->
<div class="card" style="padding:0;overflow:hidden;">
<!-- Day Names Header -->
<div style="display:grid;grid-template-columns:repeat(7, 1fr);background:#F9FAFB;border-bottom:2px solid #E5E7EB;">
<?php foreach ($arabicDays as $dayName): ?>
<div style="padding:10px 8px;text-align:center;font-weight:600;font-size:13px;color:#374151;">
<?= e($dayName) ?>
</div>
<?php endforeach; ?>
</div>
<!-- Calendar Days -->
<div style="display:grid;grid-template-columns:repeat(7, 1fr);">
<?php
// Empty cells before first day
for ($i = 0; $i < $startDayOfWeek; $i++): ?>
<div style="min-height:100px;border:1px solid #F3F4F6;background:#FAFAFA;"></div>
<?php endfor; ?>
<?php for ($day = 1; $day <= $daysInMonth; $day++):
$dateStr = sprintf('%04d-%02d-%02d', $year, $month, $day);
$isToday = ($dateStr === date('Y-m-d'));
$daySessions = $sessionsByDate[$dateStr] ?? [];
?>
<div style="min-height:100px;border:1px solid #F3F4F6;padding:6px;<?= $isToday ? 'background:#F0FDFA;border-color:#0D7377;' : '' ?>">
<div style="font-size:13px;font-weight:<?= $isToday ? '700' : '500' ?>;color:<?= $isToday ? '#0D7377' : '#374151' ?>;margin-bottom:4px;">
<?= $day ?>
</div>
<?php foreach ($daySessions as $ds):
$statusColor = TrainingSession::getStatusColor($ds['status'] ?? 'scheduled');
?>
<a href="/sessions/<?= (int) $ds['id'] ?>"
style="display:block;margin-bottom:3px;padding:3px 6px;border-radius:4px;font-size:11px;text-decoration:none;color:#fff;background:<?= $statusColor ?>;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"
title="<?= e($ds['group_name'] ?? '') ?> - <?= e(substr($ds['start_time'] ?? '', 0, 5)) ?>">
<?= e(substr($ds['start_time'] ?? '', 0, 5)) ?> <?= e($ds['group_name'] ?? '') ?>
</a>
<?php endforeach; ?>
</div>
<?php endfor; ?>
<?php
// Empty cells after last day
$totalCells = $startDayOfWeek + $daysInMonth;
$remainingCells = (7 - ($totalCells % 7)) % 7;
for ($i = 0; $i < $remainingCells; $i++): ?>
<div style="min-height:100px;border:1px solid #F3F4F6;background:#FAFAFA;"></div>
<?php endfor; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Sessions\Models\TrainingSession;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>الحصص التدريبية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sessions/calendar" class="btn btn-outline"><i data-lucide="calendar" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> عرض التقويم</a>
<button type="button" class="btn btn-primary" onclick="document.getElementById('generateModal').style.display='flex'"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء حصص الأسبوع</button>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'scheduled' => '#3B82F6',
'in_progress' => '#F59E0B',
'completed' => '#10B981',
'cancelled' => '#EF4444',
];
$typeColors = [
'regular' => '#6B7280',
'makeup' => '#7C3AED',
'extra' => '#D97706',
'assessment' => '#2563EB',
];
?>
<!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sessions" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<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>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المجموعة</label>
<select name="group_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($groups as $grp): ?>
<option value="<?= (int) $grp['id'] ?>" <?= ($filters['group_id'] ?? '') == $grp['id'] ? 'selected' : '' ?>><?= e($grp['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">الأكاديمية</label>
<select name="academy_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= ($filters['academy_id'] ?? '') == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:120px;">
<label class="form-label" style="font-size:12px;">الحالة</label>
<select name="status" class="form-input">
<option value="">الكل</option>
<?php foreach ($statuses as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['status'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:120px;">
<label class="form-label" style="font-size:12px;">النوع</label>
<select name="session_type" class="form-input">
<option value="">الكل</option>
<?php foreach ($sessionTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= ($filters['session_type'] ?? '') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</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="/sessions" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Sessions Table -->
<?php if (!empty($sessions)): ?>
<div class="card" style="padding:0;overflow:hidden;">
<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: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>
<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 ($sessions as $s):
$sStatus = $s['status'] ?? 'scheduled';
$sType = $s['session_type'] ?? 'regular';
$sColor = $statusColors[$sStatus] ?? '#6B7280';
$tColor = $typeColors[$sType] ?? '#6B7280';
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;white-space:nowrap;"><?= e($s['session_date']) ?></td>
<td style="padding:12px 16px;white-space:nowrap;"><?= e(substr($s['start_time'], 0, 5)) ?> - <?= e(substr($s['end_time'], 0, 5)) ?></td>
<td style="padding:12px 16px;"><?= e($s['group_name'] ?? '—') ?></td>
<td style="padding:12px 16px;"><?= e($s['academy_name'] ?? '—') ?></td>
<td style="padding:12px 16px;"><?= e($s['coach_name'] ?? '—') ?></td>
<td style="padding:12px 16px;text-align:center;">
<span style="font-weight:600;"><?= (int) $s['players_attended'] ?></span>
<span style="color:#9CA3AF;">/</span>
<span style="color:#6B7280;"><?= (int) $s['players_expected'] ?></span>
</td>
<td style="padding:12px 16px;text-align:center;">
<span style="padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $tColor ?>15;color:<?= $tColor ?>;">
<?= e($sessionTypes[$sType] ?? $sType) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<span style="padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $sColor ?>15;color:<?= $sColor ?>;">
<?= e($statuses[$sStatus] ?? $sStatus) ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;">
<div style="display:flex;gap:4px;justify-content:center;">
<a href="/sessions/<?= (int) $s['id'] ?>" class="btn btn-sm btn-outline" style="font-size:11px;padding:4px 8px;">
<i data-lucide="eye" style="width:13px;height:13px;vertical-align:middle;"></i>
</a>
<?php if (in_array($sStatus, ['scheduled', 'in_progress'], true)): ?>
<a href="/sessions/<?= (int) $s['id'] ?>/attendance" class="btn btn-sm btn-outline" style="font-size:11px;padding:4px 8px;color:#10B981;">
<i data-lucide="clipboard-check" style="width:13px;height:13px;vertical-align:middle;"></i>
</a>
<?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; ?>
</div>
<?php else: ?>
<div class="card" style="padding:60px 20px;text-align:center;">
<div style="margin-bottom:15px;">
<i data-lucide="calendar-days" 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['date_from']) || !empty($filters['group_id']) || !empty($filters['status'])): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء حصص تدريبية من خلال زر "إنشاء حصص الأسبوع".
<?php endif; ?>
</p>
</div>
<?php endif; ?>
<!-- Generate Sessions Modal -->
<div id="generateModal" style="display:none;position:fixed;inset:0;z-index:1000;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);">
<div class="card" style="width:100%;max-width:400px;padding:24px;">
<h3 style="margin:0 0 16px;font-size:18px;font-weight:700;">إنشاء حصص الأسبوع</h3>
<form method="POST" action="/sessions/generate">
<?= csrf_field() ?>
<div style="margin-bottom:16px;">
<label class="form-label">بداية الأسبوع (الأحد)</label>
<input type="date" name="week_start" class="form-input" required>
</div>
<p style="font-size:13px;color:#6B7280;margin-bottom:16px;">
سيتم إنشاء حصص تدريبية لجميع المجموعات التي لها جدول محدد (يوم + وقت) ولم يتم إنشاء حصص لها في هذا الأسبوع.
</p>
<div style="display:flex;gap:10px;justify-content:end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('generateModal').style.display='none'">إلغاء</button>
<button type="submit" class="btn btn-primary">إنشاء</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\Sessions\Models\TrainingSession;
use App\Modules\Sessions\Models\SessionAttendance;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>تفاصيل الحصة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sessions" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php if (in_array($session['status'], ['scheduled', 'in_progress'], true)): ?>
<a href="/sessions/<?= (int) $session['id'] ?>/attendance" class="btn btn-primary"><i data-lucide="clipboard-check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> تسجيل الحضور</a>
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$statusColors = [
'scheduled' => '#3B82F6',
'in_progress' => '#F59E0B',
'completed' => '#10B981',
'cancelled' => '#EF4444',
];
$typeColors = [
'regular' => '#6B7280',
'makeup' => '#7C3AED',
'extra' => '#D97706',
'assessment' => '#2563EB',
];
$sStatus = $session['status'] ?? 'scheduled';
$sType = $session['session_type'] ?? 'regular';
$sColor = $statusColors[$sStatus] ?? '#6B7280';
$tColor = $typeColors[$sType] ?? '#6B7280';
?>
<!-- Session Header -->
<div class="card" style="margin-bottom:20px;">
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:48px;height:48px;border-radius:12px;background:<?= $sColor ?>15;display:flex;align-items:center;justify-content:center;">
<i data-lucide="calendar-days" style="width:24px;height:24px;color:<?= $sColor ?>;"></i>
</div>
<div>
<h2 style="margin:0;font-size:20px;font-weight:700;color:#1A1A2E;">
حصة <?= e($session['group_name'] ?? '—') ?>
</h2>
<p style="margin:4px 0 0;font-size:13px;color:#6B7280;">
<?= e($session['session_date']) ?> | <?= e(substr($session['start_time'], 0, 5)) ?> - <?= e(substr($session['end_time'], 0, 5)) ?>
</p>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<span style="padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $tColor ?>15;color:<?= $tColor ?>;">
<?= e($types[$sType] ?? $sType) ?>
</span>
<span style="padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $sColor ?>15;color:<?= $sColor ?>;">
<?= e($statuses[$sStatus] ?? $sStatus) ?>
</span>
</div>
</div>
<!-- Session Details Grid -->
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:16px;">
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">الأكاديمية</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($session['academy_name'] ?? '—') ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">المدرب</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($session['coach_name'] ?? '—') ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">المنشأة</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;"><?= e($session['facility_name'] ?? '—') ?></div>
</div>
<div style="padding:12px;background:#F9FAFB;border-radius:8px;">
<div style="font-size:11px;color:#6B7280;margin-bottom:4px;">الحضور</div>
<div style="font-size:14px;font-weight:600;color:#1A1A2E;">
<?= (int) $session['players_attended'] ?> / <?= (int) $session['players_expected'] ?>
</div>
</div>
</div>
<?php if (!empty($session['notes'])): ?>
<div style="margin-top:16px;padding:12px;background:#FFFBEB;border-radius:8px;border:1px solid #FDE68A;">
<div style="font-size:11px;color:#92400E;margin-bottom:4px;">ملاحظات</div>
<div style="font-size:13px;color:#78350F;"><?= e($session['notes']) ?></div>
</div>
<?php endif; ?>
<?php if ($sStatus === 'cancelled' && !empty($session['cancellation_reason'])): ?>
<div style="margin-top:16px;padding:12px;background:#FEF2F2;border-radius:8px;border:1px solid #FECACA;">
<div style="font-size:11px;color:#991B1B;margin-bottom:4px;">سبب الإلغاء</div>
<div style="font-size:13px;color:#7F1D1D;"><?= e($session['cancellation_reason']) ?></div>
</div>
<?php endif; ?>
</div>
<!-- Actions -->
<?php if (in_array($sStatus, ['scheduled', 'in_progress'], true)): ?>
<div class="card" style="margin-bottom:20px;padding:15px;">
<div style="display:flex;gap:10px;flex-wrap:wrap;">
<?php if ($sStatus === 'in_progress'): ?>
<form method="POST" action="/sessions/<?= (int) $session['id'] ?>/complete" 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>
<?php endif; ?>
<button type="button" class="btn btn-outline" style="color:#EF4444;border-color:#EF4444;" onclick="document.getElementById('cancelModal').style.display='flex'">
<i data-lucide="x-circle" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إلغاء الحصة
</button>
</div>
</div>
<!-- Cancel Modal -->
<div id="cancelModal" style="display:none;position:fixed;inset:0;z-index:1000;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);">
<div class="card" style="width:100%;max-width:400px;padding:24px;">
<h3 style="margin:0 0 16px;font-size:18px;font-weight:700;color:#EF4444;">إلغاء الحصة</h3>
<form method="POST" action="/sessions/<?= (int) $session['id'] ?>/cancel">
<?= csrf_field() ?>
<div style="margin-bottom:16px;">
<label class="form-label">سبب الإلغاء <span style="color:#EF4444;">*</span></label>
<textarea name="cancellation_reason" class="form-input" rows="3" required placeholder="أدخل سبب إلغاء الحصة..."></textarea>
</div>
<p style="font-size:12px;color:#6B7280;margin-bottom:16px;">
سيتم إصدار رصيد تعويضي لجميع لاعبي المجموعة.
</p>
<div style="display:flex;gap:10px;justify-content:end;">
<button type="button" class="btn btn-outline" onclick="document.getElementById('cancelModal').style.display='none'">تراجع</button>
<button type="submit" class="btn" style="background:#EF4444;color:#fff;">تأكيد الإلغاء</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<!-- Attendance Table -->
<?php if (!empty($attendance)): ?>
<div class="card" style="padding:0;overflow:hidden;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:700;color:#1A1A2E;">
<i data-lucide="clipboard-list" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i>
سجل الحضور
</h3>
</div>
<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:center;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:right;font-weight:600;color:#374151;">ملاحظات</th>
</tr>
</thead>
<tbody>
<?php foreach ($attendance as $i => $att):
$attStatus = $att['status'] ?? 'present';
$attColor = SessionAttendance::getStatusColor($attStatus);
$attStatuses = SessionAttendance::getStatuses();
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 16px;color:#6B7280;"><?= $i + 1 ?></td>
<td style="padding:10px 16px;font-weight:500;"><?= e($att['player_name'] ?? '—') ?></td>
<td style="padding:10px 16px;text-align:center;">
<span style="padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $attColor ?>15;color:<?= $attColor ?>;">
<?= e($attStatuses[$attStatus] ?? $attStatus) ?>
</span>
</td>
<td style="padding:10px 16px;text-align:center;color:#6B7280;">
<?= !empty($att['check_in_time']) ? e(substr($att['check_in_time'], 0, 5)) : '—' ?>
</td>
<td style="padding:10px 16px;color:#6B7280;font-size:12px;"><?= e($att['notes'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</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;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('sessions', [
'session.view' => ['ar' => 'عرض الحصص التدريبية', 'en' => 'View Training Sessions'],
'session.manage' => ['ar' => 'إدارة الحصص التدريبية', 'en' => 'Manage Training Sessions'],
'session.attendance' => ['ar' => 'تسجيل الحضور والغياب', 'en' => 'Record Session Attendance'],
]);
MenuRegistry::register('sessions', [
'label_ar' => 'الحصص التدريبية',
'icon' => 'calendar-days',
'route' => '/sessions',
'permission' => 'session.view',
'order' => 400,
'parent' => 'sports_activities',
]);
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\TrainingGroups\Models\TrainingGroup;
use App\Modules\TrainingGroups\Models\GroupMembership;
class TrainingGroupController extends Controller
{
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'academy_id' => trim((string) $request->get('academy_id', '')),
'level_id' => trim((string) $request->get('level_id', '')),
'group_type' => trim((string) $request->get('group_type', '')),
'coach_id' => trim((string) $request->get('coach_id', '')),
];
$page = max(1, (int) $request->get('page', 1));
$result = TrainingGroup::search($filters, 24, $page);
$db = App::getInstance()->db();
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('TrainingGroups.Views.index', [
'groups' => $result['data'],
'pagination' => $result['pagination'],
'filters' => $filters,
'academies' => $academies,
'groupTypes' => TrainingGroup::getGroupTypes(),
]);
}
public function create(Request $request): Response
{
$db = App::getInstance()->db();
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
$levels = $db->select(
"SELECT id, academy_id, name_ar FROM academy_levels WHERE is_active = 1 ORDER BY level_order ASC"
);
$coaches = $db->select(
"SELECT id, full_name_ar FROM coaches WHERE is_active = 1 AND is_archived = 0 ORDER BY full_name_ar ASC"
);
$facilities = $db->select(
"SELECT id, name_ar FROM facilities WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('TrainingGroups.Views.create', [
'academies' => $academies,
'levels' => $levels,
'coaches' => $coaches,
'facilities' => $facilities,
'groupTypes' => TrainingGroup::getGroupTypes(),
'pricingTiers' => TrainingGroup::getPricingTiers(),
]);
}
public function store(Request $request): Response
{
$data = [
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'academy_id' => (int) $request->post('academy_id', 0),
'level_id' => (int) $request->post('level_id', 0),
'coach_id' => $request->post('coach_id', '') !== '' ? (int) $request->post('coach_id') : null,
'facility_id' => $request->post('facility_id', '') !== '' ? (int) $request->post('facility_id') : null,
'group_type' => trim((string) $request->post('group_type', 'group')),
'min_capacity' => max(1, (int) $request->post('min_capacity', 1)),
'max_capacity' => max(1, (int) $request->post('max_capacity', 20)),
'age_from' => $request->post('age_from', '') !== '' ? (int) $request->post('age_from') : null,
'age_to' => $request->post('age_to', '') !== '' ? (int) $request->post('age_to') : null,
'gender_restriction' => trim((string) $request->post('gender_restriction', '')) ?: null,
'day_of_week' => $request->post('day_of_week', '') !== '' ? (int) $request->post('day_of_week') : null,
'start_time' => trim((string) $request->post('start_time', '')) ?: null,
'end_time' => trim((string) $request->post('end_time', '')) ?: null,
'pricing_tier' => trim((string) $request->post('pricing_tier', 'standard')),
'is_active' => 1,
];
// Auto-generate code
$code = 'TG_' . strtoupper(substr(str_replace(' ', '', $data['name_ar']), 0, 10)) . '_' . time() % 100000;
$data['code'] = $code;
$errors = [];
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم المجموعة بالعربي مطلوب (حرفين على الأقل)';
}
if ($data['academy_id'] <= 0) {
$errors[] = 'يجب اختيار أكاديمية';
}
if ($data['level_id'] <= 0) {
$errors[] = 'يجب اختيار مستوى';
}
if (!array_key_exists($data['group_type'], TrainingGroup::getGroupTypes())) {
$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('/training-groups/create');
}
$group = TrainingGroup::create($data);
return $this->redirect('/training-groups/' . $group->id)->withSuccess('تم إنشاء المجموعة التدريبية بنجاح');
}
public function show(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$db = App::getInstance()->db();
// Get academy & level & coach names
$academy = $db->selectOne("SELECT name_ar FROM academies WHERE id = ?", [(int) $group->academy_id]);
$level = $db->selectOne("SELECT name_ar FROM academy_levels WHERE id = ?", [(int) $group->level_id]);
$coach = $group->coach_id ? $db->selectOne("SELECT full_name_ar FROM coaches WHERE id = ?", [(int) $group->coach_id]) : null;
$facility = $group->facility_id ? $db->selectOne("SELECT name_ar FROM facilities WHERE id = ?", [(int) $group->facility_id]) : null;
$members = $group->getMembers();
// Get available players for add-player form (enrolled in this academy, not in this group)
$players = $db->select(
"SELECT ae.id AS enrollment_id, ae.player_id, p.full_name_ar AS player_name
FROM academy_enrollments ae
INNER JOIN players p ON p.id = ae.player_id
WHERE ae.academy_id = ? AND ae.status = 'active'
AND ae.player_id NOT IN (
SELECT gm.player_id FROM group_memberships gm WHERE gm.group_id = ? AND gm.status = 'active'
)
ORDER BY p.full_name_ar ASC",
[(int) $group->academy_id, (int) $id]
);
// Get other groups for transfer
$otherGroups = $db->select(
"SELECT id, name_ar FROM training_groups WHERE id != ? AND academy_id = ? AND is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC",
[(int) $id, (int) $group->academy_id]
);
return $this->view('TrainingGroups.Views.show', [
'group' => $group,
'academyName' => $academy['name_ar'] ?? '—',
'levelName' => $level['name_ar'] ?? '—',
'coachName' => $coach['full_name_ar'] ?? null,
'facilityName' => $facility['name_ar'] ?? null,
'members' => $members,
'players' => $players,
'otherGroups' => $otherGroups,
'groupTypes' => TrainingGroup::getGroupTypes(),
'pricingTiers' => TrainingGroup::getPricingTiers(),
'dayNames' => self::getDayNames(),
]);
}
public function edit(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$db = App::getInstance()->db();
$academies = $db->select(
"SELECT id, name_ar FROM academies WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
$levels = $db->select(
"SELECT id, academy_id, name_ar FROM academy_levels WHERE is_active = 1 ORDER BY level_order ASC"
);
$coaches = $db->select(
"SELECT id, full_name_ar FROM coaches WHERE is_active = 1 AND is_archived = 0 ORDER BY full_name_ar ASC"
);
$facilities = $db->select(
"SELECT id, name_ar FROM facilities WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('TrainingGroups.Views.edit', [
'group' => $group,
'academies' => $academies,
'levels' => $levels,
'coaches' => $coaches,
'facilities' => $facilities,
'groupTypes' => TrainingGroup::getGroupTypes(),
'pricingTiers' => TrainingGroup::getPricingTiers(),
]);
}
public function update(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$data = [
'name_ar' => trim((string) $request->post('name_ar', '')),
'name_en' => trim((string) $request->post('name_en', '')) ?: null,
'academy_id' => (int) $request->post('academy_id', 0),
'level_id' => (int) $request->post('level_id', 0),
'coach_id' => $request->post('coach_id', '') !== '' ? (int) $request->post('coach_id') : null,
'facility_id' => $request->post('facility_id', '') !== '' ? (int) $request->post('facility_id') : null,
'group_type' => trim((string) $request->post('group_type', 'group')),
'min_capacity' => max(1, (int) $request->post('min_capacity', 1)),
'max_capacity' => max(1, (int) $request->post('max_capacity', 20)),
'age_from' => $request->post('age_from', '') !== '' ? (int) $request->post('age_from') : null,
'age_to' => $request->post('age_to', '') !== '' ? (int) $request->post('age_to') : null,
'gender_restriction' => trim((string) $request->post('gender_restriction', '')) ?: null,
'day_of_week' => $request->post('day_of_week', '') !== '' ? (int) $request->post('day_of_week') : null,
'start_time' => trim((string) $request->post('start_time', '')) ?: null,
'end_time' => trim((string) $request->post('end_time', '')) ?: null,
'pricing_tier' => trim((string) $request->post('pricing_tier', 'standard')),
];
$errors = [];
if ($data['name_ar'] === '' || mb_strlen($data['name_ar']) < 2) {
$errors[] = 'اسم المجموعة بالعربي مطلوب (حرفين على الأقل)';
}
if ($data['academy_id'] <= 0) {
$errors[] = 'يجب اختيار أكاديمية';
}
if ($data['level_id'] <= 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('/training-groups/' . $id . '/edit');
}
// Re-check if group is full based on new max_capacity
$currentCount = (int) $group->current_count;
$data['is_full'] = $currentCount >= $data['max_capacity'] ? 1 : 0;
$group->update($data);
return $this->redirect('/training-groups/' . $id)->withSuccess('تم تحديث المجموعة بنجاح');
}
public function toggle(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$newStatus = $group->is_active ? 0 : 1;
$group->update(['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل المجموعة' : 'تم إيقاف المجموعة';
return $this->redirect('/training-groups/' . $id)->withSuccess($message);
}
public function addPlayer(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$playerId = (int) $request->post('player_id', 0);
$enrollmentId = (int) $request->post('enrollment_id', 0);
if ($playerId <= 0 || $enrollmentId <= 0) {
return $this->redirect('/training-groups/' . $id)->withError('يجب اختيار لاعب');
}
// Check if already in group
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT id FROM group_memberships WHERE group_id = ? AND player_id = ? AND status = 'active'",
[(int) $id, $playerId]
);
if ($existing) {
return $this->redirect('/training-groups/' . $id)->withError('اللاعب موجود بالفعل في هذه المجموعة');
}
// Check capacity
if ((int) $group->current_count >= (int) $group->max_capacity) {
return $this->redirect('/training-groups/' . $id)->withError('المجموعة ممتلئة');
}
$db->insert('group_memberships', [
'group_id' => (int) $id,
'enrollment_id' => $enrollmentId,
'player_id' => $playerId,
'joined_at' => date('Y-m-d H:i:s'),
'status' => 'active',
]);
// Update count
$newCount = (int) $group->current_count + 1;
$isFull = $newCount >= (int) $group->max_capacity ? 1 : 0;
$group->update(['current_count' => $newCount, 'is_full' => $isFull]);
return $this->redirect('/training-groups/' . $id)->withSuccess('تم إضافة اللاعب للمجموعة');
}
public function removePlayer(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$membershipId = (int) $request->post('membership_id', 0);
if ($membershipId <= 0) {
return $this->redirect('/training-groups/' . $id)->withError('بيانات غير صالحة');
}
$db = App::getInstance()->db();
$db->update('group_memberships', [
'status' => 'removed',
'left_at' => date('Y-m-d H:i:s'),
], 'id = ? AND group_id = ?', [$membershipId, (int) $id]);
// Update count
$newCount = max(0, (int) $group->current_count - 1);
$group->update(['current_count' => $newCount, 'is_full' => 0]);
// Process waiting list for open spot
\App\Modules\TrainingGroups\Services\WaitingListService::processOpenSpot((int) $id);
return $this->redirect('/training-groups/' . $id)->withSuccess('تم إزالة اللاعب من المجموعة');
}
public function transferPlayer(Request $request, string $id): Response
{
$group = TrainingGroup::find((int) $id);
if (!$group) {
return $this->redirect('/training-groups')->withError('المجموعة غير موجودة');
}
$membershipId = (int) $request->post('membership_id', 0);
$targetGroupId = (int) $request->post('target_group_id', 0);
$reason = trim((string) $request->post('transfer_reason', ''));
if ($membershipId <= 0 || $targetGroupId <= 0) {
return $this->redirect('/training-groups/' . $id)->withError('بيانات غير صالحة');
}
$targetGroup = TrainingGroup::find($targetGroupId);
if (!$targetGroup) {
return $this->redirect('/training-groups/' . $id)->withError('المجموعة المستهدفة غير موجودة');
}
if ((int) $targetGroup->current_count >= (int) $targetGroup->max_capacity) {
return $this->redirect('/training-groups/' . $id)->withError('المجموعة المستهدفة ممتلئة');
}
$db = App::getInstance()->db();
// Get the membership
$membership = $db->selectOne(
"SELECT * FROM group_memberships WHERE id = ? AND group_id = ? AND status = 'active'",
[$membershipId, (int) $id]
);
if (!$membership) {
return $this->redirect('/training-groups/' . $id)->withError('العضوية غير موجودة');
}
// Mark old membership as transferred
$db->update('group_memberships', [
'status' => 'transferred',
'left_at' => date('Y-m-d H:i:s'),
'transferred_to_group_id' => $targetGroupId,
'transfer_reason' => $reason ?: null,
], 'id = ?', [$membershipId]);
// Create new membership in target group
$db->insert('group_memberships', [
'group_id' => $targetGroupId,
'enrollment_id' => (int) $membership['enrollment_id'],
'player_id' => (int) $membership['player_id'],
'joined_at' => date('Y-m-d H:i:s'),
'status' => 'active',
]);
// Update counts
$newSourceCount = max(0, (int) $group->current_count - 1);
$group->update(['current_count' => $newSourceCount, 'is_full' => 0]);
$newTargetCount = (int) $targetGroup->current_count + 1;
$isTargetFull = $newTargetCount >= (int) $targetGroup->max_capacity ? 1 : 0;
$targetGroup->update(['current_count' => $newTargetCount, 'is_full' => $isTargetFull]);
// Process waiting list for open spot in source group
\App\Modules\TrainingGroups\Services\WaitingListService::processOpenSpot((int) $id);
return $this->redirect('/training-groups/' . $id)->withSuccess('تم نقل اللاعب بنجاح');
}
private static function getDayNames(): array
{
return [
0 => 'الأحد',
1 => 'الإثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
6 => 'السبت',
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\TrainingGroups\Services\WaitingListService;
class WaitingListController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'status' => trim((string) $request->get('status', '')),
'discipline_id' => trim((string) $request->get('discipline_id', '')),
'q' => trim((string) $request->get('q', '')),
];
$where = ['1=1'];
$params = [];
if (!empty($filters['status'])) {
$where[] = 'wl.status = ?';
$params[] = $filters['status'];
}
if (!empty($filters['discipline_id'])) {
$where[] = 'wl.discipline_id = ?';
$params[] = (int) $filters['discipline_id'];
}
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where[] = '(p.full_name_ar LIKE ? OR p.phone LIKE ?)';
$params[] = $search;
$params[] = $search;
}
$whereClause = implode(' AND ', $where);
$page = max(1, (int) $request->get('page', 1));
$perPage = 25;
$countResult = $db->selectOne(
"SELECT COUNT(*) AS total FROM waiting_list wl LEFT JOIN players p ON p.id = wl.player_id WHERE {$whereClause}",
$params
);
$total = (int) ($countResult['total'] ?? 0);
$lastPage = max(1, (int) ceil($total / $perPage));
$page = min($page, $lastPage);
$offset = ($page - 1) * $perPage;
$entries = $db->select(
"SELECT wl.*, p.full_name_ar AS player_name, p.phone AS player_phone,
sd.name_ar AS discipline_name, al.name_ar AS level_name,
tg.name_ar AS offered_group_name
FROM waiting_list wl
LEFT JOIN players p ON p.id = wl.player_id
LEFT JOIN sport_disciplines sd ON sd.id = wl.discipline_id
LEFT JOIN academy_levels al ON al.id = wl.level_id
LEFT JOIN training_groups tg ON tg.id = wl.offered_group_id
WHERE {$whereClause}
ORDER BY wl.priority ASC, wl.created_at ASC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$disciplines = $db->select(
"SELECT id, name_ar FROM sport_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar ASC"
);
// Get available groups for offering
$availableGroups = $db->select(
"SELECT id, name_ar FROM training_groups WHERE is_active = 1 AND is_full = 0 AND is_archived = 0 ORDER BY name_ar ASC"
);
return $this->view('TrainingGroups.Views.waiting_list', [
'entries' => $entries,
'filters' => $filters,
'disciplines' => $disciplines,
'availableGroups' => $availableGroups,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => $lastPage,
],
]);
}
public function offer(Request $request, string $id): Response
{
$groupId = (int) $request->post('group_id', 0);
if ($groupId <= 0) {
return $this->redirect('/waiting-list')->withError('يجب اختيار مجموعة');
}
WaitingListService::offerSpot((int) $id, $groupId);
return $this->redirect('/waiting-list')->withSuccess('تم عرض المقعد على اللاعب');
}
public function cancel(Request $request, string $id): Response
{
WaitingListService::cancel((int) $id);
return $this->redirect('/waiting-list')->withSuccess('تم إلغاء طلب الانتظار');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Models;
use App\Core\Model;
class GroupMembership extends Model
{
protected static string $table = 'group_memberships';
protected static string $primaryKey = 'id';
protected static bool $timestamps = false;
protected static bool $softDelete = false;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'group_id',
'enrollment_id',
'player_id',
'joined_at',
'left_at',
'status',
'transferred_to_group_id',
'transfer_reason',
];
}
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Models;
use App\Core\Model;
use App\Core\App;
class TrainingGroup extends Model
{
protected static string $table = 'training_groups';
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',
'academy_id',
'level_id',
'coach_id',
'facility_id',
'group_type',
'min_capacity',
'max_capacity',
'current_count',
'age_from',
'age_to',
'gender_restriction',
'day_of_week',
'start_time',
'end_time',
'pricing_tier',
'is_full',
'is_active',
];
public static function getGroupTypes(): array
{
return [
'private' => 'خاص (فردي)',
'small_group' => 'مجموعة صغيرة (2-5)',
'group' => 'مجموعة (6-15)',
'team' => 'فريق (16+)',
];
}
public static function getPricingTiers(): array
{
return [
'premium' => 'بريميوم',
'standard' => 'عادي',
'economy' => 'اقتصادي',
];
}
public function getMembers(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT gm.*, p.full_name_ar AS player_name, p.date_of_birth, p.gender, p.phone
FROM group_memberships gm
LEFT JOIN players p ON p.id = gm.player_id
WHERE gm.group_id = ? AND gm.status = 'active'
ORDER BY gm.joined_at ASC",
[(int) $this->id]
);
}
public static function search(array $filters, int $perPage = 25, int $page = 1): array
{
$db = App::getInstance()->db();
$where = ['tg.is_archived = 0'];
$params = [];
if (!empty($filters['q'])) {
$search = '%' . $filters['q'] . '%';
$where[] = '(tg.name_ar LIKE ? OR tg.name_en LIKE ? OR tg.code LIKE ?)';
$params[] = $search;
$params[] = $search;
$params[] = $search;
}
if (!empty($filters['academy_id'])) {
$where[] = 'tg.academy_id = ?';
$params[] = (int) $filters['academy_id'];
}
if (!empty($filters['level_id'])) {
$where[] = 'tg.level_id = ?';
$params[] = (int) $filters['level_id'];
}
if (!empty($filters['group_type'])) {
$where[] = 'tg.group_type = ?';
$params[] = $filters['group_type'];
}
if (!empty($filters['coach_id'])) {
$where[] = 'tg.coach_id = ?';
$params[] = (int) $filters['coach_id'];
}
if (isset($filters['is_active']) && $filters['is_active'] !== '') {
$where[] = 'tg.is_active = ?';
$params[] = (int) $filters['is_active'];
}
$whereClause = implode(' AND ', $where);
$countSql = "SELECT COUNT(*) AS total FROM training_groups tg WHERE {$whereClause}";
$countResult = $db->selectOne($countSql, $params);
$total = (int) ($countResult['total'] ?? 0);
$lastPage = max(1, (int) ceil($total / $perPage));
$page = min($page, $lastPage);
$offset = ($page - 1) * $perPage;
$sql = "SELECT tg.*, a.name_ar AS academy_name, al.name_ar AS level_name, c.full_name_ar AS coach_name
FROM training_groups tg
LEFT JOIN academies a ON a.id = tg.academy_id
LEFT JOIN academy_levels al ON al.id = tg.level_id
LEFT JOIN coaches c ON c.id = tg.coach_id
WHERE {$whereClause}
ORDER BY tg.name_ar ASC
LIMIT {$perPage} OFFSET {$offset}";
$data = $db->select($sql, $params);
return [
'data' => $data,
'pagination' => [
'total' => $total,
'per_page' => $perPage,
'current_page' => $page,
'last_page' => $lastPage,
],
];
}
public static function getAvailableForDiscipline(int $disciplineId, int $levelId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT tg.*, a.name_ar AS academy_name, al.name_ar AS level_name, c.full_name_ar AS coach_name
FROM training_groups tg
INNER JOIN academies a ON a.id = tg.academy_id
LEFT JOIN academy_levels al ON al.id = tg.level_id
LEFT JOIN coaches c ON c.id = tg.coach_id
WHERE a.discipline_id = ?
AND tg.level_id = ?
AND tg.is_active = 1
AND tg.is_full = 0
AND tg.is_archived = 0
ORDER BY tg.current_count ASC",
[$disciplineId, $levelId]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Models;
use App\Core\Model;
class WaitingListEntry extends Model
{
protected static string $table = 'waiting_list';
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',
'level_id',
'preferred_day_of_week',
'preferred_time',
'priority',
'requested_group_type',
'status',
'offered_group_id',
'offered_at',
'offer_expires_at',
'notes',
];
}
<?php
declare(strict_types=1);
return [
['GET', '/training-groups', 'TrainingGroups\Controllers\TrainingGroupController@index', ['auth'], 'training_group.view'],
['GET', '/training-groups/create', 'TrainingGroups\Controllers\TrainingGroupController@create', ['auth'], 'training_group.manage'],
['POST', '/training-groups', 'TrainingGroups\Controllers\TrainingGroupController@store', ['auth', 'csrf'], 'training_group.manage'],
['GET', '/training-groups/{id:\d+}', 'TrainingGroups\Controllers\TrainingGroupController@show', ['auth'], 'training_group.view'],
['GET', '/training-groups/{id:\d+}/edit', 'TrainingGroups\Controllers\TrainingGroupController@edit', ['auth'], 'training_group.manage'],
['POST', '/training-groups/{id:\d+}', 'TrainingGroups\Controllers\TrainingGroupController@update', ['auth', 'csrf'], 'training_group.manage'],
['POST', '/training-groups/{id:\d+}/toggle', 'TrainingGroups\Controllers\TrainingGroupController@toggle', ['auth', 'csrf'], 'training_group.manage'],
['POST', '/training-groups/{id:\d+}/add-player', 'TrainingGroups\Controllers\TrainingGroupController@addPlayer', ['auth', 'csrf'], 'training_group.manage'],
['POST', '/training-groups/{id:\d+}/remove-player', 'TrainingGroups\Controllers\TrainingGroupController@removePlayer', ['auth', 'csrf'], 'training_group.manage'],
['POST', '/training-groups/{id:\d+}/transfer-player', 'TrainingGroups\Controllers\TrainingGroupController@transferPlayer', ['auth', 'csrf'], 'training_group.manage'],
['GET', '/waiting-list', 'TrainingGroups\Controllers\WaitingListController@index', ['auth'], 'training_group.view'],
['POST', '/waiting-list/{id:\d+}/offer', 'TrainingGroups\Controllers\WaitingListController@offer', ['auth', 'csrf'], 'training_group.manage'],
['POST', '/waiting-list/{id:\d+}/cancel', 'TrainingGroups\Controllers\WaitingListController@cancel', ['auth', 'csrf'], 'training_group.manage'],
];
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Services;
use App\Core\App;
class AutoAssignmentService
{
/**
* Attempt to auto-assign a player to a training group.
*
* @param int $playerId
* @param int $disciplineId
* @param int $levelId
* @param array $preferences ['day_of_week' => int|null, 'time' => 'AM'|'PM'|null, 'enrollment_id' => int]
* @return array ['status' => 'assigned'|'waiting_list', 'group_id' => int|null, 'waiting_list_id' => int|null]
*/
public static function assign(int $playerId, int $disciplineId, int $levelId, array $preferences = []): array
{
$db = App::getInstance()->db();
// 1. Get player's age and gender
$player = $db->selectOne(
"SELECT date_of_birth, gender FROM players WHERE id = ?",
[$playerId]
);
$playerAge = null;
if (!empty($player['date_of_birth'])) {
$dob = new \DateTime($player['date_of_birth']);
$now = new \DateTime();
$playerAge = (int) $now->diff($dob)->y;
}
$playerGender = $player['gender'] ?? null;
// 2. Query available training groups
$groups = $db->select(
"SELECT tg.*
FROM training_groups tg
INNER JOIN academies a ON a.id = tg.academy_id
WHERE a.discipline_id = ?
AND tg.level_id = ?
AND tg.is_active = 1
AND tg.is_full = 0
AND tg.is_archived = 0",
[$disciplineId, $levelId]
);
// 3. Filter by age and gender
$eligible = [];
foreach ($groups as $group) {
// Filter by gender restriction
if (!empty($group['gender_restriction']) && $playerGender !== null) {
if ($group['gender_restriction'] !== $playerGender) {
continue;
}
}
// Filter by age range
if ($playerAge !== null) {
if ($group['age_from'] !== null && $playerAge < (int) $group['age_from']) {
continue;
}
if ($group['age_to'] !== null && $playerAge > (int) $group['age_to']) {
continue;
}
}
// 5. Filter by time preference (AM/PM)
if (!empty($preferences['time']) && !empty($group['start_time'])) {
$startHour = (int) substr($group['start_time'], 0, 2);
if ($preferences['time'] === 'AM' && $startHour >= 12) {
continue;
}
if ($preferences['time'] === 'PM' && $startHour < 12) {
continue;
}
}
$eligible[] = $group;
}
// 6. Score groups
$scored = [];
foreach ($eligible as $group) {
$score = 0;
// +10 for age match (player age within range)
if ($playerAge !== null && $group['age_from'] !== null && $group['age_to'] !== null) {
if ($playerAge >= (int) $group['age_from'] && $playerAge <= (int) $group['age_to']) {
$score += 10;
}
}
// +5 for day match
if (isset($preferences['day_of_week']) && $group['day_of_week'] !== null) {
if ((int) $preferences['day_of_week'] === (int) $group['day_of_week']) {
$score += 5;
}
}
// +3 for capacity 60-80% (social density)
$maxCap = (int) $group['max_capacity'];
$currentCount = (int) $group['current_count'];
if ($maxCap > 0) {
$occupancy = $currentCount / $maxCap;
if ($occupancy >= 0.6 && $occupancy <= 0.8) {
$score += 3;
}
}
$scored[] = [
'group' => $group,
'score' => $score,
];
}
// 7. Sort by score DESC
usort($scored, fn($a, $b) => $b['score'] <=> $a['score']);
// Assign to top group
if (!empty($scored)) {
$bestGroup = $scored[0]['group'];
$groupId = (int) $bestGroup['id'];
$enrollmentId = (int) ($preferences['enrollment_id'] ?? 0);
// Create group_membership
$db->insert('group_memberships', [
'group_id' => $groupId,
'enrollment_id' => $enrollmentId,
'player_id' => $playerId,
'joined_at' => date('Y-m-d H:i:s'),
'status' => 'active',
]);
// Update current_count
$newCount = (int) $bestGroup['current_count'] + 1;
$isFull = $newCount >= (int) $bestGroup['max_capacity'] ? 1 : 0;
$db->update('training_groups', [
'current_count' => $newCount,
'is_full' => $isFull,
], 'id = ?', [$groupId]);
return [
'status' => 'assigned',
'group_id' => $groupId,
'waiting_list_id' => null,
];
}
// 8. No group found — add to waiting list
$db->insert('waiting_list', [
'player_id' => $playerId,
'discipline_id' => $disciplineId,
'level_id' => $levelId,
'preferred_day_of_week'=> $preferences['day_of_week'] ?? null,
'preferred_time' => $preferences['time'] ?? null,
'priority' => 100,
'requested_group_type' => $preferences['group_type'] ?? null,
'status' => 'waiting',
]);
$waitingListId = (int) $db->selectOne(
"SELECT id FROM waiting_list WHERE player_id = ? AND discipline_id = ? AND status = 'waiting' ORDER BY id DESC LIMIT 1",
[$playerId, $disciplineId]
)['id'];
return [
'status' => 'waiting_list',
'group_id' => null,
'waiting_list_id' => $waitingListId,
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\TrainingGroups\Services;
use App\Core\App;
class WaitingListService
{
/**
* When a spot opens in a group, check the waiting list for eligible players and offer the spot.
*/
public static function processOpenSpot(int $groupId): ?int
{
$db = App::getInstance()->db();
// Get the group details
$group = $db->selectOne(
"SELECT tg.*, a.discipline_id
FROM training_groups tg
INNER JOIN academies a ON a.id = tg.academy_id
WHERE tg.id = ?",
[$groupId]
);
if (!$group || (int) $group['is_full'] === 1) {
return null;
}
// Find eligible waiting list entries
$where = "wl.status = 'waiting' AND wl.discipline_id = ?";
$params = [(int) $group['discipline_id']];
if (!empty($group['level_id'])) {
$where .= " AND (wl.level_id IS NULL OR wl.level_id = ?)";
$params[] = (int) $group['level_id'];
}
$entries = $db->select(
"SELECT wl.*, p.date_of_birth, p.gender
FROM waiting_list wl
LEFT JOIN players p ON p.id = wl.player_id
WHERE {$where}
ORDER BY wl.priority ASC, wl.created_at ASC
LIMIT 10",
$params
);
foreach ($entries as $entry) {
// Check age eligibility
if (!empty($entry['date_of_birth']) && ($group['age_from'] !== null || $group['age_to'] !== null)) {
$dob = new \DateTime($entry['date_of_birth']);
$now = new \DateTime();
$age = (int) $now->diff($dob)->y;
if ($group['age_from'] !== null && $age < (int) $group['age_from']) {
continue;
}
if ($group['age_to'] !== null && $age > (int) $group['age_to']) {
continue;
}
}
// Check gender eligibility
if (!empty($group['gender_restriction']) && !empty($entry['gender'])) {
if ($group['gender_restriction'] !== $entry['gender']) {
continue;
}
}
// Check day preference
if ($entry['preferred_day_of_week'] !== null && $group['day_of_week'] !== null) {
if ((int) $entry['preferred_day_of_week'] !== (int) $group['day_of_week']) {
continue;
}
}
// Check time preference
if (!empty($entry['preferred_time']) && !empty($group['start_time'])) {
$startHour = (int) substr($group['start_time'], 0, 2);
if ($entry['preferred_time'] === 'AM' && $startHour >= 12) {
continue;
}
if ($entry['preferred_time'] === 'PM' && $startHour < 12) {
continue;
}
}
// This entry is eligible — offer the spot
self::offerSpot((int) $entry['id'], $groupId);
return (int) $entry['id'];
}
return null;
}
/**
* Offer a spot in a specific group to a waiting list entry.
*/
public static function offerSpot(int $waitingListId, int $groupId): void
{
$db = App::getInstance()->db();
$expiresAt = date('Y-m-d H:i:s', strtotime('+48 hours'));
$db->update('waiting_list', [
'status' => 'offered',
'offered_group_id' => $groupId,
'offered_at' => date('Y-m-d H:i:s'),
'offer_expires_at' => $expiresAt,
], 'id = ?', [$waitingListId]);
}
/**
* Cancel a waiting list entry.
*/
public static function cancel(int $waitingListId): void
{
$db = App::getInstance()->db();
$db->update('waiting_list', [
'status' => 'cancelled',
], 'id = ?', [$waitingListId]);
}
}
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>إنشاء مجموعة تدريبية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/training-groups" 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="/training-groups" id="groupForm">
<?= 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="users" 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>
</div>
<div class="form-group">
<label class="form-label">اسم المجموعة بالإنجليزي</label>
<input type="text" name="name_en" value="<?= e(old('name_en') ?? '') ?>" class="form-input" 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">الأكاديمية <span style="color:#DC2626;">*</span></label>
<select name="academy_id" class="form-select" required id="academySelect">
<option value="">-- اختر الأكاديمية --</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= (old('academy_id') ?? '') == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المستوى <span style="color:#DC2626;">*</span></label>
<select name="level_id" class="form-select" required id="levelSelect">
<option value="">-- اختر المستوى --</option>
<?php foreach ($levels as $lvl): ?>
<option value="<?= (int) $lvl['id'] ?>" data-academy="<?= (int) $lvl['academy_id'] ?>" <?= (old('level_id') ?? '') == $lvl['id'] ? 'selected' : '' ?>><?= e($lvl['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</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="coach_id" class="form-select">
<option value="">-- بدون مدرب --</option>
<?php foreach ($coaches as $coach): ?>
<option value="<?= (int) $coach['id'] ?>" <?= (old('coach_id') ?? '') == $coach['id'] ? 'selected' : '' ?>><?= e($coach['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المنشأة</label>
<select name="facility_id" class="form-select">
<option value="">-- بدون تحديد --</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= (old('facility_id') ?? '') == $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Group 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:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;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>
<select name="group_type" class="form-select" required>
<?php foreach ($groupTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('group_type') ?? 'group') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الحد الأدنى للسعة</label>
<input type="number" name="min_capacity" value="<?= e(old('min_capacity') ?? '1') ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للسعة</label>
<input type="number" name="max_capacity" value="<?= e(old('max_capacity') ?? '20') ?>" class="form-input" min="1" max="100" 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">العمر من</label>
<input type="number" name="age_from" value="<?= e(old('age_from') ?? '') ?>" class="form-input" min="3" max="60" style="direction:ltr;text-align:left;" placeholder="سنة">
</div>
<div class="form-group">
<label class="form-label">العمر إلى</label>
<input type="number" name="age_to" value="<?= e(old('age_to') ?? '') ?>" class="form-input" min="3" max="60" style="direction:ltr;text-align:left;" placeholder="سنة">
</div>
<div class="form-group">
<label class="form-label">قيد النوع</label>
<select name="gender_restriction" class="form-select">
<option value="" <?= (old('gender_restriction') ?? '') === '' ? 'selected' : '' ?>>مختلط</option>
<option value="male" <?= (old('gender_restriction') ?? '') === 'male' ? 'selected' : '' ?>>ذكور فقط</option>
<option value="female" <?= (old('gender_restriction') ?? '') === 'female' ? 'selected' : '' ?>>إناث فقط</option>
</select>
</div>
</div>
</div>
</div>
<!-- Schedule & Pricing -->
<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="clock" 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 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اليوم</label>
<select name="day_of_week" class="form-select">
<option value="">-- غير محدد --</option>
<option value="0" <?= (old('day_of_week') ?? '') === '0' ? 'selected' : '' ?>>الأحد</option>
<option value="1" <?= (old('day_of_week') ?? '') === '1' ? 'selected' : '' ?>>الإثنين</option>
<option value="2" <?= (old('day_of_week') ?? '') === '2' ? 'selected' : '' ?>>الثلاثاء</option>
<option value="3" <?= (old('day_of_week') ?? '') === '3' ? 'selected' : '' ?>>الأربعاء</option>
<option value="4" <?= (old('day_of_week') ?? '') === '4' ? 'selected' : '' ?>>الخميس</option>
<option value="5" <?= (old('day_of_week') ?? '') === '5' ? 'selected' : '' ?>>الجمعة</option>
<option value="6" <?= (old('day_of_week') ?? '') === '6' ? 'selected' : '' ?>>السبت</option>
</select>
</div>
<div class="form-group">
<label class="form-label">وقت البداية</label>
<input type="time" name="start_time" value="<?= e(old('start_time') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">وقت النهاية</label>
<input type="time" name="end_time" value="<?= e(old('end_time') ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">فئة التسعير</label>
<select name="pricing_tier" class="form-select">
<?php foreach ($pricingTiers as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('pricing_tier') ?? 'standard') === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</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="/training-groups" 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();
// Cascading level select based on academy
var academySelect = document.getElementById('academySelect');
var levelSelect = document.getElementById('levelSelect');
if (academySelect && levelSelect) {
var allOptions = Array.from(levelSelect.querySelectorAll('option[data-academy]'));
academySelect.addEventListener('change', function() {
var academyId = this.value;
levelSelect.innerHTML = '<option value="">-- اختر المستوى --</option>';
allOptions.forEach(function(opt) {
if (opt.getAttribute('data-academy') === academyId || academyId === '') {
levelSelect.appendChild(opt.cloneNode(true));
}
});
});
// Trigger on load if academy is pre-selected
if (academySelect.value) {
academySelect.dispatchEvent(new Event('change'));
}
}
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تعديل المجموعة: <?= e($group->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/training-groups/<?= (int) $group->id ?>" class="btn btn-outline"><i data-lucide="eye" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> عرض</a>
<a href="/training-groups" 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="/training-groups/<?= (int) $group->id ?>" id="groupForm">
<?= 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="users" 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') ?? $group->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') ?? $group->name_en ?? '') ?>" class="form-input" 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">الأكاديمية <span style="color:#DC2626;">*</span></label>
<select name="academy_id" class="form-select" required id="academySelect">
<option value="">-- اختر الأكاديمية --</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= ((int)(old('academy_id') ?? $group->academy_id)) === (int) $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المستوى <span style="color:#DC2626;">*</span></label>
<select name="level_id" class="form-select" required id="levelSelect">
<option value="">-- اختر المستوى --</option>
<?php foreach ($levels as $lvl): ?>
<option value="<?= (int) $lvl['id'] ?>" data-academy="<?= (int) $lvl['academy_id'] ?>" <?= ((int)(old('level_id') ?? $group->level_id)) === (int) $lvl['id'] ? 'selected' : '' ?>><?= e($lvl['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</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="coach_id" class="form-select">
<option value="">-- بدون مدرب --</option>
<?php foreach ($coaches as $coach): ?>
<option value="<?= (int) $coach['id'] ?>" <?= ((int)(old('coach_id') ?? $group->coach_id ?? 0)) === (int) $coach['id'] ? 'selected' : '' ?>><?= e($coach['full_name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">المنشأة</label>
<select name="facility_id" class="form-select">
<option value="">-- بدون تحديد --</option>
<?php foreach ($facilities as $fac): ?>
<option value="<?= (int) $fac['id'] ?>" <?= ((int)(old('facility_id') ?? $group->facility_id ?? 0)) === (int) $fac['id'] ? 'selected' : '' ?>><?= e($fac['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
</div>
<!-- Group 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:#7C3AED;"></i>
<h3 style="margin:0;color:#7C3AED;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>
<select name="group_type" class="form-select" required>
<?php foreach ($groupTypes as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('group_type') ?? $group->group_type) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">الحد الأدنى للسعة</label>
<input type="number" name="min_capacity" value="<?= e(old('min_capacity') ?? $group->min_capacity) ?>" class="form-input" min="1" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الحد الأقصى للسعة</label>
<input type="number" name="max_capacity" value="<?= e(old('max_capacity') ?? $group->max_capacity) ?>" class="form-input" min="1" max="100" 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">العمر من</label>
<input type="number" name="age_from" value="<?= e(old('age_from') ?? $group->age_from ?? '') ?>" class="form-input" min="3" max="60" style="direction:ltr;text-align:left;" placeholder="سنة">
</div>
<div class="form-group">
<label class="form-label">العمر إلى</label>
<input type="number" name="age_to" value="<?= e(old('age_to') ?? $group->age_to ?? '') ?>" class="form-input" min="3" max="60" style="direction:ltr;text-align:left;" placeholder="سنة">
</div>
<div class="form-group">
<label class="form-label">قيد النوع</label>
<select name="gender_restriction" class="form-select">
<option value="" <?= (old('gender_restriction') ?? $group->gender_restriction ?? '') === '' ? 'selected' : '' ?>>مختلط</option>
<option value="male" <?= (old('gender_restriction') ?? $group->gender_restriction ?? '') === 'male' ? 'selected' : '' ?>>ذكور فقط</option>
<option value="female" <?= (old('gender_restriction') ?? $group->gender_restriction ?? '') === 'female' ? 'selected' : '' ?>>إناث فقط</option>
</select>
</div>
</div>
</div>
</div>
<!-- Schedule & Pricing -->
<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="clock" 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 1fr 1fr;gap:20px;">
<div class="form-group">
<label class="form-label">اليوم</label>
<select name="day_of_week" class="form-select">
<option value="">-- غير محدد --</option>
<?php
$dayNames = [0=>'الأحد',1=>'الإثنين',2=>'الثلاثاء',3=>'الأربعاء',4=>'الخميس',5=>'الجمعة',6=>'السبت'];
$currentDay = old('day_of_week') ?? $group->day_of_week ?? '';
foreach ($dayNames as $dKey => $dLabel): ?>
<option value="<?= $dKey ?>" <?= (string) $currentDay === (string) $dKey ? 'selected' : '' ?>><?= e($dLabel) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">وقت البداية</label>
<input type="time" name="start_time" value="<?= e(old('start_time') ?? $group->start_time ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">وقت النهاية</label>
<input type="time" name="end_time" value="<?= e(old('end_time') ?? $group->end_time ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">فئة التسعير</label>
<select name="pricing_tier" class="form-select">
<?php foreach ($pricingTiers as $key => $label): ?>
<option value="<?= e($key) ?>" <?= (old('pricing_tier') ?? $group->pricing_tier) === $key ? 'selected' : '' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
</div>
</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="/training-groups/<?= (int) $group->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();
// Cascading level select based on academy
var academySelect = document.getElementById('academySelect');
var levelSelect = document.getElementById('levelSelect');
if (academySelect && levelSelect) {
var allOptions = Array.from(levelSelect.querySelectorAll('option[data-academy]'));
var currentLevelId = '<?= (int) $group->level_id ?>';
academySelect.addEventListener('change', function() {
var academyId = this.value;
levelSelect.innerHTML = '<option value="">-- اختر المستوى --</option>';
allOptions.forEach(function(opt) {
if (opt.getAttribute('data-academy') === academyId || academyId === '') {
var clone = opt.cloneNode(true);
if (clone.value === currentLevelId) {
clone.selected = true;
}
levelSelect.appendChild(clone);
}
});
});
// Trigger on load
if (academySelect.value) {
academySelect.dispatchEvent(new Event('change'));
}
}
});
</script>
<?php $__template->endSection(); ?>
<?php
use App\Modules\TrainingGroups\Models\TrainingGroup;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?>المجموعات التدريبية<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/training-groups/create" class="btn btn-primary"><i data-lucide="plus" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء مجموعة</a>
<a href="/waiting-list" class="btn btn-outline"><i data-lucide="clock" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> قائمة الانتظار</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<?php
$typeColors = [
'private' => '#7C3AED',
'small_group' => '#2563EB',
'group' => '#059669',
'team' => '#D97706',
];
$currentType = $filters['group_type'] ?? '';
?>
<!-- Group 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="/training-groups"
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 ($groupTypes as $typeKey => $typeLabel): ?>
<a href="/training-groups?group_type=<?= e($typeKey) ?><?= ($filters['q'] ?? '') !== '' ? '&q=' . urlencode($filters['q']) : '' ?><?= ($filters['academy_id'] ?? '') !== '' ? '&academy_id=' . urlencode($filters['academy_id']) : '' ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentType === $typeKey ? ($typeColors[$typeKey] ?? '#0D7377') : 'transparent' ?>;color:<?= $currentType === $typeKey ? ($typeColors[$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="/training-groups" 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:160px;">
<label class="form-label" style="font-size:12px;">الأكاديمية</label>
<select name="academy_id" class="form-input">
<option value="">الكل</option>
<?php foreach ($academies as $acad): ?>
<option value="<?= (int) $acad['id'] ?>" <?= ($filters['academy_id'] ?? '') == $acad['id'] ? 'selected' : '' ?>><?= e($acad['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php if ($currentType !== ''): ?>
<input type="hidden" name="group_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="/training-groups" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Groups Grid -->
<?php if (!empty($groups)): ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(320px, 1fr));gap:20px;margin-bottom:20px;">
<?php foreach ($groups as $g):
$isActive = (int) ($g['is_active'] ?? 0);
$gType = $g['group_type'] ?? 'group';
$gTypeLabel = $groupTypes[$gType] ?? $gType;
$gColor = $typeColors[$gType] ?? '#6B7280';
$currentCount = (int) ($g['current_count'] ?? 0);
$maxCapacity = (int) ($g['max_capacity'] ?? 20);
$capacityPct = $maxCapacity > 0 ? min(100, (int) round(($currentCount / $maxCapacity) * 100)) : 0;
$capacityColor = $capacityPct >= 90 ? '#DC2626' : ($capacityPct >= 70 ? '#D97706' : '#059669');
?>
<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="/training-groups/<?= (int) $g['id'] ?>" style="text-decoration:none;color:inherit;display:block;">
<div style="padding:20px 20px 15px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
<h3 style="margin:0;font-size:16px;font-weight:700;color:#1A1A2E;"><?= e($g['name_ar']) ?></h3>
<span style="padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $gColor ?>15;color:<?= $gColor ?>;"><?= e($gTypeLabel) ?></span>
</div>
<div style="font-size:12px;color:#6B7280;margin-bottom:10px;">
<span style="display:inline-flex;align-items:center;gap:4px;margin-left:12px;">
<i data-lucide="graduation-cap" style="width:13px;height:13px;"></i>
<?= e($g['academy_name'] ?? '—') ?>
</span>
<span style="display:inline-flex;align-items:center;gap:4px;margin-left:12px;">
<i data-lucide="layers" style="width:13px;height:13px;"></i>
<?= e($g['level_name'] ?? '—') ?>
</span>
</div>
<?php if (!empty($g['coach_name'])): ?>
<div style="font-size:12px;color:#6B7280;margin-bottom:10px;display:flex;align-items:center;gap:4px;">
<i data-lucide="user" style="width:13px;height:13px;"></i>
<?= e($g['coach_name']) ?>
</div>
<?php endif; ?>
<?php if ($g['day_of_week'] !== null): ?>
<?php
$dayNames = [0=>'الأحد',1=>'الإثنين',2=>'الثلاثاء',3=>'الأربعاء',4=>'الخميس',5=>'الجمعة',6=>'السبت'];
?>
<div style="font-size:12px;color:#6B7280;margin-bottom:10px;display:flex;align-items:center;gap:4px;">
<i data-lucide="calendar" style="width:13px;height:13px;"></i>
<?= e($dayNames[(int) $g['day_of_week']] ?? '') ?>
<?php if (!empty($g['start_time']) && !empty($g['end_time'])): ?>
&nbsp;|&nbsp;<?= e(substr($g['start_time'], 0, 5)) ?> - <?= e(substr($g['end_time'], 0, 5)) ?>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- Capacity Bar -->
<div style="margin-top:12px;">
<div style="display:flex;justify-content:space-between;font-size:11px;color:#6B7280;margin-bottom:4px;">
<span>السعة</span>
<span><?= $currentCount ?> / <?= $maxCapacity ?></span>
</div>
<div style="height:6px;background:#E5E7EB;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $capacityPct ?>%;background:<?= $capacityColor ?>;border-radius:3px;transition:width 0.3s;"></div>
</div>
</div>
</div>
</a>
<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($g['code'] ?? '') ?></code>
<div style="display:flex;gap:6px;">
<a href="/training-groups/<?= (int) $g['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="/training-groups/<?= (int) $g['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="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['academy_id']) || !empty($filters['group_type'])): ?>
لا توجد نتائج مطابقة. جرب تغيير معايير البحث.
<?php else: ?>
ابدأ بإنشاء مجموعة تدريبية جديدة.
<?php endif; ?>
</p>
<a href="/training-groups/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\TrainingGroups\Models\TrainingGroup;
$__template->layout('Layout.main');
?>
<?php $__template->section('title'); ?><?= e($group->name_ar) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/training-groups/<?= (int) $group->id ?>/edit" class="btn btn-outline"><i data-lucide="edit-3" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<form method="POST" action="/training-groups/<?= (int) $group->id ?>/toggle" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="color:<?= $group->is_active ? '#DC2626' : '#059669' ?>;">
<i data-lucide="<?= $group->is_active ? 'x-circle' : 'check-circle' ?>" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i>
<?= $group->is_active ? 'إيقاف' : 'تفعيل' ?>
</button>
</form>
<a href="/training-groups" 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
$typeColors = [
'private' => '#7C3AED',
'small_group' => '#2563EB',
'group' => '#059669',
'team' => '#D97706',
];
$gType = $group->group_type ?? 'group';
$gColor = $typeColors[$gType] ?? '#6B7280';
$gTypeLabel = $groupTypes[$gType] ?? $gType;
$currentCount = (int) $group->current_count;
$maxCapacity = (int) $group->max_capacity;
$capacityPct = $maxCapacity > 0 ? min(100, (int) round(($currentCount / $maxCapacity) * 100)) : 0;
$capacityColor = $capacityPct >= 90 ? '#DC2626' : ($capacityPct >= 70 ? '#D97706' : '#059669');
?>
<!-- Group Header -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:25px;display:flex;align-items:center;gap:20px;">
<div style="width:70px;height:70px;border-radius:16px;background:<?= $gColor ?>15;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i data-lucide="users" style="width:32px;height:32px;color:<?= $gColor ?>;"></i>
</div>
<div style="flex:1;">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px;">
<h2 style="margin:0;font-size:22px;color:#1A1A2E;"><?= e($group->name_ar) ?></h2>
<span style="padding:4px 12px;border-radius:10px;font-size:12px;font-weight:600;background:<?= $gColor ?>15;color:<?= $gColor ?>;"><?= e($gTypeLabel) ?></span>
<?php if (!$group->is_active): ?>
<span style="padding:4px 12px;border-radius:10px;font-size:12px;background:#FEE2E2;color:#DC2626;">معطّل</span>
<?php endif; ?>
</div>
<?php if (!empty($group->name_en)): ?>
<div style="font-size:14px;color:#6B7280;margin-bottom:6px;"><?= e($group->name_en) ?></div>
<?php endif; ?>
<div style="display:flex;gap:15px;font-size:13px;color:#6B7280;flex-wrap:wrap;">
<span style="display:flex;align-items:center;gap:4px;"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i> <?= e($academyName) ?></span>
<span style="display:flex;align-items:center;gap:4px;"><i data-lucide="layers" style="width:14px;height:14px;"></i> <?= e($levelName) ?></span>
<?php if ($coachName): ?>
<span style="display:flex;align-items:center;gap:4px;"><i data-lucide="user" style="width:14px;height:14px;"></i> <?= e($coachName) ?></span>
<?php endif; ?>
<?php if ($facilityName): ?>
<span style="display:flex;align-items:center;gap:4px;"><i data-lucide="map-pin" style="width:14px;height:14px;"></i> <?= e($facilityName) ?></span>
<?php endif; ?>
</div>
</div>
<div style="text-align:left;">
<code style="font-size:12px;color:#9CA3AF;background:#F9FAFB;padding:4px 8px;border-radius:4px;"><?= e($group->code) ?></code>
</div>
</div>
</div>
<!-- Stats -->
<div style="display:grid;grid-template-columns:repeat(4, 1fr);gap:15px;margin-bottom:20px;">
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:<?= $capacityColor ?>;"><?= $currentCount ?> / <?= $maxCapacity ?></div>
<div style="font-size:12px;color:#6B7280;">اللاعبين / السعة</div>
<div style="margin-top:8px;height:6px;background:#E5E7EB;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $capacityPct ?>%;background:<?= $capacityColor ?>;border-radius:3px;"></div>
</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#2563EB;"><?= e($pricingTiers[$group->pricing_tier] ?? $group->pricing_tier) ?></div>
<div style="font-size:12px;color:#6B7280;">فئة التسعير</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<?php if ($group->day_of_week !== null): ?>
<div style="font-size:18px;font-weight:700;color:#7C3AED;"><?= e($dayNames[(int) $group->day_of_week] ?? '—') ?></div>
<?php else: ?>
<div style="font-size:18px;font-weight:700;color:#9CA3AF;"></div>
<?php endif; ?>
<div style="font-size:12px;color:#6B7280;">اليوم</div>
</div>
<div class="card" style="padding:15px;text-align:center;">
<?php if (!empty($group->start_time) && !empty($group->end_time)): ?>
<div style="font-size:18px;font-weight:700;color:#059669;direction:ltr;"><?= e(substr($group->start_time, 0, 5)) ?> - <?= e(substr($group->end_time, 0, 5)) ?></div>
<?php else: ?>
<div style="font-size:18px;font-weight:700;color:#9CA3AF;"></div>
<?php endif; ?>
<div style="font-size:12px;color:#6B7280;">الوقت</div>
</div>
</div>
<!-- Details -->
<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="info" style="width:16px;height:16px;color:#0D7377;"></i>
<h3 style="margin:0;font-size:14px;color:#0D7377;">تفاصيل المجموعة</h3>
</div>
<div style="padding:15px 20px;">
<table style="width:100%;font-size:13px;">
<tr><td style="padding:6px 0;color:#6B7280;width:130px;">الحد الأدنى</td><td><?= (int) $group->min_capacity ?> لاعب</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">الحد الأقصى</td><td><?= (int) $group->max_capacity ?> لاعب</td></tr>
<?php if ($group->age_from !== null || $group->age_to !== null): ?>
<tr><td style="padding:6px 0;color:#6B7280;">الفئة العمرية</td><td><?= $group->age_from ?? '—' ?> - <?= $group->age_to ?? '—' ?> سنة</td></tr>
<?php endif; ?>
<?php if ($group->gender_restriction): ?>
<tr><td style="padding:6px 0;color:#6B7280;">قيد النوع</td><td><?= $group->gender_restriction === 'male' ? 'ذكور فقط' : 'إناث فقط' ?></td></tr>
<?php endif; ?>
</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="bar-chart-3" style="width:16px;height:16px;color:#059669;"></i>
<h3 style="margin:0;font-size:14px;color:#059669;">الإشغال</h3>
</div>
<div style="padding:20px;text-align:center;">
<div style="font-size:48px;font-weight:700;color:<?= $capacityColor ?>;"><?= $capacityPct ?>%</div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">نسبة الإشغال</div>
<div style="margin-top:12px;height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden;">
<div style="height:100%;width:<?= $capacityPct ?>%;background:<?= $capacityColor ?>;border-radius:5px;transition:width 0.3s;"></div>
</div>
<?php if ((int) $group->is_full): ?>
<div style="margin-top:10px;padding:4px 12px;display:inline-block;border-radius:8px;background:#FEE2E2;color:#DC2626;font-size:12px;font-weight:600;">المجموعة ممتلئة</div>
<?php endif; ?>
</div>
</div>
</div>
<!-- Members -->
<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="user-check" style="width:16px;height:16px;color:#2563EB;"></i>
<h3 style="margin:0;font-size:14px;color:#2563EB;">الأعضاء (<?= count($members) ?>)</h3>
</div>
</div>
<div style="padding:15px 20px;">
<?php if (!empty($members)): ?>
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<thead>
<tr style="background:#F9FAFB;color:#6B7280;font-size:12px;">
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">#</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">اللاعب</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">تاريخ الانضمام</th>
<th style="padding:8px;text-align:right;border-bottom:1px solid #E5E7EB;">الهاتف</th>
<th style="padding:8px;text-align:center;border-bottom:1px solid #E5E7EB;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($members as $i => $member): ?>
<tr>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= $i + 1 ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;font-weight:600;"><?= e($member['player_name'] ?? '—') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;"><?= e(substr($member['joined_at'] ?? '', 0, 10)) ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;direction:ltr;text-align:right;"><?= e($member['phone'] ?? '—') ?></td>
<td style="padding:8px;border-bottom:1px solid #F3F4F6;text-align:center;">
<div style="display:flex;gap:4px;justify-content:center;">
<!-- Remove -->
<form method="POST" action="/training-groups/<?= (int) $group->id ?>/remove-player" style="display:inline;" onsubmit="return confirm('هل أنت متأكد من إزالة اللاعب؟');">
<?= csrf_field() ?>
<input type="hidden" name="membership_id" value="<?= (int) $member['id'] ?>">
<button type="submit" class="btn btn-sm btn-outline" style="color:#DC2626;font-size:11px;padding:3px 8px;" title="إزالة">
<i data-lucide="user-minus" style="width:12px;height:12px;"></i>
</button>
</form>
<!-- Transfer -->
<?php if (!empty($otherGroups)): ?>
<button type="button" class="btn btn-sm btn-outline" style="color:#7C3AED;font-size:11px;padding:3px 8px;" title="نقل" onclick="showTransfer(<?= (int) $member['id'] ?>, '<?= e($member['player_name'] ?? '') ?>')">
<i data-lucide="move" style="width:12px;height:12px;"></i>
</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p style="color:#9CA3AF;font-size:13px;margin:0;">لا يوجد أعضاء في هذه المجموعة حالياً.</p>
<?php endif; ?>
<!-- Add Player Form -->
<?php if (!empty($players) && !(int) $group->is_full): ?>
<div style="margin-top:15px;padding-top:15px;border-top:1px solid #E5E7EB;">
<form method="POST" action="/training-groups/<?= (int) $group->id ?>/add-player" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div style="flex:1;min-width:200px;">
<label class="form-label" style="font-size:11px;">إضافة لاعب</label>
<select name="player_id" class="form-input" style="font-size:12px;" required id="addPlayerSelect">
<option value="">-- اختر لاعب --</option>
<?php foreach ($players as $p): ?>
<option value="<?= (int) $p['player_id'] ?>" data-enrollment="<?= (int) $p['enrollment_id'] ?>"><?= e($p['player_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<input type="hidden" name="enrollment_id" id="enrollmentIdInput" value="">
<button type="submit" class="btn btn-primary" style="font-size:12px;padding:8px 14px;">
<i data-lucide="user-plus" style="width:13px;height:13px;vertical-align:middle;"></i> إضافة
</button>
</form>
</div>
<?php endif; ?>
</div>
</div>
<!-- Transfer Modal -->
<?php if (!empty($otherGroups)): ?>
<div id="transferModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:1000;align-items:center;justify-content:center;">
<div style="background:white;border-radius:12px;padding:25px;width:90%;max-width:450px;margin:auto;">
<h3 style="margin:0 0 15px;font-size:16px;color:#1A1A2E;">نقل لاعب</h3>
<p id="transferPlayerName" style="font-size:13px;color:#6B7280;margin:0 0 15px;"></p>
<form method="POST" action="/training-groups/<?= (int) $group->id ?>/transfer-player" id="transferForm">
<?= csrf_field() ?>
<input type="hidden" name="membership_id" id="transferMembershipId" value="">
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">المجموعة المستهدفة</label>
<select name="target_group_id" class="form-select" required>
<option value="">-- اختر مجموعة --</option>
<?php foreach ($otherGroups as $og): ?>
<option value="<?= (int) $og['id'] ?>"><?= e($og['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">سبب النقل</label>
<textarea name="transfer_reason" class="form-input" rows="2" placeholder="اختياري..."></textarea>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn btn-outline" onclick="hideTransfer()">إلغاء</button>
<button type="submit" class="btn btn-primary">تأكيد النقل</button>
</div>
</form>
</div>
</div>
<?php endif; ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
// Set enrollment_id when player is selected
var addPlayerSelect = document.getElementById('addPlayerSelect');
if (addPlayerSelect) {
addPlayerSelect.addEventListener('change', function() {
var opt = this.options[this.selectedIndex];
document.getElementById('enrollmentIdInput').value = opt.getAttribute('data-enrollment') || '';
});
}
});
function showTransfer(membershipId, playerName) {
document.getElementById('transferMembershipId').value = membershipId;
document.getElementById('transferPlayerName').textContent = 'نقل اللاعب: ' + playerName;
document.getElementById('transferModal').style.display = 'flex';
}
function hideTransfer() {
document.getElementById('transferModal').style.display = 'none';
}
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>قائمة الانتظار<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/training-groups" 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
$statusLabels = [
'waiting' => ['label' => 'في الانتظار', 'color' => '#D97706', 'bg' => '#FEF3C7'],
'offered' => ['label' => 'تم العرض', 'color' => '#2563EB', 'bg' => '#DBEAFE'],
'accepted' => ['label' => 'مقبول', 'color' => '#059669', 'bg' => '#D1FAE5'],
'expired' => ['label' => 'منتهي', 'color' => '#6B7280', 'bg' => '#F3F4F6'],
'cancelled' => ['label' => 'ملغي', 'color' => '#DC2626', 'bg' => '#FEE2E2'],
];
$currentStatus = $filters['status'] ?? '';
?>
<!-- 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="/waiting-list"
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 ($statusLabels as $sKey => $sInfo): ?>
<a href="/waiting-list?status=<?= e($sKey) ?>"
style="padding:12px 20px;font-size:14px;font-weight:600;text-decoration:none;border-bottom:3px solid <?= $currentStatus === $sKey ? $sInfo['color'] : 'transparent' ?>;color:<?= $currentStatus === $sKey ? $sInfo['color'] : '#6B7280' ?>;white-space:nowrap;">
<?= e($sInfo['label']) ?>
</a>
<?php endforeach; ?>
</div>
</div>
<!-- Search Bar -->
<div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/waiting-list" 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:160px;">
<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 ($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="/waiting-list" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
</form>
</div>
<!-- Entries Table -->
<?php if (!empty($entries)): ?>
<div class="card" style="margin-bottom:20px;overflow-x:auto;">
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<thead>
<tr style="background:#F9FAFB;color:#6B7280;font-size:12px;">
<th style="padding:10px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">#</th>
<th style="padding:10px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">اللاعب</th>
<th style="padding:10px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">النشاط</th>
<th style="padding:10px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">المستوى</th>
<th style="padding:10px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">الوقت المفضل</th>
<th style="padding:10px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">الأولوية</th>
<th style="padding:10px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">الحالة</th>
<th style="padding:10px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($entries as $i => $entry):
$entryStatus = $entry['status'] ?? 'waiting';
$sInfo = $statusLabels[$entryStatus] ?? $statusLabels['waiting'];
$dayNames = [0=>'الأحد',1=>'الإثنين',2=>'الثلاثاء',3=>'الأربعاء',4=>'الخميس',5=>'الجمعة',6=>'السبت'];
?>
<tr>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;"><?= $i + 1 ?></td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;">
<div style="font-weight:600;"><?= e($entry['player_name'] ?? '—') ?></div>
<?php if (!empty($entry['player_phone'])): ?>
<div style="font-size:11px;color:#9CA3AF;direction:ltr;text-align:right;"><?= e($entry['player_phone']) ?></div>
<?php endif; ?>
</td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;"><?= e($entry['discipline_name'] ?? '—') ?></td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;"><?= e($entry['level_name'] ?? '—') ?></td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;">
<?php if ($entry['preferred_day_of_week'] !== null): ?>
<?= e($dayNames[(int) $entry['preferred_day_of_week']] ?? '') ?>
<?php endif; ?>
<?php if (!empty($entry['preferred_time'])): ?>
<span style="font-size:11px;color:#6B7280;">(<?= e($entry['preferred_time']) ?>)</span>
<?php endif; ?>
</td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;text-align:center;">
<span style="display:inline-block;min-width:30px;padding:2px 8px;border-radius:8px;background:#F3F4F6;font-weight:600;"><?= (int) $entry['priority'] ?></span>
</td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;text-align:center;">
<span style="padding:3px 10px;border-radius:10px;font-size:11px;font-weight:600;background:<?= $sInfo['bg'] ?>;color:<?= $sInfo['color'] ?>;"><?= e($sInfo['label']) ?></span>
<?php if ($entryStatus === 'offered' && !empty($entry['offered_group_name'])): ?>
<div style="font-size:10px;color:#6B7280;margin-top:3px;"><?= e($entry['offered_group_name']) ?></div>
<?php endif; ?>
</td>
<td style="padding:10px 12px;border-bottom:1px solid #F3F4F6;text-align:center;">
<div style="display:flex;gap:4px;justify-content:center;">
<?php if ($entryStatus === 'waiting'): ?>
<!-- Offer -->
<form method="POST" action="/waiting-list/<?= (int) $entry['id'] ?>/offer" style="display:inline-flex;gap:4px;">
<?= csrf_field() ?>
<select name="group_id" class="form-input" style="font-size:11px;padding:3px 6px;min-width:100px;" required>
<option value="">مجموعة...</option>
<?php foreach ($availableGroups as $ag): ?>
<option value="<?= (int) $ag['id'] ?>"><?= e($ag['name_ar']) ?></option>
<?php endforeach; ?>
</select>
<button type="submit" class="btn btn-sm btn-outline" style="color:#059669;font-size:11px;padding:3px 8px;" title="عرض مقعد">
<i data-lucide="send" style="width:12px;height:12px;"></i>
</button>
</form>
<!-- Cancel -->
<form method="POST" action="/waiting-list/<?= (int) $entry['id'] ?>/cancel" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline" style="color:#DC2626;font-size:11px;padding:3px 8px;" title="إلغاء" onclick="return confirm('هل أنت متأكد؟');">
<i data-lucide="x" style="width:12px;height:12px;"></i>
</button>
</form>
<?php elseif ($entryStatus === 'offered'): ?>
<form method="POST" action="/waiting-list/<?= (int) $entry['id'] ?>/cancel" style="display:inline;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-sm btn-outline" style="color:#DC2626;font-size:11px;padding:3px 8px;" title="إلغاء العرض" onclick="return confirm('هل أنت متأكد؟');">
<i data-lucide="x" style="width:12px;height:12px;"></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="clock" 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['q']) || !empty($filters['status']) || !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
declare(strict_types=1);
use App\Core\Registries\PermissionRegistry;
use App\Core\Registries\MenuRegistry;
PermissionRegistry::register('training_groups', [
'training_group.view' => ['ar' => 'عرض المجموعات التدريبية', 'en' => 'View Training Groups'],
'training_group.manage' => ['ar' => 'إدارة المجموعات التدريبية', 'en' => 'Manage Training Groups'],
]);
MenuRegistry::register('training_groups', [
'label_ar' => 'المجموعات التدريبية',
'icon' => 'users',
'route' => '/training-groups',
'permission' => 'training_group.view',
'order' => 399,
'parent' => 'sports_activities',
]);
<?php
/**
* Mirror display layout — full-screen, no sidebar/header.
* Designed for large wall-mounted displays at reception/operations.
*/
?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $__template->yield('title', 'المراية') ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: #F9FAFB;
font-family: 'Cairo', 'Segoe UI', sans-serif;
font-size: 14px;
color: #1A1A2E;
direction: rtl;
line-height: 1.6;
min-height: 100vh;
overflow-x: hidden;
}
.btn { display: inline-flex; align-items: center; gap: 4px; padding: 6px 14px; border-radius: 6px; font-size: 13px; font-weight: 600; text-decoration: none; border: none; cursor: pointer; transition: all 0.2s; }
.btn-primary { background: #0D7377; color: white; }
.btn-outline { background: transparent; border: 1px solid #E5E7EB; color: #374151; }
</style>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
</head>
<body>
<?= $__template->yield('content') ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
</body>
</html>
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
class AcademySettlementJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
$day = (int) date('j');
$hasContracts = $this->db->selectOne(
"SELECT 1 FROM academy_contracts WHERE status = 'active' AND settlement_day = ?",
[$day]
);
return $hasContracts !== null;
}
public function run(): array
{
$today = (int) date('j');
$period = date('Y-m', strtotime('-1 month'));
$generated = 0;
$contracts = $this->db->select(
"SELECT id, academy_id, minimum_revenue_guarantee, club_commission_pct,
academy_share_pct, contract_type
FROM academy_contracts
WHERE status = 'active' AND settlement_day = ?",
[$today]
);
foreach ($contracts as $contract) {
$exists = $this->db->selectOne(
"SELECT id FROM academy_settlements WHERE contract_id = ? AND period_month = ?",
[(int) $contract['id'], $period]
);
if ($exists) {
continue;
}
$revenue = $this->db->selectOne("
SELECT COALESCE(SUM(s.total_amount), 0) AS total
FROM activity_subscriptions s
JOIN academy_enrollments ae ON ae.player_id = s.player_id AND ae.academy_id = ?
WHERE s.subscription_month = ? AND s.status = 'paid'
", [(int) $contract['academy_id'], $period])['total'] ?? 0;
$totalRevenue = (float) $revenue;
$minimum = (float) $contract['minimum_revenue_guarantee'];
$clubPct = (float) $contract['club_commission_pct'];
$academyPct = (float) $contract['academy_share_pct'];
$surplus = 0;
$shortfall = 0;
$clubCommission = 0;
$academyShare = 0;
$direction = 'balanced';
$netAmount = 0;
if ($totalRevenue >= $minimum) {
$surplus = $totalRevenue - $minimum;
$clubCommission = $totalRevenue * ($clubPct / 100);
$academyShare = $surplus * ($academyPct / 100);
$netAmount = $academyShare - $clubCommission;
$direction = $netAmount > 0 ? 'club_pays_academy' : 'academy_pays_club';
$netAmount = abs($netAmount);
} else {
$shortfall = $minimum - $totalRevenue;
$direction = 'academy_pays_club';
$netAmount = $shortfall;
}
$settlementNumber = 'STL-' . date('Ym') . '-' . str_pad((string) ($generated + 1), 3, '0', STR_PAD_LEFT);
$this->db->insert('academy_settlements', [
'settlement_number' => $settlementNumber,
'contract_id' => (int) $contract['id'],
'period_month' => $period,
'total_revenue' => $totalRevenue,
'minimum_guarantee' => $minimum,
'revenue_surplus' => $surplus,
'revenue_shortfall' => $shortfall,
'club_commission' => $clubCommission,
'academy_share' => $academyShare,
'net_amount' => $netAmount,
'direction' => $direction,
'status' => 'draft',
]);
$generated++;
}
return [
'status' => 'success',
'message' => "Generated {$generated} academy settlements for period {$period}.",
];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
class AlertProcessorJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Hourly via system cron
}
public function run(): array
{
$triggered = 0;
$rules = $this->db->select(
"SELECT * FROM alert_rules WHERE is_active = 1"
);
foreach ($rules as $rule) {
$results = $this->checkRule($rule);
foreach ($results as $context) {
$this->db->insert('alert_log', [
'alert_rule_id' => (int) $rule['id'],
'context_json' => json_encode($context, JSON_UNESCAPED_UNICODE),
'status' => 'sent',
]);
$triggered++;
}
if (!empty($results)) {
$this->db->update('alert_rules', [
'last_triggered_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $rule['id']]);
}
}
return [
'status' => 'success',
'message' => "Processed alerts: {$triggered} triggered.",
];
}
private function checkRule(array $rule): array
{
$config = json_decode($rule['trigger_config_json'] ?? '{}', true) ?: [];
return match ($rule['code']) {
'MEDICAL_CERT_EXPIRY' => $this->checkMedicalExpiry($config),
'CONTRACT_EXPIRY' => $this->checkContractExpiry($config),
'ATTENDANCE_DROPPING' => $this->checkAttendanceDropping($config),
'GROUP_NEAR_CAPACITY' => $this->checkGroupCapacity($config),
'SUBSCRIPTION_OVERDUE' => $this->checkOverdueSubscriptions($config),
default => [],
};
}
private function checkMedicalExpiry(array $config): array
{
$days = $config['days_before'] ?? 30;
$targetDate = date('Y-m-d', strtotime('+' . $days . ' days'));
$rows = $this->db->select("
SELECT p.id, p.full_name_ar, 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 = ?
", [$targetDate]);
$results = [];
foreach ($rows as $row) {
$alreadyLogged = $this->db->selectOne("
SELECT id FROM alert_log
WHERE alert_rule_id = (SELECT id FROM alert_rules WHERE code = 'MEDICAL_CERT_EXPIRY' LIMIT 1)
AND JSON_EXTRACT(context_json, '$.player_id') = ?
AND triggered_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
", [(int) $row['id']]);
if (!$alreadyLogged) {
$results[] = [
'player_id' => (int) $row['id'],
'player_name' => $row['full_name_ar'],
'expiry_date' => $row['medical_expiry_date'],
'days_left' => $days,
];
}
}
return $results;
}
private function checkContractExpiry(array $config): array
{
$days = $config['days_before'] ?? 30;
$targetDate = date('Y-m-d', strtotime('+' . $days . ' days'));
$rows = $this->db->select("
SELECT ac.id, ac.contract_number, ac.end_date, a.name_ar AS academy_name
FROM academy_contracts ac
JOIN academies a ON a.id = ac.academy_id
WHERE ac.status = 'active' AND ac.end_date = ?
", [$targetDate]);
return array_map(fn($r) => [
'contract_id' => (int) $r['id'],
'contract_number' => $r['contract_number'],
'academy_name' => $r['academy_name'],
'end_date' => $r['end_date'],
], $rows);
}
private function checkAttendanceDropping(array $config): array
{
$threshold = $config['consecutive_absences'] ?? 3;
$rows = $this->db->select("
SELECT sa.player_id, p.full_name_ar,
COUNT(*) AS consecutive_absences
FROM session_attendance sa
JOIN players p ON p.id = sa.player_id
JOIN training_sessions ts ON ts.id = sa.session_id
WHERE sa.status = 'absent'
AND ts.session_date >= DATE_SUB(CURDATE(), INTERVAL 14 DAY)
GROUP BY sa.player_id
HAVING consecutive_absences >= ?
", [$threshold]);
return array_map(fn($r) => [
'player_id' => (int) $r['player_id'],
'player_name' => $r['full_name_ar'],
'absences' => (int) $r['consecutive_absences'],
], $rows);
}
private function checkGroupCapacity(array $config): array
{
$threshold = $config['capacity_pct'] ?? 90;
$rows = $this->db->select("
SELECT id, name_ar, current_count, max_capacity,
ROUND((current_count / max_capacity) * 100) AS utilization_pct
FROM training_groups
WHERE status = 'active'
AND max_capacity > 0
AND ROUND((current_count / max_capacity) * 100) >= ?
AND is_full = 0
", [$threshold]);
return array_map(fn($r) => [
'group_id' => (int) $r['id'],
'group_name' => $r['name_ar'],
'current' => (int) $r['current_count'],
'max' => (int) $r['max_capacity'],
'utilization_pct' => (int) $r['utilization_pct'],
], $rows);
}
private function checkOverdueSubscriptions(array $config): array
{
$daysOverdue = $config['days_overdue'] ?? 7;
$targetDate = date('Y-m-d', strtotime('-' . $daysOverdue . ' days'));
$rows = $this->db->select("
SELECT s.id, s.player_id, s.total_amount, s.due_date,
p.full_name_ar
FROM activity_subscriptions s
JOIN players p ON p.id = s.player_id
WHERE s.status = 'overdue'
AND s.due_date = ?
", [$targetDate]);
return array_map(fn($r) => [
'subscription_id' => (int) $r['id'],
'player_id' => (int) $r['player_id'],
'player_name' => $r['full_name_ar'],
'amount' => (float) $r['total_amount'],
'due_date' => $r['due_date'],
], $rows);
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
class CoachPaymentGeneratorJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return (int) date('j') === 1; // 1st of month
}
public function run(): array
{
$period = date('Y-m', strtotime('-1 month'));
$periodStart = $period . '-01';
$periodEnd = date('Y-m-t', strtotime($periodStart));
$generated = 0;
$coaches = $this->db->select(
"SELECT id, payment_model, session_rate, per_player_rate, monthly_rate
FROM coaches
WHERE is_active = 1 AND is_archived = 0"
);
foreach ($coaches as $coach) {
$exists = $this->db->selectOne(
"SELECT id FROM coach_payments WHERE coach_id = ? AND payment_period = ?",
[(int) $coach['id'], $period]
);
if ($exists) {
continue;
}
$sessionData = $this->db->selectOne("
SELECT COUNT(*) AS sessions_count,
COALESCE(SUM(players_attended), 0) AS total_players
FROM training_sessions
WHERE coach_id = ?
AND session_date BETWEEN ? AND ?
AND status = 'completed'
", [(int) $coach['id'], $periodStart, $periodEnd]);
$sessions = (int) ($sessionData['sessions_count'] ?? 0);
$players = (int) ($sessionData['total_players'] ?? 0);
$model = $coach['payment_model'] ?? 'monthly_fixed';
$base = 0;
$bonus = 0;
$calculation = [];
switch ($model) {
case 'per_session':
$rate = (float) ($coach['session_rate'] ?? 0);
$base = $sessions * $rate;
$calculation = ['rate' => $rate, 'sessions' => $sessions];
break;
case 'per_player':
$rate = (float) ($coach['per_player_rate'] ?? 0);
$base = $players * $rate;
$calculation = ['rate' => $rate, 'players' => $players];
break;
case 'monthly_fixed':
$base = (float) ($coach['monthly_rate'] ?? 0);
$calculation = ['monthly_rate' => $base];
break;
case 'hybrid':
$base = (float) ($coach['monthly_rate'] ?? 0);
$sessionRate = (float) ($coach['session_rate'] ?? 0);
$threshold = 12;
if ($sessions > $threshold) {
$bonus = ($sessions - $threshold) * $sessionRate;
}
$calculation = [
'base' => $base, 'threshold' => $threshold,
'extra_sessions' => max(0, $sessions - $threshold),
'session_rate' => $sessionRate,
];
break;
}
$net = $base + $bonus;
$this->db->insert('coach_payments', [
'coach_id' => (int) $coach['id'],
'payment_period' => $period,
'payment_model' => $model,
'sessions_conducted' => $sessions,
'total_players_served' => $players,
'base_amount' => $base,
'bonus' => $bonus,
'deductions' => 0,
'net_amount' => $net,
'calculation_json' => json_encode($calculation, JSON_UNESCAPED_UNICODE),
'status' => 'draft',
]);
$generated++;
}
return [
'status' => 'success',
'message' => "Generated {$generated} coach payment records for {$period}.",
];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
class MakeupCreditExpiryJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Daily
}
public function run(): array
{
$result = $this->db->query(
"UPDATE makeup_credits
SET status = 'expired'
WHERE status = 'available'
AND expires_at IS NOT NULL
AND expires_at < CURDATE()"
);
$count = $result->rowCount();
return [
'status' => 'success',
'message' => "Expired {$count} makeup credits.",
];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
class SessionAutoCompleteJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return true; // Daily
}
public function run(): array
{
$yesterday = date('Y-m-d', strtotime('-1 day'));
$result = $this->db->query(
"UPDATE training_sessions
SET status = 'completed',
players_attended = (
SELECT COUNT(*) FROM session_attendance
WHERE session_id = training_sessions.id
AND status IN ('present', 'late', 'makeup')
)
WHERE status IN ('scheduled', 'in_progress')
AND session_date < ?",
[$yesterday]
);
$count = $result->rowCount();
return [
'status' => 'success',
'message' => "Auto-completed {$count} past sessions.",
];
}
}
<?php
declare(strict_types=1);
namespace CronJobs;
use App\Core\Database;
class SessionGeneratorJob
{
private Database $db;
public function __construct(Database $db) { $this->db = $db; }
public function shouldRun(): bool
{
return (int) date('N') === 7; // Sundays
}
public function run(): array
{
$nextMonday = date('Y-m-d', strtotime('next monday'));
$created = 0;
$skipped = 0;
$groups = $this->db->select("
SELECT id, academy_id, coach_id, facility_id, day_of_week, start_time, end_time
FROM training_groups
WHERE status = 'active'
");
$dayMap = [
'saturday' => 6, 'sunday' => 0, 'monday' => 1, 'tuesday' => 2,
'wednesday' => 3, 'thursday' => 4, 'friday' => 5,
];
foreach ($groups as $group) {
$dayNum = $dayMap[$group['day_of_week']] ?? null;
if ($dayNum === null) {
$skipped++;
continue;
}
$daysFromMonday = ($dayNum - 1 + 7) % 7;
$sessionDate = date('Y-m-d', strtotime($nextMonday . ' +' . $daysFromMonday . ' days'));
$exists = $this->db->selectOne(
"SELECT id FROM training_sessions WHERE group_id = ? AND session_date = ?",
[(int) $group['id'], $sessionDate]
);
if ($exists) {
$skipped++;
continue;
}
$enrolledCount = $this->db->selectOne(
"SELECT COUNT(*) AS cnt FROM group_memberships WHERE group_id = ? AND status = 'active'",
[(int) $group['id']]
)['cnt'] ?? 0;
$this->db->insert('training_sessions', [
'group_id' => (int) $group['id'],
'academy_id' => $group['academy_id'] ? (int) $group['academy_id'] : null,
'coach_id' => $group['coach_id'] ? (int) $group['coach_id'] : null,
'facility_id' => $group['facility_id'] ? (int) $group['facility_id'] : null,
'session_date' => $sessionDate,
'start_time' => $group['start_time'],
'end_time' => $group['end_time'],
'session_type' => 'regular',
'status' => 'scheduled',
'players_expected' => (int) $enrolledCount,
]);
$created++;
}
return [
'status' => 'success',
'message' => "Generated {$created} sessions for week of {$nextMonday}, skipped {$skipped}.",
];
}
}
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `coaches` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(30) NOT NULL,
`full_name_ar` VARCHAR(300) NOT NULL,
`full_name_en` VARCHAR(300) NULL,
`national_id` VARCHAR(20) NULL,
`phone` VARCHAR(30) NULL,
`email` VARCHAR(200) NULL,
`date_of_birth` DATE NULL,
`gender` VARCHAR(10) NULL,
`photo_path` VARCHAR(500) NULL,
`bio_ar` TEXT NULL,
`certifications_json` JSON NULL,
`employment_type` VARCHAR(20) NOT NULL DEFAULT 'contract' COMMENT 'staff, contract, freelance',
`employee_id` BIGINT UNSIGNED NULL COMMENT 'Link to HR if staff coach',
`max_players_default` INT UNSIGNED NOT NULL DEFAULT 20,
`hourly_rate` DECIMAL(15,2) NULL,
`session_rate` DECIMAL(15,2) NULL,
`monthly_rate` DECIMAL(15,2) NULL,
`payment_model` VARCHAR(20) NOT NULL DEFAULT 'per_session' COMMENT 'per_session, per_player, monthly_fixed, hybrid',
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP 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,
`branch_id` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_coaches_code` (`code`),
INDEX `idx_coaches_nid` (`national_id`),
INDEX `idx_coaches_active` (`is_active`, `is_archived`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `coaches`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `coach_disciplines` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`coach_id` BIGINT UNSIGNED NOT NULL,
`discipline_id` BIGINT UNSIGNED NOT NULL,
`specialization_level` VARCHAR(30) NOT NULL DEFAULT 'general' COMMENT 'general, specialist, head_coach',
`is_primary` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uq_cd_coach_discipline` (`coach_id`, `discipline_id`),
CONSTRAINT `fk_cd_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_cd_discipline` FOREIGN KEY (`discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `coach_disciplines`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `coach_academy_assignments` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`coach_id` BIGINT UNSIGNED NOT NULL,
`academy_id` BIGINT UNSIGNED NOT NULL,
`level_id` BIGINT UNSIGNED NULL,
`role` VARCHAR(30) NOT NULL DEFAULT 'coach' COMMENT 'head_coach, coach, assistant',
`max_players` INT UNSIGNED NULL COMMENT 'Override default for this assignment',
`assigned_from` DATE NOT NULL,
`assigned_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,
INDEX `idx_caa_coach` (`coach_id`),
INDEX `idx_caa_academy` (`academy_id`),
CONSTRAINT `fk_caa_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_caa_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_caa_level` FOREIGN KEY (`level_id`) REFERENCES `academy_levels`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `coach_academy_assignments`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `coach_availability` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`coach_id` BIGINT UNSIGNED NOT NULL,
`day_of_week` TINYINT UNSIGNED NOT NULL COMMENT '0=Sunday, 6=Saturday',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`is_available` TINYINT(1) NOT NULL DEFAULT 1,
`effective_from` DATE NULL,
`effective_to` DATE NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_ca_coach_day` (`coach_id`, `day_of_week`),
CONSTRAINT `fk_ca_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `coach_availability`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `academy_schedules`
ADD COLUMN `coach_id` BIGINT UNSIGNED NULL AFTER `coach_name`,
ADD COLUMN `session_type` VARCHAR(20) NOT NULL DEFAULT 'group' COMMENT 'private, small_group, group, team' AFTER `coach_id`,
ADD INDEX `idx_as_coach` (`coach_id`),
ADD CONSTRAINT `fk_as_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE SET NULL",
'down' => "ALTER TABLE `academy_schedules`
DROP FOREIGN KEY `fk_as_coach`,
DROP INDEX `idx_as_coach`,
DROP COLUMN `session_type`,
DROP COLUMN `coach_id`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `academy_contracts` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`contract_number` VARCHAR(50) NOT NULL,
`academy_id` BIGINT UNSIGNED NOT NULL,
`contract_type` VARCHAR(30) NOT NULL DEFAULT 'revenue_share' COMMENT 'revenue_share, fixed_rent, hybrid',
`start_date` DATE NOT NULL,
`end_date` DATE NOT NULL,
`minimum_revenue_guarantee` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`club_commission_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
`academy_share_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
`fixed_monthly_rent` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`deposit_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`deposit_status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, paid, returned, forfeited',
`settlement_day` TINYINT UNSIGNED NOT NULL DEFAULT 5,
`grace_period_days` TINYINT UNSIGNED NOT NULL DEFAULT 7,
`penalty_rate_pct` DECIMAL(5,2) NOT NULL DEFAULT 0.00,
`auto_renew` TINYINT(1) NOT NULL DEFAULT 0,
`renewal_notice_days` INT UNSIGNED NOT NULL DEFAULT 30,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, pending_approval, active, suspended, expired, terminated',
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL,
`terminated_by` BIGINT UNSIGNED NULL,
`terminated_at` TIMESTAMP NULL,
`termination_reason` TEXT NULL,
`terms_json` JSON NULL,
`notes` TEXT NULL,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP 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,
`branch_id` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_ac_number` (`contract_number`),
INDEX `idx_ac_academy` (`academy_id`),
INDEX `idx_ac_status` (`status`),
INDEX `idx_ac_dates` (`start_date`, `end_date`),
CONSTRAINT `fk_ac_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `academy_contracts`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `academy_revenue_records` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`academy_id` BIGINT UNSIGNED NOT NULL,
`contract_id` BIGINT UNSIGNED NOT NULL,
`period_month` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM',
`subscription_revenue` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`registration_revenue` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`other_revenue` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`total_revenue` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`enrolled_count` INT UNSIGNED NOT NULL DEFAULT 0,
`active_players_count` INT UNSIGNED NOT NULL DEFAULT 0,
`is_finalized` TINYINT(1) NOT NULL DEFAULT 0,
`calculated_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,
UNIQUE KEY `uq_arr_academy_contract_month` (`academy_id`, `contract_id`, `period_month`),
INDEX `idx_arr_contract` (`contract_id`),
CONSTRAINT `fk_arr_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_arr_contract` FOREIGN KEY (`contract_id`) REFERENCES `academy_contracts`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `academy_revenue_records`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `academy_settlements` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`settlement_number` VARCHAR(50) NOT NULL,
`academy_id` BIGINT UNSIGNED NOT NULL,
`contract_id` BIGINT UNSIGNED NOT NULL,
`period_month` VARCHAR(7) NOT NULL,
`total_revenue` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`minimum_guarantee` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`revenue_surplus` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`revenue_shortfall` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`club_commission` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`academy_share` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`net_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`penalty_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`direction` VARCHAR(20) NOT NULL COMMENT 'club_pays_academy, academy_pays_club, balanced',
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, pending_approval, approved, paid, disputed',
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL,
`payment_id` BIGINT UNSIGNED NULL,
`paid_at` TIMESTAMP NULL,
`dispute_reason` TEXT 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_as_number` (`settlement_number`),
INDEX `idx_as_academy_month` (`academy_id`, `period_month`),
INDEX `idx_as_contract` (`contract_id`),
INDEX `idx_as_status` (`status`),
CONSTRAINT `fk_as2_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE RESTRICT,
CONSTRAINT `fk_as2_contract` FOREIGN KEY (`contract_id`) REFERENCES `academy_contracts`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `academy_settlements`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `training_groups` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(50) NOT NULL,
`name_ar` VARCHAR(255) NOT NULL,
`name_en` VARCHAR(255) NULL,
`academy_id` BIGINT UNSIGNED NOT NULL,
`level_id` BIGINT UNSIGNED NOT NULL,
`coach_id` BIGINT UNSIGNED NULL,
`facility_id` BIGINT UNSIGNED NULL,
`group_type` VARCHAR(20) NOT NULL DEFAULT 'group' COMMENT 'private, small_group, group, team',
`min_capacity` INT UNSIGNED NOT NULL DEFAULT 1,
`max_capacity` INT UNSIGNED NOT NULL DEFAULT 20,
`current_count` INT UNSIGNED NOT NULL DEFAULT 0,
`age_from` INT UNSIGNED NULL,
`age_to` INT UNSIGNED NULL,
`gender_restriction` VARCHAR(10) NULL COMMENT 'male, female, NULL=mixed',
`day_of_week` TINYINT UNSIGNED NULL,
`start_time` TIME NULL,
`end_time` TIME NULL,
`pricing_tier` VARCHAR(20) NOT NULL DEFAULT 'standard' COMMENT 'premium, standard, economy',
`is_full` TINYINT(1) NOT NULL DEFAULT 0,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`is_archived` TINYINT(1) NOT NULL DEFAULT 0,
`archived_at` TIMESTAMP 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_tg_code` (`code`),
INDEX `idx_tg_academy_level` (`academy_id`, `level_id`),
INDEX `idx_tg_coach` (`coach_id`),
INDEX `idx_tg_capacity` (`is_full`, `is_active`),
CONSTRAINT `fk_tg_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_tg_level` FOREIGN KEY (`level_id`) REFERENCES `academy_levels`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_tg_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_tg_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 `training_groups`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `group_memberships` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`group_id` BIGINT UNSIGNED NOT NULL,
`enrollment_id` BIGINT UNSIGNED NOT NULL,
`player_id` BIGINT UNSIGNED NOT NULL,
`joined_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`left_at` TIMESTAMP NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT 'active, transferred, removed',
`transferred_to_group_id` BIGINT UNSIGNED NULL,
`transfer_reason` VARCHAR(500) NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_gm_group` (`group_id`),
INDEX `idx_gm_player` (`player_id`),
CONSTRAINT `fk_gm_group` FOREIGN KEY (`group_id`) REFERENCES `training_groups`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_gm_enrollment` FOREIGN KEY (`enrollment_id`) REFERENCES `academy_enrollments`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_gm_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `group_memberships`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `waiting_list` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`discipline_id` BIGINT UNSIGNED NOT NULL,
`level_id` BIGINT UNSIGNED NULL,
`preferred_day_of_week` TINYINT UNSIGNED NULL,
`preferred_time` VARCHAR(5) NULL COMMENT 'AM or PM',
`priority` INT UNSIGNED NOT NULL DEFAULT 100,
`requested_group_type` VARCHAR(20) NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'waiting' COMMENT 'waiting, offered, accepted, expired, cancelled',
`offered_group_id` BIGINT UNSIGNED NULL,
`offered_at` TIMESTAMP NULL,
`offer_expires_at` TIMESTAMP NULL,
`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_wl_player` (`player_id`),
INDEX `idx_wl_discipline` (`discipline_id`),
INDEX `idx_wl_status_priority` (`status`, `priority`),
CONSTRAINT `fk_wl_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_wl_discipline` FOREIGN KEY (`discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `waiting_list`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `activity_pricing` ADD COLUMN `group_type` VARCHAR(20) NULL AFTER `fee_type`, ADD COLUMN `service_tier` VARCHAR(20) NULL DEFAULT 'standard' AFTER `group_type`, ADD INDEX `idx_ap_group_tier` (`group_type`, `service_tier`)",
'down' => "ALTER TABLE `activity_pricing` DROP INDEX `idx_ap_group_tier`, DROP COLUMN `service_tier`, DROP COLUMN `group_type`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `training_sessions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`group_id` BIGINT UNSIGNED NOT NULL,
`academy_id` BIGINT UNSIGNED NOT NULL,
`coach_id` BIGINT UNSIGNED NULL,
`facility_id` BIGINT UNSIGNED NULL,
`session_date` DATE NOT NULL,
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`session_type` VARCHAR(20) NOT NULL DEFAULT 'regular' COMMENT 'regular, makeup, extra, assessment',
`status` VARCHAR(20) NOT NULL DEFAULT 'scheduled' COMMENT 'scheduled, in_progress, completed, cancelled',
`cancellation_reason` VARCHAR(500) NULL,
`cancelled_by` BIGINT UNSIGNED NULL,
`players_expected` INT UNSIGNED NOT NULL DEFAULT 0,
`players_attended` INT UNSIGNED NOT NULL DEFAULT 0,
`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_ts_group` (`group_id`),
INDEX `idx_ts_academy` (`academy_id`),
INDEX `idx_ts_coach` (`coach_id`),
INDEX `idx_ts_date` (`session_date`),
INDEX `idx_ts_status` (`status`),
CONSTRAINT `fk_ts_group` FOREIGN KEY (`group_id`) REFERENCES `training_groups`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_ts_academy` FOREIGN KEY (`academy_id`) REFERENCES `academies`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_ts_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches`(`id`) ON DELETE SET NULL,
CONSTRAINT `fk_ts_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 `training_sessions`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `session_attendance` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`session_id` BIGINT UNSIGNED NOT NULL,
`player_id` BIGINT UNSIGNED NOT NULL,
`enrollment_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'present' COMMENT 'present, absent, late, excused, makeup',
`check_in_time` TIME NULL,
`check_out_time` TIME NULL,
`is_makeup` TINYINT(1) NOT NULL DEFAULT 0,
`makeup_for_session_id` BIGINT UNSIGNED NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`created_by` BIGINT UNSIGNED NULL,
UNIQUE KEY `uq_sa_session_player` (`session_id`, `player_id`),
INDEX `idx_sa_player` (`player_id`),
INDEX `idx_sa_status` (`status`),
CONSTRAINT `fk_sa_session` FOREIGN KEY (`session_id`) REFERENCES `training_sessions`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_sa_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_sa_enrollment` FOREIGN KEY (`enrollment_id`) REFERENCES `academy_enrollments`(`id`) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `session_attendance`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `makeup_credits` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`enrollment_id` BIGINT UNSIGNED NOT NULL,
`missed_session_id` BIGINT UNSIGNED NOT NULL,
`used_session_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'available' COMMENT 'available, used, expired',
`expires_at` DATE NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_mc_player` (`player_id`),
INDEX `idx_mc_status` (`status`),
CONSTRAINT `fk_mc_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_mc_missed_session` FOREIGN KEY (`missed_session_id`) REFERENCES `training_sessions`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `makeup_credits`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `players`
ADD COLUMN `birth_cert_path` VARCHAR(500) NULL AFTER `photo_path`,
ADD COLUMN `birth_cert_verified` TINYINT(1) NOT NULL DEFAULT 0 AFTER `birth_cert_path`,
ADD COLUMN `current_level_id` BIGINT UNSIGNED NULL AFTER `birth_cert_verified`,
ADD COLUMN `overall_rating` DECIMAL(3,1) NULL AFTER `current_level_id`",
'down' => "ALTER TABLE `players`
DROP COLUMN `overall_rating`,
DROP COLUMN `current_level_id`,
DROP COLUMN `birth_cert_verified`,
DROP COLUMN `birth_cert_path`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `player_medical_records`
ADD COLUMN `certificate_type` VARCHAR(30) NULL DEFAULT 'recreational' COMMENT 'recreational, academy, international' AFTER `record_type`,
ADD COLUMN `validity_months` INT UNSIGNED NULL AFTER `certificate_type`,
ADD COLUMN `issuing_authority` VARCHAR(300) NULL AFTER `clinic_name`,
ADD COLUMN `cert_number` VARCHAR(100) NULL AFTER `issuing_authority`",
'down' => "ALTER TABLE `player_medical_records`
DROP COLUMN `cert_number`,
DROP COLUMN `issuing_authority`,
DROP COLUMN `validity_months`,
DROP COLUMN `certificate_type`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `player_progressions` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`player_id` BIGINT UNSIGNED NOT NULL,
`discipline_id` BIGINT UNSIGNED NOT NULL,
`from_level_id` BIGINT UNSIGNED NULL,
`to_level_id` BIGINT UNSIGNED NOT NULL,
`progression_type` VARCHAR(30) NOT NULL COMMENT 'promotion, demotion, lateral',
`assessment_score` DECIMAL(5,2) NULL,
`assessed_by` BIGINT UNSIGNED NULL COMMENT 'Coach who assessed',
`assessment_notes` TEXT NULL,
`effective_date` DATE NOT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_pp_player` (`player_id`),
INDEX `idx_pp_discipline` (`discipline_id`),
CONSTRAINT `fk_pp_player` FOREIGN KEY (`player_id`) REFERENCES `players`(`id`) ON DELETE CASCADE,
CONSTRAINT `fk_pp_discipline` FOREIGN KEY (`discipline_id`) REFERENCES `sport_disciplines`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `player_progressions`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `pool_configurations` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`facility_id` BIGINT UNSIGNED NOT NULL,
`name_ar` VARCHAR(255) NOT NULL,
`name_en` VARCHAR(255) NULL,
`length_meters` DECIMAL(5,2) NOT NULL,
`width_meters` DECIMAL(5,2) NOT NULL,
`depth_shallow` DECIMAL(3,2) NULL,
`depth_deep` DECIMAL(3,2) NULL,
`total_lanes_lengthwise` INT UNSIGNED NOT NULL DEFAULT 6,
`total_lanes_widthwise` INT UNSIGNED NOT NULL DEFAULT 0,
`lane_widths_json` JSON NOT NULL COMMENT 'array of widths per lane',
`max_total_swimmers` INT UNSIGNED NOT NULL DEFAULT 50,
`max_per_lane` INT UNSIGNED NOT NULL DEFAULT 8,
`safety_ratio_json` JSON NULL COMMENT '{\"lap_swimming\":6,\"lessons\":4,\"free_swim\":8}',
`operating_hours_json` JSON NULL COMMENT '{\"start\":\"06:00\",\"end\":\"22:00\",\"slot_minutes\":60}',
`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_pc_facility` (`facility_id`),
CONSTRAINT `fk_pc_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 `pool_configurations`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `pool_lanes` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`pool_config_id` BIGINT UNSIGNED NOT NULL,
`lane_number` INT UNSIGNED NOT NULL,
`direction` VARCHAR(10) NOT NULL DEFAULT 'lengthwise' COMMENT 'lengthwise, widthwise',
`width_meters` DECIMAL(4,2) NOT NULL,
`label_ar` VARCHAR(50) NULL,
`label_en` VARCHAR(50) NULL,
`max_swimmers` INT UNSIGNED NOT NULL DEFAULT 8,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`sort_order` INT UNSIGNED NOT NULL DEFAULT 0,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_pl_config` (`pool_config_id`),
UNIQUE KEY `uq_pl_config_lane` (`pool_config_id`, `direction`, `lane_number`),
CONSTRAINT `fk_pl_config` FOREIGN KEY (`pool_config_id`) REFERENCES `pool_configurations`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `pool_lanes`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE IF NOT EXISTS `pool_bookings` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`booking_code` VARCHAR(50) NOT NULL,
`pool_config_id` BIGINT UNSIGNED NOT NULL,
`booking_date` DATE NOT NULL,
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`booking_type` VARCHAR(30) NOT NULL COMMENT 'lap_swimming, lessons, free_swim, academy_session, competition, maintenance',
`selection_mode` VARCHAR(20) NOT NULL COMMENT 'full_pool, lane, multi_lane',
`lanes_json` JSON NOT NULL COMMENT 'array of lane numbers booked',
`booker_type` VARCHAR(20) NOT NULL DEFAULT 'member' COMMENT 'member, non_member, academy, staff',
`booker_id` BIGINT UNSIGNED NULL,
`booker_name` VARCHAR(300) NULL,
`expected_swimmers` INT UNSIGNED NOT NULL DEFAULT 1,
`actual_swimmers` INT UNSIGNED NULL,
`unit_rate` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00,
`payment_status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending, paid, exempt',
`payment_id` BIGINT UNSIGNED NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'confirmed' COMMENT 'confirmed, checked_in, completed, cancelled, no_show',
`cancelled_by` BIGINT UNSIGNED NULL,
`cancelled_at` TIMESTAMP 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_pb_code` (`booking_code`),
INDEX `idx_pb_pool_date` (`pool_config_id`, `booking_date`),
INDEX `idx_pb_date_time` (`booking_date`, `start_time`, `end_time`),
INDEX `idx_pb_status` (`status`),
CONSTRAINT `fk_pb_pool` FOREIGN KEY (`pool_config_id`) REFERENCES `pool_configurations`(`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `pool_bookings`",
];
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE `facilities`
ADD COLUMN `maintenance_schedule_json` JSON NULL AFTER `is_archived`,
ADD COLUMN `peak_hours_json` JSON NULL AFTER `maintenance_schedule_json`",
'down' => "ALTER TABLE `facilities`
DROP COLUMN `peak_hours_json`,
DROP COLUMN `maintenance_schedule_json`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `alert_rules` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`code` VARCHAR(50) NOT NULL UNIQUE,
`name_ar` VARCHAR(300) NOT NULL,
`category` VARCHAR(50) NOT NULL COMMENT 'medical, contracts, financial, attendance, capacity, subscriptions, safety',
`trigger_type` VARCHAR(30) NOT NULL COMMENT 'threshold, expiry, schedule',
`trigger_config_json` JSON NOT NULL,
`notification_channels` VARCHAR(200) NOT NULL DEFAULT 'system' COMMENT 'comma-separated: system, sms, email',
`recipient_roles_json` JSON NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`last_triggered_at` TIMESTAMP NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_alert_category` (`category`),
INDEX `idx_alert_active` (`is_active`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `alert_rules`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `alert_log` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`alert_rule_id` BIGINT UNSIGNED NOT NULL,
`triggered_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`context_json` JSON NULL COMMENT 'entity details that triggered it',
`recipients_json` JSON NULL COMMENT 'who was notified',
`status` VARCHAR(20) NOT NULL DEFAULT 'sent' COMMENT 'sent, failed, acknowledged',
`acknowledged_by` BIGINT UNSIGNED NULL,
`acknowledged_at` TIMESTAMP NULL,
CONSTRAINT `fk_alert_log_rule` FOREIGN KEY (`alert_rule_id`) REFERENCES `alert_rules` (`id`) ON DELETE CASCADE,
INDEX `idx_alert_log_rule` (`alert_rule_id`),
INDEX `idx_alert_log_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `alert_log`",
];
<?php
declare(strict_types=1);
return [
'up' => "CREATE TABLE `coach_payments` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`coach_id` BIGINT UNSIGNED NOT NULL,
`payment_period` VARCHAR(7) NOT NULL COMMENT 'YYYY-MM format',
`payment_model` VARCHAR(30) NOT NULL COMMENT 'per_session, per_player, monthly_fixed, hybrid',
`sessions_conducted` INT UNSIGNED NOT NULL DEFAULT 0,
`total_players_served` INT UNSIGNED NOT NULL DEFAULT 0,
`base_amount` DECIMAL(10,2) NOT NULL DEFAULT 0,
`bonus` DECIMAL(10,2) NOT NULL DEFAULT 0,
`deductions` DECIMAL(10,2) NOT NULL DEFAULT 0,
`net_amount` DECIMAL(10,2) NOT NULL DEFAULT 0,
`calculation_json` JSON NULL,
`status` VARCHAR(20) NOT NULL DEFAULT 'draft' COMMENT 'draft, approved, paid',
`approved_by` BIGINT UNSIGNED NULL,
`approved_at` TIMESTAMP NULL,
`paid_at` TIMESTAMP NULL,
`notes` TEXT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_cp_coach` (`coach_id`),
INDEX `idx_cp_period` (`payment_period`),
UNIQUE KEY `uk_cp_coach_period` (`coach_id`, `payment_period`),
CONSTRAINT `fk_cp_coach` FOREIGN KEY (`coach_id`) REFERENCES `coaches` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'down' => "DROP TABLE IF EXISTS `coach_payments`",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$rules = [
[
'code' => 'MEDICAL_CERT_EXPIRY',
'name_ar' => 'انتهاء الشهادة الطبية',
'category' => 'medical',
'trigger_type' => 'expiry',
'trigger_config_json' => json_encode(['days_before' => 30]),
'notification_channels' => 'system,sms',
'recipient_roles_json' => json_encode(['sports_admin']),
'is_active' => 1,
],
[
'code' => 'CONTRACT_EXPIRY',
'name_ar' => 'انتهاء عقد أكاديمية',
'category' => 'contracts',
'trigger_type' => 'expiry',
'trigger_config_json' => json_encode(['days_before' => 30]),
'notification_channels' => 'system',
'recipient_roles_json' => json_encode(['finance', 'academy_manager']),
'is_active' => 1,
],
[
'code' => 'REVENUE_BELOW_MINIMUM',
'name_ar' => 'إيرادات أقل من الحد الأدنى',
'category' => 'financial',
'trigger_type' => 'threshold',
'trigger_config_json' => json_encode(['threshold_pct' => 80]),
'notification_channels' => 'system',
'recipient_roles_json' => json_encode(['finance', 'academy_manager']),
'is_active' => 1,
],
[
'code' => 'ATTENDANCE_DROPPING',
'name_ar' => 'تراجع حضور اللاعب',
'category' => 'attendance',
'trigger_type' => 'threshold',
'trigger_config_json' => json_encode(['consecutive_absences' => 3]),
'notification_channels' => 'system,sms',
'recipient_roles_json' => json_encode(['coach', 'guardian']),
'is_active' => 1,
],
[
'code' => 'GROUP_NEAR_CAPACITY',
'name_ar' => 'مجموعة قاربت على الامتلاء',
'category' => 'capacity',
'trigger_type' => 'threshold',
'trigger_config_json' => json_encode(['capacity_pct' => 90]),
'notification_channels' => 'system',
'recipient_roles_json' => json_encode(['sports_admin']),
'is_active' => 1,
],
[
'code' => 'SUBSCRIPTION_OVERDUE',
'name_ar' => 'اشتراك متأخر السداد',
'category' => 'subscriptions',
'trigger_type' => 'threshold',
'trigger_config_json' => json_encode(['days_overdue' => 7]),
'notification_channels' => 'system,sms',
'recipient_roles_json' => json_encode(['finance']),
'is_active' => 1,
],
[
'code' => 'POOL_SAFETY_THRESHOLD',
'name_ar' => 'حد أمان حمام السباحة',
'category' => 'safety',
'trigger_type' => 'threshold',
'trigger_config_json' => json_encode(['max_occupancy_pct' => 100]),
'notification_channels' => 'system',
'recipient_roles_json' => json_encode(['pool_admin']),
'is_active' => 1,
],
[
'code' => 'SETTLEMENT_DUE',
'name_ar' => 'موعد التسوية المالية',
'category' => 'financial',
'trigger_type' => 'schedule',
'trigger_config_json' => json_encode(['on_settlement_day' => true]),
'notification_channels' => 'system',
'recipient_roles_json' => json_encode(['finance']),
'is_active' => 1,
],
[
'code' => 'WAITING_LIST_SPOT',
'name_ar' => 'إتاحة مكان من قائمة الانتظار',
'category' => 'capacity',
'trigger_type' => 'threshold',
'trigger_config_json' => json_encode(['notify_on_spot_open' => true]),
'notification_channels' => 'system,sms',
'recipient_roles_json' => json_encode(['player']),
'is_active' => 1,
],
];
foreach ($rules as $rule) {
$exists = $db->selectOne(
"SELECT id FROM alert_rules WHERE code = ?",
[$rule['code']]
);
if (!$exists) {
$db->insert('alert_rules', $rule);
}
}
};
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