Commit d87bae91 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fix all open support tickets (TKT-001 through TKT-011)

- TKT-003: Fix facility create view accessing array as object ($disc->id → $disc['id'])
- TKT-004: Disable autoTrackAuthor on FacilityUnit model (table lacks updated_by column)
- TKT-004: Fix facility edit view same array-as-object bug
- TKT-005: Add national_id validation in coach registration (reject invalid IDs)
- TKT-006: Skip NID validation for member-type players, auto-fill from member record
- TKT-007: Fix program edit form action URL (/update suffix removed to match route)
- TKT-008: Add season start/end date validation in group store and update
- TKT-009: Catch duplicate schedule entry PDOException gracefully (skip duplicates)
- TKT-010/011: Add "Generate Training Sessions" button+route to group show page
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 7d124eb2
use the NewServer.pem File use the NewServer.pem File
the new server ip "3.68.63.185" the new server ip "3.68.63.185"
username "ubuntu" username "ubuntu"
The server has caprover setup password "Alarcade123#" manage the # in the password well plesase The server has caprover setup password "Alarcade123#" manage the # in the password well plesase
The database application on caprover is The database application on caprover is
Your app is internally available as srv-captain--mysql-db to other apps. In case of web-app, it is accessible via http://srv-captain--mysql-db from other apps Your app is internally available as srv-captain--mysql-db to other apps. In case of web-app, it is accessible via http://srv-captain--mysql-db from other apps
\ No newline at end of file
club management system link "https://clubmanagement.caprover.al-arcade.com/"
and internal conn link is
Your app is internally available as srv-captain--clubmanagement to other apps. In case of web-app, it is accessible via http://srv-captain--clubmanagement from other apps.
Do not expose as web-app externally
─────────────────────────────────────────────────────────────
DATABASE CONNECTION (from app .env inside container)
─────────────────────────────────────────────────────────────
DB_HOST=srv-captain--mysql-db
DB_PORT=3306
DB_NAME=the_club_erp
DB_USER=root
DB_PASS=Alarcade123#
SSH + Docker command to connect to MySQL directly:
ssh -i NewServer.pem ubuntu@3.68.63.185 "sudo docker exec -it $(sudo docker ps -q --filter name=mysql-db) mysql -uroot -p'Alarcade123#' the_club_erp"
Container names:
- MySQL: srv-captain--mysql-db
- App: srv-captain--clubmanagement
...@@ -85,6 +85,14 @@ class CoachController extends Controller ...@@ -85,6 +85,14 @@ class CoachController extends Controller
if ($data['full_name_ar'] === '' || mb_strlen($data['full_name_ar']) < 3) { if ($data['full_name_ar'] === '' || mb_strlen($data['full_name_ar']) < 3) {
$errors[] = 'اسم المدرب بالعربي مطلوب (3 أحرف على الأقل)'; $errors[] = 'اسم المدرب بالعربي مطلوب (3 أحرف على الأقل)';
} }
if ($nationalId !== null && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId);
if (!$parsed['is_valid']) {
$errors[] = 'الرقم القومي غير صالح: ' . ($parsed['errors'][0] ?? '');
}
} elseif ($nationalId !== null && $nationalId !== '' && strlen($nationalId) !== 14) {
$errors[] = 'الرقم القومي يجب أن يكون 14 رقم';
}
if (!array_key_exists($data['employment_type'], Coach::getEmploymentTypes())) { if (!array_key_exists($data['employment_type'], Coach::getEmploymentTypes())) {
$errors[] = 'نوع التوظيف غير صالح'; $errors[] = 'نوع التوظيف غير صالح';
} }
......
...@@ -11,6 +11,7 @@ use App\Core\Pagination; ...@@ -11,6 +11,7 @@ use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Group; use App\Modules\SportsActivity\Models\Group;
use App\Modules\SportsActivity\Models\Program; use App\Modules\SportsActivity\Models\Program;
use App\Modules\SportsActivity\Services\EnrollmentService; use App\Modules\SportsActivity\Services\EnrollmentService;
use App\Modules\SportsActivity\Services\ScheduleGeneratorService;
class GroupController extends Controller class GroupController extends Controller
{ {
...@@ -121,6 +122,10 @@ class GroupController extends Controller ...@@ -121,6 +122,10 @@ class GroupController extends Controller
$errors[] = 'المدرب مطلوب'; $errors[] = 'المدرب مطلوب';
} }
if ($seasonStart !== '' && $seasonEnd !== '' && $seasonStart >= $seasonEnd) {
$errors[] = 'تاريخ بداية الموسم يجب أن يكون قبل تاريخ النهاية';
}
// Check unique code // Check unique code
if ($code !== '') { if ($code !== '') {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -269,6 +274,10 @@ class GroupController extends Controller ...@@ -269,6 +274,10 @@ class GroupController extends Controller
$errors[] = 'المدرب مطلوب'; $errors[] = 'المدرب مطلوب';
} }
if ($seasonStart !== '' && $seasonEnd !== '' && $seasonStart >= $seasonEnd) {
$errors[] = 'تاريخ بداية الموسم يجب أن يكون قبل تاريخ النهاية';
}
// Check unique code (exclude current) // Check unique code (exclude current)
if ($code !== '') { if ($code !== '') {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
...@@ -382,19 +391,53 @@ class GroupController extends Controller ...@@ -382,19 +391,53 @@ class GroupController extends Controller
continue; continue;
} }
$db->insert('sa_group_schedule', [ try {
'group_id' => (int) $id, $db->insert('sa_group_schedule', [
'facility_unit_id' => $unitId, 'group_id' => (int) $id,
'day_of_week' => $day, 'facility_unit_id' => $unitId,
'start_time' => $start, 'day_of_week' => $day,
'end_time' => $end, 'start_time' => $start,
'is_active' => 1, 'end_time' => $end,
'created_at' => date('Y-m-d H:i:s'), 'is_active' => 1,
'updated_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'),
$inserted++; ]);
$inserted++;
} catch (\PDOException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
continue;
}
throw $e;
}
} }
return $this->redirect('/sa/groups/' . $id)->withSuccess("تم حفظ الجدول ({$inserted} حصة)"); return $this->redirect('/sa/groups/' . $id)->withSuccess("تم حفظ الجدول ({$inserted} حصة)");
} }
public function generateSessions(Request $request, string $id): Response
{
$group = Group::find((int) $id);
if (!$group) {
return $this->redirect('/sa/groups')->withError('المجموعة غير موجودة');
}
$fromDate = trim((string) $request->post('from_date', ''));
$toDate = trim((string) $request->post('to_date', ''));
if ($fromDate === '' || $toDate === '') {
return $this->redirect('/sa/groups/' . $id)->withError('يجب تحديد تاريخ البداية والنهاية');
}
if ($fromDate >= $toDate) {
return $this->redirect('/sa/groups/' . $id)->withError('تاريخ البداية يجب أن يكون قبل تاريخ النهاية');
}
$result = ScheduleGeneratorService::generateForGroup((int) $id, $fromDate, $toDate);
if (!$result['success']) {
return $this->redirect('/sa/groups/' . $id)->withError($result['error']);
}
return $this->redirect('/sa/groups/' . $id)->withSuccess("تم توليد {$result['generated']} حصة تدريبية (تم تخطي {$result['skipped']})");
}
} }
...@@ -100,8 +100,33 @@ class PlayerController extends Controller ...@@ -100,8 +100,33 @@ class PlayerController extends Controller
$guardianRelationship = trim((string) $request->post('guardian_relationship', '')); $guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$notes = trim((string) $request->post('notes', '')); $notes = trim((string) $request->post('notes', ''));
// Auto-extract DOB and gender from national ID // For members: auto-fill from member record
if ($nationalId !== '' && strlen($nationalId) === 14) { if ($playerType === 'member' && $memberId > 0) {
$member = $db->selectOne(
"SELECT full_name_ar, national_id, date_of_birth, gender FROM members WHERE membership_number = ? AND is_archived = 0",
[$memberId]
);
if (!$member) {
$member = $db->selectOne(
"SELECT full_name_ar, national_id, date_of_birth, gender FROM members WHERE id = ? AND is_archived = 0",
[$memberId]
);
}
if ($member) {
if ($fullNameAr === '') {
$fullNameAr = $member['full_name_ar'] ?? '';
}
if ($member['national_id']) {
$nationalId = $member['national_id'];
}
if ($member['date_of_birth']) {
$dateOfBirth = $member['date_of_birth'];
}
if ($member['gender']) {
$gender = $member['gender'];
}
}
} elseif ($nationalId !== '' && strlen($nationalId) === 14) {
$parsed = NationalIdParser::parse($nationalId); $parsed = NationalIdParser::parse($nationalId);
if ($parsed['is_valid']) { if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob']; $dateOfBirth = $parsed['dob'];
......
...@@ -12,7 +12,7 @@ class FacilityUnit extends Model ...@@ -12,7 +12,7 @@ class FacilityUnit extends Model
protected static bool $timestamps = true; protected static bool $timestamps = true;
protected static bool $softDelete = false; protected static bool $softDelete = false;
protected static bool $dispatchEvents = false; protected static bool $dispatchEvents = false;
protected static bool $autoTrackAuthor = true; protected static bool $autoTrackAuthor = false;
protected static array $fillable = [ protected static array $fillable = [
'facility_id', 'code', 'name_ar', 'name_en', 'unit_type', 'facility_id', 'code', 'name_ar', 'name_en', 'unit_type',
......
...@@ -95,6 +95,7 @@ return [ ...@@ -95,6 +95,7 @@ return [
['POST', '/sa/groups/{id:\d+}/enroll', 'SportsActivity\Controllers\GroupController@enroll', ['auth', 'csrf'], 'sa.group.enroll'], ['POST', '/sa/groups/{id:\d+}/enroll', 'SportsActivity\Controllers\GroupController@enroll', ['auth', 'csrf'], 'sa.group.enroll'],
['POST', '/sa/groups/{id:\d+}/remove-player', 'SportsActivity\Controllers\GroupController@removePlayer', ['auth', 'csrf'], 'sa.group.manage'], ['POST', '/sa/groups/{id:\d+}/remove-player', 'SportsActivity\Controllers\GroupController@removePlayer', ['auth', 'csrf'], 'sa.group.manage'],
['POST', '/sa/groups/{id:\d+}/schedule', 'SportsActivity\Controllers\GroupController@saveSchedule', ['auth', 'csrf'], 'sa.group.manage'], ['POST', '/sa/groups/{id:\d+}/schedule', 'SportsActivity\Controllers\GroupController@saveSchedule', ['auth', 'csrf'], 'sa.group.manage'],
['POST', '/sa/groups/{id:\d+}/generate-sessions', 'SportsActivity\Controllers\GroupController@generateSessions', ['auth', 'csrf'], 'sa.schedule.manage'],
// Schedule // Schedule
['GET', '/sa/schedule', 'SportsActivity\Controllers\ScheduleController@calendar', ['auth'], 'sa.schedule.view'], ['GET', '/sa/schedule', 'SportsActivity\Controllers\ScheduleController@calendar', ['auth'], 'sa.schedule.view'],
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
<select name="discipline_id" class="form-select"> <select name="discipline_id" class="form-select">
<option value="">بدون تحديد</option> <option value="">بدون تحديد</option>
<?php foreach ($disciplines as $disc): ?> <?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc->id ?>" <?= old('discipline_id', '') == $disc->id ? 'selected' : '' ?>><?= e($disc->name_ar) ?></option> <option value="<?= (int) $disc['id'] ?>" <?= old('discipline_id', '') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
......
...@@ -47,7 +47,7 @@ $opHours = $facility->getOperatingHours(); ...@@ -47,7 +47,7 @@ $opHours = $facility->getOperatingHours();
<select name="discipline_id" class="form-select"> <select name="discipline_id" class="form-select">
<option value="">بدون تحديد</option> <option value="">بدون تحديد</option>
<?php foreach ($disciplines as $disc): ?> <?php foreach ($disciplines as $disc): ?>
<option value="<?= (int) $disc->id ?>" <?= old('discipline_id', $facility->discipline_id ?? '') == $disc->id ? 'selected' : '' ?>><?= e($disc->name_ar) ?></option> <option value="<?= (int) $disc['id'] ?>" <?= old('discipline_id', $facility->discipline_id ?? '') == $disc['id'] ? 'selected' : '' ?>><?= e($disc['name_ar']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>
......
...@@ -243,6 +243,27 @@ $st = $group['status'] ?? 'active'; ...@@ -243,6 +243,27 @@ $st = $group['status'] ?? 'active';
</form> </form>
</div> </div>
<?php endif; ?> <?php endif; ?>
<!-- Generate Training Sessions -->
<?php if (can('sa.schedule.manage') && !empty($schedule)): ?>
<div style="padding:20px;border-top:1px solid #E5E7EB;">
<h4 style="margin:0 0 12px 0;font-size:14px;color:#374151;">توليد حصص تدريبية</h4>
<form method="POST" action="/sa/groups/<?= (int) $group['id'] ?>/generate-sessions" style="display:flex;gap:12px;align-items:end;flex-wrap:wrap;">
<?= csrf_field() ?>
<div>
<label class="form-label" style="font-size:11px;">من تاريخ</label>
<input type="date" name="from_date" class="form-input" value="<?= date('Y-m-d') ?>" required style="direction:ltr;">
</div>
<div>
<label class="form-label" style="font-size:11px;">إلى تاريخ</label>
<input type="date" name="to_date" class="form-input" value="<?= date('Y-m-d', strtotime('+4 weeks')) ?>" required style="direction:ltr;">
</div>
<button type="submit" class="btn btn-sm btn-primary" onclick="return confirm('سيتم توليد حجوزات تدريبية لكل حصة في الجدول ضمن الفترة المحددة. متابعة؟');">
<i data-lucide="calendar-plus" style="width:14px;height:14px;vertical-align:middle;margin-left:4px;"></i> توليد الحصص
</button>
</form>
</div>
<?php endif; ?>
</div> </div>
<script> <script>
......
...@@ -141,6 +141,10 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -141,6 +141,10 @@ document.addEventListener('DOMContentLoaded', function() {
var v = this.value.replace(/\D/g, ''); var v = this.value.replace(/\D/g, '');
this.value = v; this.value = v;
if (v.length === 14) { if (v.length === 14) {
if (playerType.value === 'member') {
nidStatus.style.display = 'none';
return;
}
fetch('/api/members/parse-nid', { fetch('/api/members/parse-nid', {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'}, headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<form method="POST" action="/sa/programs/<?= (int) $program->id ?>/update"> <form method="POST" action="/sa/programs/<?= (int) $program->id ?>">
<?= csrf_field() ?> <?= csrf_field() ?>
<!-- Basic Information --> <!-- Basic Information -->
......
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