Commit a6123dbb authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(sports): refactor registration wizard + add coach assessment + swimming coaches

- Registration wizard simplified: flat 100 EGP per person (no card/form fees)
- Desk flow: enter data → pay → photo → select disciplines → print form
- Group assignment removed from registration (done by coaches now)
- New Coach Assessment Wizard at /sa/coach-assessment for skill evaluation
- New Swimming Coaches section at /sa/swimming/coaches (freelance-focused)
- Added assessment columns (skill_level, notes, assessed_at) to sa_group_players
- Added selected_disciplines JSON to sa_registrations
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4dc3ccbf
# Sports Module Wizards Refactor Plan
## Context (from DB scan)
- `sa_registrations` currently has: `registration_fee`, `card_fee`, `form_fee`, `total_fees`
- `system_config`: `sa.registration_fee_member=50`, `sa.registration_fee_nonmember=100`, `sa.card_fee=25`, `sa.form_fee=10`
- Current wizard flow: lookup → pay form fee → capture photo → select activity → pay subscription → print form → generate card
- `sa_coaches` has `coach_type` (independent/academy), `employment_type` (staff/contract/freelance), `academy_id`
- `sa_coach_disciplines` links coaches to disciplines
- Swimming discipline_id = 1
- `sa_pool_zone_bookings` handles lane/zone assignments with `max_occupancy`, `current_occupancy`, `ticket_price_member/nonmember`
- `sa_pool_tickets` tracks individual entries per zone booking
---
## SECTION A: Registration Wizard Simplification (مكتب الاشتراكات)
### Business Rule Change
**OLD**: Registration fee = 50 (member) / 100 (non-member) + card_fee (25) + form_fee (10) = multi-step payments
**NEW**: Single flat fee = 100 EGP per person. No card fee, no form fee, no separate charges. One payment covers everything.
### Wizard Flow (Simplified)
1. **Enter player data** (national ID / membership number → auto-fill)
2. **Pay 100 EGP** (single payment, includes form + card + everything)
3. **Capture photo** (webcam or upload)
4. **Select activities** (which disciplines/programs the player WANTS — no group assignment here)
5. **Print form** (استمارة)
6. **DONE** — player is registered, awaiting coach assessment
### What Subscription Desk Does NOT Do
- Does NOT assign player to a specific group
- Does NOT handle monthly subscription payment
- Does NOT determine skill level
- Activity selection is a PREFERENCE not a final assignment
### Changes Required
#### 1. Update `system_config`
```sql
UPDATE system_config SET config_value = '100.00' WHERE config_key = 'sa.registration_fee_member';
UPDATE system_config SET config_value = '100.00' WHERE config_key = 'sa.registration_fee_nonmember';
UPDATE system_config SET config_value = '0.00' WHERE config_key = 'sa.card_fee';
UPDATE system_config SET config_value = '0.00' WHERE config_key = 'sa.form_fee';
```
#### 2. Modify `RegistrationWizardService::calculateFees()`
- Return flat 100 EGP regardless of member/non-member
- card_fee = 0, form_fee = 0, registration_fee = 100
#### 3. Simplify `RegistrationWizardService::startRegistration()`
- Set `form_payment_status = 'paid'` immediately (no separate form payment step)
- total_fees = 100
#### 4. New field: `sa_registrations.selected_disciplines` (JSON)
- Stores the disciplines the player indicated interest in
- This is NOT enrollment — just preference recording
#### 5. Remove `submitFormPayment` step from wizard UI
- The 100 EGP covers everything, paid in one shot
#### 6. Remove group selection from this wizard
- The `selectActivity` step becomes "select disciplines of interest" not "select specific group"
- No monthly fee calculation here
#### 7. Wizard UI Steps (new flow)
- Step 1: Player lookup / entry (unchanged)
- Step 2: Pay 100 EGP (one button, sent to cashier)
- Step 3: Capture photo
- Step 4: Select disciplines of interest (checkboxes, not dropdown)
- Step 5: Print form
- Step 6: Generate card (if needed)
### Edge Cases (40+)
1. Player already registered (same national ID) — reuse existing player, don't create duplicate
2. Player already paid 100 EGP previously (returning player) — skip payment, show "already paid"
3. Member vs non-member — same fee (100 EGP), but `player_type` still tracked for monthly pricing later
4. National ID is 14 digits — auto-parse DOB + gender
5. National ID is invalid format — show error in Arabic
6. Membership number doesn't exist — treat as non-member
7. Player has no guardian (adult) — guardian fields optional
8. Player is minor (< 18 from NID) — guardian fields required
9. Photo upload fails (file too large) — show size limit error
10. Photo upload fails (wrong format) — accept only jpg/png
11. Webcam capture fails (no camera access) — fallback to file upload
12. Player selects 0 disciplines — require at least 1
13. Player selects disciplines that don't exist — validate against DB
14. Player already has active registrations — show warning but allow
15. Double-click on pay button — idempotent (check if already sent to cashier)
16. Network timeout during payment submission — retry-safe
17. Print form with missing photo — print with placeholder
18. Print form with Arabic text — correct RTL layout
19. Cancel registration mid-way — clean up partial data
20. Player name has special characters — HTML escape properly
21. Multiple registrations same day — unique registration_number per attempt
22. Registration number collision — use transaction + retry
23. Cashier rejects payment — registration stays "pending_payment"
24. Browser back button mid-wizard — handle step regression gracefully
25. Session timeout mid-wizard — save progress, allow resume
26. Player with expired medical certificate — allow registration (medical check is separate)
27. Player with no medical certificate at all — allow (30-day grace)
28. Same person registering for 2nd time (already active) — allow, link to existing player
29. Concurrent registrations of same player by different clerks — first-write-wins
30. Form fee paid in old system — detect `registration_fee_paid = 1` and skip
31. Database connection lost mid-transaction — rollback cleanly
32. Photo path contains spaces or Arabic — URL-encode
33. Card generation before photo — block, require photo first
34. Multiple disciplines selected — store as JSON array
35. Discipline deactivated after selection — handle gracefully in coach wizard
36. Registration opened but never completed (abandoned) — auto-cancel after X days
37. Non-member later becomes member — player_type update doesn't affect registration fee
38. Printing on different paper sizes — form layout responsive to A4
39. QR code on card contains expired data — regenerate on reprint
40. Player already has card from old registration — issue new card number
41. Guardian phone same as player phone — allow
42. Empty full_name_en — optional, don't block
43. National ID starts with 2 or 3 — both valid centuries (1900s/2000s)
---
## SECTION B: Coach/Activity Assessment Wizard (ويزارد المدربين)
### Business Rule
After registration desk completes, the player goes to coaches/activity managers. THEY assess the player's skill level and assign them to the correct group in the correct program.
### Flow
1. **Coach selects a player** (from pending-assessment list)
2. **Coach sees player's preferred disciplines** (from registration)
3. **Coach assesses skill level** (تقييم فني)
4. **Coach assigns to program → group** (based on assessment)
5. **Monthly subscription is created** (auto-generated for the assigned group)
6. **Player is active** and ready to train
### Data Model
- `sa_registrations.status`: add value `'assessed'` (after coach assigns)
- `sa_group_players.assessed_by` (new column): who did the assessment
- `sa_group_players.assessment_notes` (new column): coach's notes
- `sa_group_players.skill_level` (new column): beginner/intermediate/advanced
### New Route: `/sa/coach-assessment`
- GET: List all players with `status = 'completed'` (paid + photo done) awaiting assessment
- GET `/{registration_id}`: Show assessment form for specific player
- POST `/{registration_id}/assess`: Submit assessment + group assignment
### UI Flow
- Table of pending players: name, age, preferred disciplines, registration date
- Click player → see details + preferred disciplines
- Coach selects discipline → program → group
- Coach adds assessment notes + skill level
- Submit → player enrolled in group, subscription created
### Edge Cases (40+)
1. Player paid but no photo — coach can still assess (photo not blocking)
2. Player preferred swimming but coach puts in football — allowed (preference ≠ rule)
3. Group is full (max_capacity reached) — block enrollment, show "full" badge
4. Player already in another group in same program — warn duplicate
5. Player already in same group — block, show error
6. Coach assigns to group from different discipline than preference — allowed with confirmation
7. Monthly subscription start date — first day of current month or enrollment date?
8. Player type affects monthly fee — member gets member rate, non-member gets non-member rate
9. Coach has no permission for this discipline — permission check
10. Assessment submitted twice for same registration — idempotent
11. Player is minor and medical not approved — allow enrollment (30-day grace)
12. Medical grace deadline = today + 30 days from enrollment
13. Group's season_end has passed — don't show in available groups
14. Multiple coaches can assess — any coach with permission can do it
15. Coach's own group vs another coach's group — both allowed
16. Assessment notes are empty — optional field
17. Skill level not selected — required field
18. Subscription amount = group's monthly_fee_member or monthly_fee_nonmember based on player_type
19. Mid-month enrollment — prorate or full month? (implement as config)
20. Player cancels before assessment — registration cancelled, no enrollment
21. Coach accidentally assigns wrong group — add "reassign" option
22. Transfer between groups — existing transfer logic handles it
23. Multiple disciplines selected by player — coach assesses one at a time
24. Player registered for swimming + football — separate assessment for each
25. One assessment per discipline per player per registration
26. Assessment creates enrollment (sa_group_players) with status = 'active'
27. Assessment auto-creates first month subscription
28. Assessment triggers event: `sa.player.assessed`
29. Coach views only players whose preferred disciplines match their own disciplines
30. Admin/super_admin can assess any discipline
31. Assessment date recorded for tracking
32. Reassessment (move to different group) — handled via existing transfer logic
33. Group's min_age / max_age — validate player age at enrollment
34. Player age calculated from DOB, not national ID at assessment time
35. No DOB available — skip age validation
36. Coach assessment page should show group current_count / max_capacity
37. Sort available groups by: has space → same discipline → alphabetical
38. When group becomes full after enrollment — update is_full flag
39. current_count increment on enrollment
40. If subscription generation fails — still enroll player, log error
41. Paused enrollment — cannot be created from assessment (always active)
42. Player has no preferred disciplines — coach sees ALL disciplines
43. Print assessment result — optional PDF
---
## SECTION C: Swimming Coaches Wizard (ويزارد مدربين السباحة)
### Business Rule
Swimming coaches are mostly freelance. They need their own dedicated section under "السباحة" in the sidebar. This wizard adds/manages swimming coaches specifically, and lists ONLY swimming coaches.
### Features
1. **Add swimming coach** — form pre-filtered to swimming discipline
2. **List swimming coaches** — only coaches linked to SWIMMING discipline
3. **Manage lane assignments** — coach books a lane, has max capacity
4. **Extra tickets** — coach brings extra players beyond their lane capacity, pays per-ticket
### Data Model (existing, no new tables needed)
- `sa_coaches` — add coach with `coach_type = 'independent'`, `employment_type = 'freelance'`
- `sa_coach_disciplines` — link to SWIMMING discipline (id=1)
- `sa_pool_zone_bookings` — lane assignment (zone_row = lane number)
- `sa_pool_tickets` — extra entries beyond capacity
### New Routes
- GET `/sa/swimming/coaches` — list swimming coaches
- GET `/sa/swimming/coaches/create` — add swimming coach form
- POST `/sa/swimming/coaches` — store new swimming coach
- GET `/sa/swimming/coaches/{id}` — view coach details + their lane bookings
- GET `/sa/swimming/coaches/{id}/edit` — edit coach
### The Lane/Ticket Cycle
1. Swimming coach reserves a lane (حارة) for a time slot
2. Lane has max_occupancy (e.g., 10 players)
3. Coach brings their own trainees (counted from their group enrollment)
4. Coach brings EXTRA people beyond their lane capacity
5. Extra people pay per-ticket (pool_ticket with `zone_booking_id` linked)
6. Ticket price from `sa_pool_zone_bookings.ticket_price_member/nonmember`
### Route for extra tickets: existing `/sa/pool-tickets/issue`
- Currently supports issuing tickets linked to a zone_booking_id
- Need to verify it checks `current_occupancy < max_occupancy` or allows overflow with ticket
### Edge Cases (40+)
1. Coach added without SWIMMING discipline link — auto-add discipline link
2. Coach already exists in system — detect by national_id, show "already exists"
3. Coach is freelance but works with academy — academy_id optional
4. Coach has multiple disciplines (swimming + football) — still shows in swimming list
5. Coach deactivated — disappears from swimming coach list
6. Lane booked but coach doesn't show up — booking stays "active" (manual cleanup)
7. Lane has max_occupancy = 10, coach has 8 players + brings 2 extra — 2 tickets needed
8. Lane has max_occupancy = 10, coach has 10 players + brings 3 extra — 3 tickets issued
9. Extra person is member — member ticket price applies
10. Extra person is non-member — non-member ticket price applies
11. Extra person has no national ID — guest_name field sufficient
12. Ticket issued but person leaves early — ticket still valid (no partial refund)
13. Ticket cancelled before entry — refund logic (payment_status = refunded)
14. Lane not booked for this hour — cannot issue tickets for non-existent booking
15. Coach books lane for wrong time — admin can edit booking
16. Coach books multiple lanes same time — allowed (big group, two lanes)
17. Two coaches book same lane same time — conflict detection (zone_row + booking_date + time overlap)
18. Lane maintenance — assignment_type = 'maintenance', blocks all bookings
19. Lane booking overlaps with group training booking — show conflict warning
20. Coach with 0 players in group — still can book lane (private training)
21. Swimming coach list should show: name, phone, employment_type, academy, active groups count
22. Coach edit — cannot change discipline assignment in this wizard (use main coach page)
23. Coach national_id duplicate — unique constraint, show error
24. Coach phone is optional — allow empty
25. Coach code auto-generated — `SWIM-COACH-{NNN}`
26. Filter swimming coaches by: all, freelance, staff, academy
27. Search by name or phone
28. Coach's lane bookings history — show past and upcoming
29. Lane booking date in the past — cannot create, show error
30. Lane booking for today but time already passed — allow (late registration)
31. Template expansion creates zone bookings — verify tickets work with template-generated bookings
32. Ticket price = 0 — allowed (free entry for special cases)
33. Ticket for member without member_id — just set is_member = 0
34. Coach's group_id on zone_booking — links to their training group
35. Multiple zone bookings for same coach on same day — different time slots
36. Coach leaves (deactivated) — their future zone bookings stay but show warning
37. Ticket issued to player_id (existing player) vs guest — both paths work
38. Pool ticket entry/exit tracking — entry_time/exit_time populated on scan
39. Zone booking cancelled — all linked tickets should be cancelled
40. Current_occupancy exceeds max — soft limit (allow with warning, not block)
41. Swimming coaches see only their own lane bookings (unless admin)
42. Sidebar link shows count badge: pending tickets to process
---
## SECTION D: Migration Plan
### Migration 1: Update system_config (registration fees)
```sql
UPDATE system_config SET config_value = '100.00' WHERE config_key = 'sa.registration_fee_member';
UPDATE system_config SET config_value = '100.00' WHERE config_key = 'sa.registration_fee_nonmember';
UPDATE system_config SET config_value = '0.00' WHERE config_key = 'sa.card_fee';
UPDATE system_config SET config_value = '0.00' WHERE config_key = 'sa.form_fee';
```
### Migration 2: Add `selected_disciplines` to `sa_registrations`
```sql
ALTER TABLE sa_registrations ADD COLUMN selected_disciplines JSON NULL AFTER group_id;
```
### Migration 3: Add assessment columns to `sa_group_players`
```sql
ALTER TABLE sa_group_players ADD COLUMN assessed_by BIGINT UNSIGNED NULL AFTER created_by;
ALTER TABLE sa_group_players ADD COLUMN assessment_notes TEXT NULL AFTER assessed_by;
ALTER TABLE sa_group_players ADD COLUMN skill_level VARCHAR(20) NULL AFTER assessment_notes;
ALTER TABLE sa_group_players ADD COLUMN assessed_at TIMESTAMP NULL AFTER skill_level;
```
### No new tables needed — everything works with existing schema + minor column additions.
---
## SECTION E: Execution Order
1. Run migrations (config update + 2 ALTER TABLEs)
2. Refactor `RegistrationWizardService` — flat 100 EGP, no form/card fees, discipline selection instead of group
3. Update wizard UI — remove form payment step, change activity selection to discipline checkboxes
4. Create Coach Assessment Wizard (new controller + views)
5. Create Swimming Coaches section (new controller + views under swimming)
6. Update sidebar with new navigation items
7. Test all flows end-to-end
---
## SECTION F: Files to Create/Modify
### Modify:
- `app/Modules/SportsActivity/Services/RegistrationWizardService.php` — fee logic + discipline selection
- `app/Modules/SportsActivity/Controllers/RegistrationWizardController.php` — remove form payment step
- `app/Modules/SportsActivity/Views/registration/wizard.php` — simplified wizard UI
- `app/Modules/SportsActivity/bootstrap.php` — new menu items + permissions
- `app/Modules/SportsActivity/Routes.php` — new routes
### Create:
- `database/migrations/Phase_103_001_sports_wizard_refactor.php` — all migrations
- `app/Modules/SportsActivity/Controllers/CoachAssessmentController.php` — new wizard
- `app/Modules/SportsActivity/Views/assessment/index.php` — pending players list
- `app/Modules/SportsActivity/Views/assessment/assess.php` — assessment form
- `app/Modules/SportsActivity/Controllers/Swimming/SwimmingCoachController.php` — swimming coaches CRUD
- `app/Modules/SportsActivity/Views/swimming/coaches/index.php` — list
- `app/Modules/SportsActivity/Views/swimming/coaches/form.php` — create/edit
- `app/Modules/SportsActivity/Views/swimming/coaches/show.php` — details + lane bookings
- `app/Modules/SportsActivity/Services/CoachAssessmentService.php` — assessment logic
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\CoachAssessmentService;
class CoachAssessmentController extends Controller
{
public function index(Request $request): Response
{
$players = CoachAssessmentService::getPendingPlayers();
$disciplines = App::getInstance()->db()->select(
"SELECT id, name_ar FROM sa_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY name_ar"
);
return $this->view('SportsActivity.Views.assessment.index', [
'players' => $players,
'disciplines' => $disciplines,
]);
}
public function assess(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$registrationId = (int) $id;
$registration = $db->selectOne(
"SELECT r.*, p.full_name_ar, p.full_name_en, p.national_id as player_nid,
p.date_of_birth, p.gender, p.phone, p.photo_path, p.player_type as p_type,
p.medical_status, r.selected_disciplines
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ?",
[$registrationId]
);
if (!$registration) {
return $this->redirect('/sa/coach-assessment')->withError('التسجيل غير موجود');
}
$groups = CoachAssessmentService::getAvailableGroups();
return $this->view('SportsActivity.Views.assessment.assess', [
'registration' => $registration,
'groups' => $groups,
]);
}
public function submit(Request $request, string $id): Response
{
$registrationId = (int) $id;
$groupId = (int) $request->post('group_id', 0);
$skillLevel = trim((string) $request->post('skill_level', ''));
$notes = trim((string) $request->post('assessment_notes', ''));
if ($groupId === 0) {
return $this->redirect('/sa/coach-assessment/' . $id)->withError('يجب اختيار المجموعة');
}
if ($skillLevel === '') {
return $this->redirect('/sa/coach-assessment/' . $id)->withError('يجب تحديد المستوى');
}
$employee = App::getInstance()->currentEmployee();
$assessedBy = $employee ? (int) $employee->id : 0;
$result = CoachAssessmentService::assess($registrationId, $groupId, $skillLevel, $notes, $assessedBy);
if (!$result['success']) {
return $this->redirect('/sa/coach-assessment/' . $id)->withError($result['error']);
}
return $this->redirect('/sa/coach-assessment')
->withSuccess('تم تقييم اللاعب وتعيينه في مجموعة "' . $result['group_name'] . '" بنجاح');
}
}
......@@ -126,32 +126,13 @@ class RegistrationWizardController extends Controller
return $this->redirect('/sa/registration')->withError('التسجيل غير موجود');
}
$groups = $db->select(
"SELECT g.*, p.name_ar as program_name, p.discipline_id, d.name_ar as discipline_name,
p.monthly_fee_member, p.monthly_fee_nonmember
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
WHERE g.status = 'active' AND g.is_archived = 0 AND g.is_full = 0
ORDER BY d.name_ar ASC, g.name_ar ASC"
);
$selectedGroup = null;
if ($registration['group_id']) {
$selectedGroup = $db->selectOne(
"SELECT g.*, 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
WHERE g.id = ?",
[(int) $registration['group_id']]
$disciplines = $db->select(
"SELECT id, name_ar, icon FROM sa_disciplines WHERE is_active = 1 AND is_archived = 0 ORDER BY sort_order ASC, name_ar ASC"
);
}
return $this->view('SportsActivity.Views.registration.wizard', [
'registration' => $registration,
'groups' => $groups,
'selectedGroup' => $selectedGroup,
'disciplines' => $disciplines,
'step' => $this->determineStep($registration),
]);
}
......@@ -187,19 +168,17 @@ class RegistrationWizardController extends Controller
public function selectActivity(Request $request, string $id): Response
{
$registrationId = (int) $id;
$programId = (int) $request->post('program_id', 0);
$groupId = (int) $request->post('group_id', 0);
$months = max(1, (int) $request->post('months', 1));
$hasSibling = (bool) $request->post('has_sibling', false);
if ($programId > 0) {
$result = RegistrationWizardService::selectProgram($registrationId, $programId, $months, $hasSibling);
} elseif ($groupId > 0) {
$result = RegistrationWizardService::selectGroup($registrationId, $groupId, $months, $hasSibling);
} else {
return $this->json(['success' => false, 'error' => 'اختر برنامج أو مجموعة']);
$disciplineIds = $request->post('discipline_ids', []);
if (is_string($disciplineIds)) {
$disciplineIds = json_decode($disciplineIds, true) ?: [];
}
if (empty($disciplineIds)) {
return $this->json(['success' => false, 'error' => 'يجب اختيار نشاط واحد على الأقل']);
}
$result = RegistrationWizardService::saveSelectedDisciplines($registrationId, $disciplineIds);
return $this->json($result);
}
......@@ -402,18 +381,14 @@ class RegistrationWizardController extends Controller
private function determineStep(array $registration): int
{
// Step 1: Pay form fee (استمارة اشتراك)
// Step 1: Pay 100 EGP (single registration fee)
// Step 2: Photo capture
// Step 3: Select activity/group
// Step 4: Pay subscription
// Step 5: Complete (print/card)
if ($registration['status'] === 'completed' || $registration['payment_status'] === 'paid') {
return 5;
}
if ($registration['status'] === 'pending_payment') {
// Step 3: Select disciplines of interest
// Step 4: Complete (print form + generate card)
if ($registration['status'] === 'completed' || ($registration['payment_status'] ?? '') === 'paid') {
return 4;
}
if (!empty($registration['group_id'])) {
if (!empty($registration['selected_disciplines'])) {
return 4;
}
if ((int) $registration['photo_captured'] === 1) {
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Swimming;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
class SwimmingCoachController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$search = trim((string) $request->get('search', ''));
$filter = trim((string) $request->get('filter', ''));
$sql = "SELECT c.*, cd.specialization_level,
(SELECT COUNT(*) FROM sa_group_coaches gc
INNER JOIN sa_groups g ON g.id = gc.group_id AND g.status = 'active'
WHERE gc.coach_id = c.id) as active_groups
FROM sa_coaches c
INNER JOIN sa_coach_disciplines cd ON cd.coach_id = c.id
INNER JOIN sa_disciplines d ON d.id = cd.discipline_id
WHERE d.code = 'SWIMMING' AND c.is_active = 1 AND c.is_archived = 0";
$params = [];
if ($search !== '') {
$sql .= " AND (c.full_name_ar LIKE ? OR c.phone LIKE ? OR c.code LIKE ?)";
$params[] = "%{$search}%";
$params[] = "%{$search}%";
$params[] = "%{$search}%";
}
if ($filter !== '' && in_array($filter, ['freelance', 'staff', 'contract'])) {
$sql .= " AND c.employment_type = ?";
$params[] = $filter;
}
$sql .= " ORDER BY c.full_name_ar ASC";
$coaches = $db->select($sql, $params);
return $this->view('SportsActivity.Views.swimming.coaches.index', [
'coaches' => $coaches,
'search' => $search,
'filter' => $filter,
]);
}
public function create(Request $request): Response
{
return $this->view('SportsActivity.Views.swimming.coaches.form', [
'coach' => null,
]);
}
public function store(Request $request): Response
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$fullNameAr = trim((string) $request->post('full_name_ar', ''));
$fullNameEn = trim((string) $request->post('full_name_en', ''));
$nationalId = trim((string) $request->post('national_id', ''));
$phone = trim((string) $request->post('phone', ''));
$employmentType = trim((string) $request->post('employment_type', 'freelance'));
if ($fullNameAr === '') {
return $this->redirect('/sa/swimming/coaches/create')->withError('الاسم بالعربي مطلوب');
}
if ($nationalId !== '') {
$existing = $db->selectOne(
"SELECT id FROM sa_coaches WHERE national_id = ? AND is_archived = 0",
[$nationalId]
);
if ($existing) {
return $this->redirect('/sa/swimming/coaches/create')
->withError('يوجد مدرب بنفس الرقم القومي — كود: ' . ($existing['code'] ?? $existing['id']));
}
}
$code = self::generateCode($db);
$coachId = $db->insert('sa_coaches', [
'code' => $code,
'full_name_ar' => $fullNameAr,
'full_name_en' => $fullNameEn ?: null,
'national_id' => $nationalId ?: null,
'phone' => $phone ?: null,
'coach_type' => 'independent',
'employment_type' => $employmentType,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
$swimmingDiscipline = $db->selectOne(
"SELECT id FROM sa_disciplines WHERE code = 'SWIMMING' AND is_archived = 0"
);
if ($swimmingDiscipline) {
$db->insert('sa_coach_disciplines', [
'coach_id' => $coachId,
'discipline_id' => (int) $swimmingDiscipline['id'],
'specialization_level' => 'primary',
'created_at' => date('Y-m-d H:i:s'),
]);
}
return $this->redirect('/sa/swimming/coaches')->withSuccess('تم إضافة مدرب السباحة "' . $fullNameAr . '" بنجاح');
}
public function show(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$coach = $db->selectOne(
"SELECT c.* FROM sa_coaches c WHERE c.id = ? AND c.is_archived = 0",
[(int) $id]
);
if (!$coach) {
return $this->redirect('/sa/swimming/coaches')->withError('المدرب غير موجود');
}
$laneBookings = $db->select(
"SELECT zb.*, f.name_ar as facility_name
FROM sa_pool_zone_bookings zb
LEFT JOIN sa_facilities f ON f.id = zb.facility_id
WHERE zb.coach_id = ?
ORDER BY zb.booking_date DESC, zb.start_time DESC
LIMIT 20",
[(int) $id]
);
$groups = $db->select(
"SELECT g.name_ar, g.code, gc.role
FROM sa_group_coaches gc
INNER JOIN sa_groups g ON g.id = gc.group_id AND g.status = 'active'
WHERE gc.coach_id = ?",
[(int) $id]
);
return $this->view('SportsActivity.Views.swimming.coaches.show', [
'coach' => $coach,
'laneBookings' => $laneBookings,
'groups' => $groups,
]);
}
public function edit(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$coach = $db->selectOne(
"SELECT * FROM sa_coaches WHERE id = ? AND is_archived = 0",
[(int) $id]
);
if (!$coach) {
return $this->redirect('/sa/swimming/coaches')->withError('المدرب غير موجود');
}
return $this->view('SportsActivity.Views.swimming.coaches.form', [
'coach' => $coach,
]);
}
public function update(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$coach = $db->selectOne("SELECT * FROM sa_coaches WHERE id = ? AND is_archived = 0", [(int) $id]);
if (!$coach) {
return $this->redirect('/sa/swimming/coaches')->withError('المدرب غير موجود');
}
$fullNameAr = trim((string) $request->post('full_name_ar', ''));
$fullNameEn = trim((string) $request->post('full_name_en', ''));
$phone = trim((string) $request->post('phone', ''));
$employmentType = trim((string) $request->post('employment_type', 'freelance'));
if ($fullNameAr === '') {
return $this->redirect('/sa/swimming/coaches/' . $id . '/edit')->withError('الاسم بالعربي مطلوب');
}
$db->update('sa_coaches', [
'full_name_ar' => $fullNameAr,
'full_name_en' => $fullNameEn ?: null,
'phone' => $phone ?: null,
'employment_type' => $employmentType,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [(int) $id]);
return $this->redirect('/sa/swimming/coaches')->withSuccess('تم تحديث بيانات المدرب بنجاح');
}
private static function generateCode($db): string
{
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(code, 12) AS UNSIGNED)) as max_num FROM sa_coaches WHERE code LIKE 'SWIM-COACH-%'"
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return 'SWIM-COACH-' . str_pad((string) $next, 3, '0', STR_PAD_LEFT);
}
}
......@@ -310,8 +310,19 @@ return [
['POST', '/api/sa/subscriptions/pause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@pause', ['auth', 'csrf'], 'sa.subscription.generate'],
['POST', '/api/sa/subscriptions/unpause', 'SportsActivity\Controllers\Api\SubscriptionPreviewApiController@unpause', ['auth', 'csrf'], 'sa.subscription.generate'],
// ─── Coach Assessment Wizard ───────────────────────────────────────────────
['GET', '/sa/coach-assessment', 'SportsActivity\Controllers\CoachAssessmentController@index', ['auth'], 'sa.coach_assessment.view'],
['GET', '/sa/coach-assessment/{id:\d+}', 'SportsActivity\Controllers\CoachAssessmentController@assess', ['auth'], 'sa.coach_assessment.manage'],
['POST', '/sa/coach-assessment/{id:\d+}/submit', 'SportsActivity\Controllers\CoachAssessmentController@submit', ['auth', 'csrf'], 'sa.coach_assessment.manage'],
// ─── Swimming Section ───────────────────────────────────────────────────────
['GET', '/sa/swimming', 'SportsActivity\Controllers\Swimming\SwimmingDashboardController@index', ['auth'], 'sa.swimming.dashboard'],
['GET', '/sa/swimming/coaches', 'SportsActivity\Controllers\Swimming\SwimmingCoachController@index', ['auth'], 'sa.swimming.coach_manage'],
['GET', '/sa/swimming/coaches/create', 'SportsActivity\Controllers\Swimming\SwimmingCoachController@create', ['auth'], 'sa.swimming.coach_manage'],
['POST', '/sa/swimming/coaches', 'SportsActivity\Controllers\Swimming\SwimmingCoachController@store', ['auth', 'csrf'], 'sa.swimming.coach_manage'],
['GET', '/sa/swimming/coaches/{id:\d+}', 'SportsActivity\Controllers\Swimming\SwimmingCoachController@show', ['auth'], 'sa.swimming.coach_manage'],
['GET', '/sa/swimming/coaches/{id:\d+}/edit', 'SportsActivity\Controllers\Swimming\SwimmingCoachController@edit', ['auth'], 'sa.swimming.coach_manage'],
['POST', '/sa/swimming/coaches/{id:\d+}', 'SportsActivity\Controllers\Swimming\SwimmingCoachController@update', ['auth', 'csrf'], 'sa.swimming.coach_manage'],
['GET', '/sa/swimming/register', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@index', ['auth'], 'sa.swimming.register'],
['POST', '/sa/swimming/register/lookup', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@lookup', ['auth', 'csrf'], 'sa.swimming.register'],
['GET', '/sa/swimming/register/{id:\d+}', 'SportsActivity\Controllers\Swimming\SwimmingRegistrationController@step', ['auth'], 'sa.swimming.register'],
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Cashier\Services\PaymentRequestService;
final class CoachAssessmentService
{
public static function getPendingPlayers(?int $coachId = null): array
{
$db = App::getInstance()->db();
$sql = "SELECT r.id as registration_id, r.registration_number, r.selected_disciplines,
r.player_type, r.created_at as registration_date,
p.id as player_id, p.full_name_ar, p.full_name_en, p.national_id,
p.date_of_birth, p.gender, p.phone, p.photo_path, p.medical_status
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.status = 'completed' AND r.payment_status = 'paid'
AND NOT EXISTS (
SELECT 1 FROM sa_group_players gp
WHERE gp.player_id = r.player_id AND gp.status IN ('active','pending_payment')
)
ORDER BY r.created_at DESC";
return $db->select($sql);
}
public static function getAvailableGroups(?int $disciplineId = null): array
{
$db = App::getInstance()->db();
$sql = "SELECT g.id, g.name_ar, g.code, g.current_count, g.max_capacity, g.is_full,
p.id as program_id, p.name_ar as program_name,
p.monthly_fee_member, p.monthly_fee_nonmember,
d.id as discipline_id, 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.is_full = 0";
$params = [];
if ($disciplineId) {
$sql .= " AND d.id = ?";
$params[] = $disciplineId;
}
$sql .= " ORDER BY d.name_ar ASC, g.name_ar ASC";
return $db->select($sql, $params);
}
public static function assess(int $registrationId, int $groupId, string $skillLevel, string $notes, int $assessedBy): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$registration = $db->selectOne(
"SELECT r.*, p.full_name_ar, p.player_type, p.member_id
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ? AND r.payment_status = 'paid'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو لم يتم الدفع'];
}
$playerId = (int) $registration['player_id'];
$existing = $db->selectOne(
"SELECT id FROM sa_group_players WHERE group_id = ? AND player_id = ? AND status IN ('active','pending_payment')",
[$groupId, $playerId]
);
if ($existing) {
return ['success' => false, 'error' => 'اللاعب مسجل بالفعل في هذه المجموعة'];
}
$group = $db->selectOne(
"SELECT g.*, p.monthly_fee_member, p.monthly_fee_nonmember, p.name_ar as program_name
FROM sa_groups g
LEFT JOIN sa_programs p ON p.id = g.program_id
WHERE g.id = ? AND g.status = 'active' AND g.is_archived = 0",
[$groupId]
);
if (!$group) {
return ['success' => false, 'error' => 'المجموعة غير موجودة أو غير نشطة'];
}
if ((int) $group['current_count'] >= (int) $group['max_capacity']) {
return ['success' => false, 'error' => 'المجموعة ممتلئة — السعة القصوى ' . $group['max_capacity']];
}
$playerType = $registration['player_type'] ?? 'non_member';
$monthlyFee = $playerType === 'member'
? (float) ($group['monthly_fee_member'] ?? 0)
: (float) ($group['monthly_fee_nonmember'] ?? 0);
$db->beginTransaction();
try {
$enrollmentId = $db->insert('sa_group_players', [
'group_id' => $groupId,
'player_id' => $playerId,
'enrolled_at' => date('Y-m-d'),
'status' => 'active',
'assessed_by' => $assessedBy,
'assessment_notes' => $notes ?: null,
'skill_level' => $skillLevel,
'assessed_at' => date('Y-m-d H:i:s'),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : $assessedBy,
]);
$newCount = (int) $group['current_count'] + 1;
$db->update('sa_groups', [
'current_count' => $newCount,
'is_full' => $newCount >= (int) $group['max_capacity'] ? 1 : 0,
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$groupId]);
$db->update('sa_registrations', [
'group_id' => $groupId,
'status' => 'assessed',
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
if ($monthlyFee > 0) {
$periodStart = date('Y-m-01');
$periodEnd = date('Y-m-t');
$db->insert('sa_subscriptions', [
'subscription_number' => self::generateSubNumber(),
'player_id' => $playerId,
'group_id' => $groupId,
'period_start' => $periodStart,
'period_end' => $periodEnd,
'amount' => $monthlyFee,
'final_amount' => $monthlyFee,
'payment_status' => 'unpaid',
'auto_generated' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
return ['success' => false, 'error' => 'فشل التقييم: ' . $e->getMessage()];
}
EventBus::dispatch('sa.player.assessed', [
'registration_id' => $registrationId,
'player_id' => $playerId,
'group_id' => $groupId,
'skill_level' => $skillLevel,
'assessed_by' => $assessedBy,
]);
return [
'success' => true,
'enrollment_id' => $enrollmentId,
'group_name' => $group['name_ar'],
'monthly_fee' => $monthlyFee,
];
}
private static function generateSubNumber(): string
{
$db = App::getInstance()->db();
$prefix = 'SUB-' . date('Y') . '-';
$row = $db->selectOne(
"SELECT MAX(CAST(SUBSTRING(subscription_number, " . (strlen($prefix) + 1) . ") AS UNSIGNED)) as max_num
FROM sa_subscriptions WHERE subscription_number LIKE ?",
[$prefix . '%']
);
$next = ((int) ($row['max_num'] ?? 0)) + 1;
return $prefix . str_pad((string) $next, 6, '0', STR_PAD_LEFT);
}
}
......@@ -119,10 +119,10 @@ final class RegistrationWizardService
'national_id' => $nationalId ?: null,
'status' => 'in_progress',
'registration_fee' => $formAlreadyPaid ? 0 : $fees['registration_fee'],
'card_fee' => $fees['card_fee'],
'form_fee' => $fees['form_fee'],
'total_fees' => ($formAlreadyPaid ? 0 : $fees['registration_fee']) + $fees['card_fee'] + $fees['form_fee'],
'form_payment_status' => $formAlreadyPaid ? 'paid' : 'unpaid',
'card_fee' => 0,
'form_fee' => 0,
'total_fees' => $formAlreadyPaid ? 0 : $fees['total_fees'],
'form_payment_status' => 'unpaid',
'branch_id' => $branch ? (int) $branch['id'] : null,
'created_by' => $employee ? (int) $employee->id : null,
'created_at' => date('Y-m-d H:i:s'),
......@@ -280,23 +280,11 @@ final class RegistrationWizardService
public static function calculateFees(string $playerType): array
{
$db = App::getInstance()->db();
$regFeeKey = $playerType === 'member' ? 'sa.registration_fee_member' : 'sa.registration_fee_nonmember';
$regFeeRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", [$regFeeKey]);
$cardFeeRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['sa.card_fee']);
$formFeeRow = $db->selectOne("SELECT config_value FROM system_config WHERE config_key = ?", ['sa.form_fee']);
$registrationFee = (float) ($regFeeRow['config_value'] ?? ($playerType === 'member' ? '50.00' : '100.00'));
$cardFee = (float) ($cardFeeRow['config_value'] ?? '25.00');
$formFee = (float) ($formFeeRow['config_value'] ?? '10.00');
return [
'registration_fee' => $registrationFee,
'card_fee' => $cardFee,
'form_fee' => $formFee,
'total_fees' => $registrationFee + $cardFee + $formFee,
'registration_fee' => 100.0,
'card_fee' => 0.0,
'form_fee' => 0.0,
'total_fees' => 100.0,
];
}
......@@ -319,20 +307,20 @@ final class RegistrationWizardService
return ['success' => true, 'already_paid' => true];
}
$formFee = (float) $registration['registration_fee'];
if ($formFee <= 0) {
return ['success' => false, 'error' => 'رسوم الاستمارة غير محددة'];
$totalFee = (float) $registration['total_fees'];
if ($totalFee <= 0) {
$totalFee = 100.0;
}
$memberId = (int) ($registration['member_id'] ?? 0);
$description = 'استمارة اشتراك نشاط رياضي — ' . ($registration['full_name_ar'] ?? '');
$description = 'رسوم تسجيل نشاط رياضي (100 ج.م) — ' . ($registration['full_name_ar'] ?? '');
$result = PaymentRequestService::createRequest([
'member_id' => $memberId,
'payment_type' => 'sa_form_fee',
'amount' => (string) $formFee,
'payment_type' => 'sa_registration_fee',
'amount' => (string) $totalFee,
'description_ar' => $description,
'related_entity_type' => 'sa_registration_form',
'related_entity_type' => 'sa_registrations',
'related_entity_id' => $registrationId,
]);
......@@ -350,70 +338,46 @@ final class RegistrationWizardService
'success' => true,
'request_id' => $result['request_id'],
'request_number' => $result['request_number'],
'amount' => $formFee,
'amount' => $totalFee,
];
}
public static function submitToPaymentQueue(int $registrationId): array
public static function saveSelectedDisciplines(int $registrationId, array $disciplineIds): array
{
$db = App::getInstance()->db();
$registration = $db->selectOne(
"SELECT r.*, p.full_name_ar, p.member_id
FROM sa_registrations r
INNER JOIN sa_players p ON p.id = r.player_id
WHERE r.id = ? AND r.status = 'in_progress'",
"SELECT * FROM sa_registrations WHERE id = ? AND status = 'in_progress'",
[$registrationId]
);
if (!$registration) {
return ['success' => false, 'error' => 'التسجيل غير موجود أو مكتمل'];
}
if ((int) $registration['photo_captured'] === 0) {
return ['success' => false, 'error' => 'يجب التقاط الصورة أولاً'];
if (empty($disciplineIds)) {
return ['success' => false, 'error' => 'يجب اختيار نشاط واحد على الأقل'];
}
if (empty($registration['group_id'])) {
return ['success' => false, 'error' => 'يجب اختيار النشاط أولاً'];
}
$subscriptionAmount = (float) ($registration['subscription_amount'] ?? 0);
$cardFee = (float) $registration['card_fee'];
$formFee = (float) $registration['form_fee'];
$totalSubscription = $subscriptionAmount + $cardFee + $formFee;
if ($totalSubscription <= 0) {
return ['success' => false, 'error' => 'إجمالي الرسوم غير صالح'];
}
$memberId = (int) ($registration['member_id'] ?? 0);
$description = 'اشتراك نشاط رياضي — ' . ($registration['full_name_ar'] ?? '');
$result = PaymentRequestService::createRequest([
'member_id' => $memberId,
'payment_type' => 'sports_subscription',
'amount' => (string) $totalSubscription,
'description_ar' => $description,
'related_entity_type' => 'sa_registrations',
'related_entity_id' => $registrationId,
]);
$disciplines = $db->select(
"SELECT id, name_ar FROM sa_disciplines WHERE id IN (" . implode(',', array_map('intval', $disciplineIds)) . ") AND is_active = 1 AND is_archived = 0"
);
if (!$result['success']) {
return $result;
$selected = [];
foreach ($disciplines as $d) {
$selected[] = ['id' => (int) $d['id'], 'name' => $d['name_ar']];
}
$db->update('sa_registrations', [
'status' => 'pending_payment',
'payment_request_id' => (int) $result['request_id'],
'payment_status' => 'pending',
'selected_disciplines' => json_encode($selected, JSON_UNESCAPED_UNICODE),
'updated_at' => date('Y-m-d H:i:s'),
], 'id = ?', [$registrationId]);
return [
'success' => true,
'request_id' => $result['request_id'],
'request_number' => $result['request_number'],
'amount' => $totalSubscription,
];
return ['success' => true, 'selected' => $selected];
}
public static function submitToPaymentQueue(int $registrationId): array
{
return ['success' => false, 'error' => 'تم إلغاء هذه الخطوة — الدفع يتم في خطوة الاستمارة'];
}
public static function generateCard(int $registrationId): array
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقييم: <?= e($registration['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/coach-assessment" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة للقائمة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Player Info Card -->
<div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;display:flex;align-items:center;gap:14px;">
<div style="width:64px;height:64px;border-radius:10px;overflow:hidden;background:#F3F4F6;flex-shrink:0;">
<?php if (!empty($registration['photo_path'])): ?>
<img src="/<?= e($registration['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9CA3AF;"><i data-lucide="user" style="width:28px;height:28px;"></i></div>
<?php endif; ?>
</div>
<div style="flex:1;">
<div style="font-size:18px;font-weight:700;color:#1A1A2E;"><?= e($registration['full_name_ar']) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;">
<span style="padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;background:<?= ($registration['p_type'] ?? 'non_member') === 'member' ? '#ECFDF5' : '#FEF3C7' ?>;color:<?= ($registration['p_type'] ?? 'non_member') === 'member' ? '#059669' : '#D97706' ?>;">
<?= ($registration['p_type'] ?? 'non_member') === 'member' ? 'عضو' : 'غير عضو' ?>
</span>
<?php if (!empty($registration['player_nid'])): ?>
<span style="margin-right:8px;direction:ltr;display:inline-block;"><?= e($registration['player_nid']) ?></span>
<?php endif; ?>
<?php if (!empty($registration['date_of_birth'])): ?>
<?php
$age = (int) ((time() - strtotime($registration['date_of_birth'])) / (365.25 * 86400));
?>
<span style="margin-right:8px;"><?= $age ?> سنة</span>
<?php endif; ?>
</div>
</div>
<div style="text-align:left;">
<div style="font-size:11px;color:#6B7280;">رقم التسجيل</div>
<div style="font-size:13px;font-weight:600;direction:ltr;"><?= e($registration['registration_number']) ?></div>
</div>
</div>
<?php
$selectedDisc = [];
if (!empty($registration['selected_disciplines'])) {
$decoded = json_decode($registration['selected_disciplines'], true);
if (is_array($decoded)) $selectedDisc = $decoded;
}
if (!empty($selectedDisc)):
?>
<div style="padding:0 20px 16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<span style="font-size:12px;color:#6B7280;font-weight:600;">الأنشطة المطلوبة:</span>
<?php foreach ($selectedDisc as $disc): ?>
<span style="padding:4px 10px;border-radius:6px;background:#EFF6FF;color:#2563EB;font-size:12px;font-weight:600;"><?= e($disc['name'] ?? $disc) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Assessment Form -->
<form method="POST" action="/sa/coach-assessment/<?= (int) $registration['id'] ?>/submit">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="target" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">التقييم الفني</h3>
</div>
<div style="padding:20px;">
<!-- Skill Level -->
<div class="form-group" style="margin-bottom:20px;">
<label class="form-label">المستوى <span style="color:#DC2626;">*</span></label>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;">
<label style="display:flex;align-items:center;justify-content:center;gap:8px;padding:14px;border:2px solid #E5E7EB;border-radius:10px;cursor:pointer;transition:all 0.2s;">
<input type="radio" name="skill_level" value="beginner" required style="accent-color:#2563EB;width:18px;height:18px;">
<span style="font-weight:600;font-size:14px;">مبتدئ</span>
</label>
<label style="display:flex;align-items:center;justify-content:center;gap:8px;padding:14px;border:2px solid #E5E7EB;border-radius:10px;cursor:pointer;transition:all 0.2s;">
<input type="radio" name="skill_level" value="intermediate" style="accent-color:#059669;width:18px;height:18px;">
<span style="font-weight:600;font-size:14px;">متوسط</span>
</label>
<label style="display:flex;align-items:center;justify-content:center;gap:8px;padding:14px;border:2px solid #E5E7EB;border-radius:10px;cursor:pointer;transition:all 0.2s;">
<input type="radio" name="skill_level" value="advanced" style="accent-color:#7C3AED;width:18px;height:18px;">
<span style="font-weight:600;font-size:14px;">متقدم</span>
</label>
</div>
</div>
<!-- Assessment Notes -->
<div class="form-group" style="margin-bottom:20px;">
<label class="form-label">ملاحظات التقييم</label>
<textarea name="assessment_notes" class="form-input" rows="3" placeholder="ملاحظات المدرب عن مستوى اللاعب..."></textarea>
</div>
</div>
</div>
<!-- Group Selection -->
<div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="users" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">تعيين في مجموعة <span style="color:#DC2626;">*</span></h3>
</div>
<div style="padding:16px;">
<input type="text" id="groupSearch" class="form-input" placeholder="ابحث عن مجموعة..." style="margin-bottom:12px;padding:12px;font-size:14px;border-radius:10px;">
<div style="max-height:400px;overflow-y:auto;display:grid;gap:8px;">
<?php foreach ($groups as $g): ?>
<label class="group-radio-option" data-name="<?= e($g['name_ar'] . ' ' . ($g['discipline_name'] ?? '') . ' ' . ($g['program_name'] ?? '')) ?>"
style="display:flex;align-items:center;gap:12px;padding:14px;border:2px solid #E5E7EB;border-radius:10px;cursor:pointer;transition:all 0.2s;">
<input type="radio" name="group_id" value="<?= (int) $g['id'] ?>" required style="accent-color:#2563EB;width:20px;height:20px;flex-shrink:0;">
<div style="flex:1;min-width:0;">
<div style="font-weight:600;font-size:14px;"><?= e($g['name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;margin-top:2px;">
<?= e($g['discipline_name'] ?? '') ?><?= e($g['program_name'] ?? '') ?>
<?php if (!empty($g['coach_name'])): ?> | مدرب: <?= e($g['coach_name']) ?><?php endif; ?>
</div>
</div>
<div style="text-align:left;flex-shrink:0;">
<div style="font-size:14px;font-weight:700;color:#059669;">
<?= number_format(($registration['p_type'] ?? 'non_member') === 'member' ? (float) $g['monthly_fee_member'] : (float) $g['monthly_fee_nonmember'], 0) ?> ج.م
</div>
<div style="font-size:11px;color:#6B7280;"><?= (int) $g['current_count'] ?>/<?= (int) $g['max_capacity'] ?></div>
</div>
</label>
<?php endforeach; ?>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="width:100%;padding:16px;font-size:16px;font-weight:700;border-radius:12px;">
<i data-lucide="check-circle" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> تأكيد التقييم والتعيين
</button>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var search = document.getElementById('groupSearch');
if (search) {
search.addEventListener('input', function() {
var q = this.value.toLowerCase();
document.querySelectorAll('.group-radio-option').forEach(function(el) {
el.style.display = (!q || el.dataset.name.toLowerCase().indexOf(q) >= 0) ? '' : 'none';
});
});
}
document.querySelectorAll('input[name="group_id"]').forEach(function(radio) {
radio.addEventListener('change', function() {
document.querySelectorAll('.group-radio-option').forEach(function(el) {
el.style.borderColor = '#E5E7EB';
el.style.background = '';
});
if (this.checked) {
this.closest('.group-radio-option').style.borderColor = '#2563EB';
this.closest('.group-radio-option').style.background = '#F0F7FF';
}
});
});
document.querySelectorAll('input[name="skill_level"]').forEach(function(radio) {
radio.addEventListener('change', function() {
document.querySelectorAll('input[name="skill_level"]').forEach(function(r) {
r.closest('label').style.borderColor = '#E5E7EB';
r.closest('label').style.background = '';
});
this.closest('label').style.borderColor = '#2563EB';
this.closest('label').style.background = '#EFF6FF';
});
});
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تقييم اللاعبين — المدربين<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="clipboard-check" style="width:20px;height:20px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:16px;">لاعبين في انتظار التقييم الفني</h3>
<span style="margin-right:auto;background:#FEF3C7;color:#92400E;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:700;"><?= count($players) ?></span>
</div>
<?php if (empty($players)): ?>
<div style="padding:60px 24px;text-align:center;color:#6B7280;">
<i data-lucide="check-circle" style="width:48px;height:48px;color:#D1D5DB;display:block;margin:0 auto 15px;"></i>
<div style="font-size:15px;">لا يوجد لاعبين في انتظار التقييم</div>
<div style="font-size:13px;margin-top:8px;">جميع اللاعبين المسجلين تم تعيينهم في مجموعات</div>
</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">اللاعب</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">النوع</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">الأنشطة المطلوبة</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;color:#374151;">تاريخ التسجيل</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;color:#374151;">إجراء</th>
</tr>
</thead>
<tbody>
<?php foreach ($players as $p): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;">
<div style="display:flex;align-items:center;gap:10px;">
<div style="width:40px;height:40px;border-radius:8px;overflow:hidden;background:#F3F4F6;flex-shrink:0;">
<?php if (!empty($p['photo_path'])): ?>
<img src="/<?= e($p['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;">
<?php else: ?>
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#9CA3AF;"><i data-lucide="user" style="width:18px;height:18px;"></i></div>
<?php endif; ?>
</div>
<div>
<div style="font-weight:600;"><?= e($p['full_name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;"><?= e($p['national_id'] ?? '—') ?></div>
</div>
</div>
</td>
<td style="padding:12px 16px;">
<span style="padding:3px 8px;border-radius:6px;font-size:11px;font-weight:600;background:<?= $p['player_type'] === 'member' ? '#ECFDF5' : '#FEF3C7' ?>;color:<?= $p['player_type'] === 'member' ? '#059669' : '#D97706' ?>;">
<?= $p['player_type'] === 'member' ? 'عضو' : 'غير عضو' ?>
</span>
</td>
<td style="padding:12px 16px;">
<?php
$disciplines = [];
if (!empty($p['selected_disciplines'])) {
$decoded = json_decode($p['selected_disciplines'], true);
if (is_array($decoded)) {
$disciplines = $decoded;
}
}
if (!empty($disciplines)):
foreach ($disciplines as $disc):
?>
<span style="display:inline-block;padding:2px 8px;border-radius:4px;background:#EFF6FF;color:#2563EB;font-size:11px;font-weight:600;margin-left:4px;"><?= e($disc['name'] ?? $disc) ?></span>
<?php endforeach; else: ?>
<span style="color:#9CA3AF;font-size:12px;">لم يحدد</span>
<?php endif; ?>
</td>
<td style="padding:12px 16px;font-size:13px;color:#6B7280;direction:ltr;text-align:right;">
<?= e(substr($p['registration_date'], 0, 10)) ?>
</td>
<td style="padding:12px 16px;text-align:center;">
<a href="/sa/coach-assessment/<?= (int) $p['registration_id'] ?>" class="btn btn-sm btn-primary" style="padding:8px 16px;font-size:13px;">
<i data-lucide="clipboard-check" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> تقييم
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
......@@ -16,7 +16,7 @@
<div style="display:flex;justify-content:space-between;align-items:center;position:relative;">
<div style="position:absolute;top:50%;left:40px;right:40px;height:3px;background:#E5E7EB;z-index:0;transform:translateY(-50%);"></div>
<?php
$steps = ['الاستمارة', 'الصورة', 'النشاط', 'الدفع', 'الاستلام'];
$steps = ['الدفع', 'الصورة', 'الأنشطة', 'الاستلام'];
foreach ($steps as $i => $label):
$stepNum = $i + 1;
$isActive = $stepNum == ($step ?? 1);
......@@ -60,21 +60,21 @@
</div>
</div>
<!-- Step 1: Form Fee Payment (استمارة اشتراك) -->
<div id="step-form" class="wizard-step" style="<?= ($step ?? 1) == 1 ? '' : 'display:none;' ?>">
<!-- Step 1: Pay 100 EGP -->
<div id="step-pay" class="wizard-step" style="<?= ($step ?? 1) == 1 ? '' : 'display:none;' ?>">
<div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:16px;font-weight:600;"><i data-lucide="file-text" 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="banknote" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> رسوم التسجيل</h3>
</div>
<div style="padding:24px;text-align:center;">
<div style="max-width:400px;margin:0 auto;">
<div style="width:80px;height:80px;border-radius:50%;background:#EFF6FF;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;">
<i data-lucide="receipt" style="width:40px;height:40px;color:#2563EB;"></i>
</div>
<p style="font-size:15px;color:#374151;margin:0 0 8px;font-weight:600;">رسوم استمارة الاشتراك</p>
<p style="font-size:13px;color:#6B7280;margin:0 0 20px;">يجب سداد رسوم الاستمارة قبل إتمام التسجيل والتقاط الصورة</p>
<div style="font-size:36px;font-weight:800;color:#2563EB;margin-bottom:8px;"><?= number_format((float) $registration['registration_fee'], 0) ?> <span style="font-size:16px;">ج.م</span></div>
<div style="font-size:12px;color:#6B7280;margin-bottom:24px;"><?= $registration['player_type'] === 'member' ? 'رسوم عضو' : 'رسوم غير عضو' ?></div>
<p style="font-size:15px;color:#374151;margin:0 0 8px;font-weight:600;">رسوم تسجيل النشاط الرياضي</p>
<p style="font-size:13px;color:#6B7280;margin:0 0 20px;">مبلغ شامل يغطي الاستمارة والكارت — بدون أي رسوم إضافية</p>
<div style="font-size:42px;font-weight:800;color:#2563EB;margin-bottom:8px;">100 <span style="font-size:18px;">ج.م</span></div>
<div style="font-size:12px;color:#6B7280;margin-bottom:24px;">مبلغ ثابت لكل شخص</div>
<?php if (($registration['form_payment_status'] ?? 'unpaid') === 'pending'): ?>
<div style="padding:16px;background:#FEF3C7;border-radius:10px;">
......@@ -83,7 +83,7 @@
</div>
<?php else: ?>
<button type="button" id="btnPayForm" class="btn btn-primary" style="width:100%;padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;">
<i data-lucide="banknote" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i> إرسال للخزينة — <?= number_format((float) $registration['registration_fee'], 0) ?> ج.م
<i data-lucide="banknote" style="width:20px;height:20px;vertical-align:middle;margin-left:6px;"></i> إرسال للخزينة — 100 ج.م
</button>
<?php endif; ?>
</div>
......@@ -123,163 +123,66 @@
</div>
</div>
<button type="button" id="btnPhotoNext" class="btn btn-primary" style="width:100%;padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;" <?= (int) $registration['photo_captured'] ? '' : 'disabled' ?>>
<i data-lucide="arrow-left" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> التالي — اختيار النشاط
<i data-lucide="arrow-left" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> التالي — اختيار الأنشطة
</button>
</div>
<!-- Step 3: Activity/Group Selection -->
<!-- Step 3: Discipline Selection (checkboxes) -->
<div id="step-activity" class="wizard-step" style="<?= ($step ?? 1) == 3 ? '' : 'display:none;' ?>">
<?php if (!in_array($registration['medical_status'] ?? 'pending', ['fit', 'conditional'])): ?>
<div style="background:#FEF3C7;border:1px solid #F59E0B;border-radius:8px;padding:10px 14px;margin-bottom:12px;display:flex;align-items:center;gap:8px;">
<i data-lucide="alert-triangle" style="width:16px;height:16px;color:#D97706;flex-shrink:0;"></i>
<span style="font-size:12px;color:#92400E;font-weight:600;">اللاعب بدون شهادة طبية سارية — يمكنه التسجيل لكن سيظهر بعلامة تحذير</span>
</div>
<?php endif; ?>
<div class="card" style="margin-bottom:16px;">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;">
<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 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>
<p style="font-size:13px;color:#6B7280;margin:0 0 16px;">اختر الأنشطة التي يرغب اللاعب في ممارستها — سيتم التقييم الفني وتعيين المجموعة لاحقاً بواسطة المدرب.</p>
<div id="disciplinesList" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<?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>';
$selectedIds = [];
if (!empty($registration['selected_disciplines'])) {
$decoded = json_decode($registration['selected_disciplines'], true);
if (is_array($decoded)) {
foreach ($decoded as $d) {
$selectedIds[] = is_array($d) ? (int) ($d['id'] ?? 0) : (int) $d;
}
}
}
foreach ($disciplines ?? [] as $d):
$isChecked = in_array((int) $d['id'], $selectedIds);
?>
</div>
<div style="margin-bottom:12px;">
<input type="text" id="groupSearch" class="form-input" placeholder="ابحث عن نشاط أو مجموعة..." style="padding:14px;font-size:15px;border-radius:10px;">
</div>
<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): ?>
<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;">
<div style="display:flex;justify-content:space-between;align-items:flex-start;">
<div style="flex:1;">
<div style="font-weight:700;font-size:15px;color:#1A1A2E;"><?= e($g['name_ar']) ?></div>
<div style="font-size:13px;color:#6B7280;margin-top:4px;"><?= e($g['discipline_name'] ?? '') ?><?= e($g['program_name'] ?? '') ?></div>
</div>
<div style="text-align:left;flex-shrink:0;padding-right:10px;">
<div style="font-size:16px;font-weight:800;color:#059669;">
<?= number_format($registration['player_type'] === 'member' ? (float) $g['monthly_fee_member'] : (float) $g['monthly_fee_nonmember'], 0) ?>
</div>
<div style="font-size:11px;color:#6B7280;">ج.م / شهر</div>
</div>
</div>
<div style="margin-top:8px;display:flex;align-items:center;gap:8px;">
<div style="flex:1;height:6px;background:#E5E7EB;border-radius:3px;overflow:hidden;">
<div style="height:100%;background:<?= ((int) $g['current_count'] / max(1, (int) $g['max_capacity'])) > 0.8 ? '#F59E0B' : '#059669' ?>;width:<?= min(100, ((int) $g['current_count'] / max(1, (int) $g['max_capacity'])) * 100) ?>%;border-radius:3px;"></div>
</div>
<span style="font-size:11px;color:#6B7280;white-space:nowrap;"><?= (int) $g['current_count'] ?>/<?= (int) $g['max_capacity'] ?></span>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
<!-- Subscription Options -->
<div class="card" style="margin-bottom:16px;" id="subscriptionOptions">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;font-weight:600;">مدة الاشتراك والخصومات</h3>
</div>
<div style="padding:16px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px;">
<label style="display:flex;align-items:center;justify-content:center;gap:8px;padding:14px;border:2px solid #2563EB;border-radius:10px;cursor:pointer;background:#EFF6FF;min-height:50px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;">
<input type="radio" name="subscription_months" value="1" checked style="accent-color:#2563EB;width:18px;height:18px;">
<span style="font-weight:700;font-size:15px;">شهر</span>
<label class="disc-option" style="display:flex;align-items:center;gap:10px;padding:14px 16px;border:2px solid <?= $isChecked ? '#2563EB' : '#E5E7EB' ?>;border-radius:10px;cursor:pointer;transition:all 0.2s;background:<?= $isChecked ? '#F0F7FF' : '' ?>;">
<input type="checkbox" name="discipline_ids[]" value="<?= (int) $d['id'] ?>" <?= $isChecked ? 'checked' : '' ?> style="accent-color:#2563EB;width:20px;height:20px;flex-shrink:0;">
<i data-lucide="<?= e($d['icon'] ?? 'activity') ?>" style="width:20px;height:20px;color:#0D7377;flex-shrink:0;"></i>
<span style="font-weight:600;font-size:14px;"><?= e($d['name_ar']) ?></span>
</label>
<label style="display:flex;align-items:center;justify-content:center;gap:8px;padding:14px;border:2px solid #E5E7EB;border-radius:10px;cursor:pointer;position:relative;min-height:50px;touch-action:manipulation;-webkit-tap-highlight-color:transparent;">
<input type="radio" name="subscription_months" value="3" style="accent-color:#059669;width:18px;height:18px;">
<span style="font-weight:700;font-size:15px;">3 أشهر</span>
<span style="position:absolute;top:-8px;left:50%;transform:translateX(-50%);background:#059669;color:#fff;font-size:10px;padding:2px 8px;border-radius:4px;font-weight:700;white-space:nowrap;">خصم 15%</span>
</label>
</div>
<label style="display:flex;align-items:center;gap:10px;padding:14px;border:1px solid #E5E7EB;border-radius:10px;cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;">
<input type="checkbox" id="hasSibling" style="accent-color:#7C3AED;width:20px;height:20px;">
<span style="font-size:14px;font-weight:500;">يوجد أخ/أخت مسجل بنفس النشاط</span>
<span style="font-size:11px;color:#7C3AED;font-weight:700;margin-right:auto;">خصم أشقاء</span>
</label>
</div>
<?php endforeach; ?>
</div>
<!-- Fee Breakdown -->
<div class="card" style="margin-bottom:16px;" id="feeBreakdown">
<div style="padding:16px;">
<table style="width:100%;font-size:14px;">
<tr><td style="padding:8px 0;">رسوم الكارت</td><td style="text-align:left;font-weight:600;" id="feeCard"><?= number_format((float) $registration['card_fee'], 0) ?> ج.م</td></tr>
<tr><td style="padding:8px 0;">رسوم الاستمارة</td><td style="text-align:left;font-weight:600;" id="feeForm"><?= number_format((float) $registration['form_fee'], 0) ?> ج.م</td></tr>
<tr><td style="padding:8px 0;">اشتراك النشاط</td><td style="text-align:left;font-weight:700;color:#059669;" id="feeSub"><?= number_format((float) ($registration['subscription_amount'] ?? 0), 0) ?> ج.م</td></tr>
<tr id="feeDiscountRow" style="display:none;"><td style="padding:8px 0;color:#059669;">خصم</td><td style="text-align:left;font-weight:600;color:#059669;" id="feeDiscount"></td></tr>
<tr style="border-top:2px solid #E5E7EB;"><td style="padding:12px 0;font-weight:800;font-size:16px;">إجمالي الاشتراك</td><td style="text-align:left;font-weight:800;font-size:18px;color:#2563EB;" id="feeTotal"><?= number_format((float) $registration['subscription_amount'] + (float) $registration['card_fee'] + (float) $registration['form_fee'], 0) ?> ج.م</td></tr>
</table>
<div id="discountNotes" style="display:none;margin-top:10px;padding:10px 14px;background:#ECFDF5;border-radius:8px;font-size:12px;color:#059669;"></div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr auto;gap:10px;">
<button type="button" id="btnActivityNext" class="btn btn-primary" style="padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;" <?= $selectedGroup ? '' : 'disabled' ?>>
<i data-lucide="banknote" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> إرسال للخزينة
<button type="button" id="btnSaveDisciplines" class="btn btn-primary" style="width:100%;padding:16px;font-size:16px;font-weight:700;border-radius:12px;min-height:56px;">
<i data-lucide="check" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> حفظ الأنشطة المختارة
</button>
<button type="button" class="btn btn-outline" style="padding:16px;border-radius:12px;min-height:56px;" onclick="showStep('photo')">
<i data-lucide="arrow-right" style="width:18px;height:18px;"></i>
</button>
</div>
</div>
<!-- Step 4: Pending Payment -->
<div id="step-payment" class="wizard-step" style="<?= ($step ?? 1) == 4 ? '' : 'display:none;' ?>">
<div class="card" style="margin-bottom:16px;">
<div style="padding:40px 24px;text-align:center;">
<div style="width:80px;height:80px;border-radius:50%;background:#FEF3C7;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;">
<i data-lucide="clock" style="width:40px;height:40px;color:#D97706;"></i>
</div>
<h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#1A1A2E;">بانتظار دفع الاشتراك</h3>
<p style="color:#6B7280;font-size:14px;margin:0;">تم إرسال طلب دفع الاشتراك — ينتظر التحصيل من الخزينة</p>
<div style="margin-top:16px;font-size:24px;font-weight:800;color:#2563EB;"><?= number_format((float) $registration['subscription_amount'] + (float) $registration['card_fee'] + (float) $registration['form_fee'], 0) ?> ج.م</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<a href="/sa/registration/<?= (int) $registration['id'] ?>/print-form" target="_blank" class="btn btn-outline" style="padding:14px;font-size:14px;border-radius:10px;min-height:50px;text-align:center;">
<i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة الاستمارة
</a>
<form method="POST" action="/sa/registration/<?= (int) $registration['id'] ?>/cancel" style="display:contents;">
<?= csrf_field() ?>
<button type="submit" class="btn btn-outline" style="padding:14px;font-size:14px;border-radius:10px;min-height:50px;color:#DC2626;border-color:#DC2626;" onclick="return confirm('هل تريد إلغاء التسجيل؟')">
<i data-lucide="x" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إلغاء
</button>
</form>
</div>
</div>
<!-- Step 5: Complete -->
<div id="step-complete" class="wizard-step" style="<?= ($step ?? 1) == 5 ? '' : 'display:none;' ?>">
<!-- Step 4: Complete (print + card) -->
<div id="step-complete" class="wizard-step" style="<?= ($step ?? 1) == 4 ? '' : 'display:none;' ?>">
<div class="card" style="margin-bottom:16px;">
<div style="padding:40px 24px;text-align:center;">
<div style="width:80px;height:80px;border-radius:50%;background:#ECFDF5;margin:0 auto 16px;display:flex;align-items:center;justify-content:center;">
<i data-lucide="check-circle" style="width:40px;height:40px;color:#059669;"></i>
</div>
<h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#059669;">تم التسجيل بنجاح!</h3>
<p style="color:#6B7280;font-size:14px;margin:0;">تم الدفع وتفعيل الاشتراك</p>
<p style="color:#6B7280;font-size:14px;margin:0;">تم دفع رسوم التسجيل — اللاعب في انتظار التقييم الفني من المدرب</p>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<a href="/sa/registration/<?= (int) $registration['id'] ?>/print-form" target="_blank" class="btn btn-outline" style="padding:14px;font-size:14px;border-radius:10px;min-height:50px;text-align:center;">
<i data-lucide="file-text" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة الاستمارة
<i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة الاستمارة
</a>
<button type="button" id="btnGenerateCard" class="btn btn-primary" style="padding:14px;font-size:14px;border-radius:10px;min-height:50px;" <?= (int) $registration['card_generated'] ? 'disabled' : '' ?>>
<button type="button" id="btnGenerateCard" class="btn btn-primary" style="padding:14px;font-size:14px;border-radius:10px;min-height:50px;" <?= (int) ($registration['card_generated'] ?? 0) ? 'disabled' : '' ?>>
<i data-lucide="credit-card" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> إنشاء الكارت
</button>
</div>
<?php if ((int) $registration['card_generated']): ?>
<?php if ((int) ($registration['card_generated'] ?? 0)): ?>
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<a href="/sa/registration/<?= (int) $registration['id'] ?>/print-card" target="_blank" class="btn btn-outline" style="padding:14px;font-size:14px;border-radius:10px;min-height:50px;text-align:center;">
<i data-lucide="printer" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> طباعة الكارت
......@@ -301,7 +204,6 @@
<div style="padding:20px;">
<form id="startForm">
<div style="display:grid;grid-template-columns:1fr;gap:14px;">
<!-- Member Quick Lookup -->
<div>
<label class="form-label">رقم العضوية <span style="font-weight:400;color:#6B7280;font-size:12px;">(للأعضاء — يملأ كل البيانات تلقائياً)</span></label>
<input type="text" name="membership_number" class="form-input" style="padding:14px;font-size:18px;border-radius:10px;direction:ltr;text-align:right;letter-spacing:1px;" inputmode="numeric" placeholder="أدخل رقم العضوية" id="regMembershipNo">
......@@ -351,7 +253,7 @@
</div>
<div style="padding:0;">
<?php foreach ($recentRegistrations as $reg): ?>
<a href="/sa/registration/<?= (int) $reg['id'] ?>" style="display:flex;align-items:center;padding:14px 16px;border-bottom:1px solid #F3F4F6;text-decoration:none;color:inherit;touch-action:manipulation;-webkit-tap-highlight-color:rgba(37,99,235,0.1);">
<a href="/sa/registration/<?= (int) $reg['id'] ?>" style="display:flex;align-items:center;padding:14px 16px;border-bottom:1px solid #F3F4F6;text-decoration:none;color:inherit;">
<div style="width:40px;height:40px;border-radius:8px;overflow:hidden;background:#F3F4F6;flex-shrink:0;display:flex;align-items:center;justify-content:center;margin-left:12px;">
<?php if (!empty($reg['photo_path'])): ?>
<img src="/<?= e($reg['photo_path']) ?>" style="width:100%;height:100%;object-fit:cover;">
......@@ -365,7 +267,7 @@
</div>
<div style="text-align:left;flex-shrink:0;">
<?php
$statusMap = ['in_progress' => ['جاري', '#F59E0B'], 'pending_payment' => ['بانتظار', '#D97706'], 'completed' => ['مكتمل', '#059669'], 'cancelled' => ['ملغى', '#DC2626']];
$statusMap = ['in_progress' => ['جاري', '#F59E0B'], 'pending_payment' => ['بانتظار', '#D97706'], 'completed' => ['مكتمل', '#059669'], 'assessed' => ['تم التقييم', '#2563EB'], 'cancelled' => ['ملغى', '#DC2626']];
$s = $statusMap[$reg['status']] ?? [$reg['status'], '#6B7280'];
?>
<span style="padding:3px 8px;border-radius:6px;font-size:11px;font-weight:600;background:<?= $s[1] ?>15;color:<?= $s[1] ?>;"><?= $s[0] ?></span>
......@@ -404,7 +306,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.membership_number && !regMembershipNo.value) regMembershipNo.value = data.membership_number;
}
// Membership number lookup
var regMemberTimer = null;
if (regMembershipNo) {
regMembershipNo.addEventListener('input', function() {
......@@ -425,7 +326,7 @@ document.addEventListener('DOMContentLoaded', function() {
if (data.success && data.member_id) {
memberLookupBox.style.background = '#ECFDF5';
memberLookupBox.style.color = '#059669';
memberLookupBox.innerHTML = '<strong>عضو فعال</strong> — ' + (data.name || '') + (data.is_member ? '' : ' <span style="color:#92400E;">(' + (data.reason || 'غير فعال') + ')</span>');
memberLookupBox.innerHTML = '<strong>عضو فعال</strong> — ' + (data.name || '');
fillMemberData(data);
} else {
memberLookupBox.style.background = '#FEF2F2';
......@@ -437,7 +338,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
// NID lookup
if (regNid) {
regNid.addEventListener('input', function() {
this.value = this.value.replace(/\D/g, '');
......@@ -452,10 +352,6 @@ document.addEventListener('DOMContentLoaded', function() {
memberBadge.style.background = '#ECFDF5';
memberBadge.style.color = '#059669';
memberBadge.innerHTML = '<strong>عضو فعال</strong>' + (data.membership_number ? ' — رقم ' + data.membership_number : '');
} else if (data.reason) {
memberBadge.style.background = '#FEF3C7';
memberBadge.style.color = '#92400E';
memberBadge.innerHTML = '<strong>غير عضو</strong> — ' + data.reason;
} else {
memberBadge.style.background = '#F3F4F6';
memberBadge.style.color = '#374151';
......@@ -507,7 +403,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
window.showStep = showStep;
// Step 1: Pay form fee
// Step 1: Pay 100 EGP
var btnPayForm = document.getElementById('btnPayForm');
if (btnPayForm) {
btnPayForm.addEventListener('click', function() {
......@@ -523,7 +419,7 @@ document.addEventListener('DOMContentLoaded', function() {
} else {
alert(data.error || 'حدث خطأ');
btnPayForm.disabled = false;
btnPayForm.textContent = 'إرسال للخزينة';
btnPayForm.textContent = 'إرسال للخزينة — 100 ج.م';
}
});
});
......@@ -606,131 +502,53 @@ document.addEventListener('DOMContentLoaded', function() {
btnPhotoNext.addEventListener('click', function() { showStep('activity'); });
}
// Step 3: Activity selection
var selectedGroupId = null;
function getSubscriptionMonths() {
var checked = document.querySelector('input[name="subscription_months"]:checked');
return checked ? parseInt(checked.value) : 1;
}
function submitActivitySelection(gid) {
var months = getSubscriptionMonths();
var hasSibling = document.getElementById('hasSibling').checked ? 1 : 0;
fetch('/sa/registration/' + regId + '/activity', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({group_id: gid, months: months, has_sibling: hasSibling, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
var sub = data.subscription_amount || 0;
var card = <?= isset($registration) ? (int) $registration['card_fee'] : 0 ?>;
var form = <?= isset($registration) ? (int) $registration['form_fee'] : 0 ?>;
document.getElementById('feeSub').textContent = Number(sub).toLocaleString() + ' ج.م';
document.getElementById('feeTotal').textContent = Number(sub + card + form).toLocaleString() + ' ج.م';
document.getElementById('feeBreakdown').style.display = '';
document.getElementById('btnActivityNext').disabled = false;
var discountRow = document.getElementById('feeDiscountRow');
var discountNotes = document.getElementById('discountNotes');
if (data.total_discount && data.total_discount > 0) {
discountRow.style.display = '';
document.getElementById('feeDiscount').textContent = '- ' + data.total_discount.toLocaleString() + ' ج.م';
var notes = '';
if (data.discounts && data.discounts.length > 0) {
data.discounts.forEach(function(d) { notes += '✓ ' + (d.name_ar || d.code) + ' (' + d.amount.toLocaleString() + ' ج.م)<br>'; });
}
if (notes) { discountNotes.innerHTML = notes; discountNotes.style.display = ''; }
// Step 3: Discipline selection (checkboxes)
document.querySelectorAll('.disc-option input[type="checkbox"]').forEach(function(cb) {
cb.addEventListener('change', function() {
var label = this.closest('.disc-option');
if (this.checked) {
label.style.borderColor = '#2563EB';
label.style.background = '#F0F7FF';
} else {
discountRow.style.display = 'none';
discountNotes.style.display = 'none';
}
}
});
label.style.borderColor = '#E5E7EB';
label.style.background = '';
}
document.querySelectorAll('.group-option').forEach(function(el) {
el.addEventListener('click', function() {
document.querySelectorAll('.group-option').forEach(function(g) {
g.style.borderColor = '#E5E7EB';
g.style.background = '';
});
this.style.borderColor = '#2563EB';
this.style.background = '#F0F7FF';
selectedGroupId = this.dataset.groupId;
submitActivitySelection(selectedGroupId);
});
});
document.querySelectorAll('input[name="subscription_months"]').forEach(function(radio) {
radio.addEventListener('change', function() {
document.querySelectorAll('input[name="subscription_months"]').forEach(function(r) {
r.closest('label').style.borderColor = '#E5E7EB';
r.closest('label').style.background = '';
});
this.closest('label').style.borderColor = this.value === '3' ? '#059669' : '#2563EB';
this.closest('label').style.background = this.value === '3' ? '#F0FDF4' : '#EFF6FF';
if (selectedGroupId) submitActivitySelection(selectedGroupId);
});
});
var btnSaveDisciplines = document.getElementById('btnSaveDisciplines');
if (btnSaveDisciplines) {
btnSaveDisciplines.addEventListener('click', function() {
var checked = document.querySelectorAll('.disc-option input[type="checkbox"]:checked');
var ids = [];
checked.forEach(function(cb) { ids.push(parseInt(cb.value)); });
var siblingCheckbox = document.getElementById('hasSibling');
if (siblingCheckbox) {
siblingCheckbox.addEventListener('change', function() {
if (selectedGroupId) submitActivitySelection(selectedGroupId);
});
if (ids.length === 0) {
alert('يجب اختيار نشاط واحد على الأقل');
return;
}
// 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
var groupSearch = document.getElementById('groupSearch');
if (groupSearch) {
groupSearch.addEventListener('input', filterGroups);
}
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';
});
}
// Submit subscription to payment
var btnActivityNext = document.getElementById('btnActivityNext');
if (btnActivityNext) {
btnActivityNext.addEventListener('click', function() {
this.disabled = true;
fetch('/sa/registration/' + regId + '/pay', {
this.textContent = 'جاري الحفظ...';
fetch('/sa/registration/' + regId + '/activity', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({_csrf_token: csrfToken})
body: JSON.stringify({discipline_ids: ids, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success) {
showStep('payment');
showStep('complete');
} else {
alert(data.error || 'حدث خطأ');
btnActivityNext.disabled = false;
btnSaveDisciplines.disabled = false;
btnSaveDisciplines.innerHTML = '<i data-lucide="check" style="width:18px;height:18px;vertical-align:middle;margin-left:6px;"></i> حفظ الأنشطة المختارة';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
});
});
}
// Generate card
// Step 4: Generate card
var btnGenCard = document.getElementById('btnGenerateCard');
if (btnGenCard) {
btnGenCard.addEventListener('click', function() {
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?><?= $coach ? 'تعديل مدرب: ' . e($coach['full_name_ar']) : 'إضافة مدرب سباحة' ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming/coaches" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<form method="POST" action="<?= $coach ? '/sa/swimming/coaches/' . (int) $coach['id'] : '/sa/swimming/coaches' ?>">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="waves" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;color:#0D7377;font-size:15px;">بيانات المدرب</h3>
</div>
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div class="form-group">
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e(old('full_name_ar') ?: ($coach['full_name_ar'] ?? '')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" value="<?= e(old('full_name_en') ?: ($coach['full_name_en'] ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" value="<?= e(old('national_id') ?: ($coach['national_id'] ?? '')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;" <?= $coach ? 'readonly' : '' ?>>
</div>
<div class="form-group">
<label class="form-label">الهاتف</label>
<input type="text" name="phone" value="<?= e(old('phone') ?: ($coach['phone'] ?? '')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نوع التوظيف <span style="color:#DC2626;">*</span></label>
<select name="employment_type" class="form-select" required>
<?php $et = old('employment_type') ?: ($coach['employment_type'] ?? 'freelance'); ?>
<option value="freelance" <?= $et === 'freelance' ? 'selected' : '' ?>>مستقل (Freelance)</option>
<option value="staff" <?= $et === 'staff' ? 'selected' : '' ?>>موظف (Staff)</option>
<option value="contract" <?= $et === 'contract' ? 'selected' : '' ?>>تعاقد (Contract)</option>
</select>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary" style="padding:14px 30px;font-size:15px;">
<i data-lucide="check" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> <?= $coach ? 'حفظ التعديلات' : 'إضافة المدرب' ?>
</button>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مدربين السباحة<?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming/coaches/create" class="btn btn-primary"><i data-lucide="plus" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> إضافة مدرب</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Filters -->
<div class="card" style="margin-bottom:16px;padding:14px 16px;">
<form method="GET" action="/sa/swimming/coaches" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;">
<div style="flex:1;min-width:200px;">
<input type="text" name="search" value="<?= e($search) ?>" class="form-input" placeholder="بحث بالاسم أو الهاتف..." style="padding:10px 14px;">
</div>
<select name="filter" class="form-select" style="min-width:150px;padding:10px 14px;">
<option value="">— جميع الأنواع —</option>
<option value="freelance" <?= $filter === 'freelance' ? 'selected' : '' ?>>مستقل (freelance)</option>
<option value="staff" <?= $filter === 'staff' ? 'selected' : '' ?>>موظف (staff)</option>
<option value="contract" <?= $filter === 'contract' ? 'selected' : '' ?>>تعاقد (contract)</option>
</select>
<button type="submit" class="btn btn-outline" style="padding:10px 18px;">
<i data-lucide="search" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> بحث
</button>
</form>
</div>
<!-- Coaches List -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;">
<i data-lucide="waves" style="width:18px;height:18px;color:#0D7377;"></i>
<h3 style="margin:0;font-size:15px;color:#0D7377;">مدربين السباحة</h3>
<span style="margin-right:auto;background:#EFF6FF;color:#2563EB;padding:3px 10px;border-radius:12px;font-size:12px;font-weight:700;"><?= count($coaches) ?></span>
</div>
<?php if (empty($coaches)): ?>
<div style="padding:50px 24px;text-align:center;color:#6B7280;">
<i data-lucide="waves" style="width:48px;height:48px;color:#D1D5DB;display:block;margin:0 auto 15px;"></i>
<div style="font-size:15px;">لا يوجد مدربين سباحة</div>
</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:14px;">
<thead>
<tr style="background:#F9FAFB;border-bottom:1px solid #E5E7EB;">
<th style="padding:12px 16px;text-align:right;font-weight:600;">الكود</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;">الاسم</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;">الهاتف</th>
<th style="padding:12px 16px;text-align:right;font-weight:600;">نوع التوظيف</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;">مجموعات نشطة</th>
<th style="padding:12px 16px;text-align:center;font-weight:600;">إجراءات</th>
</tr>
</thead>
<tbody>
<?php foreach ($coaches as $c): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:12px 16px;font-size:12px;direction:ltr;text-align:right;color:#6B7280;"><?= e($c['code']) ?></td>
<td style="padding:12px 16px;font-weight:600;"><?= e($c['full_name_ar']) ?></td>
<td style="padding:12px 16px;direction:ltr;text-align:right;color:#6B7280;"><?= e($c['phone'] ?? '—') ?></td>
<td style="padding:12px 16px;">
<?php
$types = ['freelance' => 'مستقل', 'staff' => 'موظف', 'contract' => 'تعاقد'];
$colors = ['freelance' => '#7C3AED', 'staff' => '#059669', 'contract' => '#D97706'];
$t = $c['employment_type'] ?? 'freelance';
?>
<span style="padding:3px 8px;border-radius:6px;font-size:11px;font-weight:600;background:<?= $colors[$t] ?? '#6B7280' ?>15;color:<?= $colors[$t] ?? '#6B7280' ?>;">
<?= $types[$t] ?? $t ?>
</span>
</td>
<td style="padding:12px 16px;text-align:center;font-weight:600;"><?= (int) ($c['active_groups'] ?? 0) ?></td>
<td style="padding:12px 16px;text-align:center;">
<a href="/sa/swimming/coaches/<?= (int) $c['id'] ?>" class="btn btn-sm btn-outline" style="padding:6px 12px;font-size:12px;">عرض</a>
<a href="/sa/swimming/coaches/<?= (int) $c['id'] ?>/edit" class="btn btn-sm btn-outline" style="padding:6px 12px;font-size:12px;margin-right:4px;">تعديل</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>مدرب: <?= e($coach['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/swimming/coaches/<?= (int) $coach['id'] ?>/edit" class="btn btn-outline"><i data-lucide="edit" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> تعديل</a>
<a href="/sa/swimming/coaches" class="btn btn-outline"><i data-lucide="arrow-right" style="width:15px;height:15px;vertical-align:middle;margin-left:4px;"></i> العودة</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<!-- Coach Info -->
<div class="card" style="margin-bottom:16px;">
<div style="padding:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:16px;">
<div>
<div style="font-size:12px;color:#6B7280;">الكود</div>
<div style="font-size:15px;font-weight:600;direction:ltr;text-align:right;"><?= e($coach['code']) ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">الهاتف</div>
<div style="font-size:15px;font-weight:600;direction:ltr;text-align:right;"><?= e($coach['phone'] ?? '—') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">نوع التوظيف</div>
<?php $types = ['freelance' => 'مستقل', 'staff' => 'موظف', 'contract' => 'تعاقد']; ?>
<div style="font-size:15px;font-weight:600;"><?= $types[$coach['employment_type'] ?? ''] ?? ($coach['employment_type'] ?? '—') ?></div>
</div>
<div>
<div style="font-size:12px;color:#6B7280;">الرقم القومي</div>
<div style="font-size:15px;font-weight:600;direction:ltr;text-align:right;"><?= e($coach['national_id'] ?? '—') ?></div>
</div>
</div>
</div>
</div>
<!-- Active Groups -->
<div class="card" style="margin-bottom:16px;">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;font-weight:600;"><i data-lucide="users" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;color:#0D7377;"></i> المجموعات النشطة</h3>
</div>
<?php if (empty($groups)): ?>
<div style="padding:24px;text-align:center;color:#6B7280;font-size:13px;">لا يوجد مجموعات نشطة</div>
<?php else: ?>
<div style="padding:0;">
<?php foreach ($groups as $g): ?>
<div style="padding:12px 16px;border-bottom:1px solid #F3F4F6;display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-weight:600;font-size:14px;"><?= e($g['name_ar']) ?></div>
<div style="font-size:12px;color:#6B7280;"><?= e($g['code']) ?></div>
</div>
<span style="padding:3px 8px;border-radius:6px;font-size:11px;font-weight:600;background:#ECFDF5;color:#059669;"><?= e($g['role'] ?? 'مدرب') ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Lane Bookings -->
<div class="card">
<div style="padding:14px 16px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;font-size:15px;font-weight:600;"><i data-lucide="calendar" style="width:16px;height:16px;vertical-align:middle;margin-left:6px;color:#0D7377;"></i> حجوزات الحارات</h3>
</div>
<?php if (empty($laneBookings)): ?>
<div style="padding:24px;text-align:center;color:#6B7280;font-size:13px;">لا توجد حجوزات</div>
<?php else: ?>
<div style="overflow-x:auto;">
<table style="width:100%;border-collapse:collapse;font-size:13px;">
<thead>
<tr style="background:#F9FAFB;">
<th style="padding:10px 14px;text-align:right;">المرفق</th>
<th style="padding:10px 14px;text-align:right;">التاريخ</th>
<th style="padding:10px 14px;text-align:right;">الوقت</th>
<th style="padding:10px 14px;text-align:center;">الإشغال</th>
</tr>
</thead>
<tbody>
<?php foreach ($laneBookings as $b): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px 14px;"><?= e($b['facility_name'] ?? '—') ?></td>
<td style="padding:10px 14px;direction:ltr;text-align:right;"><?= e($b['booking_date'] ?? '') ?></td>
<td style="padding:10px 14px;direction:ltr;text-align:right;"><?= e(substr($b['start_time'] ?? '', 0, 5)) ?> - <?= e(substr($b['end_time'] ?? '', 0, 5)) ?></td>
<td style="padding:10px 14px;text-align:center;"><?= (int) ($b['current_occupancy'] ?? 0) ?>/<?= (int) ($b['max_occupancy'] ?? 0) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
});
</script>
<?php $__template->endSection(); ?>
......@@ -45,8 +45,11 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'إيجارات اللوكرات', 'label_en' => 'Locker Rentals', 'route' => '/sa/locker-rentals', 'permission' => 'sa.locker_rental.view','order' => 21],
['label_ar' => 'تذاكر السباحة', 'label_en' => 'Pool Tickets', 'route' => '/sa/pool-tickets', 'permission' => 'sa.pool_ticket.view', 'order' => 22],
['label_ar' => 'الألعاب الترفيهية','label_en' => 'Recreational Games','route' => '/sa/games', 'permission' => 'sa.game.view', 'order' => 23],
['label_ar' => '── تقييم المدربين ──','label_en' => '── Coach Assessment ──','route' => '#', 'permission' => 'sa.coach_assessment.view','order' => 25],
['label_ar' => 'تقييم اللاعبين', 'label_en' => 'Player Assessment','route' => '/sa/coach-assessment','permission' => 'sa.coach_assessment.view','order' => 26],
['label_ar' => '── السباحة ──', 'label_en' => '── Swimming ──', 'route' => '#', 'permission' => 'sa.swimming.dashboard','order' => 30],
['label_ar' => 'لوحة تحكم السباحة','label_en' => 'Swimming Dashboard','route' => '/sa/swimming', 'permission' => 'sa.swimming.dashboard','order' => 31],
['label_ar' => 'مدربين السباحة', 'label_en' => 'Swimming Coaches','route' => '/sa/swimming/coaches','permission' => 'sa.swimming.coach_manage','order' => 31.5],
['label_ar' => 'تسجيل لاعب سباحة', 'label_en' => 'Register Swimmer','route' => '/sa/swimming/register','permission' => 'sa.swimming.register','order' => 32],
['label_ar' => 'تعيين في مجموعة', 'label_en' => 'Assign to Group', 'route' => '/sa/swimming/assign','permission' => 'sa.swimming.assign', 'order' => 33],
],
......@@ -119,6 +122,9 @@ PermissionRegistry::register('sports_activity', [
'sa.institution.view' => ['ar' => 'عرض المؤسسات', 'en' => 'View Institutions'],
'sa.institution.manage' => ['ar' => 'إدارة المؤسسات', 'en' => 'Manage Institutions'],
'sa.enrollment.manage' => ['ar' => 'إدارة تسجيلات اللاعبين', 'en' => 'Manage Player Enrollments'],
'sa.coach_assessment.view' => ['ar' => 'عرض تقييم اللاعبين', 'en' => 'View Player Assessments'],
'sa.coach_assessment.manage' => ['ar' => 'إدارة تقييم اللاعبين', 'en' => 'Manage Player Assessments'],
'sa.swimming.coach_manage' => ['ar' => 'إدارة مدربين السباحة', 'en' => 'Manage Swimming Coaches'],
]);
// ─── Event Listeners ────────────────────────────────────────────────────────
......
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
// 1. Update system_config: flat 100 EGP, no card/form fees
$db->query(
"UPDATE system_config SET config_value = '100.00' WHERE config_key = 'sa.registration_fee_member'"
);
$db->query(
"UPDATE system_config SET config_value = '100.00' WHERE config_key = 'sa.registration_fee_nonmember'"
);
$db->query(
"UPDATE system_config SET config_value = '0.00' WHERE config_key = 'sa.card_fee'"
);
$db->query(
"UPDATE system_config SET config_value = '0.00' WHERE config_key = 'sa.form_fee'"
);
// 2. Add selected_disciplines JSON to sa_registrations
$col = $db->selectOne(
"SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'sa_registrations' AND column_name = 'selected_disciplines'"
);
if (!$col) {
$db->query("ALTER TABLE sa_registrations ADD COLUMN selected_disciplines JSON NULL AFTER group_id");
}
// 3. Add assessment columns to sa_group_players
$col2 = $db->selectOne(
"SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'sa_group_players' AND column_name = 'assessed_by'"
);
if (!$col2) {
$db->query("ALTER TABLE sa_group_players ADD COLUMN assessed_by BIGINT UNSIGNED NULL AFTER created_by");
$db->query("ALTER TABLE sa_group_players ADD COLUMN assessment_notes TEXT NULL AFTER assessed_by");
$db->query("ALTER TABLE sa_group_players ADD COLUMN skill_level VARCHAR(20) NULL AFTER assessment_notes");
$db->query("ALTER TABLE sa_group_players ADD COLUMN assessed_at TIMESTAMP NULL AFTER skill_level");
}
// 4. Add sa.coach_assessment permission keys
$existing = $db->selectOne(
"SELECT 1 FROM system_config WHERE config_key = 'sa.swimming.coach_manage'"
);
if (!$existing) {
$db->insert('system_config', [
'config_key' => 'sa.swimming.coach_manage',
'config_value' => '1',
'description' => 'Swimming Coach Management permission flag',
]);
}
};
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