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
the new server ip "3.68.63.185"
username "ubuntu"
The server has caprover setup password "Alarcade123#" manage the # in the password well plesase
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
\ No newline at end of file
The server has caprover setup password "Alarcade123#" manage the # in the password well plesase
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
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
if ($data['full_name_ar'] === '' || mb_strlen($data['full_name_ar']) < 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())) {
$errors[] = 'نوع التوظيف غير صالح';
}
......
......@@ -11,6 +11,7 @@ use App\Core\Pagination;
use App\Modules\SportsActivity\Models\Group;
use App\Modules\SportsActivity\Models\Program;
use App\Modules\SportsActivity\Services\EnrollmentService;
use App\Modules\SportsActivity\Services\ScheduleGeneratorService;
class GroupController extends Controller
{
......@@ -121,6 +122,10 @@ class GroupController extends Controller
$errors[] = 'المدرب مطلوب';
}
if ($seasonStart !== '' && $seasonEnd !== '' && $seasonStart >= $seasonEnd) {
$errors[] = 'تاريخ بداية الموسم يجب أن يكون قبل تاريخ النهاية';
}
// Check unique code
if ($code !== '') {
$db = App::getInstance()->db();
......@@ -269,6 +274,10 @@ class GroupController extends Controller
$errors[] = 'المدرب مطلوب';
}
if ($seasonStart !== '' && $seasonEnd !== '' && $seasonStart >= $seasonEnd) {
$errors[] = 'تاريخ بداية الموسم يجب أن يكون قبل تاريخ النهاية';
}
// Check unique code (exclude current)
if ($code !== '') {
$db = App::getInstance()->db();
......@@ -382,19 +391,53 @@ class GroupController extends Controller
continue;
}
$db->insert('sa_group_schedule', [
'group_id' => (int) $id,
'facility_unit_id' => $unitId,
'day_of_week' => $day,
'start_time' => $start,
'end_time' => $end,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$inserted++;
try {
$db->insert('sa_group_schedule', [
'group_id' => (int) $id,
'facility_unit_id' => $unitId,
'day_of_week' => $day,
'start_time' => $start,
'end_time' => $end,
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$inserted++;
} catch (\PDOException $e) {
if (str_contains($e->getMessage(), 'Duplicate entry')) {
continue;
}
throw $e;
}
}
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
$guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$notes = trim((string) $request->post('notes', ''));
// Auto-extract DOB and gender from national ID
if ($nationalId !== '' && strlen($nationalId) === 14) {
// For members: auto-fill from member record
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);
if ($parsed['is_valid']) {
$dateOfBirth = $parsed['dob'];
......
......@@ -12,7 +12,7 @@ class FacilityUnit extends Model
protected static bool $timestamps = true;
protected static bool $softDelete = false;
protected static bool $dispatchEvents = false;
protected static bool $autoTrackAuthor = true;
protected static bool $autoTrackAuthor = false;
protected static array $fillable = [
'facility_id', 'code', 'name_ar', 'name_en', 'unit_type',
......
......@@ -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+}/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+}/generate-sessions', 'SportsActivity\Controllers\GroupController@generateSessions', ['auth', 'csrf'], 'sa.schedule.manage'],
// Schedule
['GET', '/sa/schedule', 'SportsActivity\Controllers\ScheduleController@calendar', ['auth'], 'sa.schedule.view'],
......
......@@ -43,7 +43,7 @@
<select name="discipline_id" class="form-select">
<option value="">بدون تحديد</option>
<?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; ?>
</select>
</div>
......
......@@ -47,7 +47,7 @@ $opHours = $facility->getOperatingHours();
<select name="discipline_id" class="form-select">
<option value="">بدون تحديد</option>
<?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; ?>
</select>
</div>
......
......@@ -243,6 +243,27 @@ $st = $group['status'] ?? 'active';
</form>
</div>
<?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>
<script>
......
......@@ -141,6 +141,10 @@ document.addEventListener('DOMContentLoaded', function() {
var v = this.value.replace(/\D/g, '');
this.value = v;
if (v.length === 14) {
if (playerType.value === 'member') {
nidStatus.style.display = 'none';
return;
}
fetch('/api/members/parse-nid', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest'},
......
......@@ -7,7 +7,7 @@
<?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() ?>
<!-- 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