Commit 2054907b authored by Mahmoud Aglan's avatar Mahmoud Aglan

kokowawa

parent 280686c4
Subproject commit 280686c4f5a46880d5a32e240b58389d11a56a78
Subproject commit 280686c4f5a46880d5a32e240b58389d11a56a78
Subproject commit 280686c4f5a46880d5a32e240b58389d11a56a78
Subproject commit 280686c4f5a46880d5a32e240b58389d11a56a78
Subproject commit 280686c4f5a46880d5a32e240b58389d11a56a78
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class AcademyContractController extends Controller
{
/**
* List contracts for an academy.
*/
public function index(Request $request, string $aid): Response
{
$db = App::getInstance()->db();
$academy = $db->selectOne("SELECT * FROM sa_academies WHERE id = ?", [(int) $aid]);
if (!$academy) {
return $this->redirect('/sa/academies')->withError('الأكاديمية غير موجودة');
}
$contracts = $db->select(
"SELECT * FROM sa_academy_contracts WHERE academy_id = ? ORDER BY created_at DESC",
[(int) $aid]
);
return $this->view('SportsActivity.Views.academies.contracts', [
'academy' => $academy,
'contracts' => $contracts,
]);
}
/**
* Show create contract form for an academy.
*/
public function create(Request $request, string $aid): Response
{
$db = App::getInstance()->db();
$academy = $db->selectOne("SELECT * FROM sa_academies WHERE id = ?", [(int) $aid]);
if (!$academy) {
return $this->redirect('/sa/academies')->withError('الأكاديمية غير موجودة');
}
return $this->view('SportsActivity.Views.academies.contract_form', [
'academy' => $academy,
'contract' => null,
]);
}
/**
* Validate and store a new contract.
*/
public function store(Request $request, string $aid): Response
{
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$academy = $db->selectOne("SELECT * FROM sa_academies WHERE id = ?", [(int) $aid]);
if (!$academy) {
return $this->redirect('/sa/academies')->withError('الأكاديمية غير موجودة');
}
$contractNumber = trim((string) $request->post('contract_number', ''));
$contractType = trim((string) $request->post('contract_type', 'revenue_share'));
$startDate = trim((string) $request->post('start_date', ''));
$endDate = trim((string) $request->post('end_date', ''));
$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);
$notes = trim((string) $request->post('notes', ''));
// Validation
$errors = [];
if ($contractNumber === '') {
$errors[] = 'رقم العقد مطلوب';
}
if ($startDate === '') {
$errors[] = 'تاريخ البداية مطلوب';
}
if ($endDate === '') {
$errors[] = 'تاريخ النهاية مطلوب';
}
if ($startDate !== '' && $endDate !== '' && $startDate >= $endDate) {
$errors[] = 'تاريخ البداية يجب أن يكون قبل تاريخ النهاية';
}
// File upload validation
$file = $_FILES['contract_pdf'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'ملف العقد (PDF) مطلوب';
}
if (!empty($errors)) {
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $request->all());
return $this->redirect('/sa/academies/' . $aid . '/contracts/create');
}
// Upload PDF
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$newName = uniqid('contract_') . '.' . $ext;
$uploadDir = dirname(__DIR__, 4) . '/public/uploads/sa_contracts/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$dest = $uploadDir . $newName;
move_uploaded_file($file['tmp_name'], $dest);
$pdfPath = 'uploads/sa_contracts/' . $newName;
$data = [
'academy_id' => (int) $aid,
'contract_number' => $contractNumber,
'contract_type' => $contractType,
'start_date' => $startDate,
'end_date' => $endDate,
'club_commission_pct' => $clubCommissionPct,
'academy_share_pct' => $academySharePct,
'fixed_monthly_rent' => $fixedMonthlyRent,
'deposit_amount' => $depositAmount,
'contract_pdf_path' => $pdfPath,
'status' => 'pending_approval',
'notes' => $notes ?: null,
'created_by' => (int) ($session->get('employee_id') ?? 0),
'created_at' => now(),
'updated_at' => now(),
];
$db->insert('sa_academy_contracts', $data);
$contractId = (int) $db->selectOne("SELECT LAST_INSERT_ID() as id")['id'];
return $this->redirect('/sa/academy-contracts/' . $contractId)->withSuccess('تم إضافة العقد بنجاح');
}
/**
* Show contract detail.
*/
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$contract = $db->selectOne(
"SELECT c.*, a.name_ar as academy_name, a.code as academy_code
FROM sa_academy_contracts c
LEFT JOIN sa_academies a ON a.id = c.academy_id
WHERE c.id = ?",
[(int) $id]
);
if (!$contract) {
return $this->redirect('/sa/academies')->withError('العقد غير موجود');
}
return $this->view('SportsActivity.Views.academies.contract_show', [
'contract' => $contract,
]);
}
/**
* Approve a contract — set status='active', record approved_by and approved_at.
*/
public function approve(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$contract = $db->selectOne("SELECT * FROM sa_academy_contracts WHERE id = ?", [(int) $id]);
if (!$contract) {
return $this->redirect('/sa/academies')->withError('العقد غير موجود');
}
if ($contract['status'] !== 'pending_approval') {
return $this->redirect('/sa/academy-contracts/' . $id)->withError('لا يمكن اعتماد هذا العقد - الحالة الحالية لا تسمح بذلك');
}
$db->update('sa_academy_contracts', [
'status' => 'active',
'approved_by' => (int) ($session->get('employee_id') ?? 0),
'approved_at' => now(),
'updated_at' => now(),
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/academy-contracts/' . $id)->withSuccess('تم اعتماد العقد بنجاح');
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Api;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\SportsActivity\Services\PricingCalculatorService;
class BookingApiController extends Controller
{
public function pricePreview(Request $request): Response
{
$unitId = (int) $request->get('unit_id', 0);
$date = $request->get('date', '');
$startTime = $request->get('start_time', '');
$endTime = $request->get('end_time', '');
$participants = (int) $request->get('participants', 1);
$isMember = (bool) $request->get('is_member', 0);
if (!$unitId || !$date || !$startTime || !$endTime) {
return $this->json(['error' => 'معاملات ناقصة'], 400);
}
$pricing = PricingCalculatorService::calculate($unitId, $date, $startTime, $endTime, $participants, $isMember);
if (!$pricing) {
return $this->json(['error' => 'لا توجد قاعدة تسعير لهذه المعاملات', 'pricing' => null]);
}
return $this->json(['pricing' => $pricing]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Api;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\SportsActivity\Services\MirrorStateService;
class MirrorApiController extends Controller
{
public function state(Request $request, string $id): Response
{
$date = $request->get('date', date('Y-m-d'));
$state = MirrorStateService::getFacilityState((int) $id, $date);
if (isset($state['error'])) {
return $this->json(['error' => $state['error']], 404);
}
return $this->json($state);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Api;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
class PlayerSearchApiController extends Controller
{
public function search(Request $request): Response
{
$q = trim((string) $request->get('q', ''));
if (strlen($q) < 2) {
return $this->json(['results' => []]);
}
$db = App::getInstance()->db();
$like = '%' . $q . '%';
$results = $db->select(
"SELECT id, full_name_ar, registration_serial, national_id, player_type, medical_status
FROM sa_players
WHERE is_archived = 0
AND (full_name_ar LIKE ? OR registration_serial LIKE ? OR national_id LIKE ? OR phone LIKE ?)
ORDER BY full_name_ar
LIMIT 20",
[$like, $like, $like, $like]
);
return $this->json(['results' => $results]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Api;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\SportsActivity\Services\SlotAvailabilityService;
use App\Modules\SportsActivity\Services\ConflictDetectionService;
class ScheduleApiController extends Controller
{
public function availability(Request $request): Response
{
$unitId = (int) $request->get('unit_id', 0);
$date = $request->get('date', '');
$startTime = $request->get('start_time', '');
$endTime = $request->get('end_time', '');
$spots = (int) $request->get('spots', 1);
if (!$unitId || !$date || !$startTime || !$endTime) {
return $this->json(['error' => 'معاملات ناقصة'], 400);
}
$result = SlotAvailabilityService::check($unitId, $date, $startTime, $endTime, $spots);
return $this->json($result);
}
public function conflicts(Request $request): Response
{
$unitId = (int) $request->get('unit_id', 0);
$date = $request->get('date', '');
$startTime = $request->get('start_time', '');
$endTime = $request->get('end_time', '');
$coachId = $request->get('coach_id') ? (int) $request->get('coach_id') : null;
if (!$unitId || !$date || !$startTime || !$endTime) {
return $this->json(['error' => 'معاملات ناقصة'], 400);
}
$conflicts = ConflictDetectionService::check($unitId, $date, $startTime, $endTime, $coachId);
return $this->json(['conflicts' => $conflicts, 'has_blocking' => !empty(array_filter($conflicts, fn($c) => $c['severity'] === 'blocking'))]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\Pagination;
class AttendanceController extends Controller
{
/**
* List today's training bookings for attendance recording.
*/
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
$bookings = $db->select(
"SELECT b.*, fu.name_ar as unit_name, f.name_ar as facility_name,
g.name_ar as group_name, c.full_name_ar as coach_name
FROM sa_bookings b
JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
JOIN sa_facilities f ON f.id = fu.facility_id
LEFT JOIN sa_groups g ON g.id = b.group_id
LEFT JOIN sa_coaches c ON c.id = b.coach_id
WHERE b.booking_type = 'training'
AND b.booking_date = ?
AND b.status NOT IN ('cancelled', 'no_show')
ORDER BY b.start_time ASC",
[$today]
);
return $this->view('SportsActivity.Views.attendance.index', [
'bookings' => $bookings,
'today' => $today,
]);
}
/**
* Show attendance form for a specific booking.
*/
public function record(Request $request, string $bookingId): Response
{
$db = App::getInstance()->db();
$booking = $db->selectOne(
"SELECT b.*, fu.name_ar as unit_name, f.name_ar as facility_name,
g.name_ar as group_name, g.id as group_id, c.full_name_ar as coach_name
FROM sa_bookings b
JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
JOIN sa_facilities f ON f.id = fu.facility_id
LEFT JOIN sa_groups g ON g.id = b.group_id
LEFT JOIN sa_coaches c ON c.id = b.coach_id
WHERE b.id = ?",
[(int) $bookingId]
);
if (!$booking) {
return $this->redirect('/sa/attendance')->withError('الحجز غير موجود');
}
$players = [];
if ($booking['group_id']) {
$players = $db->select(
"SELECT gp.player_id, p.full_name_ar as player_name, p.code as player_code
FROM sa_group_players gp
JOIN sa_players p ON p.id = gp.player_id
WHERE gp.group_id = ? AND gp.status = 'active'
ORDER BY p.full_name_ar ASC",
[(int) $booking['group_id']]
);
}
// Get existing attendance records for this booking
$existingAttendance = $db->select(
"SELECT * FROM sa_attendance WHERE booking_id = ?",
[(int) $bookingId]
);
$attendanceMap = [];
foreach ($existingAttendance as $att) {
$attendanceMap[(int) $att['player_id']] = $att['status'];
}
return $this->view('SportsActivity.Views.attendance.record', [
'booking' => $booking,
'players' => $players,
'attendanceMap' => $attendanceMap,
]);
}
/**
* Store attendance records for a booking.
*/
public function store(Request $request, string $bookingId): Response
{
$db = App::getInstance()->db();
$booking = $db->selectOne(
"SELECT * FROM sa_bookings WHERE id = ?",
[(int) $bookingId]
);
if (!$booking) {
return $this->redirect('/sa/attendance')->withError('الحجز غير موجود');
}
$playerIds = $request->post('player_ids', []);
$statuses = $request->post('statuses', []);
if (!is_array($playerIds) || !is_array($statuses)) {
return $this->redirect('/sa/attendance/record/' . $bookingId)->withError('بيانات غير صالحة');
}
$session = App::getInstance()->session();
$employeeId = (int) ($session->get('employee_id') ?? 0);
$now = date('Y-m-d H:i:s');
// Delete existing records for this booking then re-insert
$db->delete('sa_attendance', 'booking_id = ?', [(int) $bookingId]);
$recorded = 0;
foreach ($playerIds as $index => $playerId) {
$status = $statuses[$index] ?? 'absent';
if (!in_array($status, ['present', 'absent', 'late', 'excused'], true)) {
$status = 'absent';
}
$db->insert('sa_attendance', [
'booking_id' => (int) $bookingId,
'player_id' => (int) $playerId,
'status' => $status,
'recorded_by' => $employeeId,
'recorded_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
$recorded++;
}
return $this->redirect('/sa/attendance')->withSuccess(
sprintf('تم تسجيل حضور %d لاعب بنجاح', $recorded)
);
}
/**
* Attendance report with filters.
*/
public function report(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'group_id' => trim((string) $request->get('group_id', '')),
'player_id' => trim((string) $request->get('player_id', '')),
'date_from' => trim((string) $request->get('date_from', '')),
'date_to' => trim((string) $request->get('date_to', '')),
];
$where = [];
$params = [];
$records = [];
$summary = null;
$hasFilter = $filters['group_id'] !== '' || $filters['player_id'] !== ''
|| $filters['date_from'] !== '' || $filters['date_to'] !== '';
if ($hasFilter) {
if ($filters['group_id'] !== '') {
$where[] = "b.group_id = ?";
$params[] = (int) $filters['group_id'];
}
if ($filters['player_id'] !== '') {
$where[] = "a.player_id = ?";
$params[] = (int) $filters['player_id'];
}
if ($filters['date_from'] !== '') {
$where[] = "b.booking_date >= ?";
$params[] = $filters['date_from'];
}
if ($filters['date_to'] !== '') {
$where[] = "b.booking_date <= ?";
$params[] = $filters['date_to'];
}
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$records = $db->select(
"SELECT a.*, p.full_name_ar as player_name, g.name_ar as group_name,
b.booking_date, b.start_time, b.end_time
FROM sa_attendance a
JOIN sa_bookings b ON b.id = a.booking_id
JOIN sa_players p ON p.id = a.player_id
LEFT JOIN sa_groups g ON g.id = b.group_id
{$whereClause}
ORDER BY b.booking_date DESC, b.start_time DESC
LIMIT 200",
$params
);
// Summary counts
$summaryRow = $db->selectOne(
"SELECT
COUNT(*) as total,
SUM(CASE WHEN a.status = 'present' THEN 1 ELSE 0 END) as present_count,
SUM(CASE WHEN a.status = 'absent' THEN 1 ELSE 0 END) as absent_count,
SUM(CASE WHEN a.status = 'late' THEN 1 ELSE 0 END) as late_count,
SUM(CASE WHEN a.status = 'excused' THEN 1 ELSE 0 END) as excused_count
FROM sa_attendance a
JOIN sa_bookings b ON b.id = a.booking_id
{$whereClause}",
$params
);
$summary = $summaryRow;
}
$groups = $db->select(
"SELECT id, name_ar FROM sa_groups WHERE status = 'active' AND is_archived = 0 ORDER BY name_ar",
[]
);
return $this->view('SportsActivity.Views.attendance.report', [
'records' => $records,
'summary' => $summary,
'filters' => $filters,
'groups' => $groups,
]);
}
}
This diff is collapsed.
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class DashboardController extends Controller
{
/**
* Main SportsActivity dashboard with summary cards.
*/
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$today = date('Y-m-d');
// Total active disciplines
$activeDisciplines = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_disciplines WHERE is_active = 1",
[]
)['cnt'] ?? 0);
// Total active facilities + units
$activeFacilities = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_facilities WHERE is_active = 1 AND is_archived = 0",
[]
)['cnt'] ?? 0);
$activeUnits = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_facility_units WHERE is_active = 1",
[]
)['cnt'] ?? 0);
// Total active coaches
$activeCoaches = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_coaches WHERE status = 'active' AND is_archived = 0",
[]
)['cnt'] ?? 0);
// Total registered players (not archived)
$totalPlayers = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_players WHERE is_archived = 0",
[]
)['cnt'] ?? 0);
// Total active groups + enrolled players
$activeGroups = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_groups WHERE status = 'active' AND is_archived = 0",
[]
)['cnt'] ?? 0);
$enrolledPlayers = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_group_players WHERE status = 'active'",
[]
)['cnt'] ?? 0);
// Today's bookings count
$todayBookings = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_bookings WHERE booking_date = ? AND status NOT IN ('cancelled', 'no_show')",
[$today]
)['cnt'] ?? 0);
// Pending medical approvals
$pendingMedical = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_player_documents WHERE document_type = 'medical' AND status = 'pending'",
[]
)['cnt'] ?? 0);
// Overdue subscriptions
$overdueSubscriptions = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_subscriptions WHERE payment_status IN ('unpaid', 'overdue') AND period_end < ?",
[$today]
)['cnt'] ?? 0);
return $this->view('SportsActivity.Views.dashboard', [
'stats' => [
'active_disciplines' => $activeDisciplines,
'active_facilities' => $activeFacilities,
'active_units' => $activeUnits,
'active_coaches' => $activeCoaches,
'total_players' => $totalPlayers,
'active_groups' => $activeGroups,
'enrolled_players' => $enrolledPlayers,
'today_bookings' => $todayBookings,
'pending_medical' => $pendingMedical,
'overdue_subscriptions' => $overdueSubscriptions,
],
'today' => $today,
]);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Discipline;
class DisciplineController extends Controller
{
/**
* List all disciplines with search filter and pagination.
*/
public function index(Request $request): Response
{
$filters = [
'q' => trim((string) $request->get('q', '')),
'category' => trim((string) $request->get('category', '')),
];
$page = max(1, (int) $request->get('page', 1));
$perPage = 25;
$query = Discipline::query();
if ($filters['q'] !== '') {
$query->where('name_ar', 'LIKE', '%' . $filters['q'] . '%');
}
if ($filters['category'] !== '') {
$query->where('category', '=', $filters['category']);
}
// Count total
$countQuery = Discipline::query();
if ($filters['q'] !== '') {
$countQuery->where('name_ar', 'LIKE', '%' . $filters['q'] . '%');
}
if ($filters['category'] !== '') {
$countQuery->where('category', '=', $filters['category']);
}
$total = count($countQuery->get());
$pagination = Pagination::paginate($total, $perPage, $page);
$disciplines = $query
->orderBy('sort_order', 'ASC')
->orderBy('name_ar', 'ASC')
->limit($perPage)
->offset(($page - 1) * $perPage)
->get();
return $this->view('SportsActivity.Views.disciplines.index', [
'disciplines' => $disciplines,
'pagination' => $pagination,
'filters' => $filters,
'categories' => Discipline::getCategoryOptions(),
]);
}
/**
* Show the create discipline form.
*/
public function create(Request $request): Response
{
return $this->view('SportsActivity.Views.disciplines.create', [
'categories' => Discipline::getCategoryOptions(),
]);
}
/**
* Validate and store a new discipline.
*/
public function store(Request $request): Response
{
$code = strtoupper(trim((string) $request->post('code', '')));
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$category = trim((string) $request->post('category', ''));
$icon = trim((string) $request->post('icon', 'activity'));
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($code === '') {
$errors[] = 'كود النشاط مطلوب';
}
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
// Check unique code
if ($code !== '') {
$existing = Discipline::query()
->where('code', '=', $code)
->first();
if ($existing) {
$errors[] = 'كود النشاط مستخدم بالفعل — جرب كود مختلف';
}
}
// Validate category if provided
if ($category !== '' && !array_key_exists($category, Discipline::getCategoryOptions())) {
$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('/sa/disciplines/create');
}
$discipline = Discipline::create([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'category' => $category ?: null,
'icon' => $icon ?: null,
'description_ar' => $descriptionAr ?: null,
'sort_order' => $sortOrder,
'is_active' => 1,
]);
return $this->redirect('/sa/disciplines/' . $discipline->id)->withSuccess('تم إضافة النشاط الرياضي بنجاح');
}
/**
* Show discipline detail page.
*/
public function show(Request $request, string $id): Response
{
$discipline = Discipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/sa/disciplines')->withError('النشاط الرياضي غير موجود');
}
return $this->view('SportsActivity.Views.disciplines.show', [
'discipline' => $discipline,
'categories' => Discipline::getCategoryOptions(),
]);
}
/**
* Show edit form for a discipline.
*/
public function edit(Request $request, string $id): Response
{
$discipline = Discipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/sa/disciplines')->withError('النشاط الرياضي غير موجود');
}
return $this->view('SportsActivity.Views.disciplines.edit', [
'discipline' => $discipline,
'categories' => Discipline::getCategoryOptions(),
]);
}
/**
* Validate and update an existing discipline.
*/
public function update(Request $request, string $id): Response
{
$discipline = Discipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/sa/disciplines')->withError('النشاط الرياضي غير موجود');
}
$code = strtoupper(trim((string) $request->post('code', '')));
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$category = trim((string) $request->post('category', ''));
$icon = trim((string) $request->post('icon', 'activity'));
$descriptionAr = trim((string) $request->post('description_ar', ''));
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($code === '') {
$errors[] = 'كود النشاط مطلوب';
}
if ($nameAr === '' || mb_strlen($nameAr) < 2) {
$errors[] = 'الاسم بالعربي مطلوب (حرفان على الأقل)';
}
// Check unique code (exclude current)
if ($code !== '') {
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT id FROM disciplines WHERE code = ? AND id != ?",
[$code, (int) $id]
);
if ($existing) {
$errors[] = 'كود النشاط مستخدم بالفعل — جرب كود مختلف';
}
}
// Validate category if provided
if ($category !== '' && !array_key_exists($category, Discipline::getCategoryOptions())) {
$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('/sa/disciplines/' . $id . '/edit');
}
$discipline->update([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'category' => $category ?: null,
'icon' => $icon ?: null,
'description_ar' => $descriptionAr ?: null,
'sort_order' => $sortOrder,
]);
return $this->redirect('/sa/disciplines/' . $id)->withSuccess('تم تحديث النشاط الرياضي بنجاح');
}
/**
* Toggle the is_active status of a discipline.
*/
public function toggle(Request $request, string $id): Response
{
$discipline = Discipline::find((int) $id);
if (!$discipline) {
return $this->redirect('/sa/disciplines')->withError('النشاط الرياضي غير موجود');
}
$newStatus = $discipline->is_active ? 0 : 1;
$discipline->update(['is_active' => $newStatus]);
$message = $newStatus ? 'تم تفعيل النشاط الرياضي' : 'تم إيقاف النشاط الرياضي';
return $this->redirect('/sa/disciplines/' . $id)->withSuccess($message);
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Models\Facility;
use App\Modules\SportsActivity\Models\FacilityUnit;
class FacilityUnitController extends Controller
{
public function index(Request $request, string $fid): Response
{
$this->authorize('sa.facility.view');
$facility = Facility::find((int) $fid);
if (!$facility) {
return $this->redirect('/sa/facilities')->withError('المنشأة غير موجودة');
}
$units = FacilityUnit::getByFacility((int) $fid);
return $this->view('SportsActivity.Views.facilities.units', [
'facility' => $facility,
'units' => $units,
'unitTypeOptions' => FacilityUnit::getUnitTypeOptions(),
'bookingModes' => FacilityUnit::getBookingModeOptions(),
]);
}
public function create(Request $request, string $fid): Response
{
$this->authorize('sa.facility.manage');
$facility = Facility::find((int) $fid);
if (!$facility) {
return $this->redirect('/sa/facilities')->withError('المنشأة غير موجودة');
}
return $this->view('SportsActivity.Views.facilities.unit_form', [
'facility' => $facility,
'unit' => null,
'unitTypeOptions' => FacilityUnit::getUnitTypeOptions(),
'bookingModes' => FacilityUnit::getBookingModeOptions(),
]);
}
public function store(Request $request, string $fid): Response
{
$this->authorize('sa.facility.manage');
$facility = Facility::find((int) $fid);
if (!$facility) {
return $this->redirect('/sa/facilities')->withError('المنشأة غير موجودة');
}
$code = trim((string) $request->post('code', ''));
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$unitType = trim((string) $request->post('unit_type', ''));
$bookingMode = trim((string) $request->post('booking_mode', ''));
$maxCapacity = (int) $request->post('max_capacity', 1);
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($code === '') {
$errors[] = 'الكود مطلوب';
}
if ($nameAr === '') {
$errors[] = 'الاسم بالعربية مطلوب';
}
if ($unitType === '' || !isset(FacilityUnit::getUnitTypeOptions()[$unitType])) {
$errors[] = 'نوع الوحدة مطلوب';
}
if ($bookingMode === '' || !isset(FacilityUnit::getBookingModeOptions()[$bookingMode])) {
$errors[] = 'نمط الحجز مطلوب';
}
if ($maxCapacity < 1) {
$errors[] = 'السعة القصوى يجب أن تكون 1 على الأقل';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_old_input', $_POST);
$alerts = array_map(fn($msg) => ['type' => 'error', 'message' => $msg], $errors);
$session->flash('_alerts', $alerts);
return $this->redirect("/sa/facilities/{$fid}/units/create");
}
// Check duplicate code within facility
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT id FROM sa_facility_units WHERE code = ? AND facility_id = ? AND is_active = 1",
[$code, (int) $fid]
);
if ($existing) {
$session = App::getInstance()->session();
$session->flash('_old_input', $_POST);
$session->flash('_alerts', [['type' => 'error', 'message' => 'الكود مستخدم بالفعل في هذه المنشأة']]);
return $this->redirect("/sa/facilities/{$fid}/units/create");
}
FacilityUnit::create([
'facility_id' => (int) $fid,
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'unit_type' => $unitType,
'booking_mode' => $bookingMode,
'max_capacity' => $maxCapacity,
'sort_order' => $sortOrder,
'is_active' => 1,
]);
return $this->redirect("/sa/facilities/{$fid}/units")->withSuccess('تم إضافة الوحدة بنجاح');
}
public function edit(Request $request, string $fid, string $id): Response
{
$this->authorize('sa.facility.manage');
$facility = Facility::find((int) $fid);
if (!$facility) {
return $this->redirect('/sa/facilities')->withError('المنشأة غير موجودة');
}
$unit = FacilityUnit::find((int) $id);
if (!$unit || (int) $unit->facility_id !== (int) $fid) {
return $this->redirect("/sa/facilities/{$fid}/units")->withError('الوحدة غير موجودة');
}
return $this->view('SportsActivity.Views.facilities.unit_form', [
'facility' => $facility,
'unit' => $unit,
'unitTypeOptions' => FacilityUnit::getUnitTypeOptions(),
'bookingModes' => FacilityUnit::getBookingModeOptions(),
]);
}
public function update(Request $request, string $fid, string $id): Response
{
$this->authorize('sa.facility.manage');
$facility = Facility::find((int) $fid);
if (!$facility) {
return $this->redirect('/sa/facilities')->withError('المنشأة غير موجودة');
}
$unit = FacilityUnit::find((int) $id);
if (!$unit || (int) $unit->facility_id !== (int) $fid) {
return $this->redirect("/sa/facilities/{$fid}/units")->withError('الوحدة غير موجودة');
}
$code = trim((string) $request->post('code', ''));
$nameAr = trim((string) $request->post('name_ar', ''));
$nameEn = trim((string) $request->post('name_en', ''));
$unitType = trim((string) $request->post('unit_type', ''));
$bookingMode = trim((string) $request->post('booking_mode', ''));
$maxCapacity = (int) $request->post('max_capacity', 1);
$sortOrder = (int) $request->post('sort_order', 0);
// Validation
$errors = [];
if ($code === '') {
$errors[] = 'الكود مطلوب';
}
if ($nameAr === '') {
$errors[] = 'الاسم بالعربية مطلوب';
}
if ($unitType === '' || !isset(FacilityUnit::getUnitTypeOptions()[$unitType])) {
$errors[] = 'نوع الوحدة مطلوب';
}
if ($bookingMode === '' || !isset(FacilityUnit::getBookingModeOptions()[$bookingMode])) {
$errors[] = 'نمط الحجز مطلوب';
}
if ($maxCapacity < 1) {
$errors[] = 'السعة القصوى يجب أن تكون 1 على الأقل';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_old_input', $_POST);
$alerts = array_map(fn($msg) => ['type' => 'error', 'message' => $msg], $errors);
$session->flash('_alerts', $alerts);
return $this->redirect("/sa/facilities/{$fid}/units/{$id}/edit");
}
// Check duplicate code (excluding current)
$db = App::getInstance()->db();
$existing = $db->selectOne(
"SELECT id FROM sa_facility_units WHERE code = ? AND facility_id = ? AND is_active = 1 AND id != ?",
[$code, (int) $fid, (int) $id]
);
if ($existing) {
$session = App::getInstance()->session();
$session->flash('_old_input', $_POST);
$session->flash('_alerts', [['type' => 'error', 'message' => 'الكود مستخدم بالفعل في هذه المنشأة']]);
return $this->redirect("/sa/facilities/{$fid}/units/{$id}/edit");
}
$unit->update([
'code' => $code,
'name_ar' => $nameAr,
'name_en' => $nameEn ?: null,
'unit_type' => $unitType,
'booking_mode' => $bookingMode,
'max_capacity' => $maxCapacity,
'sort_order' => $sortOrder,
]);
return $this->redirect("/sa/facilities/{$fid}/units")->withSuccess('تم تحديث الوحدة بنجاح');
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Models\Facility;
use App\Modules\SportsActivity\Services\MirrorStateService;
class MirrorController extends Controller
{
/**
* List all active facilities with links to mirror views.
*/
public function index(Request $request): Response
{
$facilities = Facility::getActive();
return $this->view('SportsActivity.Views.mirror.index', [
'facilities' => $facilities,
]);
}
/**
* Mirror view for a facility on today's date.
*/
public function facility(Request $request, string $id): Response
{
$today = date('Y-m-d');
return $this->renderFacilityView((int) $id, $today);
}
/**
* Mirror view for a facility on a specific date.
*/
public function facilityDate(Request $request, string $id, string $date): Response
{
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return $this->redirect('/sa/mirror/' . $id)->withError('تاريخ غير صالح');
}
return $this->renderFacilityView((int) $id, $date);
}
/**
* Render the facility mirror grid.
*/
private function renderFacilityView(int $facilityId, string $date): Response
{
$state = MirrorStateService::getFacilityState($facilityId, $date);
if (isset($state['error'])) {
return $this->redirect('/sa/mirror')->withError($state['error']);
}
return $this->view('SportsActivity.Views.mirror.facility_view', [
'facility' => $state['facility'],
'units' => $state['units'],
'grid' => $state['grid'],
'slots' => $state['slots'],
'date' => $state['date'],
]);
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class PlayerDocumentController extends Controller
{
/**
* List documents for a player.
*/
public function index(Request $request, string $pid): Response
{
$db = App::getInstance()->db();
$player = $db->selectOne("SELECT * FROM sa_players WHERE id = ?", [(int) $pid]);
if (!$player) {
return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
}
$documents = $db->select(
"SELECT * FROM sa_player_documents WHERE player_id = ? ORDER BY created_at DESC",
[(int) $pid]
);
return $this->view('SportsActivity.Views.players.documents', [
'player' => $player,
'documents' => $documents,
]);
}
/**
* Handle file upload for a player document.
*/
public function upload(Request $request, string $pid): Response
{
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$player = $db->selectOne("SELECT * FROM sa_players WHERE id = ?", [(int) $pid]);
if (!$player) {
return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
}
$documentType = trim((string) $request->post('document_type', ''));
$title = trim((string) $request->post('title', ''));
// Validation
$errors = [];
if ($documentType === '') {
$errors[] = 'نوع المستند مطلوب';
}
if ($title === '') {
$errors[] = 'عنوان المستند مطلوب';
}
$file = $_FILES['document_file'] ?? null;
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
$errors[] = 'الملف مطلوب';
}
if (!empty($errors)) {
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
return $this->redirect('/sa/players/' . $pid . '/documents');
}
// Upload file
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$newName = uniqid('doc_') . '.' . $ext;
$uploadDir = dirname(__DIR__, 4) . '/public/uploads/sa_documents/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$dest = $uploadDir . $newName;
move_uploaded_file($file['tmp_name'], $dest);
$filePath = 'uploads/sa_documents/' . $newName;
$data = [
'player_id' => (int) $pid,
'document_type' => $documentType,
'title' => $title,
'file_path' => $filePath,
'original_name' => $file['name'],
'approval_status' => 'pending',
'uploaded_by' => (int) (App::getInstance()->session()->get('employee_id') ?? 0),
'created_at' => now(),
'updated_at' => now(),
];
$db->insert('sa_player_documents', $data);
return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم رفع المستند بنجاح');
}
/**
* Approve a document (medical certificate).
* Sets approval_status='approved', updates player medical_status='fit' with expiry_date.
*/
public function approve(Request $request, string $pid, string $id): Response
{
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$player = $db->selectOne("SELECT * FROM sa_players WHERE id = ?", [(int) $pid]);
if (!$player) {
return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
}
$document = $db->selectOne(
"SELECT * FROM sa_player_documents WHERE id = ? AND player_id = ?",
[(int) $id, (int) $pid]
);
if (!$document) {
return $this->redirect('/sa/players/' . $pid . '/documents')->withError('المستند غير موجود');
}
$expiryDate = trim((string) $request->post('expiry_date', ''));
if ($expiryDate === '') {
$session->flash('_alerts', [['type' => 'error', 'message' => 'تاريخ انتهاء الصلاحية مطلوب']]);
return $this->redirect('/sa/players/' . $pid . '/documents');
}
// Update document
$db->update('sa_player_documents', [
'approval_status' => 'approved',
'approved_by' => (int) ($session->get('employee_id') ?? 0),
'approved_at' => now(),
'expiry_date' => $expiryDate,
'updated_at' => now(),
], 'id = ?', [(int) $id]);
// Update player medical status
$db->update('sa_players', [
'medical_status' => 'fit',
'medical_expiry_date' => $expiryDate,
'updated_at' => now(),
], 'id = ?', [(int) $pid]);
return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم اعتماد المستند وتحديث الحالة الطبية');
}
/**
* Reject a document.
* Sets approval_status='rejected', updates player medical_status='unfit'.
*/
public function reject(Request $request, string $pid, string $id): Response
{
$db = App::getInstance()->db();
$session = App::getInstance()->session();
$player = $db->selectOne("SELECT * FROM sa_players WHERE id = ?", [(int) $pid]);
if (!$player) {
return $this->redirect('/sa/players')->withError('اللاعب غير موجود');
}
$document = $db->selectOne(
"SELECT * FROM sa_player_documents WHERE id = ? AND player_id = ?",
[(int) $id, (int) $pid]
);
if (!$document) {
return $this->redirect('/sa/players/' . $pid . '/documents')->withError('المستند غير موجود');
}
$rejectionReason = trim((string) $request->post('rejection_reason', ''));
if ($rejectionReason === '') {
$session->flash('_alerts', [['type' => 'error', 'message' => 'سبب الرفض مطلوب']]);
return $this->redirect('/sa/players/' . $pid . '/documents');
}
// Update document
$db->update('sa_player_documents', [
'approval_status' => 'rejected',
'rejection_reason' => $rejectionReason,
'approved_by' => (int) ($session->get('employee_id') ?? 0),
'approved_at' => now(),
'updated_at' => now(),
], 'id = ?', [(int) $id]);
// Update player medical status
$db->update('sa_players', [
'medical_status' => 'unfit',
'updated_at' => now(),
], 'id = ?', [(int) $pid]);
return $this->redirect('/sa/players/' . $pid . '/documents')->withSuccess('تم رفض المستند');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\App;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
class PricingController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$facilities = $db->select(
"SELECT f.id, f.code, f.name_ar, f.facility_type,
COUNT(DISTINCT fu.id) as unit_count,
COUNT(DISTINCT pr.id) as rule_count
FROM sa_facilities f
LEFT JOIN sa_facility_units fu ON fu.facility_id = f.id AND fu.is_active = 1
LEFT JOIN sa_pricing_rules pr ON pr.facility_unit_id = fu.id AND pr.is_active = 1
WHERE f.is_archived = 0 AND f.is_active = 1
GROUP BY f.id
ORDER BY f.name_ar",
[]
);
return $this->view('SportsActivity.Views.pricing.index', [
'facilities' => $facilities,
]);
}
public function rules(Request $request): Response
{
$db = App::getInstance()->db();
$unitId = (int) $request->get('unit_id', 0);
$facilityId = (int) $request->get('facility_id', 0);
if ($facilityId > 0) {
$facility = $db->selectOne("SELECT * FROM sa_facilities WHERE id = ? AND is_archived = 0", [$facilityId]);
if (!$facility) {
return $this->redirect('/sa/pricing')->withError('المرفق غير موجود');
}
$units = $db->select(
"SELECT * FROM sa_facility_units WHERE facility_id = ? AND is_active = 1 ORDER BY sort_order",
[$facilityId]
);
$brackets = $db->select(
"SELECT * FROM sa_time_brackets WHERE facility_id = ? AND is_active = 1 ORDER BY start_time",
[$facilityId]
);
} else {
$facility = null;
$units = [];
$brackets = [];
}
$rules = [];
if ($unitId > 0) {
$rules = $db->select(
"SELECT pr.*, tb.name_ar as bracket_name, tb.bracket_type, tb.start_time as bracket_start, tb.end_time as bracket_end
FROM sa_pricing_rules pr
JOIN sa_time_brackets tb ON tb.id = pr.time_bracket_id
WHERE pr.facility_unit_id = ? AND pr.is_active = 1
ORDER BY tb.start_time, pr.group_size_min",
[$unitId]
);
}
$allFacilities = $db->select(
"SELECT id, name_ar FROM sa_facilities WHERE is_archived = 0 AND is_active = 1 ORDER BY name_ar",
[]
);
return $this->view('SportsActivity.Views.pricing.rules', [
'facility' => $facility,
'facilities' => $allFacilities,
'units' => $units,
'brackets' => $brackets,
'rules' => $rules,
'selectedUnit' => $unitId,
'selectedFacility' => $facilityId,
]);
}
public function storeRule(Request $request): Response
{
$db = App::getInstance()->db();
$data = [
'facility_unit_id' => (int) $request->post('facility_unit_id', 0),
'time_bracket_id' => (int) $request->post('time_bracket_id', 0),
'group_size_min' => (int) $request->post('group_size_min', 1),
'group_size_max' => (int) $request->post('group_size_max', 1),
'price_per_person_member' => $request->post('price_per_person_member', '0'),
'price_per_person_nonmember' => $request->post('price_per_person_nonmember', '0'),
'effective_from' => $request->post('effective_from', ''),
'effective_to' => $request->post('effective_to', '') ?: null,
];
$errors = [];
if ($data['facility_unit_id'] <= 0) $errors[] = 'الوحدة مطلوبة';
if ($data['time_bracket_id'] <= 0) $errors[] = 'الفترة الزمنية مطلوبة';
if ($data['group_size_min'] < 1) $errors[] = 'الحد الأدنى للمجموعة يجب أن يكون 1 على الأقل';
if ($data['group_size_max'] < $data['group_size_min']) $errors[] = 'الحد الأقصى يجب أن يكون أكبر أو يساوي الحد الأدنى';
if ((float) $data['price_per_person_member'] <= 0) $errors[] = 'سعر العضو مطلوب';
if ((float) $data['price_per_person_nonmember'] <= 0) $errors[] = 'سعر غير العضو مطلوب';
if ($data['effective_from'] === '') $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());
$unit = $db->selectOne("SELECT facility_id FROM sa_facility_units WHERE id = ?", [$data['facility_unit_id']]);
$facilityId = $unit ? (int) $unit['facility_id'] : 0;
return $this->redirect("/sa/pricing/rules?facility_id={$facilityId}&unit_id={$data['facility_unit_id']}");
}
$data['is_active'] = 1;
$data['created_by'] = (int) (App::getInstance()->session()->get('employee_id') ?? 0);
$data['created_at'] = date('Y-m-d H:i:s');
$data['updated_at'] = date('Y-m-d H:i:s');
$db->insert('sa_pricing_rules', $data);
$unit = $db->selectOne("SELECT facility_id FROM sa_facility_units WHERE id = ?", [$data['facility_unit_id']]);
$facilityId = $unit ? (int) $unit['facility_id'] : 0;
return $this->redirect("/sa/pricing/rules?facility_id={$facilityId}&unit_id={$data['facility_unit_id']}")
->withSuccess('تم إضافة قاعدة التسعير');
}
public function updateRule(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$rule = $db->selectOne("SELECT * FROM sa_pricing_rules WHERE id = ?", [(int) $id]);
if (!$rule) {
return $this->redirect('/sa/pricing')->withError('القاعدة غير موجودة');
}
$update = [
'group_size_min' => (int) $request->post('group_size_min', $rule['group_size_min']),
'group_size_max' => (int) $request->post('group_size_max', $rule['group_size_max']),
'price_per_person_member' => $request->post('price_per_person_member', $rule['price_per_person_member']),
'price_per_person_nonmember' => $request->post('price_per_person_nonmember', $rule['price_per_person_nonmember']),
'effective_from' => $request->post('effective_from', $rule['effective_from']),
'effective_to' => $request->post('effective_to', '') ?: null,
'updated_at' => date('Y-m-d H:i:s'),
];
$db->update('sa_pricing_rules', $update, 'id = ?', [(int) $id]);
$facilityId = 0;
$unit = $db->selectOne("SELECT facility_id FROM sa_facility_units WHERE id = ?", [(int) $rule['facility_unit_id']]);
if ($unit) $facilityId = (int) $unit['facility_id'];
return $this->redirect("/sa/pricing/rules?facility_id={$facilityId}&unit_id={$rule['facility_unit_id']}")
->withSuccess('تم تحديث القاعدة');
}
public function deactivateRule(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$rule = $db->selectOne("SELECT * FROM sa_pricing_rules WHERE id = ?", [(int) $id]);
if (!$rule) {
return $this->redirect('/sa/pricing')->withError('القاعدة غير موجودة');
}
$db->update('sa_pricing_rules', ['is_active' => 0, 'updated_at' => date('Y-m-d H:i:s')], 'id = ?', [(int) $id]);
$facilityId = 0;
$unit = $db->selectOne("SELECT facility_id FROM sa_facility_units WHERE id = ?", [(int) $rule['facility_unit_id']]);
if ($unit) $facilityId = (int) $unit['facility_id'];
return $this->redirect("/sa/pricing/rules?facility_id={$facilityId}&unit_id={$rule['facility_unit_id']}")
->withSuccess('تم تعطيل القاعدة');
}
}
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\ScheduleGeneratorService;
class ScheduleController extends Controller
{
/**
* Redirect to daily view with today's date.
*/
public function calendar(Request $request): Response
{
return $this->redirect('/sa/schedule/daily/' . date('Y-m-d'));
}
/**
* Show all bookings for a given date across all facilities.
*/
public function daily(Request $request, string $date): Response
{
$db = App::getInstance()->db();
// Validate date format
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
$date = date('Y-m-d');
}
$bookings = $db->select(
"SELECT b.*, fu.name_ar as unit_name, fu.code as unit_code,
f.name_ar as facility_name, f.id as facility_id,
g.name_ar as group_name, c.name_ar as coach_name
FROM sa_bookings b
JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
JOIN sa_facilities f ON f.id = fu.facility_id
LEFT JOIN sa_groups g ON g.id = b.group_id
LEFT JOIN sa_coaches c ON c.id = b.coach_id
WHERE b.booking_date = ? AND b.status != 'cancelled'
ORDER BY f.name_ar ASC, fu.name_ar ASC, b.start_time ASC",
[$date]
);
// Group bookings by facility -> unit
$byFacility = [];
foreach ($bookings as $b) {
$key = $b['facility_name'];
if (!isset($byFacility[$key])) {
$byFacility[$key] = [];
}
$unitKey = $b['unit_name'] . ' (' . $b['unit_code'] . ')';
if (!isset($byFacility[$key][$unitKey])) {
$byFacility[$key][$unitKey] = [];
}
$byFacility[$key][$unitKey][] = $b;
}
// Get all active facility units for the full grid
$allUnits = $db->select(
"SELECT fu.id, fu.name_ar, fu.code, f.name_ar as facility_name
FROM sa_facility_units fu
JOIN sa_facilities f ON f.id = fu.facility_id
WHERE fu.is_archived = 0 AND fu.is_active = 1
ORDER BY f.name_ar ASC, fu.name_ar ASC",
[]
);
// Groups for the generate form
$groups = $db->select(
"SELECT g.id, g.name_ar, g.code FROM sa_groups g WHERE g.status = 'active' AND g.is_archived = 0 ORDER BY g.name_ar",
[]
);
return $this->view('SportsActivity.Views.schedule.daily', [
'date' => $date,
'byFacility' => $byFacility,
'allUnits' => $allUnits,
'groups' => $groups,
]);
}
/**
* Show current week schedule as grid (facilities x days).
*/
public function weekly(Request $request): Response
{
$db = App::getInstance()->db();
// Determine week start (Saturday) and end (Friday) for Arabic week
$weekStart = trim((string) $request->get('week_start', ''));
if ($weekStart === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $weekStart)) {
// Find last Saturday
$now = new \DateTime();
$dayOfWeek = (int) $now->format('w'); // 0=Sun, 6=Sat
$diff = ($dayOfWeek + 1) % 7; // days since Saturday
$now->modify("-{$diff} days");
$weekStart = $now->format('Y-m-d');
}
$startDate = new \DateTime($weekStart);
$endDate = (clone $startDate)->modify('+6 days');
$days = [];
$dayNames = ['السبت', 'الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة'];
$current = clone $startDate;
for ($i = 0; $i < 7; $i++) {
$days[] = [
'date' => $current->format('Y-m-d'),
'name' => $dayNames[$i],
'day' => $current->format('d'),
];
$current->modify('+1 day');
}
// Get facility units
$units = $db->select(
"SELECT fu.id, fu.name_ar, fu.code, f.name_ar as facility_name
FROM sa_facility_units fu
JOIN sa_facilities f ON f.id = fu.facility_id
WHERE fu.is_archived = 0 AND fu.is_active = 1
ORDER BY f.name_ar ASC, fu.name_ar ASC",
[]
);
// Get booking counts per unit per day
$counts = $db->select(
"SELECT facility_unit_id, booking_date, COUNT(*) as cnt
FROM sa_bookings
WHERE booking_date BETWEEN ? AND ? AND status != 'cancelled'
GROUP BY facility_unit_id, booking_date",
[$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]
);
// Build lookup: unit_id -> date -> count
$countMap = [];
foreach ($counts as $row) {
$countMap[(int) $row['facility_unit_id']][$row['booking_date']] = (int) $row['cnt'];
}
return $this->view('SportsActivity.Views.schedule.weekly', [
'days' => $days,
'units' => $units,
'countMap' => $countMap,
'weekStart' => $startDate->format('Y-m-d'),
'prevWeek' => (clone $startDate)->modify('-7 days')->format('Y-m-d'),
'nextWeek' => (clone $startDate)->modify('+7 days')->format('Y-m-d'),
]);
}
/**
* Generate training bookings for a group + date range.
*/
public function generate(Request $request): Response
{
$groupId = (int) $request->post('group_id', 0);
$fromDate = trim((string) $request->post('from_date', ''));
$toDate = trim((string) $request->post('to_date', ''));
$errors = [];
if ($groupId === 0) {
$errors[] = 'يرجى اختيار المجموعة';
}
if ($fromDate === '') {
$errors[] = 'تاريخ البداية مطلوب';
}
if ($toDate === '') {
$errors[] = 'تاريخ النهاية مطلوب';
}
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
return $this->redirect('/sa/schedule/daily/' . ($fromDate ?: date('Y-m-d')));
}
$result = ScheduleGeneratorService::generateForGroup($groupId, $fromDate, $toDate);
if ($result['success']) {
$msg = "تم توليد {$result['generated']} حجز تدريبي";
if ($result['skipped'] > 0) {
$msg .= " (تم تخطي {$result['skipped']})";
}
return $this->redirect('/sa/schedule/daily/' . $fromDate)->withSuccess($msg);
}
return $this->redirect('/sa/schedule/daily/' . ($fromDate ?: date('Y-m-d')))->withError($result['error']);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Subscription;
use App\Modules\SportsActivity\Models\Group;
use App\Modules\SportsActivity\Services\SubscriptionGeneratorService;
class SubscriptionController extends Controller
{
/**
* List subscriptions with filters and pagination.
*/
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'month' => trim((string) $request->get('month', '')),
'group_id' => trim((string) $request->get('group_id', '')),
'payment_status' => trim((string) $request->get('payment_status', '')),
];
$page = max(1, (int) $request->get('page', 1));
$perPage = 25;
$where = [];
$params = [];
if ($filters['month'] !== '') {
$periodStart = $filters['month'] . '-01';
$where[] = "s.period_start = ?";
$params[] = $periodStart;
}
if ($filters['group_id'] !== '') {
$where[] = "s.group_id = ?";
$params[] = (int) $filters['group_id'];
}
if ($filters['payment_status'] !== '') {
$where[] = "s.payment_status = ?";
$params[] = $filters['payment_status'];
}
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$total = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_subscriptions s {$whereClause}",
$params
)['cnt'] ?? 0);
$pagination = Pagination::paginate($total, $perPage, $page);
$offset = ($page - 1) * $perPage;
$subscriptions = $db->select(
"SELECT s.*, p.full_name_ar as player_name, g.name_ar as group_name
FROM sa_subscriptions s
LEFT JOIN sa_players p ON p.id = s.player_id
LEFT JOIN sa_groups g ON g.id = s.group_id
{$whereClause}
ORDER BY s.period_start DESC, s.created_at DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$groups = Group::getActive();
return $this->view('SportsActivity.Views.subscriptions.index', [
'subscriptions' => $subscriptions,
'pagination' => $pagination,
'filters' => $filters,
'groups' => $groups,
'statusOptions' => Subscription::getPaymentStatusOptions(),
]);
}
/**
* Generate subscriptions for a given month.
*/
public function generate(Request $request): Response
{
$yearMonth = trim((string) $request->post('year_month', ''));
if ($yearMonth === '' || !preg_match('/^\d{4}-\d{2}$/', $yearMonth)) {
return $this->redirect('/sa/subscriptions')->withError('يرجى تحديد شهر صالح');
}
$result = SubscriptionGeneratorService::generateForMonth($yearMonth);
$message = sprintf(
'تم توليد %d اشتراك جديد (تم تخطي %d موجود مسبقا) لشهر %s',
$result['generated'],
$result['skipped'],
$yearMonth
);
return $this->redirect('/sa/subscriptions?month=' . $yearMonth)->withSuccess($message);
}
/**
* Show subscription detail.
*/
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$subscription = $db->selectOne(
"SELECT s.*, p.full_name_ar as player_name, p.code as player_code,
g.name_ar as group_name, g.code as group_code
FROM sa_subscriptions s
LEFT JOIN sa_players p ON p.id = s.player_id
LEFT JOIN sa_groups g ON g.id = s.group_id
WHERE s.id = ?",
[(int) $id]
);
if (!$subscription) {
return $this->redirect('/sa/subscriptions')->withError('الاشتراك غير موجود');
}
return $this->view('SportsActivity.Views.subscriptions.show', [
'subscription' => $subscription,
'statusOptions' => Subscription::getPaymentStatusOptions(),
]);
}
/**
* Mark subscription as paid.
*/
public function pay(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$subscription = $db->selectOne(
"SELECT * FROM sa_subscriptions WHERE id = ?",
[(int) $id]
);
if (!$subscription) {
return $this->redirect('/sa/subscriptions')->withError('الاشتراك غير موجود');
}
$db->update('sa_subscriptions', [
'payment_status' => 'paid',
'paid_at' => date('Y-m-d H:i:s'),
'paid_amount' => (float) $subscription['final_amount'],
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/subscriptions/' . $id)->withSuccess('تم تسجيل الدفع بنجاح');
}
/**
* Mark subscription as exempt.
*/
public function exempt(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$subscription = $db->selectOne(
"SELECT * FROM sa_subscriptions WHERE id = ?",
[(int) $id]
);
if (!$subscription) {
return $this->redirect('/sa/subscriptions')->withError('الاشتراك غير موجود');
}
$reason = trim((string) $request->post('exemption_reason', ''));
if ($reason === '') {
return $this->redirect('/sa/subscriptions/' . $id)->withError('يرجى إدخال سبب الإعفاء');
}
$session = App::getInstance()->session();
$employeeId = (int) ($session->get('employee_id') ?? 0);
$db->update('sa_subscriptions', [
'payment_status' => 'exempt',
'exemption_reason' => $reason,
'exempted_by' => $employeeId,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/subscriptions/' . $id)->withSuccess('تم تسجيل الإعفاء بنجاح');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Waitlist;
use App\Modules\SportsActivity\Models\Group;
class WaitlistController extends Controller
{
/**
* List all waitlist entries with filters.
*/
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$filters = [
'group_id' => trim((string) $request->get('group_id', '')),
'status' => trim((string) $request->get('status', '')),
];
$page = max(1, (int) $request->get('page', 1));
$perPage = 25;
$where = [];
$params = [];
if ($filters['group_id'] !== '') {
$where[] = "w.group_id = ?";
$params[] = (int) $filters['group_id'];
}
if ($filters['status'] !== '') {
$where[] = "w.status = ?";
$params[] = $filters['status'];
}
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$total = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_waitlist w {$whereClause}",
$params
)['cnt'] ?? 0);
$pagination = Pagination::paginate($total, $perPage, $page);
$offset = ($page - 1) * $perPage;
$entries = $db->select(
"SELECT w.*, p.full_name_ar as player_name, p.code as player_code,
g.name_ar as group_name
FROM sa_waitlist w
LEFT JOIN sa_players p ON p.id = w.player_id
LEFT JOIN sa_groups g ON g.id = w.group_id
{$whereClause}
ORDER BY w.position ASC, w.created_at ASC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
$groups = Group::getActive();
return $this->view('SportsActivity.Views.waitlist.index', [
'entries' => $entries,
'pagination' => $pagination,
'filters' => $filters,
'groups' => $groups,
'statusOptions' => Waitlist::getStatusOptions(),
]);
}
/**
* Offer a spot to a waitlisted player.
*/
public function offer(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$entry = $db->selectOne(
"SELECT * FROM sa_waitlist WHERE id = ?",
[(int) $id]
);
if (!$entry) {
return $this->redirect('/sa/waitlist')->withError('السجل غير موجود');
}
if ($entry['status'] !== 'waiting') {
return $this->redirect('/sa/waitlist')->withError('لا يمكن تقديم عرض لهذا السجل بحالته الحالية');
}
$now = date('Y-m-d H:i:s');
$expiresAt = date('Y-m-d H:i:s', strtotime('+48 hours'));
$db->update('sa_waitlist', [
'status' => 'offered',
'offered_at' => $now,
'expires_at' => $expiresAt,
'updated_at' => $now,
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/waitlist')->withSuccess('تم تقديم العرض بنجاح - ينتهي خلال 48 ساعة');
}
/**
* Cancel a waitlist entry.
*/
public function cancel(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$entry = $db->selectOne(
"SELECT * FROM sa_waitlist WHERE id = ?",
[(int) $id]
);
if (!$entry) {
return $this->redirect('/sa/waitlist')->withError('السجل غير موجود');
}
$db->update('sa_waitlist', [
'status' => 'cancelled',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/waitlist')->withSuccess('تم إلغاء السجل من قائمة الانتظار');
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class Academy extends Model
{
protected static string $table = 'sa_academies';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code',
'name_ar',
'name_en',
'discipline_id',
'academy_type',
'contact_person',
'contact_phone',
'contact_email',
'description_ar',
'logo_path',
'is_active',
'branch_id',
];
/**
* Get academy type options with Arabic labels.
*/
public static function getAcademyTypeOptions(): array
{
return [
'internal' => 'داخلية',
'external' => 'خارجية',
];
}
/**
* Get all contracts for this academy.
*/
public function getContracts(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM `sa_academy_contracts`
WHERE `academy_id` = ? AND `is_archived` = 0
ORDER BY `start_date` DESC",
[$this->id]
);
}
/**
* Get all active (non-archived, active) academies.
*/
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->where('is_archived', '=', 0)
->orderBy('name_ar', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class AcademyContract extends Model
{
protected static string $table = 'sa_academy_contracts';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'contract_number',
'academy_id',
'contract_type',
'start_date',
'end_date',
'club_commission_pct',
'academy_share_pct',
'fixed_monthly_rent',
'minimum_revenue_guarantee',
'deposit_amount',
'deposit_status',
'contract_pdf_path',
'status',
'approved_by',
'approved_at',
'terminated_by',
'terminated_at',
'termination_reason',
'terms_json',
'notes',
'branch_id',
];
/**
* Get contract type options with Arabic labels.
*/
public static function getContractTypeOptions(): array
{
return [
'revenue_share' => 'نسبة من الإيرادات',
'fixed_rent' => 'إيجار ثابت',
'hybrid' => 'مختلط',
];
}
/**
* Get contract status options with Arabic labels.
*/
public static function getStatusOptions(): array
{
return [
'draft' => 'مسودة',
'pending_approval' => 'قيد الاعتماد',
'active' => 'ساري',
'suspended' => 'موقوف',
'expired' => 'منتهي',
'terminated' => 'ملغى',
];
}
/**
* Get deposit status options with Arabic labels.
*/
public static function getDepositStatusOptions(): array
{
return [
'pending' => 'قيد السداد',
'paid' => 'مدفوع',
'returned' => 'مسترد',
'forfeited' => 'مصادر',
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
class Booking extends Model
{
protected static string $table = 'sa_bookings';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'booking_number', 'facility_unit_id', 'booking_type', 'booking_date',
'start_time', 'end_time', 'group_id', 'coach_id', 'booker_type',
'booker_id', 'booker_name', 'participant_count', 'spots_reserved',
'total_amount', 'payment_status', 'status', 'is_recurring',
'recurrence_pattern_json', 'parent_booking_id', 'cancellation_reason',
'cancelled_by', 'cancelled_at', 'notes', 'branch_id',
];
public static function getBookingTypeOptions(): array
{
return [
'hourly' => 'حجز بالساعة',
'training' => 'تدريب',
'maintenance' => 'صيانة',
'blocked' => 'محجوز',
];
}
public static function getStatusOptions(): array
{
return [
'pending' => 'في الانتظار',
'confirmed' => 'مؤكد',
'checked_in' => 'تم الحضور',
'completed' => 'مكتمل',
'cancelled' => 'ملغي',
'no_show' => 'لم يحضر',
];
}
public static function getPaymentStatusOptions(): array
{
return [
'unpaid' => 'غير مدفوع',
'paid' => 'مدفوع',
'partial' => 'مدفوع جزئياً',
'refunded' => 'مسترد',
];
}
public static function generateNumber(): string
{
return 'BK-' . date('Ymd') . '-' . str_pad((string)random_int(1, 9999), 4, '0', STR_PAD_LEFT);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class Coach extends Model
{
protected static string $table = 'sa_coaches';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = 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', 'payment_model',
'hourly_rate', 'session_rate', 'monthly_rate',
'max_groups', 'is_active', 'branch_id',
];
public static function getEmploymentTypeOptions(): array
{
return [
'staff' => 'موظف',
'contract' => 'تعاقد',
'freelance' => 'حر',
];
}
public static function getPaymentModelOptions(): array
{
return [
'per_session' => 'بالحصة',
'per_player' => 'باللاعب',
'monthly_fixed' => 'شهري ثابت',
'hybrid' => 'مختلط',
];
}
public function getDisciplines(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT cd.*, d.name_ar as discipline_name, d.code as discipline_code
FROM sa_coach_disciplines cd
JOIN sa_disciplines d ON d.id = cd.discipline_id
WHERE cd.coach_id = ?
ORDER BY cd.specialization_level = 'primary' DESC",
[$this->id]
);
}
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('full_name_ar', 'ASC')
->get();
}
public static function findByCode(string $code): ?static
{
$row = static::query()->where('code', '=', $code)->first();
if ($row === null) {
return null;
}
$instance = new static($row);
$instance->exists = true;
return $instance;
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
class Discipline extends Model
{
protected static string $table = 'sa_disciplines';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'category', 'icon',
'description_ar', 'config_json', 'sort_order', 'is_active',
];
public static function getCategoryOptions(): array
{
return [
'individual' => 'فردية',
'team' => 'جماعية',
'racket' => 'مضرب',
'aquatic' => 'مائية',
'combat' => 'قتالية',
'leisure' => 'ترفيهية',
];
}
public function getCategoryLabel(): string
{
return static::getCategoryOptions()[$this->category] ?? $this->category;
}
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('sort_order', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class Facility extends Model
{
protected static string $table = 'sa_facilities';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'facility_type', 'discipline_id',
'location_description', 'operating_hours_json', 'is_active', 'branch_id',
];
public static function getTypeOptions(): array
{
return [
'pool' => 'حمام سباحة',
'court' => 'ملعب (كورت)',
'pitch' => 'ملعب كبير',
'gym' => 'صالة رياضية',
'track' => 'مضمار',
'multipurpose' => 'متعدد الاستخدام',
];
}
public function getTypeLabel(): string
{
return static::getTypeOptions()[$this->facility_type] ?? $this->facility_type;
}
public function getOperatingHours(): array
{
$json = $this->operating_hours_json;
if (is_string($json)) {
return json_decode($json, true) ?: ['start' => '06:00', 'end' => '22:00', 'slot_minutes' => 60];
}
return $json ?: ['start' => '06:00', 'end' => '22:00', 'slot_minutes' => 60];
}
public function getUnits(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM sa_facility_units WHERE facility_id = ? AND is_active = 1 ORDER BY sort_order, id",
[$this->id]
);
}
public function getTimeBrackets(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT * FROM sa_time_brackets WHERE facility_id = ? AND is_active = 1 ORDER BY start_time",
[$this->id]
);
}
public static function getActive(): array
{
return static::query()
->where('is_active', '=', 1)
->orderBy('name_ar', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
class FacilityUnit extends Model
{
protected static string $table = 'sa_facility_units';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'facility_id', 'code', 'name_ar', 'name_en', 'unit_type',
'booking_mode', 'max_capacity', 'sort_order', 'dimensions_json',
'config_json', 'is_active',
];
public static function getUnitTypeOptions(): array
{
return [
'lane' => 'حارة',
'court' => 'كورت',
'pitch' => 'ملعب',
'ring' => 'حلبة',
'mat' => 'بساط',
'room' => 'غرفة',
];
}
public static function getBookingModeOptions(): array
{
return [
'exclusive' => 'حصري (حجز كامل)',
'shared' => 'مشترك (سعة متعددة)',
];
}
public function isShared(): bool
{
return $this->booking_mode === 'shared';
}
public function isExclusive(): bool
{
return $this->booking_mode === 'exclusive';
}
public static function getByFacility(int $facilityId): array
{
return static::query()
->where('facility_id', '=', $facilityId)
->where('is_active', '=', 1)
->orderBy('sort_order', 'ASC')
->get();
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
use App\Core\App;
class Group extends Model
{
protected static string $table = 'sa_groups';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = true;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'code', 'name_ar', 'name_en', 'program_id', 'coach_id',
'min_capacity', 'max_capacity', 'current_count', 'is_full',
'monthly_fee_member', 'monthly_fee_nonmember',
'season_start', 'season_end', 'status',
];
public static function getStatusOptions(): array
{
return [
'active' => 'نشط',
'paused' => 'متوقف',
'completed' => 'مكتمل',
'cancelled' => 'ملغي',
];
}
public static function getSchedule(int $groupId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT gs.*, fu.name_ar as unit_name, fu.code as unit_code
FROM sa_group_schedule gs
LEFT JOIN sa_facility_units fu ON fu.id = gs.facility_unit_id
WHERE gs.group_id = ? AND gs.is_active = 1
ORDER BY gs.day_of_week ASC, gs.start_time ASC",
[$groupId]
);
}
public static function getPlayers(int $groupId): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT gp.*, p.name_ar as player_name, p.code as player_code, p.phone
FROM sa_group_players gp
JOIN sa_players p ON p.id = gp.player_id
WHERE gp.group_id = ? AND gp.status = 'active'
ORDER BY p.name_ar ASC",
[$groupId]
);
}
public static function getActive(): array
{
$db = App::getInstance()->db();
return $db->select(
"SELECT g.*, p.name_ar as program_name, c.name_ar as coach_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id
WHERE g.status = 'active' AND g.is_archived = 0
ORDER BY g.name_ar ASC",
[]
);
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
class GroupPlayer extends Model
{
protected static string $table = 'sa_group_players';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'group_id', 'player_id', 'enrolled_at',
'left_at', 'status', 'notes',
];
public static function getStatusOptions(): array
{
return [
'active' => 'نشط',
'paused' => 'متوقف',
'transferred' => 'منقول',
'withdrawn' => 'منسحب',
];
}
}
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
class GroupSchedule extends Model
{
protected static string $table = 'sa_group_schedule';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = true;
protected static array $fillable = [
'group_id', 'facility_unit_id', 'day_of_week',
'start_time', 'end_time', 'is_active',
];
public static function getDayName(int $dayNum): string
{
$days = [
0 => 'الأحد',
1 => 'الاثنين',
2 => 'الثلاثاء',
3 => 'الأربعاء',
4 => 'الخميس',
5 => 'الجمعة',
6 => 'السبت',
];
return $days[$dayNum] ?? '';
}
}
This diff is collapsed.
This diff is collapsed.
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Models;
use App\Core\Model;
class PricingRule extends Model
{
protected static string $table = 'sa_pricing_rules';
protected static string $primaryKey = 'id';
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = false;
protected static bool $autoTrackAuthor = true;
protected static array $fillable = [
'facility_unit_id', 'time_bracket_id', 'group_size_min', 'group_size_max',
'price_per_person_member', 'price_per_person_nonmember',
'effective_from', 'effective_to', 'is_active',
];
public static function getByUnit(int $unitId): array
{
return static::query()
->where('facility_unit_id', '=', $unitId)
->where('is_active', '=', 1)
->orderBy('group_size_min', 'ASC')
->get();
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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