Commit 2aaa27a2 authored by Mahmoud Aglan's avatar Mahmoud Aglan

searches and filters sa

parent a5018bba
...@@ -20,14 +20,39 @@ class PlayerSearchApiController extends Controller ...@@ -20,14 +20,39 @@ class PlayerSearchApiController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$like = '%' . $q . '%'; $like = '%' . $q . '%';
$disciplineId = (int) $request->get('discipline_id', 0);
$groupId = (int) $request->get('group_id', 0);
$excludeGroup = (int) $request->get('exclude_group', 0);
$joins = '';
$where = 'p.is_archived = 0 AND (p.full_name_ar LIKE ? OR p.registration_serial LIKE ? OR p.national_id LIKE ? OR p.phone LIKE ?)';
$params = [$like, $like, $like, $like];
if ($disciplineId > 0) {
$joins .= ' INNER JOIN sa_group_players gp_d ON gp_d.player_id = p.id AND gp_d.status = "active"';
$joins .= ' INNER JOIN sa_groups g_d ON g_d.id = gp_d.group_id AND g_d.is_archived = 0';
$joins .= ' INNER JOIN sa_programs pr_d ON pr_d.id = g_d.program_id AND pr_d.discipline_id = ?';
$params[] = $disciplineId;
}
if ($groupId > 0) {
$joins .= ' INNER JOIN sa_group_players gp_g ON gp_g.player_id = p.id AND gp_g.group_id = ? AND gp_g.status = "active"';
$params[] = $groupId;
}
if ($excludeGroup > 0) {
$where .= ' AND p.id NOT IN (SELECT player_id FROM sa_group_players WHERE group_id = ? AND status IN ("active","pending_payment"))';
$params[] = $excludeGroup;
}
$results = $db->select( $results = $db->select(
"SELECT id, full_name_ar, registration_serial, national_id, player_type, medical_status "SELECT DISTINCT p.id, p.full_name_ar, p.registration_serial, p.national_id, p.player_type, p.medical_status
FROM sa_players FROM sa_players p
WHERE is_archived = 0 {$joins}
AND (full_name_ar LIKE ? OR registration_serial LIKE ? OR national_id LIKE ? OR phone LIKE ?) WHERE {$where}
ORDER BY full_name_ar ORDER BY p.full_name_ar
LIMIT 20", LIMIT 20",
[$like, $like, $like, $like] $params
); );
return $this->json(['results' => $results]); 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;
class SmartFilterApiController extends Controller
{
public function groups(Request $request): Response
{
$db = App::getInstance()->db();
$q = trim((string) $request->get('q', ''));
$disciplineId = (int) $request->get('discipline_id', 0);
$programId = (int) $request->get('program_id', 0);
$coachId = (int) $request->get('coach_id', 0);
$activeOnly = (int) $request->get('active_only', 1);
$where = ['g.is_archived = 0'];
$params = [];
if ($activeOnly) {
$where[] = "g.status = 'active'";
}
if ($q !== '' && strlen($q) >= 2) {
$where[] = '(g.name_ar LIKE ? OR g.code LIKE ?)';
$params[] = "%{$q}%";
$params[] = "%{$q}%";
}
if ($disciplineId > 0) {
$where[] = 'p.discipline_id = ?';
$params[] = $disciplineId;
}
if ($programId > 0) {
$where[] = 'g.program_id = ?';
$params[] = $programId;
}
if ($coachId > 0) {
$where[] = 'g.coach_id = ?';
$params[] = $coachId;
}
$whereSql = implode(' AND ', $where);
$results = $db->select(
"SELECT g.id, g.code, g.name_ar, g.current_count, g.max_capacity, g.is_full,
c.full_name_ar as coach_name, p.name_ar as program_name, d.name_ar as discipline_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id
WHERE {$whereSql}
ORDER BY g.name_ar ASC
LIMIT 30",
$params
);
return $this->json(['results' => $results]);
}
public function coaches(Request $request): Response
{
$db = App::getInstance()->db();
$q = trim((string) $request->get('q', ''));
$disciplineId = (int) $request->get('discipline_id', 0);
$where = ['c.is_archived = 0', 'c.is_active = 1'];
$params = [];
$joins = '';
if ($q !== '' && strlen($q) >= 2) {
$where[] = '(c.full_name_ar LIKE ? OR c.code LIKE ?)';
$params[] = "%{$q}%";
$params[] = "%{$q}%";
}
if ($disciplineId > 0) {
$joins = 'INNER JOIN sa_coach_disciplines cd ON cd.coach_id = c.id AND cd.discipline_id = ?';
$params[] = $disciplineId;
}
$whereSql = implode(' AND ', $where);
$results = $db->select(
"SELECT DISTINCT c.id, c.code, c.full_name_ar, c.phone
FROM sa_coaches c
{$joins}
WHERE {$whereSql}
ORDER BY c.full_name_ar ASC
LIMIT 20",
$params
);
return $this->json(['results' => $results]);
}
public function disciplines(Request $request): Response
{
$db = App::getInstance()->db();
$results = $db->select(
"SELECT id, code, name_ar, icon
FROM sa_disciplines
WHERE is_active = 1 AND is_archived = 0
ORDER BY sort_order ASC, name_ar ASC",
[]
);
return $this->json(['results' => $results]);
}
}
...@@ -8,6 +8,7 @@ use App\Core\Request; ...@@ -8,6 +8,7 @@ use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\App; use App\Core\App;
use App\Core\Pagination; use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Discipline;
class AttendanceController extends Controller class AttendanceController extends Controller
{ {
...@@ -19,6 +20,22 @@ class AttendanceController extends Controller ...@@ -19,6 +20,22 @@ class AttendanceController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$today = date('Y-m-d'); $today = date('Y-m-d');
$disciplineId = trim((string) $request->get('discipline_id', ''));
$coachId = trim((string) $request->get('coach_id', ''));
$where = "b.booking_type = 'training' AND b.booking_date = ? AND b.status NOT IN ('cancelled', 'no_show')";
$params = [$today];
if ($disciplineId !== '') {
$where .= " AND pr.discipline_id = ?";
$params[] = (int) $disciplineId;
}
if ($coachId !== '') {
$where .= " AND b.coach_id = ?";
$params[] = (int) $coachId;
}
$bookings = $db->select( $bookings = $db->select(
"SELECT b.*, fu.name_ar as unit_name, f.name_ar as facility_name, "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 g.name_ar as group_name, c.full_name_ar as coach_name
...@@ -26,17 +43,22 @@ class AttendanceController extends Controller ...@@ -26,17 +43,22 @@ class AttendanceController extends Controller
JOIN sa_facility_units fu ON fu.id = b.facility_unit_id JOIN sa_facility_units fu ON fu.id = b.facility_unit_id
JOIN sa_facilities f ON f.id = fu.facility_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_groups g ON g.id = b.group_id
LEFT JOIN sa_programs pr ON pr.id = g.program_id
LEFT JOIN sa_coaches c ON c.id = b.coach_id LEFT JOIN sa_coaches c ON c.id = b.coach_id
WHERE b.booking_type = 'training' WHERE {$where}
AND b.booking_date = ?
AND b.status NOT IN ('cancelled', 'no_show')
ORDER BY b.start_time ASC", ORDER BY b.start_time ASC",
[$today] $params
); );
$disciplines = Discipline::getActive();
$coaches = $db->select("SELECT id, full_name_ar as name_ar FROM sa_coaches WHERE is_archived = 0 AND is_active = 1 ORDER BY full_name_ar", []);
return $this->view('SportsActivity.Views.attendance.index', [ return $this->view('SportsActivity.Views.attendance.index', [
'bookings' => $bookings, 'bookings' => $bookings,
'today' => $today, 'today' => $today,
'disciplines' => $disciplines,
'coaches' => $coaches,
'filters' => ['discipline_id' => $disciplineId, 'coach_id' => $coachId],
]); ]);
} }
......
...@@ -19,11 +19,13 @@ class CoachController extends Controller ...@@ -19,11 +19,13 @@ class CoachController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$search = trim((string) $request->get('q', '')); $search = trim((string) $request->get('q', ''));
$disciplineFilter = trim((string) $request->get('discipline_id', ''));
$page = max(1, (int) $request->get('page', 1)); $page = max(1, (int) $request->get('page', 1));
$perPage = 25; $perPage = 25;
$where = ['c.is_archived = 0']; $where = ['c.is_archived = 0'];
$params = []; $params = [];
$joins = '';
if ($search !== '') { if ($search !== '') {
$where[] = '(c.full_name_ar LIKE ? OR c.full_name_en LIKE ? OR c.national_id LIKE ? OR c.code LIKE ?)'; $where[] = '(c.full_name_ar LIKE ? OR c.full_name_en LIKE ? OR c.national_id LIKE ? OR c.code LIKE ?)';
...@@ -33,10 +35,15 @@ class CoachController extends Controller ...@@ -33,10 +35,15 @@ class CoachController extends Controller
$params[] = "%{$search}%"; $params[] = "%{$search}%";
} }
if ($disciplineFilter !== '') {
$joins = 'INNER JOIN sa_coach_disciplines cdf ON cdf.coach_id = c.id AND cdf.discipline_id = ?';
$params[] = (int) $disciplineFilter;
}
$whereSql = implode(' AND ', $where); $whereSql = implode(' AND ', $where);
$total = (int) $db->selectOne( $total = (int) $db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_coaches c WHERE {$whereSql}", "SELECT COUNT(DISTINCT c.id) as cnt FROM sa_coaches c {$joins} WHERE {$whereSql}",
$params $params
)['cnt']; )['cnt'];
...@@ -47,6 +54,7 @@ class CoachController extends Controller ...@@ -47,6 +54,7 @@ class CoachController extends Controller
"SELECT c.*, "SELECT c.*,
GROUP_CONCAT(d.name_ar SEPARATOR '، ') as discipline_names GROUP_CONCAT(d.name_ar SEPARATOR '، ') as discipline_names
FROM sa_coaches c FROM sa_coaches c
{$joins}
LEFT JOIN sa_coach_disciplines cd ON cd.coach_id = c.id LEFT JOIN sa_coach_disciplines cd ON cd.coach_id = c.id
LEFT JOIN sa_disciplines d ON d.id = cd.discipline_id LEFT JOIN sa_disciplines d ON d.id = cd.discipline_id
WHERE {$whereSql} WHERE {$whereSql}
...@@ -56,10 +64,14 @@ class CoachController extends Controller ...@@ -56,10 +64,14 @@ class CoachController extends Controller
$params $params
); );
$disciplines = Discipline::getActive();
return $this->view('SportsActivity.Views.coaches.index', [ return $this->view('SportsActivity.Views.coaches.index', [
'coaches' => $coaches, 'coaches' => $coaches,
'pagination' => $pagination, 'pagination' => $pagination,
'search' => $search, 'search' => $search,
'disciplines' => $disciplines,
'disciplineFilter' => $disciplineFilter,
]); ]);
} }
......
...@@ -14,42 +14,209 @@ class DashboardController extends Controller ...@@ -14,42 +14,209 @@ class DashboardController extends Controller
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$today = date('Y-m-d'); $today = date('Y-m-d');
$now = date('H:i:s');
$dayOfWeek = (int) date('w');
$branchScope = ''; // ─── Core Counts ────────────────────────────────────────────────────
$branchParams = []; $counts = $db->selectOne("
if (function_exists('branch_scope')) {
$branchScope = branch_scope('', 'branch_id');
$branchParams = branch_params();
}
$stats = $db->selectOne("
SELECT SELECT
(SELECT COUNT(*) FROM sa_disciplines WHERE is_active = 1) as active_disciplines, (SELECT COUNT(*) FROM sa_players WHERE is_archived = 0) as total_players,
(SELECT COUNT(*) FROM sa_facilities WHERE is_active = 1 AND is_archived = 0) as active_facilities,
(SELECT COUNT(*) FROM sa_facility_units WHERE is_active = 1) as active_units,
(SELECT COUNT(*) FROM sa_coaches WHERE is_active = 1 AND is_archived = 0) as active_coaches,
(SELECT COUNT(*) FROM sa_players WHERE is_archived = 0{$branchScope}) as total_players,
(SELECT COUNT(*) FROM sa_groups WHERE status = 'active' AND is_archived = 0) as active_groups, (SELECT COUNT(*) FROM sa_groups WHERE status = 'active' AND is_archived = 0) as active_groups,
(SELECT COUNT(*) FROM sa_group_players WHERE status = 'active') as enrolled_players, (SELECT COUNT(*) FROM sa_group_players WHERE status = 'active') as enrolled_players,
(SELECT COUNT(*) FROM sa_bookings WHERE booking_date = ? AND status NOT IN ('cancelled','no_show'){$branchScope}) as today_bookings, (SELECT COUNT(*) FROM sa_coaches WHERE is_active = 1 AND is_archived = 0) as active_coaches,
(SELECT COUNT(*) FROM sa_player_documents WHERE document_type = 'medical_cert' AND approval_status = 'pending') as pending_medical, (SELECT COUNT(*) FROM sa_bookings WHERE booking_date = ? AND status NOT IN ('cancelled','no_show')) as today_bookings,
(SELECT COUNT(*) FROM sa_subscriptions WHERE payment_status IN ('unpaid','overdue') AND period_end < ?) as overdue_subscriptions (SELECT COUNT(*) FROM sa_bookings WHERE booking_date = ? AND booking_type = 'training' AND status NOT IN ('cancelled','no_show')) as today_training_sessions,
", array_merge([$today], $branchParams, $branchParams, [$today])); (SELECT COUNT(*) FROM sa_bookings WHERE booking_date = ? AND booking_type = 'hourly' AND status NOT IN ('cancelled','no_show')) as today_hourly_bookings
", [$today, $today, $today]);
// ─── Alerts (Action Required) ───────────────────────────────────────
$alerts = [];
$pendingMedical = (int) $db->selectOne(
"SELECT COUNT(*) as c FROM sa_player_documents WHERE document_type = 'medical_cert' AND approval_status = 'pending'"
)['c'];
if ($pendingMedical > 0) {
$alerts[] = ['type' => 'warning', 'icon' => 'heart-pulse', 'text' => $pendingMedical . ' شهادة طبية بانتظار الموافقة', 'link' => '/sa/players', 'count' => $pendingMedical];
}
$overdueSubscriptions = (int) $db->selectOne(
"SELECT COUNT(*) as c FROM sa_subscriptions WHERE payment_status IN ('unpaid','overdue') AND period_end < ?",
[$today]
)['c'];
if ($overdueSubscriptions > 0) {
$alerts[] = ['type' => 'danger', 'icon' => 'alert-triangle', 'text' => $overdueSubscriptions . ' اشتراك متأخر السداد', 'link' => '/sa/subscriptions?payment_status=overdue', 'count' => $overdueSubscriptions];
}
$fullGroups = (int) $db->selectOne(
"SELECT COUNT(*) as c FROM sa_groups WHERE status = 'active' AND is_archived = 0 AND is_full = 1"
)['c'];
if ($fullGroups > 0) {
$alerts[] = ['type' => 'info', 'icon' => 'users', 'text' => $fullGroups . ' مجموعة ممتلئة (تحتاج فتح مجموعات جديدة)', 'link' => '/sa/groups', 'count' => $fullGroups];
}
$waitlistCount = (int) $db->selectOne(
"SELECT COUNT(*) as c FROM sa_waitlist WHERE status = 'waiting'"
)['c'];
if ($waitlistCount > 0) {
$alerts[] = ['type' => 'info', 'icon' => 'clock', 'text' => $waitlistCount . ' لاعب في قائمة الانتظار', 'link' => '/sa/waitlist', 'count' => $waitlistCount];
}
$expiringCards = (int) $db->selectOne(
"SELECT COUNT(*) as c FROM sa_player_cards WHERE status = 'active' AND valid_until BETWEEN ? AND DATE_ADD(?, INTERVAL 7 DAY)",
[$today, $today]
)['c'];
if ($expiringCards > 0) {
$alerts[] = ['type' => 'warning', 'icon' => 'credit-card', 'text' => $expiringCards . ' كارت ينتهي خلال أسبوع', 'link' => '/sa/cards', 'count' => $expiringCards];
}
// ─── Live Now: What's happening right now ───────────────────────────
$liveNow = $db->select(
"SELECT b.start_time, b.end_time, b.booking_type, b.booker_name, b.booker_type,
b.organization_name, b.participant_count,
g.name_ar as group_name, c.full_name_ar as coach_name,
fu.name_ar as unit_name, f.name_ar as facility_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.start_time <= ? AND b.end_time > ?
AND b.status NOT IN ('cancelled','no_show')
ORDER BY b.start_time",
[$today, $now, $now]
);
// ─── Upcoming (next 2 hours) ───────────────────────────────────────
$twoHoursLater = date('H:i:s', strtotime('+2 hours'));
$upcoming = $db->select(
"SELECT b.start_time, b.end_time, b.booking_type, b.booker_name, b.booker_type,
b.organization_name, b.participant_count,
g.name_ar as group_name, c.full_name_ar as coach_name,
fu.name_ar as unit_name, f.name_ar as facility_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.start_time > ? AND b.start_time <= ?
AND b.status NOT IN ('cancelled','no_show')
ORDER BY b.start_time
LIMIT 10",
[$today, $now, $twoHoursLater]
);
// ─── Facility Utilization Today ─────────────────────────────────────
$facilityUsage = $db->select(
"SELECT f.id, f.name_ar, f.facility_type,
COUNT(DISTINCT b.id) as booking_count,
(SELECT COUNT(*) FROM sa_facility_units WHERE facility_id = f.id AND is_active = 1) as unit_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_bookings b ON b.facility_unit_id = fu.id AND b.booking_date = ? AND b.status NOT IN ('cancelled','no_show')
WHERE f.is_active = 1 AND f.is_archived = 0
GROUP BY f.id, f.name_ar, f.facility_type
ORDER BY booking_count DESC",
[$today]
);
// ─── Groups Near Capacity (80%+) ────────────────────────────────────
$groupsNearCapacity = $db->select(
"SELECT g.id, g.name_ar, g.current_count, g.max_capacity, g.is_full,
p.name_ar as program_name, d.name_ar as discipline_name,
c.full_name_ar as coach_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
LEFT JOIN sa_coaches c ON c.id = g.coach_id
WHERE g.status = 'active' AND g.is_archived = 0
AND g.current_count >= (g.max_capacity * 0.8)
ORDER BY (g.current_count / g.max_capacity) DESC
LIMIT 10"
);
// ─── Recent Registrations (last 7 days) ────────────────────────────
$recentRegistrations = $db->select(
"SELECT r.id, r.registration_number, r.status, r.created_at, r.player_type,
p.full_name_ar as player_name,
g.name_ar as group_name, d.name_ar as discipline_name
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
LEFT JOIN sa_groups g ON g.id = r.group_id
LEFT JOIN sa_programs pr ON pr.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = pr.discipline_id
WHERE r.created_at >= DATE_SUB(?, INTERVAL 7 DAY) AND r.status != 'cancelled'
ORDER BY r.created_at DESC
LIMIT 8",
[$today]
);
// ─── Today's Training Schedule Summary ──────────────────────────────
$todaySchedule = $db->select(
"SELECT gs.start_time, gs.end_time, g.name_ar as group_name, g.current_count,
c.full_name_ar as coach_name, fu.name_ar as unit_name, f.name_ar as facility_name,
d.name_ar as discipline_name
FROM sa_group_schedule gs
INNER JOIN sa_groups g ON g.id = gs.group_id AND g.status = 'active'
LEFT JOIN sa_coaches c ON c.id = g.coach_id
LEFT JOIN sa_facility_units fu ON fu.id = gs.facility_unit_id
LEFT JOIN sa_facilities f ON f.id = fu.facility_id
LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
WHERE gs.day_of_week = ? AND gs.is_active = 1
ORDER BY gs.start_time, f.name_ar",
[$dayOfWeek]
);
// ─── Coach Load Today ───────────────────────────────────────────────
$coachLoad = $db->select(
"SELECT c.id, c.full_name_ar as name,
COUNT(DISTINCT gs.group_id) as group_count,
MIN(gs.start_time) as first_session,
MAX(gs.end_time) as last_session
FROM sa_coaches c
INNER JOIN sa_groups g ON g.coach_id = c.id AND g.status = 'active'
INNER JOIN sa_group_schedule gs ON gs.group_id = g.id AND gs.day_of_week = ? AND gs.is_active = 1
WHERE c.is_active = 1 AND c.is_archived = 0
GROUP BY c.id, c.full_name_ar
ORDER BY group_count DESC
LIMIT 10",
[$dayOfWeek]
);
// ─── Discipline Breakdown ───────────────────────────────────────────
$disciplineBreakdown = $db->select(
"SELECT d.id, d.name_ar,
COUNT(DISTINCT g.id) as group_count,
SUM(g.current_count) as total_players,
SUM(g.max_capacity) as total_capacity
FROM sa_disciplines d
INNER JOIN sa_programs p ON p.discipline_id = d.id
INNER JOIN sa_groups g ON g.program_id = p.id AND g.status = 'active' AND g.is_archived = 0
WHERE d.is_active = 1 AND d.is_archived = 0
GROUP BY d.id, d.name_ar
ORDER BY total_players DESC"
);
return $this->view('SportsActivity.Views.dashboard', [ return $this->view('SportsActivity.Views.dashboard', [
'stats' => [ 'counts' => $counts,
'active_disciplines' => (int) ($stats['active_disciplines'] ?? 0), 'alerts' => $alerts,
'active_facilities' => (int) ($stats['active_facilities'] ?? 0), 'liveNow' => $liveNow,
'active_units' => (int) ($stats['active_units'] ?? 0), 'upcoming' => $upcoming,
'active_coaches' => (int) ($stats['active_coaches'] ?? 0), 'facilityUsage' => $facilityUsage,
'total_players' => (int) ($stats['total_players'] ?? 0), 'groupsNearCapacity' => $groupsNearCapacity,
'active_groups' => (int) ($stats['active_groups'] ?? 0), 'recentRegistrations' => $recentRegistrations,
'enrolled_players' => (int) ($stats['enrolled_players'] ?? 0), 'todaySchedule' => $todaySchedule,
'today_bookings' => (int) ($stats['today_bookings'] ?? 0), 'coachLoad' => $coachLoad,
'pending_medical' => (int) ($stats['pending_medical'] ?? 0), 'disciplineBreakdown' => $disciplineBreakdown,
'overdue_subscriptions' => (int) ($stats['overdue_subscriptions'] ?? 0), 'today' => $today,
], 'now' => $now,
'today' => $today, 'dayName' => self::arabicDayName($dayOfWeek),
]); ]);
} }
private static function arabicDayName(int $dayOfWeek): string
{
$days = ['الأحد', 'الاثنين', 'الثلاثاء', 'الأربعاء', 'الخميس', 'الجمعة', 'السبت'];
return $days[$dayOfWeek] ?? '';
}
} }
...@@ -23,9 +23,10 @@ class GroupController extends Controller ...@@ -23,9 +23,10 @@ class GroupController extends Controller
public function index(Request $request): Response public function index(Request $request): Response
{ {
$filters = [ $filters = [
'q' => trim((string) $request->get('q', '')), 'q' => trim((string) $request->get('q', '')),
'program_id' => trim((string) $request->get('program_id', '')), 'discipline_id' => trim((string) $request->get('discipline_id', '')),
'coach_id' => trim((string) $request->get('coach_id', '')), 'program_id' => trim((string) $request->get('program_id', '')),
'coach_id' => trim((string) $request->get('coach_id', '')),
]; ];
$page = max(1, (int) $request->get('page', 1)); $page = max(1, (int) $request->get('page', 1));
...@@ -41,6 +42,11 @@ class GroupController extends Controller ...@@ -41,6 +42,11 @@ class GroupController extends Controller
$params[] = '%' . $filters['q'] . '%'; $params[] = '%' . $filters['q'] . '%';
} }
if ($filters['discipline_id'] !== '') {
$where .= " AND p.discipline_id = ?";
$params[] = (int) $filters['discipline_id'];
}
if ($filters['program_id'] !== '') { if ($filters['program_id'] !== '') {
$where .= " AND g.program_id = ?"; $where .= " AND g.program_id = ?";
$params[] = (int) $filters['program_id']; $params[] = (int) $filters['program_id'];
...@@ -51,7 +57,10 @@ class GroupController extends Controller ...@@ -51,7 +57,10 @@ class GroupController extends Controller
$params[] = (int) $filters['coach_id']; $params[] = (int) $filters['coach_id'];
} }
$total = (int) ($db->selectOne("SELECT COUNT(*) as cnt FROM sa_groups g WHERE {$where}", $params)['cnt'] ?? 0); $total = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_groups g LEFT JOIN sa_programs p ON p.id = g.program_id WHERE {$where}",
$params
)['cnt'] ?? 0);
$pagination = Pagination::paginate($total, $perPage, $page); $pagination = Pagination::paginate($total, $perPage, $page);
$offset = ($page - 1) * $perPage; $offset = ($page - 1) * $perPage;
...@@ -66,15 +75,17 @@ class GroupController extends Controller ...@@ -66,15 +75,17 @@ class GroupController extends Controller
$params $params
); );
$disciplines = Discipline::getActive();
$programs = Program::getActive(); $programs = Program::getActive();
$coaches = $db->select("SELECT id, full_name_ar as name_ar FROM sa_coaches WHERE is_archived = 0 ORDER BY full_name_ar", []); $coaches = $db->select("SELECT id, full_name_ar as name_ar FROM sa_coaches WHERE is_archived = 0 ORDER BY full_name_ar", []);
return $this->view('SportsActivity.Views.groups.index', [ return $this->view('SportsActivity.Views.groups.index', [
'groups' => $groups, 'groups' => $groups,
'pagination' => $pagination, 'pagination' => $pagination,
'filters' => $filters, 'filters' => $filters,
'programs' => $programs, 'disciplines' => $disciplines,
'coaches' => $coaches, 'programs' => $programs,
'coaches' => $coaches,
]); ]);
} }
...@@ -188,19 +199,6 @@ class GroupController extends Controller ...@@ -188,19 +199,6 @@ class GroupController extends Controller
$players = Group::getPlayers((int) $id); $players = Group::getPlayers((int) $id);
$schedule = Group::getSchedule((int) $id); $schedule = Group::getSchedule((int) $id);
// Available players for enrollment (not already in this group)
$availablePlayers = $db->select(
"SELECT p.id, p.full_name_ar as name_ar, p.registration_serial as code,
(SELECT COUNT(*) FROM sa_subscriptions s
WHERE s.player_id = p.id AND s.payment_status IN ('unpaid','overdue','partial')) as unpaid_count
FROM sa_players p
WHERE p.is_archived = 0 AND p.id NOT IN (
SELECT player_id FROM sa_group_players WHERE group_id = ? AND status IN ('active','pending_payment')
)
ORDER BY p.full_name_ar ASC LIMIT 100",
[(int) $id]
);
// Facility units for schedule // Facility units for schedule
$facilityUnits = $db->select( $facilityUnits = $db->select(
"SELECT fu.id, fu.name_ar, fu.code, f.name_ar as facility_name "SELECT fu.id, fu.name_ar, fu.code, f.name_ar as facility_name
...@@ -212,12 +210,11 @@ class GroupController extends Controller ...@@ -212,12 +210,11 @@ class GroupController extends Controller
); );
return $this->view('SportsActivity.Views.groups.show', [ return $this->view('SportsActivity.Views.groups.show', [
'group' => $group, 'group' => $group,
'players' => $players, 'players' => $players,
'schedule' => $schedule, 'schedule' => $schedule,
'availablePlayers' => $availablePlayers, 'facilityUnits' => $facilityUnits,
'facilityUnits' => $facilityUnits, 'statuses' => Group::getStatusOptions(),
'statuses' => Group::getStatusOptions(),
]); ]);
} }
......
...@@ -104,10 +104,16 @@ class LockerRentalController extends Controller ...@@ -104,10 +104,16 @@ class LockerRentalController extends Controller
ORDER BY l.code" ORDER BY l.code"
); );
// Players $players = [];
$players = $db->select( $session = App::getInstance()->session();
"SELECT id, full_name_ar, registration_serial FROM sa_players WHERE is_archived = 0 ORDER BY full_name_ar LIMIT 500" $oldInput = $session->get('_old_input', []);
); $oldPlayerId = (int) ($oldInput['player_id'] ?? 0);
if ($oldPlayerId > 0) {
$players = $db->select(
"SELECT id, full_name_ar, registration_serial FROM sa_players WHERE id = ?",
[$oldPlayerId]
);
}
return $this->view('SportsActivity.Views.locker-rentals.create', [ return $this->view('SportsActivity.Views.locker-rentals.create', [
'lockers' => $lockers, 'lockers' => $lockers,
......
...@@ -9,6 +9,8 @@ use App\Core\Response; ...@@ -9,6 +9,8 @@ use App\Core\Response;
use App\Core\App; use App\Core\App;
use App\Core\Pagination; use App\Core\Pagination;
use App\Modules\Members\Services\NationalIdParser; use App\Modules\Members\Services\NationalIdParser;
use App\Modules\SportsActivity\Models\Discipline;
use App\Modules\SportsActivity\Models\Group;
class PlayerController extends Controller class PlayerController extends Controller
{ {
...@@ -22,11 +24,14 @@ class PlayerController extends Controller ...@@ -22,11 +24,14 @@ class PlayerController extends Controller
$search = trim((string) $request->get('q', '')); $search = trim((string) $request->get('q', ''));
$playerType = trim((string) $request->get('player_type', '')); $playerType = trim((string) $request->get('player_type', ''));
$medicalStatus = trim((string) $request->get('medical_status', '')); $medicalStatus = trim((string) $request->get('medical_status', ''));
$disciplineId = trim((string) $request->get('discipline_id', ''));
$groupId = trim((string) $request->get('group_id', ''));
$page = max(1, (int) $request->get('page', 1)); $page = max(1, (int) $request->get('page', 1));
$perPage = 25; $perPage = 25;
$where = ['1=1']; $where = ['p.is_archived = 0'];
$params = []; $params = [];
$joins = '';
if ($search !== '') { if ($search !== '') {
$where[] = '(p.full_name_ar LIKE ? OR p.national_id LIKE ? OR p.registration_serial LIKE ?)'; $where[] = '(p.full_name_ar LIKE ? OR p.national_id LIKE ? OR p.registration_serial LIKE ?)';
...@@ -45,26 +50,41 @@ class PlayerController extends Controller ...@@ -45,26 +50,41 @@ class PlayerController extends Controller
$params[] = $medicalStatus; $params[] = $medicalStatus;
} }
if ($groupId !== '') {
$joins .= ' INNER JOIN sa_group_players gp ON gp.player_id = p.id AND gp.group_id = ? AND gp.status = "active"';
$params[] = (int) $groupId;
} elseif ($disciplineId !== '') {
$joins .= ' INNER JOIN sa_group_players gp ON gp.player_id = p.id AND gp.status = "active"';
$joins .= ' INNER JOIN sa_groups g ON g.id = gp.group_id AND g.is_archived = 0';
$joins .= ' INNER JOIN sa_programs pr ON pr.id = g.program_id AND pr.discipline_id = ?';
$params[] = (int) $disciplineId;
}
$whereSql = implode(' AND ', $where); $whereSql = implode(' AND ', $where);
// Count total $countSql = "SELECT COUNT(DISTINCT p.id) as total FROM sa_players p {$joins} WHERE {$whereSql}";
$countSql = "SELECT COUNT(*) as total FROM sa_players p WHERE {$whereSql}";
$total = (int) $db->selectOne($countSql, $params)['total']; $total = (int) $db->selectOne($countSql, $params)['total'];
$pagination = Pagination::paginate($total, $perPage, $page); $pagination = Pagination::paginate($total, $perPage, $page);
$offset = ($pagination['current_page'] - 1) * $perPage; $offset = ($pagination['current_page'] - 1) * $perPage;
// Fetch players $sql = "SELECT DISTINCT p.* FROM sa_players p {$joins} WHERE {$whereSql} ORDER BY p.created_at DESC LIMIT {$perPage} OFFSET {$offset}";
$sql = "SELECT p.* FROM sa_players p WHERE {$whereSql} ORDER BY p.created_at DESC LIMIT {$perPage} OFFSET {$offset}";
$players = $db->select($sql, $params); $players = $db->select($sql, $params);
$disciplines = Discipline::getActive();
$groups = Group::getActive();
return $this->view('SportsActivity.Views.players.index', [ return $this->view('SportsActivity.Views.players.index', [
'players' => $players, 'players' => $players,
'pagination' => $pagination, 'pagination' => $pagination,
'filters' => [ 'disciplines' => $disciplines,
'groups' => $groups,
'filters' => [
'q' => $search, 'q' => $search,
'player_type' => $playerType, 'player_type' => $playerType,
'medical_status' => $medicalStatus, 'medical_status' => $medicalStatus,
'discipline_id' => $disciplineId,
'group_id' => $groupId,
], ],
]); ]);
} }
......
...@@ -118,7 +118,7 @@ class RegistrationWizardController extends Controller ...@@ -118,7 +118,7 @@ class RegistrationWizardController extends Controller
} }
$groups = $db->select( $groups = $db->select(
"SELECT g.*, p.name_ar as program_name, d.name_ar as discipline_name "SELECT g.*, p.name_ar as program_name, p.discipline_id, d.name_ar as discipline_name
FROM sa_groups g FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id LEFT JOIN sa_programs p ON p.id = g.program_id
LEFT JOIN sa_disciplines d ON d.id = p.discipline_id LEFT JOIN sa_disciplines d ON d.id = p.discipline_id
......
...@@ -10,6 +10,7 @@ use App\Core\App; ...@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\Pagination; use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Subscription; use App\Modules\SportsActivity\Models\Subscription;
use App\Modules\SportsActivity\Models\Group; use App\Modules\SportsActivity\Models\Group;
use App\Modules\SportsActivity\Models\Discipline;
use App\Modules\SportsActivity\Services\SubscriptionGeneratorService; use App\Modules\SportsActivity\Services\SubscriptionGeneratorService;
use App\Modules\SportsActivity\Services\SaPaymentService; use App\Modules\SportsActivity\Services\SaPaymentService;
...@@ -23,7 +24,9 @@ class SubscriptionController extends Controller ...@@ -23,7 +24,9 @@ class SubscriptionController extends Controller
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$filters = [ $filters = [
'q' => trim((string) $request->get('q', '')),
'month' => trim((string) $request->get('month', '')), 'month' => trim((string) $request->get('month', '')),
'discipline_id' => trim((string) $request->get('discipline_id', '')),
'group_id' => trim((string) $request->get('group_id', '')), 'group_id' => trim((string) $request->get('group_id', '')),
'payment_status' => trim((string) $request->get('payment_status', '')), 'payment_status' => trim((string) $request->get('payment_status', '')),
]; ];
...@@ -33,6 +36,13 @@ class SubscriptionController extends Controller ...@@ -33,6 +36,13 @@ class SubscriptionController extends Controller
$where = []; $where = [];
$params = []; $params = [];
$joins = 'LEFT JOIN sa_players p ON p.id = s.player_id LEFT JOIN sa_groups g ON g.id = s.group_id';
if ($filters['q'] !== '') {
$where[] = '(p.full_name_ar LIKE ? OR p.national_id LIKE ?)';
$params[] = '%' . $filters['q'] . '%';
$params[] = '%' . $filters['q'] . '%';
}
if ($filters['month'] !== '') { if ($filters['month'] !== '') {
$periodStart = $filters['month'] . '-01'; $periodStart = $filters['month'] . '-01';
...@@ -40,6 +50,12 @@ class SubscriptionController extends Controller ...@@ -40,6 +50,12 @@ class SubscriptionController extends Controller
$params[] = $periodStart; $params[] = $periodStart;
} }
if ($filters['discipline_id'] !== '') {
$joins .= ' LEFT JOIN sa_programs pr ON pr.id = g.program_id';
$where[] = 'pr.discipline_id = ?';
$params[] = (int) $filters['discipline_id'];
}
if ($filters['group_id'] !== '') { if ($filters['group_id'] !== '') {
$where[] = "s.group_id = ?"; $where[] = "s.group_id = ?";
$params[] = (int) $filters['group_id']; $params[] = (int) $filters['group_id'];
...@@ -53,7 +69,7 @@ class SubscriptionController extends Controller ...@@ -53,7 +69,7 @@ class SubscriptionController extends Controller
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : ''; $whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
$total = (int) ($db->selectOne( $total = (int) ($db->selectOne(
"SELECT COUNT(*) as cnt FROM sa_subscriptions s {$whereClause}", "SELECT COUNT(*) as cnt FROM sa_subscriptions s {$joins} {$whereClause}",
$params $params
)['cnt'] ?? 0); )['cnt'] ?? 0);
...@@ -63,20 +79,21 @@ class SubscriptionController extends Controller ...@@ -63,20 +79,21 @@ class SubscriptionController extends Controller
$subscriptions = $db->select( $subscriptions = $db->select(
"SELECT s.*, p.full_name_ar as player_name, g.name_ar as group_name "SELECT s.*, p.full_name_ar as player_name, g.name_ar as group_name
FROM sa_subscriptions s FROM sa_subscriptions s
LEFT JOIN sa_players p ON p.id = s.player_id {$joins}
LEFT JOIN sa_groups g ON g.id = s.group_id
{$whereClause} {$whereClause}
ORDER BY s.period_start DESC, s.created_at DESC ORDER BY s.period_start DESC, s.created_at DESC
LIMIT {$perPage} OFFSET {$offset}", LIMIT {$perPage} OFFSET {$offset}",
$params $params
); );
$disciplines = Discipline::getActive();
$groups = Group::getActive(); $groups = Group::getActive();
return $this->view('SportsActivity.Views.subscriptions.index', [ return $this->view('SportsActivity.Views.subscriptions.index', [
'subscriptions' => $subscriptions, 'subscriptions' => $subscriptions,
'pagination' => $pagination, 'pagination' => $pagination,
'filters' => $filters, 'filters' => $filters,
'disciplines' => $disciplines,
'groups' => $groups, 'groups' => $groups,
'statusOptions' => Subscription::getPaymentStatusOptions(), 'statusOptions' => Subscription::getPaymentStatusOptions(),
]); ]);
......
...@@ -184,6 +184,9 @@ return [ ...@@ -184,6 +184,9 @@ return [
['GET', '/api/sa/schedule/conflicts', 'SportsActivity\Controllers\Api\ScheduleApiController@conflicts', ['auth'], 'sa.schedule.view'], ['GET', '/api/sa/schedule/conflicts', 'SportsActivity\Controllers\Api\ScheduleApiController@conflicts', ['auth'], 'sa.schedule.view'],
['GET', '/api/sa/bookings/price-preview', 'SportsActivity\Controllers\Api\BookingApiController@pricePreview', ['auth'], 'sa.booking.view'], ['GET', '/api/sa/bookings/price-preview', 'SportsActivity\Controllers\Api\BookingApiController@pricePreview', ['auth'], 'sa.booking.view'],
['GET', '/api/sa/players/search', 'SportsActivity\Controllers\Api\PlayerSearchApiController@search', ['auth'], 'sa.player.view'], ['GET', '/api/sa/players/search', 'SportsActivity\Controllers\Api\PlayerSearchApiController@search', ['auth'], 'sa.player.view'],
['GET', '/api/sa/groups/search', 'SportsActivity\Controllers\Api\SmartFilterApiController@groups', ['auth'], 'sa.group.view'],
['GET', '/api/sa/coaches/search', 'SportsActivity\Controllers\Api\SmartFilterApiController@coaches', ['auth'], 'sa.group.view'],
['GET', '/api/sa/disciplines/list', 'SportsActivity\Controllers\Api\SmartFilterApiController@disciplines', ['auth'], 'sa.group.view'],
['GET', '/api/sa/mirror/{id:\d+}/state', 'SportsActivity\Controllers\Api\MirrorApiController@state', ['auth'], 'sa.mirror.view'], ['GET', '/api/sa/mirror/{id:\d+}/state', 'SportsActivity\Controllers\Api\MirrorApiController@state', ['auth'], 'sa.mirror.view'],
['GET', '/api/sa/pool-grid/{id:\d+}/state', 'SportsActivity\Controllers\Api\PoolGridApiController@state', ['auth'], 'sa.pool-grid.manage'], ['GET', '/api/sa/pool-grid/{id:\d+}/state', 'SportsActivity\Controllers\Api\PoolGridApiController@state', ['auth'], 'sa.pool-grid.manage'],
['POST', '/api/sa/pool-grid/{id:\d+}/assign', 'SportsActivity\Controllers\Api\PoolGridApiController@assign', ['auth', 'csrf'], 'sa.pool-grid.manage'], ['POST', '/api/sa/pool-grid/{id:\d+}/assign', 'SportsActivity\Controllers\Api\PoolGridApiController@assign', ['auth', 'csrf'], 'sa.pool-grid.manage'],
......
...@@ -11,6 +11,34 @@ ...@@ -11,6 +11,34 @@
<strong>حصص التدريب اليوم:</strong> <?= e($today) ?> <strong>حصص التدريب اليوم:</strong> <?= e($today) ?>
</div> </div>
<!-- Filters -->
<div class="card" style="margin-bottom:15px;padding:12px 15px;">
<form method="GET" action="/sa/attendance" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">النشاط الرياضي</label>
<select name="discipline_id" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المدرب</label>
<select name="coach_id" class="form-select" data-searchable="true" data-placeholder="-- الكل --">
<option value="">-- الكل --</option>
<?php foreach ($coaches as $c): ?>
<option value="<?= (int) $c['id'] ?>" <?= ($filters['coach_id'] ?? '') == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-outline"><i data-lucide="filter" style="width:14px;height:14px;vertical-align:middle;"></i> تصفية</button>
<?php if (($filters['discipline_id'] ?? '') !== '' || ($filters['coach_id'] ?? '') !== ''): ?>
<a href="/sa/attendance" class="btn btn-sm btn-outline" style="color:#6B7280;">مسح</a>
<?php endif; ?>
</form>
</div>
<div class="card"> <div class="card">
<div class="table-responsive"> <div class="table-responsive">
<table class="data-table"> <table class="data-table">
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<div style="padding:20px;"> <div style="padding:20px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">الوحدة / الملعب <span style="color:#DC2626;">*</span></label> <label class="form-label">الوحدة / الملعب <span style="color:#DC2626;">*</span></label>
<select name="facility_unit_id" class="form-select" required id="unitSelect"> <select name="facility_unit_id" class="form-select" required id="unitSelect" data-searchable="true" data-placeholder="ابحث عن الوحدة...">
<option value="">-- اختر الوحدة --</option> <option value="">-- اختر الوحدة --</option>
<?php foreach ($facilityUnitsGrouped as $facilityName => $units): ?> <?php foreach ($facilityUnitsGrouped as $facilityName => $units): ?>
<optgroup label="<?= e($facilityName) ?>"> <optgroup label="<?= e($facilityName) ?>">
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
</div> </div>
<div style="min-width:160px;"> <div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المرفق</label> <label class="form-label" style="font-size:12px;">المرفق</label>
<select name="facility_id" class="form-select"> <select name="facility_id" class="form-select" data-searchable="true" data-placeholder="-- الكل --">
<option value="">-- الكل --</option> <option value="">-- الكل --</option>
<?php foreach ($facilities as $f): ?> <?php foreach ($facilities as $f): ?>
<option value="<?= (int) $f['id'] ?>" <?= ($filters['facility_id'] ?? '') == $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option> <option value="<?= (int) $f['id'] ?>" <?= ($filters['facility_id'] ?? '') == $f['id'] ? 'selected' : '' ?>><?= e($f['name_ar']) ?></option>
......
...@@ -67,6 +67,9 @@ ...@@ -67,6 +67,9 @@
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="map-pin" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> اختيار المرفق</h3> <h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="map-pin" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> اختيار المرفق</h3>
</div> </div>
<div style="padding:20px;"> <div style="padding:20px;">
<div style="margin-bottom:12px;">
<input type="text" id="unitSearchInput" class="form-input" placeholder="ابحث عن مرفق أو ملعب..." style="padding:12px;font-size:14px;border-radius:10px;">
</div>
<div id="facilitiesList" style="display:grid;grid-template-columns:1fr;gap:12px;"> <div id="facilitiesList" style="display:grid;grid-template-columns:1fr;gap:12px;">
<div style="text-align:center;padding:40px;color:#9CA3AF;">جاري التحميل...</div> <div style="text-align:center;padding:40px;color:#9CA3AF;">جاري التحميل...</div>
</div> </div>
...@@ -337,6 +340,20 @@ ...@@ -337,6 +340,20 @@
document.getElementById('btnStep2Back').addEventListener('click', function() { setStep(1); }); document.getElementById('btnStep2Back').addEventListener('click', function() { setStep(1); });
document.getElementById('unitSearchInput').addEventListener('input', function() {
var q = this.value.toLowerCase();
document.querySelectorAll('#facilitiesList > div').forEach(function(group) {
var cards = group.querySelectorAll('.unit-card');
var anyVisible = false;
cards.forEach(function(card) {
var match = (card.dataset.unitName + ' ' + card.dataset.facilityName).toLowerCase().indexOf(q) >= 0;
card.style.display = match ? '' : 'none';
if (match) anyVisible = true;
});
group.style.display = (!q || anyVisible) ? '' : 'none';
});
});
// Step 3: Time slots // Step 3: Time slots
var dateInput = document.getElementById('bkDate'); var dateInput = document.getElementById('bkDate');
var participantsInput = document.getElementById('bkParticipants'); var participantsInput = document.getElementById('bkParticipants');
......
...@@ -16,8 +16,17 @@ ...@@ -16,8 +16,17 @@
<label class="form-label" style="font-size:12px;">بحث</label> <label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($search ?? '') ?>" placeholder="ابحث بالاسم، الرقم القومي، أو الكود..." class="form-input"> <input type="text" name="q" value="<?= e($search ?? '') ?>" placeholder="ابحث بالاسم، الرقم القومي، أو الكود..." class="form-input">
</div> </div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">النشاط الرياضي</label>
<select name="discipline_id" class="form-select">
<option value="">-- الكل --</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($disciplineFilter ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></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> <button type="submit" class="btn btn-outline"><i data-lucide="search" style="width:16px;height:16px;vertical-align:middle;"></i> بحث</button>
<?php if (($search ?? '') !== ''): ?> <?php if (($search ?? '') !== '' || ($disciplineFilter ?? '') !== ''): ?>
<a href="/sa/coaches" class="btn btn-outline" style="color:#6B7280;">مسح</a> <a href="/sa/coaches" class="btn btn-outline" style="color:#6B7280;">مسح</a>
<?php endif; ?> <?php endif; ?>
</form> </form>
......
...@@ -3,114 +3,377 @@ ...@@ -3,114 +3,377 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(240px, 1fr));gap:15px;margin-bottom:20px;"> <!-- Header: Day + Time -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div>
<h2 style="margin:0;font-size:20px;font-weight:700;color:#1A1A2E;"><?= e($dayName) ?>، <?= e($today) ?></h2>
<span style="font-size:13px;color:#6B7280;">آخر تحديث: <?= e($now) ?></span>
</div>
<div style="display:flex;gap:8px;">
<a href="/sa/mirror" class="btn btn-primary" style="font-size:12px;"><i data-lucide="monitor" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>المرآة</a>
<a href="/sa/bookings/wizard" class="btn btn-outline" style="font-size:12px;"><i data-lucide="plus-circle" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>حجز جديد</a>
<a href="/sa/registrations/wizard" class="btn btn-outline" style="font-size:12px;"><i data-lucide="user-plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i>تسجيل جديد</a>
</div>
</div>
<!-- Active Disciplines --> <!-- Alerts (Action Required) -->
<a href="/sa/disciplines" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #8B5CF6;"> <?php if (!empty($alerts)): ?>
<div style="display:flex;align-items:center;gap:12px;"> <div style="margin-bottom:20px;display:flex;flex-direction:column;gap:8px;">
<div style="width:44px;height:44px;background:#EDE9FE;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <?php foreach ($alerts as $alert): ?>
<i data-lucide="activity" style="width:22px;height:22px;color:#8B5CF6;"></i> <a href="<?= e($alert['link']) ?>" style="display:flex;align-items:center;gap:10px;padding:10px 14px;border-radius:8px;text-decoration:none;color:inherit;background:<?= $alert['type'] === 'danger' ? '#FEF2F2' : ($alert['type'] === 'warning' ? '#FFFBEB' : '#EFF6FF') ?>;border-right:4px solid <?= $alert['type'] === 'danger' ? '#DC2626' : ($alert['type'] === 'warning' ? '#F59E0B' : '#3B82F6') ?>;">
</div> <i data-lucide="<?= e($alert['icon']) ?>" style="width:18px;height:18px;color:<?= $alert['type'] === 'danger' ? '#DC2626' : ($alert['type'] === 'warning' ? '#F59E0B' : '#3B82F6') ?>;flex-shrink:0;"></i>
<div> <span style="font-size:13px;font-weight:500;flex:1;"><?= e($alert['text']) ?></span>
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['active_disciplines'] ?></div> <span style="font-size:12px;font-weight:700;background:<?= $alert['type'] === 'danger' ? '#DC2626' : ($alert['type'] === 'warning' ? '#F59E0B' : '#3B82F6') ?>;color:white;padding:2px 8px;border-radius:10px;"><?= (int) $alert['count'] ?></span>
<div style="font-size:13px;color:#6B7280;">الأنشطة الرياضية</div>
</div>
</div>
</a> </a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Active Facilities + Units --> <!-- Core Counts Row -->
<a href="/sa/facilities" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #3B82F6;"> <div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:12px;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:12px;"> <a href="/sa/players" class="card" style="padding:14px;text-decoration:none;color:inherit;border-right:3px solid #8B5CF6;">
<div style="width:44px;height:44px;background:#DBEAFE;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <div style="font-size:22px;font-weight:700;color:#1A1A2E;"><?= (int) $counts['total_players'] ?></div>
<i data-lucide="building" style="width:22px;height:22px;color:#3B82F6;"></i> <div style="font-size:12px;color:#6B7280;">إجمالي اللاعبين</div>
</div> </a>
<div> <a href="/sa/groups" class="card" style="padding:14px;text-decoration:none;color:inherit;border-right:3px solid #EC4899;">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['active_facilities'] ?> <span style="font-size:14px;color:#6B7280;font-weight:400;">(<?= (int) $stats['active_units'] ?> وحدة)</span></div> <div style="font-size:22px;font-weight:700;color:#1A1A2E;"><?= (int) $counts['active_groups'] ?></div>
<div style="font-size:13px;color:#6B7280;">المرافق النشطة</div> <div style="font-size:12px;color:#6B7280;">مجموعة نشطة (<?= (int) $counts['enrolled_players'] ?> مسجل)</div>
</div> </a>
</div> <a href="/sa/coaches" class="card" style="padding:14px;text-decoration:none;color:inherit;border-right:3px solid #059669;">
<div style="font-size:22px;font-weight:700;color:#1A1A2E;"><?= (int) $counts['active_coaches'] ?></div>
<div style="font-size:12px;color:#6B7280;">مدرب نشط</div>
</a>
<a href="/sa/mirror" class="card" style="padding:14px;text-decoration:none;color:inherit;border-right:3px solid #06B6D4;">
<div style="font-size:22px;font-weight:700;color:#1A1A2E;"><?= (int) $counts['today_bookings'] ?></div>
<div style="font-size:12px;color:#6B7280;">حجوزات اليوم (<?= (int) $counts['today_training_sessions'] ?> تدريب / <?= (int) $counts['today_hourly_bookings'] ?> ساعي)</div>
</a> </a>
</div>
<!-- Active Coaches --> <!-- Two Column Layout: Live Now + Upcoming -->
<a href="/sa/coaches" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #059669;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<div style="display:flex;align-items:center;gap:12px;">
<div style="width:44px;height:44px;background:#ECFDF5;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <!-- Live Now -->
<i data-lucide="user-check" style="width:22px;height:22px;color:#059669;"></i> <div class="card">
</div> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<div> <span style="width:8px;height:8px;background:#DC2626;border-radius:50%;animation:pulse 2s infinite;"></span>
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['active_coaches'] ?></div> <span style="font-weight:700;font-size:14px;">الآن (<?= count($liveNow) ?>)</span>
<div style="font-size:13px;color:#6B7280;">المدربين النشطين</div> </div>
<?php if (empty($liveNow)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا يوجد نشاط حالياً</div>
<?php else: ?>
<div style="max-height:300px;overflow-y:auto;">
<?php foreach ($liveNow as $live): ?>
<div style="padding:10px 15px;border-bottom:1px solid #F3F4F6;font-size:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;font-size:13px;"><?= e($live['facility_name']) ?><?= e($live['unit_name']) ?></span>
<span style="color:#6B7280;"><?= e(substr($live['start_time'], 0, 5)) ?> - <?= e(substr($live['end_time'], 0, 5)) ?></span>
</div>
<div style="margin-top:4px;color:#374151;">
<?php if ($live['group_name']): ?>
<span style="background:#EDE9FE;padding:1px 6px;border-radius:4px;font-size:11px;"><?= e($live['group_name']) ?></span>
<?php endif; ?>
<?php if ($live['coach_name']): ?>
<span style="color:#6B7280;"><?= e($live['coach_name']) ?></span>
<?php endif; ?>
<?php if ($live['booking_type'] === 'hourly'): ?>
<span style="background:#FEF3C7;padding:1px 6px;border-radius:4px;font-size:11px;">ساعي</span>
<span><?= e($live['booker_name'] ?? '') ?></span>
<?php if ($live['organization_name']): ?>
<span style="background:#FFEDD5;padding:1px 6px;border-radius:4px;font-size:11px;"><?= e($live['organization_name']) ?></span>
<?php endif; ?>
<?php if ((int) ($live['participant_count'] ?? 0) > 0): ?>
<span style="color:#6B7280;">(<?= (int) $live['participant_count'] ?> مشارك)</span>
<?php endif; ?>
<?php endif; ?>
</div>
</div> </div>
<?php endforeach; ?>
</div> </div>
</a> <?php endif; ?>
</div>
<!-- Total Players --> <!-- Upcoming -->
<a href="/sa/players" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #F59E0B;"> <div class="card">
<div style="display:flex;align-items:center;gap:12px;"> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<div style="width:44px;height:44px;background:#FEF3C7;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <span style="font-weight:700;font-size:14px;">قادم (الساعتين القادمتين)</span>
<i data-lucide="users" style="width:22px;height:22px;color:#F59E0B;"></i> </div>
</div> <?php if (empty($upcoming)): ?>
<div> <div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا يوجد حجوزات قادمة</div>
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['total_players'] ?></div> <?php else: ?>
<div style="font-size:13px;color:#6B7280;">اللاعبين المسجلين</div> <div style="max-height:300px;overflow-y:auto;">
<?php foreach ($upcoming as $up): ?>
<div style="padding:10px 15px;border-bottom:1px solid #F3F4F6;font-size:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:600;font-size:13px;"><?= e($up['facility_name']) ?><?= e($up['unit_name']) ?></span>
<span style="color:#059669;font-weight:600;"><?= e(substr($up['start_time'], 0, 5)) ?></span>
</div>
<div style="margin-top:4px;color:#374151;">
<?php if ($up['group_name']): ?>
<span style="background:#EDE9FE;padding:1px 6px;border-radius:4px;font-size:11px;"><?= e($up['group_name']) ?></span>
<?php if ($up['coach_name']): ?><span style="color:#6B7280;"><?= e($up['coach_name']) ?></span><?php endif; ?>
<?php elseif ($up['booking_type'] === 'hourly'): ?>
<span style="background:#FEF3C7;padding:1px 6px;border-radius:4px;font-size:11px;">ساعي</span>
<span><?= e($up['booker_name'] ?? '') ?></span>
<?php if ($up['organization_name']): ?>
<span style="background:#FFEDD5;padding:1px 6px;border-radius:4px;font-size:11px;"><?= e($up['organization_name']) ?></span>
<?php endif; ?>
<?php endif; ?>
</div>
</div> </div>
<?php endforeach; ?>
</div> </div>
</a> <?php endif; ?>
</div>
<!-- Active Groups --> </div>
<a href="/sa/groups" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #EC4899;">
<div style="display:flex;align-items:center;gap:12px;"> <!-- Facility Utilization + Discipline Breakdown -->
<div style="width:44px;height:44px;background:#FCE7F3;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:20px;">
<i data-lucide="layers" style="width:22px;height:22px;color:#EC4899;"></i>
</div> <!-- Facility Utilization -->
<div> <div class="card">
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['active_groups'] ?> <span style="font-size:14px;color:#6B7280;font-weight:400;">(<?= (int) $stats['enrolled_players'] ?> مسجل)</span></div> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<div style="font-size:13px;color:#6B7280;">المجموعات النشطة</div> <span style="font-weight:700;font-size:14px;">إشغال المرافق اليوم</span>
</div>
<?php if (empty($facilityUsage)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد مرافق</div>
<?php else: ?>
<div style="padding:12px 15px;">
<?php foreach ($facilityUsage as $fu): ?>
<?php
$unitCount = max(1, (int) $fu['unit_count']);
$bookingCount = (int) $fu['booking_count'];
$pct = min(100, round(($bookingCount / ($unitCount * 8)) * 100));
$barColor = $pct >= 80 ? '#DC2626' : ($pct >= 50 ? '#F59E0B' : '#059669');
?>
<div style="margin-bottom:10px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:12px;font-weight:600;"><?= e($fu['name_ar']) ?></span>
<span style="font-size:11px;color:#6B7280;"><?= $bookingCount ?> حجز (<?= $unitCount ?> وحدة)</span>
</div>
<div style="height:6px;background:#F3F4F6;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $pct ?>%;background:<?= $barColor ?>;border-radius:3px;transition:width 0.3s;"></div>
</div>
</div> </div>
<?php endforeach; ?>
</div> </div>
</a> <?php endif; ?>
</div>
<!-- Today's Bookings --> <!-- Discipline Breakdown -->
<a href="/sa/mirror" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #06B6D4;"> <div class="card">
<div style="display:flex;align-items:center;gap:12px;"> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<div style="width:44px;height:44px;background:#CFFAFE;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <span style="font-weight:700;font-size:14px;">الأنشطة الرياضية</span>
<i data-lucide="calendar" style="width:22px;height:22px;color:#06B6D4;"></i> </div>
</div> <?php if (empty($disciplineBreakdown)): ?>
<div> <div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد أنشطة</div>
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['today_bookings'] ?></div> <?php else: ?>
<div style="font-size:13px;color:#6B7280;">حجوزات اليوم</div> <div style="padding:12px 15px;">
<?php foreach ($disciplineBreakdown as $disc): ?>
<?php
$totalCap = max(1, (int) $disc['total_capacity']);
$totalPlayers = (int) $disc['total_players'];
$fillPct = min(100, round(($totalPlayers / $totalCap) * 100));
$fillColor = $fillPct >= 90 ? '#DC2626' : ($fillPct >= 70 ? '#F59E0B' : '#3B82F6');
?>
<div style="margin-bottom:10px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-size:12px;font-weight:600;"><?= e($disc['name_ar']) ?></span>
<span style="font-size:11px;color:#6B7280;"><?= $totalPlayers ?>/<?= $totalCap ?> لاعب • <?= (int) $disc['group_count'] ?> مجموعة</span>
</div>
<div style="height:6px;background:#F3F4F6;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $fillPct ?>%;background:<?= $fillColor ?>;border-radius:3px;"></div>
</div>
</div> </div>
<?php endforeach; ?>
</div> </div>
</a> <?php endif; ?>
</div>
<!-- Pending Medical --> </div>
<a href="/sa/players" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #F97316;">
<div style="display:flex;align-items:center;gap:12px;"> <!-- Groups Near Capacity -->
<div style="width:44px;height:44px;background:#FFEDD5;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <?php if (!empty($groupsNearCapacity)): ?>
<i data-lucide="heart-pulse" style="width:22px;height:22px;color:#F97316;"></i> <div class="card" style="margin-bottom:20px;">
</div> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<div> <span style="font-weight:700;font-size:14px;">مجموعات قاربت الامتلاء</span>
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['pending_medical'] ?></div> </div>
<div style="font-size:13px;color:#6B7280;">بانتظار موافقة طبية</div> <table style="width:100%;border-collapse:collapse;font-size:12px;">
</div> <thead>
<tr style="background:#F9FAFB;">
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">المجموعة</th>
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">النشاط</th>
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">المدرب</th>
<th style="padding:8px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">الإشغال</th>
<th style="padding:8px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($groupsNearCapacity as $g): ?>
<?php
$cap = max(1, (int) $g['max_capacity']);
$cur = (int) $g['current_count'];
$pct = round(($cur / $cap) * 100);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 12px;font-weight:600;">
<a href="/sa/groups/<?= (int) $g['id'] ?>" style="color:#1A1A2E;text-decoration:none;"><?= e($g['name_ar']) ?></a>
</td>
<td style="padding:8px 12px;color:#6B7280;"><?= e($g['discipline_name'] ?? '-') ?></td>
<td style="padding:8px 12px;color:#6B7280;"><?= e($g['coach_name'] ?? '-') ?></td>
<td style="padding:8px 12px;text-align:center;">
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
<div style="width:60px;height:5px;background:#F3F4F6;border-radius:3px;overflow:hidden;">
<div style="height:100%;width:<?= $pct ?>%;background:<?= $pct >= 100 ? '#DC2626' : '#F59E0B' ?>;border-radius:3px;"></div>
</div>
<span style="font-size:11px;font-weight:600;"><?= $cur ?>/<?= $cap ?></span>
</div>
</td>
<td style="padding:8px 12px;text-align:center;">
<?php if ($g['is_full']): ?>
<span style="background:#FEE2E2;color:#DC2626;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;">ممتلئة</span>
<?php else: ?>
<span style="background:#FEF3C7;color:#92400E;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;"><?= $pct ?>%</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<!-- Today's Schedule + Coach Load -->
<div style="display:grid;grid-template-columns:2fr 1fr;gap:15px;margin-bottom:20px;">
<!-- Today Schedule -->
<div class="card">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<span style="font-weight:700;font-size:14px;">جدول التدريب اليوم (<?= e($dayName) ?>)</span>
<span style="font-size:12px;color:#6B7280;margin-right:8px;"><?= count($todaySchedule) ?> حصة</span>
</div> </div>
</a> <?php if (empty($todaySchedule)): ?>
<div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا يوجد تدريبات اليوم</div>
<?php else: ?>
<div style="max-height:350px;overflow-y:auto;">
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#F9FAFB;position:sticky;top:0;">
<th style="padding:8px 10px;text-align:right;border-bottom:1px solid #E5E7EB;">الوقت</th>
<th style="padding:8px 10px;text-align:right;border-bottom:1px solid #E5E7EB;">المجموعة</th>
<th style="padding:8px 10px;text-align:right;border-bottom:1px solid #E5E7EB;">النشاط</th>
<th style="padding:8px 10px;text-align:right;border-bottom:1px solid #E5E7EB;">المدرب</th>
<th style="padding:8px 10px;text-align:right;border-bottom:1px solid #E5E7EB;">المرفق</th>
<th style="padding:8px 10px;text-align:center;border-bottom:1px solid #E5E7EB;">عدد</th>
</tr>
</thead>
<tbody>
<?php foreach ($todaySchedule as $s): ?>
<?php
$isPast = ($now > ($s['end_time'] ?? ''));
$isNow = ($now >= ($s['start_time'] ?? '') && $now < ($s['end_time'] ?? ''));
?>
<tr style="border-bottom:1px solid #F3F4F6;<?= $isPast ? 'opacity:0.5;' : '' ?><?= $isNow ? 'background:#ECFDF5;' : '' ?>">
<td style="padding:8px 10px;font-weight:600;white-space:nowrap;">
<?= e(substr($s['start_time'] ?? '', 0, 5)) ?> - <?= e(substr($s['end_time'] ?? '', 0, 5)) ?>
<?php if ($isNow): ?><span style="color:#059669;font-size:10px;"></span><?php endif; ?>
</td>
<td style="padding:8px 10px;font-weight:500;"><?= e($s['group_name'] ?? '-') ?></td>
<td style="padding:8px 10px;color:#6B7280;"><?= e($s['discipline_name'] ?? '-') ?></td>
<td style="padding:8px 10px;color:#6B7280;"><?= e($s['coach_name'] ?? '-') ?></td>
<td style="padding:8px 10px;color:#6B7280;font-size:11px;"><?= e($s['facility_name'] ?? '') ?><?= ($s['unit_name'] ?? '') ? ' — ' . e($s['unit_name']) : '' ?></td>
<td style="padding:8px 10px;text-align:center;font-weight:600;"><?= (int) ($s['current_count'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<!-- Overdue Subscriptions --> <!-- Coach Load -->
<a href="/sa/subscriptions?payment_status=overdue" class="card" style="padding:20px;text-decoration:none;color:inherit;border-right:4px solid #DC2626;"> <div class="card">
<div style="display:flex;align-items:center;gap:12px;"> <div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;">
<div style="width:44px;height:44px;background:#FEE2E2;border-radius:10px;display:flex;align-items:center;justify-content:center;"> <span style="font-weight:700;font-size:14px;">المدربين اليوم</span>
<i data-lucide="alert-triangle" style="width:22px;height:22px;color:#DC2626;"></i> </div>
</div> <?php if (empty($coachLoad)): ?>
<div> <div style="padding:30px;text-align:center;color:#9CA3AF;font-size:13px;">لا يوجد مدربين</div>
<div style="font-size:24px;font-weight:700;color:#1A1A2E;"><?= (int) $stats['overdue_subscriptions'] ?></div> <?php else: ?>
<div style="font-size:13px;color:#6B7280;">اشتراكات متأخرة</div> <div style="padding:10px 15px;">
<?php foreach ($coachLoad as $cl): ?>
<div style="padding:8px 0;border-bottom:1px solid #F3F4F6;display:flex;justify-content:space-between;align-items:center;">
<div>
<div style="font-size:13px;font-weight:600;"><?= e($cl['name']) ?></div>
<div style="font-size:11px;color:#6B7280;"><?= e(substr($cl['first_session'] ?? '', 0, 5)) ?><?= e(substr($cl['last_session'] ?? '', 0, 5)) ?></div>
</div>
<span style="background:#EDE9FE;color:#7C3AED;padding:3px 8px;border-radius:8px;font-size:12px;font-weight:700;"><?= (int) $cl['group_count'] ?> مجموعة</span>
</div> </div>
<?php endforeach; ?>
</div> </div>
</a> <?php endif; ?>
</div>
</div> </div>
<!-- Recent Registrations -->
<?php if (!empty($recentRegistrations)): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:12px 15px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<span style="font-weight:700;font-size:14px;">تسجيلات آخر 7 أيام</span>
<a href="/sa/registrations" style="font-size:12px;color:#3B82F6;text-decoration:none;">عرض الكل →</a>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">رقم</th>
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">اللاعب</th>
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">النشاط</th>
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">المجموعة</th>
<th style="padding:8px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">النوع</th>
<th style="padding:8px 12px;text-align:center;border-bottom:1px solid #E5E7EB;">الحالة</th>
<th style="padding:8px 12px;text-align:right;border-bottom:1px solid #E5E7EB;">التاريخ</th>
</tr>
</thead>
<tbody>
<?php foreach ($recentRegistrations as $reg): ?>
<?php
$regStatusColors = ['pending' => '#FEF3C7', 'approved' => '#ECFDF5', 'active' => '#DBEAFE', 'completed' => '#F3F4F6'];
$regStatusLabels = ['pending' => 'معلق', 'approved' => 'مقبول', 'active' => 'نشط', 'completed' => 'مكتمل'];
$typeLabels = ['new' => 'جديد', 'renewal' => 'تجديد', 'transfer' => 'نقل'];
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><?= e($reg['registration_number'] ?? '') ?></td>
<td style="padding:8px 12px;font-weight:500;"><?= e($reg['player_name'] ?? '-') ?></td>
<td style="padding:8px 12px;color:#6B7280;"><?= e($reg['discipline_name'] ?? '-') ?></td>
<td style="padding:8px 12px;color:#6B7280;"><?= e($reg['group_name'] ?? '-') ?></td>
<td style="padding:8px 12px;text-align:center;">
<span style="font-size:11px;"><?= e($typeLabels[$reg['player_type'] ?? ''] ?? $reg['player_type'] ?? '-') ?></span>
</td>
<td style="padding:8px 12px;text-align:center;">
<span style="background:<?= $regStatusColors[$reg['status']] ?? '#F3F4F6' ?>;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600;"><?= e($regStatusLabels[$reg['status']] ?? $reg['status'] ?? '-') ?></span>
</td>
<td style="padding:8px 12px;font-size:11px;color:#6B7280;"><?= e(substr($reg['created_at'] ?? '', 0, 10)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@media (max-width: 900px) {
[style*="grid-template-columns: 1fr 1fr"],
[style*="grid-template-columns:1fr 1fr"],
[style*="grid-template-columns:2fr 1fr"] {
grid-template-columns: 1fr !important;
}
}
</style>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); } if (typeof lucide !== 'undefined') { lucide.createIcons(); }
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">المدرب <span style="color:#DC2626;">*</span></label> <label class="form-label">المدرب <span style="color:#DC2626;">*</span></label>
<select name="coach_id" id="coach_id" class="form-select" required> <select name="coach_id" id="coach_id" class="form-select" required data-searchable="true" data-placeholder="-- اختر المدرب --">
<option value="">-- اختر المدرب --</option> <option value="">-- اختر المدرب --</option>
<?php foreach ($coaches as $c): ?> <?php foreach ($coaches as $c): ?>
<option value="<?= (int) $c['id'] ?>" <?= old('coach_id') == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option> <option value="<?= (int) $c['id'] ?>" <?= old('coach_id') == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option>
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">المدرب <span style="color:#DC2626;">*</span></label> <label class="form-label">المدرب <span style="color:#DC2626;">*</span></label>
<select name="coach_id" id="coach_id" class="form-select" required> <select name="coach_id" id="coach_id" class="form-select" required data-searchable="true" data-placeholder="-- اختر المدرب --">
<option value="">-- اختر المدرب --</option> <option value="">-- اختر المدرب --</option>
<?php foreach ($coaches as $c): ?> <?php foreach ($coaches as $c): ?>
<option value="<?= (int) $c['id'] ?>" <?= (old('coach_id', $group->coach_id)) == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option> <option value="<?= (int) $c['id'] ?>" <?= (old('coach_id', $group->coach_id)) == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option>
......
...@@ -16,9 +16,22 @@ ...@@ -16,9 +16,22 @@
<label class="form-label" style="font-size:12px;">بحث</label> <label class="form-label" style="font-size:12px;">بحث</label>
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الكود..." class="form-input"> <input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الكود..." class="form-input">
</div> </div>
<div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">النشاط الرياضي</label>
<select name="discipline_id" class="form-select" id="filterDiscipline">
<option value="">-- الكل --</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:180px;"> <div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">البرنامج</label> <label class="form-label" style="font-size:12px;">البرنامج</label>
<select name="program_id" class="form-select"> <select name="program_id" class="form-select" id="filterProgram"
data-searchable="true"
data-cascade-from="discipline_id"
data-cascade-url="/api/sa/activities/programs-by-discipline?discipline_id={value}"
data-placeholder="-- الكل --">
<option value="">-- الكل --</option> <option value="">-- الكل --</option>
<?php foreach ($programs as $p): ?> <?php foreach ($programs as $p): ?>
<option value="<?= (int) $p['id'] ?>" <?= ($filters['program_id'] ?? '') == $p['id'] ? 'selected' : '' ?>><?= e($p['name_ar']) ?></option> <option value="<?= (int) $p['id'] ?>" <?= ($filters['program_id'] ?? '') == $p['id'] ? 'selected' : '' ?>><?= e($p['name_ar']) ?></option>
...@@ -27,7 +40,11 @@ ...@@ -27,7 +40,11 @@
</div> </div>
<div style="min-width:160px;"> <div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">المدرب</label> <label class="form-label" style="font-size:12px;">المدرب</label>
<select name="coach_id" class="form-select"> <select name="coach_id" class="form-select"
data-searchable="true"
data-cascade-from="discipline_id"
data-cascade-url="/api/sa/activities/coaches-by-discipline?discipline_id={value}"
data-placeholder="-- الكل --">
<option value="">-- الكل --</option> <option value="">-- الكل --</option>
<?php foreach ($coaches as $c): ?> <?php foreach ($coaches as $c): ?>
<option value="<?= (int) $c['id'] ?>" <?= ($filters['coach_id'] ?? '') == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option> <option value="<?= (int) $c['id'] ?>" <?= ($filters['coach_id'] ?? '') == $c['id'] ? 'selected' : '' ?>><?= e($c['name_ar']) ?></option>
......
...@@ -87,13 +87,12 @@ $st = $group['status'] ?? 'active'; ...@@ -87,13 +87,12 @@ $st = $group['status'] ?? 'active';
<?= csrf_field() ?> <?= csrf_field() ?>
<div style="flex:1;"> <div style="flex:1;">
<label class="form-label" style="font-size:12px;">تسجيل لاعب جديد</label> <label class="form-label" style="font-size:12px;">تسجيل لاعب جديد</label>
<select name="player_id" class="form-select" required id="enrollPlayerSelect"> <select name="player_id" class="form-select" required id="enrollPlayerSelect"
<option value="" data-unpaid="0">-- اختر لاعب --</option> data-searchable="true"
<?php foreach ($availablePlayers as $ap): ?> data-search-url="/api/sa/players/search?exclude_group=<?= (int) $group['id'] ?>"
<option value="<?= (int) $ap['id'] ?>" data-unpaid="<?= (int) ($ap['unpaid_count'] ?? 0) ?>"> data-search-min="2"
<?= e($ap['name_ar']) ?> (<?= e($ap['code']) ?>)<?= (int)($ap['unpaid_count'] ?? 0) > 0 ? ' ⚠️ ' . (int)$ap['unpaid_count'] . ' اشتراك غير مدفوع' : '' ?> data-placeholder="ابحث بالاسم أو الرقم القومي أو المسلسل...">
</option> <option value="">ابحث بالاسم أو الرقم القومي...</option>
<?php endforeach; ?>
</select> </select>
</div> </div>
<button type="submit" class="btn btn-primary" style="padding:8px 20px;"> <button type="submit" class="btn btn-primary" style="padding:8px 20px;">
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;"> <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div class="form-group"> <div class="form-group">
<label class="form-label">اللوكر <span style="color:#DC2626;">*</span></label> <label class="form-label">اللوكر <span style="color:#DC2626;">*</span></label>
<select name="locker_id" class="form-select" required> <select name="locker_id" class="form-select" required data-searchable="true" data-placeholder="ابحث بالكود أو الاسم...">
<option value="">-- اختر لوكر متاح --</option> <option value="">-- اختر لوكر متاح --</option>
<?php foreach ($lockers as $locker): ?> <?php foreach ($lockers as $locker): ?>
<option value="<?= (int) $locker['id'] ?>" <?= old('locker_id') == $locker['id'] ? 'selected' : '' ?>> <option value="<?= (int) $locker['id'] ?>" <?= old('locker_id') == $locker['id'] ? 'selected' : '' ?>>
...@@ -33,13 +33,21 @@ ...@@ -33,13 +33,21 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label">اللاعب <span style="color:#DC2626;">*</span></label> <label class="form-label">اللاعب <span style="color:#DC2626;">*</span></label>
<select name="player_id" class="form-select" required id="playerSelect"> <select name="player_id" class="form-select" required id="playerSelect"
<option value="">-- اختر اللاعب --</option> data-searchable="true"
data-search-url="/api/sa/players/search"
data-search-min="2"
data-placeholder="ابحث بالاسم أو الرقم القومي...">
<option value="">ابحث بالاسم أو الرقم القومي...</option>
<?php if (old('player_id')): ?>
<?php foreach ($players as $player): ?> <?php foreach ($players as $player): ?>
<option value="<?= (int) $player['id'] ?>" <?= old('player_id') == $player['id'] ? 'selected' : '' ?>> <?php if ((string) $player['id'] === old('player_id')): ?>
<option value="<?= (int) $player['id'] ?>" selected>
<?= e($player['full_name_ar']) ?> (<?= e($player['registration_serial'] ?? '') ?>) <?= e($player['full_name_ar']) ?> (<?= e($player['registration_serial'] ?? '') ?>)
</option> </option>
<?php endif; ?>
<?php endforeach; ?> <?php endforeach; ?>
<?php endif; ?>
</select> </select>
</div> </div>
</div> </div>
......
...@@ -19,6 +19,28 @@ $__template->layout('Layout.main'); ...@@ -19,6 +19,28 @@ $__template->layout('Layout.main');
<input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الرقم القومي أو المسلسل..." class="form-input"> <input type="text" name="q" value="<?= e($filters['q'] ?? '') ?>" placeholder="ابحث بالاسم أو الرقم القومي أو المسلسل..." class="form-input">
</div> </div>
<div style="min-width:150px;"> <div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">النشاط الرياضي</label>
<select name="discipline_id" class="form-select">
<option value="">الكل</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">المجموعة</label>
<select name="group_id" class="form-select"
data-searchable="true"
data-cascade-from="discipline_id"
data-cascade-url="/api/sa/groups/search?active_only=1&discipline_id={value}"
data-placeholder="-- الكل --">
<option value="">الكل</option>
<?php foreach ($groups as $g): ?>
<option value="<?= (int) $g['id'] ?>" <?= ($filters['group_id'] ?? '') == $g['id'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:130px;">
<label class="form-label" style="font-size:12px;">نوع اللاعب</label> <label class="form-label" style="font-size:12px;">نوع اللاعب</label>
<select name="player_type" class="form-select"> <select name="player_type" class="form-select">
<option value="">الكل</option> <option value="">الكل</option>
...@@ -26,7 +48,7 @@ $__template->layout('Layout.main'); ...@@ -26,7 +48,7 @@ $__template->layout('Layout.main');
<option value="non_member" <?= ($filters['player_type'] ?? '') === 'non_member' ? 'selected' : '' ?>>غير عضو</option> <option value="non_member" <?= ($filters['player_type'] ?? '') === 'non_member' ? 'selected' : '' ?>>غير عضو</option>
</select> </select>
</div> </div>
<div style="min-width:150px;"> <div style="min-width:130px;">
<label class="form-label" style="font-size:12px;">الحالة الطبية</label> <label class="form-label" style="font-size:12px;">الحالة الطبية</label>
<select name="medical_status" class="form-select"> <select name="medical_status" class="form-select">
<option value="">الكل</option> <option value="">الكل</option>
......
...@@ -246,7 +246,7 @@ $gridCols = (int) ($facility['pool_grid_cols'] ?? 0); ...@@ -246,7 +246,7 @@ $gridCols = (int) ($facility['pool_grid_cols'] ?? 0);
<div id="pgModalContent"> <div id="pgModalContent">
<div id="pgModalGroup"> <div id="pgModalGroup">
<label>المجموعة</label> <label>المجموعة</label>
<select id="pgGroupSelect"> <select id="pgGroupSelect" data-searchable="true" data-placeholder="ابحث عن مجموعة...">
<option value="">اختر مجموعة...</option> <option value="">اختر مجموعة...</option>
<?php foreach ($groups as $g): ?> <?php foreach ($groups as $g): ?>
<option value="<?= (int) $g['id'] ?>"> <option value="<?= (int) $g['id'] ?>">
......
...@@ -134,12 +134,27 @@ ...@@ -134,12 +134,27 @@
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="trophy" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> اختيار النشاط والمجموعة</h3> <h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="trophy" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> اختيار النشاط والمجموعة</h3>
</div> </div>
<div style="padding:16px;"> <div style="padding:16px;">
<!-- Discipline Filter Chips -->
<div id="disciplineChips" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px;">
<button type="button" class="disc-chip active" data-disc="" style="padding:6px 14px;border-radius:16px;border:1px solid #E5E7EB;background:#2563EB;color:white;font-size:12px;font-weight:600;cursor:pointer;">الكل</button>
<?php
$seenDisc = [];
foreach ($groups as $g) {
$dName = $g['discipline_name'] ?? '';
$dId = $g['discipline_id'] ?? '';
if ($dName && !isset($seenDisc[$dId])) {
$seenDisc[$dId] = $dName;
echo '<button type="button" class="disc-chip" data-disc="' . e($dId) . '" style="padding:6px 14px;border-radius:16px;border:1px solid #E5E7EB;background:white;color:#374151;font-size:12px;font-weight:600;cursor:pointer;">' . e($dName) . '</button>';
}
}
?>
</div>
<div style="margin-bottom:12px;"> <div style="margin-bottom:12px;">
<input type="text" id="groupSearch" class="form-input" placeholder="ابحث عن نشاط أو مجموعة..." style="padding:14px;font-size:15px;border-radius:10px;"> <input type="text" id="groupSearch" class="form-input" placeholder="ابحث عن نشاط أو مجموعة..." style="padding:14px;font-size:15px;border-radius:10px;">
</div> </div>
<div id="groupsList" style="display:grid;grid-template-columns:1fr;gap:10px;max-height:50vh;overflow-y:auto;-webkit-overflow-scrolling:touch;"> <div id="groupsList" style="display:grid;grid-template-columns:1fr;gap:10px;max-height:50vh;overflow-y:auto;-webkit-overflow-scrolling:touch;">
<?php foreach ($groups as $g): ?> <?php foreach ($groups as $g): ?>
<div class="group-option" data-group-id="<?= (int) $g['id'] ?>" data-name="<?= e($g['name_ar'] . ' ' . ($g['discipline_name'] ?? '')) ?>" <div class="group-option" data-group-id="<?= (int) $g['id'] ?>" data-disc-id="<?= (int) ($g['discipline_id'] ?? 0) ?>" data-name="<?= e($g['name_ar'] . ' ' . ($g['discipline_name'] ?? '')) ?>"
style="border:2px solid <?= (isset($selectedGroup) && (int)$selectedGroup['id'] === (int)$g['id']) ? '#2563EB' : '#E5E7EB' ?>;border-radius:12px;padding:16px;cursor:pointer;transition:all 0.2s;-webkit-tap-highlight-color:transparent;touch-action:manipulation;"> style="border:2px solid <?= (isset($selectedGroup) && (int)$selectedGroup['id'] === (int)$g['id']) ? '#2563EB' : '#E5E7EB' ?>;border-radius:12px;padding:16px;cursor:pointer;transition:all 0.2s;-webkit-tap-highlight-color:transparent;touch-action:manipulation;">
<div style="display:flex;justify-content:space-between;align-items:flex-start;"> <div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div style="flex:1;"> <div style="flex:1;">
...@@ -612,14 +627,31 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -612,14 +627,31 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// Discipline chip filtering
var activeDiscId = '';
document.querySelectorAll('.disc-chip').forEach(function(chip) {
chip.addEventListener('click', function() {
document.querySelectorAll('.disc-chip').forEach(function(c) {
c.style.background = 'white'; c.style.color = '#374151';
});
this.style.background = '#2563EB'; this.style.color = 'white';
activeDiscId = this.dataset.disc;
filterGroups();
});
});
// Group search // Group search
var groupSearch = document.getElementById('groupSearch'); var groupSearch = document.getElementById('groupSearch');
if (groupSearch) { if (groupSearch) {
groupSearch.addEventListener('input', function() { groupSearch.addEventListener('input', filterGroups);
var q = this.value.toLowerCase(); }
document.querySelectorAll('.group-option').forEach(function(el) {
el.style.display = el.dataset.name.toLowerCase().indexOf(q) >= 0 ? '' : 'none'; function filterGroups() {
}); var q = (groupSearch ? groupSearch.value : '').toLowerCase();
document.querySelectorAll('.group-option').forEach(function(el) {
var matchText = !q || el.dataset.name.toLowerCase().indexOf(q) >= 0;
var matchDisc = !activeDiscId || el.dataset.discId === activeDiscId;
el.style.display = (matchText && matchDisc) ? '' : 'none';
}); });
} }
......
...@@ -21,13 +21,30 @@ ...@@ -21,13 +21,30 @@
<!-- Filters --> <!-- Filters -->
<div class="card" style="margin-bottom:20px;padding:15px;"> <div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/sa/subscriptions" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;"> <form method="GET" action="/sa/subscriptions" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="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:160px;"> <div style="min-width:160px;">
<label class="form-label" style="font-size:12px;">الشهر</label> <label class="form-label" style="font-size:12px;">الشهر</label>
<input type="month" name="month" value="<?= e($filters['month'] ?? '') ?>" class="form-input"> <input type="month" name="month" value="<?= e($filters['month'] ?? '') ?>" class="form-input">
</div> </div>
<div style="min-width:150px;">
<label class="form-label" style="font-size:12px;">النشاط الرياضي</label>
<select name="discipline_id" class="form-select" id="subFilterDiscipline">
<option value="">-- الكل --</option>
<?php foreach ($disciplines as $d): ?>
<option value="<?= (int) $d['id'] ?>" <?= ($filters['discipline_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="min-width:180px;"> <div style="min-width:180px;">
<label class="form-label" style="font-size:12px;">المجموعة</label> <label class="form-label" style="font-size:12px;">المجموعة</label>
<select name="group_id" class="form-select"> <select name="group_id" class="form-select"
data-searchable="true"
data-cascade-from="discipline_id"
data-cascade-url="/api/sa/groups/search?active_only=1&discipline_id={value}"
data-placeholder="-- الكل --">
<option value="">-- الكل --</option> <option value="">-- الكل --</option>
<?php foreach ($groups as $g): ?> <?php foreach ($groups as $g): ?>
<option value="<?= (int) $g['id'] ?>" <?= ($filters['group_id'] ?? '') === (string) $g['id'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option> <option value="<?= (int) $g['id'] ?>" <?= ($filters['group_id'] ?? '') === (string) $g['id'] ? 'selected' : '' ?>><?= e($g['name_ar']) ?></option>
......
...@@ -99,6 +99,8 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); ...@@ -99,6 +99,8 @@ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
</div> </div>
<script src="<?= url('assets/js/app.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/app.js') ?: time() ?>"></script> <script src="<?= url('assets/js/app.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/app.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/searchable-select.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/searchable-select.js') ?: time() ?>"></script>
<script src="<?= url('assets/js/forms-engine.js') ?>?v=<?= @filemtime(dirname(__DIR__, 3) . '/public/assets/js/forms-engine.js') ?: time() ?>"></script>
<script> <script>
// Initialize Lucide icons // Initialize Lucide icons
lucide.createIcons(); lucide.createIcons();
......
/**
* Searchable Select — RTL-compatible dropdown with search & AJAX support
*
* Usage:
* Static: <select data-searchable="true">
* AJAX: <select data-searchable="true" data-search-url="/api/sa/players/search" data-search-min="2">
* Cascade: <select data-searchable="true" data-cascade-from="discipline_id" data-cascade-url="/api/sa/groups/search?discipline_id={value}">
*/
var SearchableSelect = (function() {
'use strict';
var instances = [];
function init() {
var selects = document.querySelectorAll('select[data-searchable]');
selects.forEach(function(sel) {
if (sel._ssInitialized) return;
sel._ssInitialized = true;
instances.push(new Instance(sel));
});
}
function Instance(originalSelect) {
this.select = originalSelect;
this.options = [];
this.filteredOptions = [];
this.isOpen = false;
this.highlightIndex = -1;
this.ajaxUrl = originalSelect.getAttribute('data-search-url') || '';
this.ajaxMin = parseInt(originalSelect.getAttribute('data-search-min') || '2', 10);
this.cascadeFrom = originalSelect.getAttribute('data-cascade-from') || '';
this.cascadeUrl = originalSelect.getAttribute('data-cascade-url') || '';
this.placeholder = originalSelect.getAttribute('data-placeholder') || originalSelect.querySelector('option[value=""]')?.textContent || '-- اختر --';
this.debounceTimer = null;
this.abortController = null;
this.build();
this.cacheOptions();
this.bindEvents();
this.bindCascade();
}
Instance.prototype.build = function() {
var wrapper = document.createElement('div');
wrapper.className = 'ss-wrapper';
wrapper.style.cssText = 'position:relative;width:100%;';
var trigger = document.createElement('div');
trigger.className = 'ss-trigger form-select';
trigger.setAttribute('tabindex', '0');
trigger.style.cssText = 'cursor:pointer;display:flex;align-items:center;justify-content:space-between;min-height:38px;';
trigger.innerHTML = '<span class="ss-label" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + escHtml(this.placeholder) + '</span><svg style="width:14px;height:14px;flex-shrink:0;opacity:0.5;margin-right:6px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>';
var dropdown = document.createElement('div');
dropdown.className = 'ss-dropdown';
dropdown.style.cssText = 'display:none;position:absolute;top:100%;right:0;left:0;z-index:9999;background:#fff;border:1px solid #D1D5DB;border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,0.1);margin-top:4px;max-height:260px;overflow:hidden;';
var searchWrap = document.createElement('div');
searchWrap.style.cssText = 'padding:8px;border-bottom:1px solid #F3F4F6;';
var searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'ss-search';
searchInput.placeholder = 'ابحث...';
searchInput.style.cssText = 'width:100%;padding:6px 10px;border:1px solid #E5E7EB;border-radius:4px;font-size:13px;outline:none;';
searchInput.setAttribute('autocomplete', 'off');
searchWrap.appendChild(searchInput);
var list = document.createElement('div');
list.className = 'ss-list';
list.style.cssText = 'max-height:200px;overflow-y:auto;padding:4px 0;';
dropdown.appendChild(searchWrap);
dropdown.appendChild(list);
wrapper.appendChild(trigger);
wrapper.appendChild(dropdown);
this.select.style.display = 'none';
this.select.parentNode.insertBefore(wrapper, this.select.nextSibling);
this.wrapper = wrapper;
this.trigger = trigger;
this.dropdown = dropdown;
this.searchInput = searchInput;
this.list = list;
this.label = trigger.querySelector('.ss-label');
};
Instance.prototype.cacheOptions = function() {
var self = this;
self.options = [];
var opts = self.select.querySelectorAll('option');
opts.forEach(function(opt) {
if (opt.value === '') return;
self.options.push({
value: opt.value,
text: opt.textContent.trim(),
group: opt.parentElement.tagName === 'OPTGROUP' ? opt.parentElement.label : '',
el: opt
});
});
self.filteredOptions = self.options.slice();
self.renderList();
self.syncLabel();
};
Instance.prototype.renderList = function() {
var self = this;
var html = '';
var lastGroup = null;
if (self.filteredOptions.length === 0) {
html = '<div style="padding:12px;text-align:center;color:#9CA3AF;font-size:13px;">لا توجد نتائج</div>';
} else {
self.filteredOptions.forEach(function(opt, idx) {
if (opt.group && opt.group !== lastGroup) {
html += '<div style="padding:4px 12px;font-size:11px;font-weight:700;color:#6B7280;background:#F9FAFB;">' + escHtml(opt.group) + '</div>';
lastGroup = opt.group;
}
var isSelected = self.select.value === opt.value;
var isHL = idx === self.highlightIndex;
html += '<div class="ss-option" data-idx="' + idx + '" data-value="' + escHtml(opt.value) + '" style="padding:7px 12px;cursor:pointer;font-size:13px;' + (isSelected ? 'background:#EFF6FF;font-weight:600;' : '') + (isHL ? 'background:#F3F4F6;' : '') + '">' + escHtml(opt.text) + '</div>';
});
}
self.list.innerHTML = html;
};
Instance.prototype.syncLabel = function() {
var val = this.select.value;
if (!val) {
this.label.textContent = this.placeholder;
this.label.style.color = '#9CA3AF';
} else {
var found = this.options.find(function(o) { return o.value === val; });
this.label.textContent = found ? found.text : val;
this.label.style.color = '';
}
};
Instance.prototype.open = function() {
this.isOpen = true;
this.dropdown.style.display = '';
this.searchInput.value = '';
this.highlightIndex = -1;
if (!this.ajaxUrl) {
this.filteredOptions = this.options.slice();
this.renderList();
}
var self = this;
setTimeout(function() { self.searchInput.focus(); }, 50);
};
Instance.prototype.close = function() {
this.isOpen = false;
this.dropdown.style.display = 'none';
};
Instance.prototype.selectValue = function(value) {
this.select.value = value;
this.syncLabel();
this.close();
this.select.dispatchEvent(new Event('change', { bubbles: true }));
};
Instance.prototype.filter = function(query) {
var self = this;
query = query.trim().toLowerCase();
if (self.ajaxUrl) {
if (query.length < self.ajaxMin) {
self.filteredOptions = [];
self.renderList();
return;
}
clearTimeout(self.debounceTimer);
self.debounceTimer = setTimeout(function() { self.fetchRemote(query); }, 250);
return;
}
if (query === '') {
self.filteredOptions = self.options.slice();
} else {
self.filteredOptions = self.options.filter(function(o) {
return o.text.toLowerCase().indexOf(query) !== -1 || o.value.indexOf(query) !== -1;
});
}
self.highlightIndex = -1;
self.renderList();
};
Instance.prototype.fetchRemote = function(query) {
var self = this;
if (self.abortController) self.abortController.abort();
self.abortController = new AbortController();
var url = self.ajaxUrl + (self.ajaxUrl.indexOf('?') > -1 ? '&' : '?') + 'q=' + encodeURIComponent(query);
var cascadeParent = self.cascadeFrom ? document.querySelector('[name="' + self.cascadeFrom + '"]') : null;
if (cascadeParent && cascadeParent.value) {
url += '&' + self.cascadeFrom + '=' + encodeURIComponent(cascadeParent.value);
}
self.list.innerHTML = '<div style="padding:12px;text-align:center;color:#9CA3AF;font-size:12px;">جاري البحث...</div>';
fetch(url, {
signal: self.abortController.signal,
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function(r) { return r.json(); })
.then(function(data) {
var results = data.results || data.options || [];
self.filteredOptions = results.map(function(r) {
return {
value: String(r.id || r.value || ''),
text: r.full_name_ar || r.name_ar || r.name || r.label || r.text || '',
group: r.group || ''
};
});
self.highlightIndex = -1;
self.renderList();
})
.catch(function(e) {
if (e.name !== 'AbortError') {
self.list.innerHTML = '<div style="padding:12px;text-align:center;color:#DC2626;font-size:12px;">خطأ في البحث</div>';
}
});
};
Instance.prototype.bindEvents = function() {
var self = this;
self.trigger.addEventListener('click', function(e) {
e.stopPropagation();
if (self.isOpen) self.close(); else self.open();
});
self.trigger.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); self.open(); }
});
self.searchInput.addEventListener('input', function() {
self.filter(this.value);
});
self.searchInput.addEventListener('keydown', function(e) {
if (e.key === 'ArrowDown') {
e.preventDefault();
self.highlightIndex = Math.min(self.highlightIndex + 1, self.filteredOptions.length - 1);
self.renderList();
self.scrollToHighlight();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
self.highlightIndex = Math.max(self.highlightIndex - 1, 0);
self.renderList();
self.scrollToHighlight();
} else if (e.key === 'Enter') {
e.preventDefault();
if (self.highlightIndex >= 0 && self.filteredOptions[self.highlightIndex]) {
self.selectValue(self.filteredOptions[self.highlightIndex].value);
}
} else if (e.key === 'Escape') {
self.close();
self.trigger.focus();
}
});
self.list.addEventListener('click', function(e) {
var opt = e.target.closest('.ss-option');
if (opt) {
self.selectValue(opt.getAttribute('data-value'));
}
});
self.list.addEventListener('mouseover', function(e) {
var opt = e.target.closest('.ss-option');
if (opt) {
self.highlightIndex = parseInt(opt.getAttribute('data-idx'), 10);
self.renderList();
}
});
document.addEventListener('click', function(e) {
if (!self.wrapper.contains(e.target)) {
self.close();
}
});
};
Instance.prototype.scrollToHighlight = function() {
var el = this.list.querySelector('.ss-option[data-idx="' + this.highlightIndex + '"]');
if (el) el.scrollIntoView({ block: 'nearest' });
};
Instance.prototype.bindCascade = function() {
var self = this;
if (!self.cascadeFrom) return;
var parent = document.querySelector('[name="' + self.cascadeFrom + '"]');
if (!parent) return;
parent.addEventListener('change', function() {
var parentVal = parent.value;
self.select.innerHTML = '<option value="">' + escHtml(self.placeholder) + '</option>';
self.options = [];
self.filteredOptions = [];
self.renderList();
self.syncLabel();
if (!parentVal || !self.cascadeUrl) return;
var url = self.cascadeUrl.replace('{value}', encodeURIComponent(parentVal));
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function(r) { return r.json(); })
.then(function(data) {
var items = data.results || data.options || data.programs || data.groups || data.coaches || [];
items.forEach(function(item) {
var opt = document.createElement('option');
opt.value = item.id || item.value || '';
opt.textContent = item.name_ar || item.full_name_ar || item.name || item.label || '';
self.select.appendChild(opt);
});
self.cacheOptions();
})
.catch(function() {});
});
};
Instance.prototype.refresh = function() {
this.cacheOptions();
};
function escHtml(str) {
var d = document.createElement('div');
d.textContent = str || '';
return d.innerHTML;
}
function refreshAll() {
instances.forEach(function(inst) { inst.refresh(); });
}
function initNew(container) {
var selects = (container || document).querySelectorAll('select[data-searchable]:not([data-ss-done])');
selects.forEach(function(sel) {
if (sel._ssInitialized) return;
sel._ssInitialized = true;
sel.setAttribute('data-ss-done', '1');
instances.push(new Instance(sel));
});
}
return {
init: init,
refresh: refreshAll,
initNew: initNew
};
})();
document.addEventListener('DOMContentLoaded', function() {
SearchableSelect.init();
});
# Sports Activity Module — Smart Filtering & Search Plan
## المشكلة
الـ dropdowns في الموديل كله مفيش فيها filtering ذكي ولا search. لو اخترت نشاط رياضي معين، المجموعات والمدربين مش بيتفلترو. ولو عندي 500+ لاعب في dropdown مقدرش أدور فيها.
---
## الحل: مكونين أساسيين
### 1. Searchable Select Component
كل dropdown فيها أكتر من 10 عناصر أو فيها أسماء (لاعبين/مدربين) لازم تبقى searchable. هنعمل component واحد `searchable-select` يشتغل بـ vanilla JS.
### 2. Cascading/Dependent Filtering
لو اخترت parent field، الـ child fields لازم تتفلتر. ده موجود جزئياً في Groups create فقط، محتاج يتعمم.
---
## Edge Cases بالتفصيل (حسب الشاشة)
---
### A. Groups (المجموعات) — `Views/groups/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| A1 | create/edit | `program_id` | ✅ بيتفلتر من discipline (موجود) | لا يحتاج تغيير |
| A2 | create/edit | `coach_id` | ✅ بيتفلتر من discipline (موجود) | لا يحتاج تغيير — بس محتاج يبقى searchable |
| A3 | show (enrolled players) | عرض اللاعبين | لو المجموعة فيها 50+ لاعب مفيش search | إضافة filter/search في الجدول |
| A4 | show (add player) | `player_id` dropdown | بيجيب كل اللاعبين الغير مسجلين في المجموعة دي — لو 1000 لاعب الdropdown بطيئة | **Searchable select + AJAX search** بدل ما يجيبهم كلهم |
| A5 | index (filters) | `program_id` filter | مفيش فلتر بالنشاط الرياضي — لازم discipline → program cascading | إضافة discipline filter + cascading |
| A6 | index (filters) | `coach_id` filter | مفيش فلتر بالمدرب | إضافة coach filter (searchable) |
---
### B. Subscriptions (الاشتراكات) — `Views/subscriptions/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| B1 | index | `group_id` filter | بيجيب كل المجموعات بدون cascading من النشاط | إضافة discipline filter قبلها + cascading |
| B2 | index | لاعب معين | مفيش search بالاسم أو الرقم القومي | إضافة search field |
---
### C. Bookings Wizard (معالج الحجز) — `Views/bookings/wizard.php`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| C1 | Step 2 | Unit selection | الوحدات مجمعة بالمرفق ✅ بس لو كتير مفيش search | Searchable select |
| C2 | Step 2 | Facility filter | مفيش فلتر بنوع المرفق (ملعب/حمام سباحة/صالة) | إضافة type filter chips |
---
### D. Bookings Create (إنشاء حجز بسيط) — `Views/bookings/create.php`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| D1 | create | `facility_unit_id` | بيجيب كل الوحدات في optgroup — لو 30+ وحدة صعب تلاقي | Searchable select |
---
### E. Locker Rentals (تأجير اللوكرز) — `Views/locker-rentals/create.php`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| E1 | create | `player_id` | **بيجيب كل اللاعبين** — أكبر مشكلة! | **AJAX searchable select** (بالاسم/الرقم القومي/السيريال) |
| E2 | create | `locker_id` | بيعرض اللوكرز المتاحة بس — ممكن تكون كتير | Searchable + فلتر بالمرفق |
---
### F. Registration Wizard (معالج التسجيل) — `Views/registration/wizard.php`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| F1 | Activity step | Group selection | ✅ فيه client-side search (data-name) | كافي — بس لو المجموعات > 50 يحتاج server-side |
| F2 | Activity step | مفيش discipline filter | اللاعب بيشوف كل المجموعات بدون فلتر بالنشاط | إضافة discipline tabs/chips قبل المجموعات |
| F3 | Activity step | مفيش program filter | بعد اختيار النشاط مفيش فلتر بالبرنامج | إضافة program sub-filter |
---
### G. Coaches (المدربين) — `Views/coaches/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| G1 | create/edit | `discipline_ids[]` | القائمة قصيرة عادةً | لا يحتاج تغيير |
| G2 | create/edit | `academy_id` | بيظهر بس لو coach_type=academy ✅ | لا يحتاج — بس لو الأكاديميات كتير يبقى searchable |
| G3 | index | مفيش فلتر بالنشاط الرياضي | المدربين بيتعرضو كلهم | إضافة discipline filter |
---
### H. Players (اللاعبين) — `Views/players/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| H1 | index | مفيش فلتر بالنشاط/المجموعة | بيعرض كل اللاعبين | إضافة discipline + group filters |
| H2 | index | Search | ✅ موجود (اسم/رقم قومي/سيريال) | كافي |
---
### I. Attendance (الحضور) — `Views/attendance/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| I1 | index | مفيش اختيار مجموعة/مدرب/نشاط | بيعرض كل جدول اليوم | إضافة discipline + coach filter |
---
### J. Facilities (المرافق) — `Views/facilities/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| J1 | create/edit | `discipline_id` | dropdown عادي — غالباً قصير | لا يحتاج searchable |
| J2 | index | `type` filter | ✅ موجود | كافي |
---
### K. Programs (البرامج) — `Views/programs/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| K1 | index | `discipline_id` filter | ✅ موجود | كافي |
| K2 | create/edit | `discipline_id` | dropdown عادي — قصير | لا يحتاج |
| K3 | create/edit | `academy_id` | لو الأكاديميات > 10 محتاج search | Searchable إذا لزم |
---
### L. Pool Grid — `Views/pool-grid/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| L1 | manage | Player assignment | لو بيختار لاعب لـ zone يحتاج search | AJAX player search |
---
### M. Mirror (المرآة) — `Views/mirror/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| M1 | index | Discipline filter | ✅ موجود (chips) | كافي |
---
### N. Service Desk — `Views/service-desk/`
| # | الشاشة | الحقل | المشكلة | الحل |
|---|--------|-------|---------|------|
| N1 | index | Player lookup | ✅ NID-based lookup | كافي |
---
## ملخص الأولويات
### 🔴 Critical (مطلوب فوراً — UX مكسور)
| # | المكان | المشكلة |
|---|--------|---------|
| **E1** | Locker Rentals → player_id | كل اللاعبين في dropdown واحد بدون search |
| **A4** | Group Show → add player | كل اللاعبين المتاحين بدون search |
| **L1** | Pool Grid → player assignment | اختيار لاعب بدون search |
### 🟠 High Priority (Cascading مفقود)
| # | المكان | المشكلة |
|---|--------|---------|
| **A5** | Groups Index | مفيش فلتر discipline → program cascading |
| **B1** | Subscriptions Index | group filter بدون discipline parent |
| **F2** | Registration Wizard | المجموعات بدون فلتر بالنشاط |
| **H1** | Players Index | مفيش فلتر بالنشاط/المجموعة |
| **G3** | Coaches Index | مفيش فلتر بالنشاط |
| **I1** | Attendance | مفيش فلتر بالنشاط/المدرب |
### 🟡 Medium (Search مطلوب لـ UX أحسن)
| # | المكان | المشكلة |
|---|--------|---------|
| **A2** | Groups create/edit → coach_id | searchable مطلوب لو المدربين > 10 |
| **C1** | Booking Wizard → unit | searchable لو الوحدات كتير |
| **D1** | Booking Create → facility_unit_id | searchable |
| **E2** | Locker Rentals → locker_id | searchable + facility filter |
| **B2** | Subscriptions Index | search بالاسم/الرقم القومي |
| **A6** | Groups Index | coach filter (searchable) |
### 🟢 Low (Nice to have)
| # | المكان | المشكلة |
|---|--------|---------|
| **C2** | Booking Wizard | type filter chips |
| **F3** | Registration Wizard | program sub-filter |
| **K3** | Programs create/edit → academy_id | searchable لو كتير |
| **G2** | Coaches create/edit → academy_id | searchable لو كتير |
---
## التنفيذ التقني
### Step 1: Searchable Select Component (`public/assets/js/searchable-select.js`)
- Vanilla JS component يحول أي `<select>` لـ searchable dropdown
- يشتغل بـ attribute: `data-searchable="true"`
- يدعم AJAX loading: `data-search-url="/api/sa/players/search"` + `data-search-min="2"`
- يدعم grouping (optgroup)
- RTL compatible
- Keyboard navigation
### Step 2: API Endpoints المطلوبة
```
GET /api/sa/players/search?q={query}&discipline_id={id}&group_id={id}
GET /api/sa/groups/search?q={query}&discipline_id={id}&program_id={id}
GET /api/sa/coaches/search?q={query}&discipline_id={id}
```
### Step 3: Enhanced Cascading via `forms-engine.js`
- توسيع `data-depends-on` ليدعم multiple parents
- إضافة `data-cascade-reset="true"` — يفرغ الحقل لما الparent يتغير
- إضافة `data-cascade-disable="true"` — يعطل الحقل لحد ما الparent يتملى
### Step 4: Filter Enhancement per Page
- إضافة discipline filter في كل index page مفيهوش
- ربط cascading بين discipline → program → group في كل الصفحات
---
## ملاحظات
- الـ `forms-engine.js` الموجود فيه `data-depends-on` بس مستخدم بشكل محدود
- الـ `PlayerSearchApiController` موجود فعلاً ويرجع نتائج — محتاج بس يتربط بالـ UI
- Groups create/edit عنده cascading discipline→programs/coaches — ده الpattern المطلوب يتعمم
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