Commit f834d38b authored by Mahmoud Aglan's avatar Mahmoud Aglan

Fixed Members

parent 2aaa27a2
......@@ -183,36 +183,8 @@ final class LedgerService
$acc['period_debit'] = $debitMovement;
$acc['period_credit'] = $creditMovement;
// Calculate closing balance
if ($acc['account_nature'] === 'debit') {
$balance = bcadd($opening, bcsub($debitMovement, $creditMovement, 2), 2);
} else {
$balance = bcadd($opening, bcsub($creditMovement, $debitMovement, 2), 2);
}
// If no journal movements, use current_balance from DB and restore opening from stored value
if (bccomp($debitMovement, '0.00', 2) === 0 && bccomp($creditMovement, '0.00', 2) === 0) {
$balance = (string) $acc['current_balance'];
$opening = (string) $acc['opening_balance'];
if (bccomp($opening, '0.00', 2) >= 0) {
$acc['opening_debit'] = $opening;
$acc['opening_credit'] = '0.00';
} else {
$acc['opening_debit'] = '0.00';
$acc['opening_credit'] = bcmul($opening, '-1', 2);
}
// Derive period movement as difference between closing and opening
$closingNet = (string) $acc['current_balance'];
$periodNet = bcsub($closingNet, $opening, 2);
if (bccomp($periodNet, '0.00', 2) >= 0) {
$acc['period_debit'] = $periodNet;
$acc['period_credit'] = '0.00';
} else {
$acc['period_debit'] = '0.00';
$acc['period_credit'] = bcmul($periodNet, '-1', 2);
}
}
// Calculate closing balance (signed convention: positive=debit, negative=credit)
$balance = bcadd($opening, bcsub($debitMovement, $creditMovement, 2), 2);
// Closing debit/credit split
if (bccomp($balance, '0.00', 2) >= 0) {
......
......@@ -89,7 +89,7 @@
<small style="color:#6B7280;">صورة واضحة للوجه — مطلوبة لإصدار الكارنيه</small>
</div>
<div style="padding:20px;">
<?php $currentPhoto = null; $required = true; $fieldName = 'photo'; $label = 'صورة الابن/الابنة'; include App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
<?php $currentPhoto = null; $required = true; $fieldName = 'photo'; $label = 'صورة الابن/الابنة'; include \App\Core\App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
</div>
</div>
......
......@@ -26,7 +26,7 @@
<!-- Profile Photo -->
<div class="card" style="padding:20px;margin-bottom:20px;border-right:4px solid #7C3AED;">
<h3 style="color:#7C3AED;margin-bottom:15px;">الصورة الشخصية</h3>
<?php $currentPhoto = $child->photo_path ?? null; $required = false; $fieldName = 'photo'; $label = 'الصورة الشخصية'; include App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
<?php $currentPhoto = $child->photo_path ?? null; $required = false; $fieldName = 'photo'; $label = 'الصورة الشخصية'; include \App\Core\App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
</div>
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
......
......@@ -22,13 +22,39 @@ final class MembershipValidationService
'terminated' => 'عضوية منتهية بقرار',
];
public static function checkByMembershipNumber(string $membershipNumber): array
{
$db = App::getInstance()->db();
$member = $db->selectOne(
"SELECT id, membership_number, full_name_ar, full_name_en, national_id,
date_of_birth, gender, phone_mobile, status, photo_path
FROM members WHERE membership_number = ? AND is_archived = 0",
[$membershipNumber]
);
if (!$member) {
return [
'found' => false,
'member_id' => null,
'membership_number' => null,
'member' => null,
'is_active_member' => false,
'effective_type' => 'non_member',
'reason' => 'رقم العضوية غير موجود',
];
}
return self::validateMember($member);
}
public static function checkByNationalId(string $nationalId): array
{
$db = App::getInstance()->db();
$member = $db->selectOne(
"SELECT id, membership_number, full_name_ar, full_name_en, national_id,
date_of_birth, gender, phone, status, photo_path
date_of_birth, gender, phone_mobile, status, photo_path
FROM members WHERE national_id = ? AND is_archived = 0",
[$nationalId]
);
......@@ -45,6 +71,13 @@ final class MembershipValidationService
];
}
return self::validateMember($member);
}
private static function validateMember(array $member): array
{
$db = App::getInstance()->db();
$base = [
'found' => true,
'member_id' => (int) $member['id'],
......
......@@ -142,7 +142,7 @@
<!-- Profile Photo -->
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #7C3AED;">
<h3 style="color:#7C3AED;margin-bottom:15px;">الصورة الشخصية</h3>
<?php $currentPhoto = $member->photo_path ?? null; $required = false; $fieldName = 'photo'; $label = 'الصورة الشخصية'; include App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
<?php $currentPhoto = $member->photo_path ?? null; $required = false; $fieldName = 'photo'; $label = 'الصورة الشخصية'; include \App\Core\App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
</div>
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
......
<?php
declare(strict_types=1);
namespace App\Modules\SportsActivity\Controllers\Api;
use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Modules\Members\Services\MembershipValidationService;
use App\Modules\Members\Services\NationalIdParser;
class MemberLookupApiController extends Controller
{
public function lookup(Request $request): Response
{
$membershipNumber = trim((string) $request->post('membership_number', ''));
$nationalId = trim((string) $request->post('national_id', ''));
if ($membershipNumber === '' && $nationalId === '') {
return $this->json(['success' => false, 'error' => 'أدخل رقم العضوية أو الرقم القومي']);
}
$result = [
'success' => true,
'member_id' => null,
'membership_number' => $membershipNumber,
'national_id' => $nationalId,
'is_member' => false,
'effective_type' => 'non_member',
'reason' => null,
'name' => null,
'phone' => null,
'gender' => null,
'date_of_birth' => null,
'age' => null,
];
$membership = null;
if ($membershipNumber !== '') {
$membership = MembershipValidationService::checkByMembershipNumber($membershipNumber);
} elseif ($nationalId !== '' && strlen($nationalId) === 14) {
$membership = MembershipValidationService::checkByNationalId($nationalId);
$nidParsed = NationalIdParser::parse($nationalId);
if ($nidParsed && ($nidParsed['is_valid'] ?? false)) {
$result['date_of_birth'] = $nidParsed['dob'] ?? null;
$result['age'] = $nidParsed['age_years'] ?? null;
$result['gender'] = $nidParsed['gender'] ?? null;
}
}
if ($membership) {
$result['effective_type'] = $membership['effective_type'];
$result['is_member'] = $membership['is_active_member'];
$result['member_id'] = $membership['member_id'];
$result['reason'] = $membership['reason'];
if ($membership['found'] && $membership['member']) {
$m = $membership['member'];
$result['name'] = $m['full_name_ar'] ?? null;
$result['phone'] = $m['phone_mobile'] ?? null;
$result['membership_number'] = $m['membership_number'] ?? null;
$result['national_id'] = $m['national_id'] ?? $result['national_id'];
if (!empty($m['date_of_birth'])) {
$result['date_of_birth'] = $m['date_of_birth'];
$result['age'] = (int) date_diff(date_create($m['date_of_birth']), date_create('today'))->y;
}
if (!empty($m['gender'])) {
$result['gender'] = $m['gender'];
}
}
}
return $this->json($result);
}
}
......@@ -43,35 +43,43 @@ class BookingWizardController extends Controller
$this->authorize('sa.booking_wizard.use');
$nationalId = trim((string) $request->post('national_id', ''));
$membershipNumber = trim((string) $request->post('membership_number', ''));
$name = trim((string) $request->post('name', ''));
if ($nationalId === '' && $name === '') {
return $this->json(['success' => false, 'error' => 'أدخل الرقم القومي أو الاسم']);
if ($nationalId === '' && $membershipNumber === '' && $name === '') {
return $this->json(['success' => false, 'error' => 'أدخل رقم العضوية أو الرقم القومي أو الاسم']);
}
$result = [
'success' => true,
'national_id' => $nationalId,
'effective_type' => 'non_member',
'is_member' => false,
'member_id' => null,
'reason' => null,
'name' => $name,
'phone' => null,
'gender' => null,
'date_of_birth' => null,
'age' => null,
'success' => true,
'national_id' => $nationalId,
'membership_number' => $membershipNumber,
'effective_type' => 'non_member',
'is_member' => false,
'member_id' => null,
'reason' => null,
'name' => $name,
'phone' => null,
'gender' => null,
'date_of_birth' => null,
'age' => null,
];
if ($nationalId !== '' && strlen($nationalId) === 14) {
$membership = null;
if ($membershipNumber !== '') {
$membership = MembershipValidationService::checkByMembershipNumber($membershipNumber);
} elseif ($nationalId !== '' && strlen($nationalId) === 14) {
$nidParsed = NationalIdParser::parse($nationalId);
if ($nidParsed && ($nidParsed['is_valid'] ?? false)) {
$result['date_of_birth'] = $nidParsed['dob'] ?? null;
$result['age'] = $nidParsed['age_years'] ?? null;
$result['gender'] = $nidParsed['gender'] ?? null;
}
$membership = MembershipValidationService::checkByNationalId($nationalId);
}
if ($membership) {
$result['effective_type'] = $membership['effective_type'];
$result['is_member'] = $membership['is_active_member'];
$result['member_id'] = $membership['member_id'];
......@@ -79,13 +87,15 @@ class BookingWizardController extends Controller
if ($membership['found'] && $membership['member']) {
$m = $membership['member'];
$result['name'] = $result['name'] ?: ($m['full_name_ar'] ?? '');
$result['phone'] = $m['phone'] ?? null;
$result['name'] = $m['full_name_ar'] ?? $result['name'];
$result['phone'] = $m['phone_mobile'] ?? null;
$result['membership_number'] = $m['membership_number'] ?? null;
if (!$result['date_of_birth'] && !empty($m['date_of_birth'])) {
$result['national_id'] = $m['national_id'] ?? $result['national_id'];
if (!empty($m['date_of_birth'])) {
$result['date_of_birth'] = $m['date_of_birth'];
$result['age'] = (int) date_diff(date_create($m['date_of_birth']), date_create('today'))->y;
}
if (!$result['gender'] && !empty($m['gender'])) {
if (!empty($m['gender'])) {
$result['gender'] = $m['gender'];
}
}
......
......@@ -90,6 +90,16 @@ class MirrorController extends Controller
private function renderFacilityView(int $facilityId, string $date): Response
{
$db = App::getInstance()->db();
$facility = $db->selectOne(
"SELECT facility_type FROM sa_facilities WHERE id = ? AND is_active = 1 AND is_archived = 0",
[$facilityId]
);
if ($facility && $facility['facility_type'] === 'pool') {
return $this->redirect('/sa/mirror/pool/' . $facilityId . '?date=' . $date);
}
$state = MirrorStateService::getFacilityState($facilityId, $date);
if (isset($state['error'])) {
......@@ -104,4 +114,41 @@ class MirrorController extends Controller
'date' => $state['date'],
]);
}
public function poolMirror(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$date = trim((string) $request->get('date', date('Y-m-d')));
$facility = \App\Modules\SportsActivity\Services\PoolGridService::getFacility((int) $id);
if (!$facility) {
return $this->redirect('/sa/mirror')->withError('المرفق غير موجود');
}
$slots = \App\Modules\SportsActivity\Services\PoolGridService::getTimeSlots($facility);
$groups = $db->select(
"SELECT g.id, g.code, g.name_ar, g.current_count, g.max_capacity,
c.full_name_ar as coach_name, p.name_ar as program_name
FROM sa_groups g
LEFT JOIN sa_coaches c ON c.id = g.coach_id
LEFT JOIN sa_programs p ON p.id = g.program_id
WHERE g.status = 'active' AND g.is_archived = 0
ORDER BY g.name_ar",
[]
);
$allPools = $db->select(
"SELECT id, name_ar FROM sa_facilities WHERE is_active = 1 AND is_archived = 0 AND facility_type = 'pool' ORDER BY name_ar",
[]
);
return $this->view('SportsActivity.Views.pool-grid.manage', [
'facility' => $facility,
'slots' => $slots,
'groups' => $groups,
'date' => $date,
'allPools' => $allPools,
]);
}
}
......@@ -7,67 +7,21 @@ use App\Core\Controller;
use App\Core\Request;
use App\Core\Response;
use App\Core\App;
use App\Modules\SportsActivity\Services\PoolGridService;
class PoolGridController extends Controller
{
public function index(Request $request): Response
{
$db = App::getInstance()->db();
$facilities = $db->select(
"SELECT f.*, d.name_ar as discipline_name
FROM sa_facilities f
LEFT JOIN sa_disciplines d ON d.id = f.discipline_id
WHERE f.is_active = 1 AND f.is_archived = 0
AND f.facility_type = 'pool'
ORDER BY f.name_ar",
[]
);
if (count($facilities) === 1) {
return $this->redirect('/sa/pool-grid/' . $facilities[0]['id']);
}
return $this->view('SportsActivity.Views.pool-grid.index', [
'facilities' => $facilities,
]);
return $this->redirect('/sa/mirror');
}
public function manage(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$date = $request->get('date', date('Y-m-d'));
$facility = PoolGridService::getFacility((int) $id);
if (!$facility) {
return $this->redirect('/sa/pool-grid')->withError('المرفق غير موجود');
$date = $request->get('date', '');
$url = '/sa/mirror/pool/' . $id;
if ($date !== '') {
$url .= '?date=' . urlencode($date);
}
$slots = PoolGridService::getTimeSlots($facility);
$groups = $db->select(
"SELECT g.id, g.code, g.name_ar, g.current_count, g.max_capacity,
c.full_name_ar as coach_name, p.name_ar as program_name
FROM sa_groups g
LEFT JOIN sa_coaches c ON c.id = g.coach_id
LEFT JOIN sa_programs p ON p.id = g.program_id
WHERE g.status = 'active' AND g.is_archived = 0
ORDER BY g.name_ar",
[]
);
$allPools = $db->select(
"SELECT id, name_ar FROM sa_facilities WHERE is_active = 1 AND is_archived = 0 AND facility_type = 'pool' ORDER BY name_ar",
[]
);
return $this->view('SportsActivity.Views.pool-grid.manage', [
'facility' => $facility,
'slots' => $slots,
'groups' => $groups,
'date' => $date,
'allPools' => $allPools,
]);
return $this->redirect($url);
}
}
......@@ -38,6 +38,7 @@ class RegistrationWizardController extends Controller
public function lookupPlayer(Request $request): Response
{
$nationalId = trim((string) $request->post('national_id', ''));
$membershipNumber = trim((string) $request->post('membership_number', ''));
$fullNameAr = trim((string) $request->post('full_name_ar', ''));
$fullNameEn = trim((string) $request->post('full_name_en', ''));
$phone = trim((string) $request->post('phone', ''));
......@@ -47,16 +48,19 @@ class RegistrationWizardController extends Controller
$guardianRelationship = trim((string) $request->post('guardian_relationship', ''));
$nidParsed = null;
if ($nationalId !== '' && strlen($nationalId) === 14) {
$nidParsed = NationalIdParser::parse($nationalId);
}
$playerType = 'non_member';
$memberId = 0;
$membershipReason = null;
$membership = null;
if ($nationalId !== '' && strlen($nationalId) === 14) {
if ($membershipNumber !== '') {
$membership = MembershipValidationService::checkByMembershipNumber($membershipNumber);
} elseif ($nationalId !== '' && strlen($nationalId) === 14) {
$nidParsed = NationalIdParser::parse($nationalId);
$membership = MembershipValidationService::checkByNationalId($nationalId);
}
if ($membership) {
$playerType = $membership['effective_type'];
$membershipReason = $membership['reason'];
if ($membership['found'] && $membership['member']) {
......@@ -64,12 +68,16 @@ class RegistrationWizardController extends Controller
$m = $membership['member'];
$fullNameAr = $fullNameAr ?: ($m['full_name_ar'] ?? '');
$fullNameEn = $fullNameEn ?: ($m['full_name_en'] ?? '');
$phone = $phone ?: ($m['phone'] ?? '');
$phone = $phone ?: ($m['phone_mobile'] ?? '');
$nationalId = $nationalId ?: ($m['national_id'] ?? '');
if ($nationalId !== '' && strlen($nationalId) === 14 && !$nidParsed) {
$nidParsed = NationalIdParser::parse($nationalId);
}
}
}
if ($fullNameAr === '' && $nidParsed === null) {
return $this->json(['success' => false, 'error' => 'أدخل الرقم القومي أو الاسم']);
return $this->json(['success' => false, 'error' => 'أدخل رقم العضوية أو الرقم القومي أو الاسم']);
}
$result = RegistrationWizardService::startRegistration([
......
......@@ -150,6 +150,7 @@ return [
['GET', '/sa/mirror/{id:\d+}/{date}', 'SportsActivity\Controllers\MirrorController@facilityDate', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/facility/{id:\d+}', 'SportsActivity\Controllers\MirrorController@facility', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/facility/{id:\d+}/{date}', 'SportsActivity\Controllers\MirrorController@facilityDate', ['auth'], 'sa.mirror.view'],
['GET', '/sa/mirror/pool/{id:\d+}', 'SportsActivity\Controllers\MirrorController@poolMirror', ['auth'], 'sa.mirror.view'],
// Waitlist
['GET', '/sa/waitlist', 'SportsActivity\Controllers\WaitlistController@index', ['auth'], 'sa.waitlist.view'],
......@@ -184,6 +185,7 @@ return [
['GET', '/api/sa/schedule/conflicts', 'SportsActivity\Controllers\Api\ScheduleApiController@conflicts', ['auth'], 'sa.schedule.view'],
['GET', '/api/sa/bookings/price-preview', 'SportsActivity\Controllers\Api\BookingApiController@pricePreview', ['auth'], 'sa.booking.view'],
['GET', '/api/sa/players/search', 'SportsActivity\Controllers\Api\PlayerSearchApiController@search', ['auth'], 'sa.player.view'],
['POST', '/api/sa/member-lookup', 'SportsActivity\Controllers\Api\MemberLookupApiController@lookup', ['auth', 'csrf'], null],
['GET', '/api/sa/groups/search', 'SportsActivity\Controllers\Api\SmartFilterApiController@groups', ['auth'], 'sa.group.view'],
['GET', '/api/sa/coaches/search', 'SportsActivity\Controllers\Api\SmartFilterApiController@coaches', ['auth'], 'sa.group.view'],
['GET', '/api/sa/disciplines/list', 'SportsActivity\Controllers\Api\SmartFilterApiController@disciplines', ['auth'], 'sa.group.view'],
......
......@@ -85,7 +85,7 @@ final class MirrorStateService
if ($facility['facility_type'] === 'pool') {
$poolZoneBookings = $db->select(
"SELECT pzb.start_time, pzb.end_time, pzb.status as pzb_status, pzb.action,
"SELECT pzb.start_time, pzb.end_time, pzb.status as pzb_status, pzb.assignment_type,
pzb.zone_row, pzb.zone_col, g.name_ar as group_name, c.full_name_ar as coach_name
FROM sa_pool_zone_bookings pzb
LEFT JOIN sa_groups g ON g.id = pzb.group_id
......@@ -105,7 +105,7 @@ final class MirrorStateService
$bookings[] = [
'id' => null,
'facility_unit_id' => $mappedUnitId,
'booking_type' => $pzb['action'] ?: 'training',
'booking_type' => $pzb['assignment_type'] ?: 'training',
'booking_date' => $date,
'start_time' => $pzb['start_time'],
'end_time' => $pzb['end_time'],
......
......@@ -34,6 +34,16 @@
</div>
<div style="padding:20px;">
<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" id="bkMembershipNo" class="form-input" style="padding:14px;font-size:18px;border-radius:10px;direction:ltr;text-align:right;letter-spacing:1px;" inputmode="numeric" placeholder="أدخل رقم العضوية">
<div id="memberLookupResult" style="display:none;margin-top:10px;padding:12px;border-radius:8px;font-size:13px;"></div>
</div>
<div style="text-align:center;color:#9CA3AF;font-size:12px;position:relative;">
<span style="background:white;padding:0 12px;position:relative;z-index:1;">أو أدخل البيانات يدوياً</span>
<div style="position:absolute;top:50%;left:0;right:0;height:1px;background:#E5E7EB;z-index:0;"></div>
</div>
<div>
<label class="form-label">الرقم القومي</label>
<input type="text" id="bkNid" class="form-input" style="padding:14px;font-size:18px;border-radius:10px;direction:ltr;text-align:right;letter-spacing:1px;" maxlength="14" inputmode="numeric" pattern="[0-9]*" placeholder="أدخل 14 رقم (اختياري للزوار)">
......@@ -231,7 +241,8 @@
}
}
// Step 1: NID input
// Step 1: Identity inputs
var membershipInput = document.getElementById('bkMembershipNo');
var nidInput = document.getElementById('bkNid');
var nameInput = document.getElementById('bkName');
var btnStep1 = document.getElementById('btnStep1Next');
......@@ -241,6 +252,70 @@
}
nameInput.addEventListener('input', checkStep1Valid);
function applyLookupResult(data) {
if (!data.success) return;
state.nationalId = data.national_id || '';
state.effectiveType = data.effective_type;
state.isMember = data.is_member;
state.memberId = data.member_id;
state.membershipNumber = data.membership_number || '';
if (data.name) nameInput.value = data.name;
if (data.national_id && !nidInput.value) nidInput.value = data.national_id;
if (data.membership_number && !membershipInput.value) membershipInput.value = data.membership_number;
document.getElementById('bkGender').textContent = data.gender === 'male' ? 'ذكر' : (data.gender === 'female' ? 'أنثى' : '—');
document.getElementById('bkAge').textContent = data.age ? data.age + ' سنة' : '—';
checkStep1Valid();
}
function showLookupBadge(box, data) {
if (data.is_member) {
box.style.background = '#ECFDF5';
box.style.color = '#059669';
box.innerHTML = '<strong>عضو فعال</strong>' + (data.membership_number ? ' — رقم ' + data.membership_number : '') + (data.name ? ' — ' + data.name : '');
} else if (data.reason) {
box.style.background = '#FEF3C7';
box.style.color = '#92400E';
box.innerHTML = '<strong>غير عضو</strong> — ' + data.reason;
} else {
box.style.background = '#F3F4F6';
box.style.color = '#374151';
box.innerHTML = 'غير عضو';
}
box.style.display = '';
}
// Membership number lookup
var memberLookupTimer = null;
membershipInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
var box = document.getElementById('memberLookupResult');
if (val.length < 1) { box.style.display = 'none'; return; }
clearTimeout(memberLookupTimer);
memberLookupTimer = setTimeout(function() {
box.style.display = '';
box.style.background = '#F3F4F6';
box.style.color = '#6B7280';
box.textContent = 'جاري البحث...';
fetch('/sa/booking-wizard/lookup', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({membership_number: val, _csrf_token: csrfToken})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.success && data.member_id) {
applyLookupResult(data);
showLookupBadge(box, data);
} else {
box.style.background = '#FEF2F2';
box.style.color = '#DC2626';
box.textContent = data.reason || 'رقم العضوية غير موجود';
}
});
}, 400);
});
// NID lookup
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
......@@ -252,32 +327,8 @@
}).then(function(r) { return r.json(); }).then(function(data) {
var box = document.getElementById('nidResult');
if (data.success) {
state.nationalId = val;
state.effectiveType = data.effective_type;
state.isMember = data.is_member;
state.memberId = data.member_id;
if (data.name && !nameInput.value.trim()) {
nameInput.value = data.name;
}
document.getElementById('bkGender').textContent = data.gender === 'male' ? 'ذكر' : (data.gender === 'female' ? 'أنثى' : '—');
document.getElementById('bkAge').textContent = data.age ? data.age + ' سنة' : '—';
if (data.is_member) {
box.style.background = '#ECFDF5';
box.style.color = '#059669';
box.innerHTML = '<strong>عضو فعال</strong>' + (data.membership_number ? ' — رقم ' + data.membership_number : '');
} else if (data.reason) {
box.style.background = '#FEF3C7';
box.style.color = '#92400E';
box.innerHTML = '<strong>غير عضو</strong> — ' + data.reason;
} else {
box.style.background = '#F3F4F6';
box.style.color = '#374151';
box.innerHTML = 'غير عضو';
}
box.style.display = '';
checkStep1Valid();
applyLookupResult(data);
showLookupBadge(box, data);
} else {
box.style.background = '#FEF2F2';
box.style.color = '#DC2626';
......
......@@ -27,8 +27,10 @@ $__template->layout('Layout.main');
</select>
</div>
<div id="member_id_field" style="display:none;">
<label class="form-label">رقم العضوية <span style="color:#DC2626;">*</span></label>
<input type="number" name="member_id" value="<?= e(old('member_id') ?? '') ?>" class="form-input" placeholder="أدخل رقم العضوية">
<label class="form-label">رقم العضوية <span style="color:#DC2626;">*</span> <span style="font-weight:400;color:#6B7280;font-size:12px;">(يملأ البيانات تلقائياً)</span></label>
<input type="text" id="membershipNoInput" value="<?= e(old('membership_number') ?? '') ?>" class="form-input" placeholder="أدخل رقم العضوية" inputmode="numeric" dir="ltr" style="text-align:right;letter-spacing:1px;">
<input type="hidden" name="member_id" id="memberIdHidden" value="<?= e(old('member_id') ?? '') ?>">
<div id="memberLookupStatus" style="display:none;margin-top:6px;padding:8px 12px;border-radius:6px;font-size:12px;"></div>
</div>
<div>
<label class="form-label">الاسم بالعربي <span style="color:#DC2626;">*</span></label>
......@@ -122,6 +124,12 @@ document.addEventListener('DOMContentLoaded', function() {
var playerType = document.getElementById('player_type');
var memberField = document.getElementById('member_id_field');
var membershipNoInput = document.getElementById('membershipNoInput');
var memberIdHidden = document.getElementById('memberIdHidden');
var memberLookupStatus = document.getElementById('memberLookupStatus');
var csrfToken = document.querySelector('meta[name="csrf-token"]') ? document.querySelector('meta[name="csrf-token"]').content : '';
var csrfInput = document.querySelector('input[name="_csrf_token"]');
if (csrfInput) csrfToken = csrfInput.value;
function toggleMemberId() {
memberField.style.display = playerType.value === 'member' ? '' : 'none';
......@@ -129,6 +137,50 @@ document.addEventListener('DOMContentLoaded', function() {
playerType.addEventListener('change', toggleMemberId);
toggleMemberId();
// Membership number auto-fill
var memberLookupTimer = null;
if (membershipNoInput) {
membershipNoInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
if (val.length < 1) { memberLookupStatus.style.display = 'none'; memberIdHidden.value = ''; return; }
clearTimeout(memberLookupTimer);
memberLookupTimer = setTimeout(function() {
memberLookupStatus.style.display = '';
memberLookupStatus.style.background = '#F3F4F6';
memberLookupStatus.style.color = '#6B7280';
memberLookupStatus.textContent = 'جاري البحث...';
fetch('/api/sa/member-lookup', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({membership_number: val, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
if (data.success && data.member_id) {
memberIdHidden.value = data.member_id;
memberLookupStatus.style.background = '#ECFDF5';
memberLookupStatus.style.color = '#059669';
memberLookupStatus.innerHTML = '<strong>' + (data.name || '') + '</strong>' + (data.is_member ? ' — عضو فعال' : ' — <span style="color:#92400E;">' + (data.reason || 'غير فعال') + '</span>');
// Auto-fill form fields
var nameInput = document.querySelector('[name="full_name_ar"]');
var phoneInput = document.querySelector('[name="phone"]');
var nidField = document.getElementById('nidInput');
if (data.name && nameInput && !nameInput.value.trim()) nameInput.value = data.name;
if (data.phone && phoneInput && !phoneInput.value.trim()) phoneInput.value = data.phone;
if (data.national_id && nidField && !nidField.value.trim()) {
nidField.value = data.national_id;
nidField.dispatchEvent(new Event('input'));
}
} else {
memberIdHidden.value = '';
memberLookupStatus.style.background = '#FEF2F2';
memberLookupStatus.style.color = '#DC2626';
memberLookupStatus.textContent = data.reason || 'رقم العضوية غير موجود';
}
});
}, 400);
});
}
var nidInput = document.getElementById('nidInput');
var nidStatus = document.getElementById('nidStatus');
var dobInput = document.getElementById('dobInput');
......
......@@ -6,7 +6,7 @@ $__template->layout('Layout.main');
<?php $__template->section('content'); ?>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(300px, 1fr));gap:20px;">
<?php foreach ($facilities as $f): ?>
<a href="/sa/pool-grid/<?= (int) $f['id'] ?>" class="card" style="text-decoration:none;color:inherit;padding:24px;transition:box-shadow .2s;">
<a href="/sa/mirror/pool/<?= (int) $f['id'] ?>" class="card" style="text-decoration:none;color:inherit;padding:24px;transition:box-shadow .2s;">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px;">
<div style="width:48px;height:48px;border-radius:12px;background:#DBEAFE;display:flex;align-items:center;justify-content:center;">
<i data-lucide="waves" style="width:24px;height:24px;color:#2563EB;"></i>
......
......@@ -6,7 +6,7 @@ $gridCols = (int) ($facility['pool_grid_cols'] ?? 0);
<?php $__template->section('title'); ?>شبكة <?= e($facility['name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('page_actions'); ?>
<a href="/sa/pool-grid" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رجوع</a>
<a href="/sa/mirror" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;vertical-align:middle;margin-left:4px;"></i> رجوع للمراية</a>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
......@@ -149,7 +149,7 @@ $gridCols = (int) ($facility['pool_grid_cols'] ?? 0);
<?php if (!empty($allPools) && count($allPools) > 1): ?>
<div style="display:flex;gap:0;border-bottom:2px solid #E5E7EB;margin-bottom:12px;">
<?php foreach ($allPools as $pool): ?>
<a href="/sa/pool-grid/<?= (int) $pool['id'] ?>?date=<?= e($date) ?>"
<a href="/sa/mirror/pool/<?= (int) $pool['id'] ?>?date=<?= e($date) ?>"
style="padding:8px 16px;font-size:13px;font-weight:600;text-decoration:none;border-bottom:2px solid transparent;margin-bottom:-2px;<?= (int) $pool['id'] === (int) $facility['id'] ? 'color:#2563EB;border-bottom-color:#2563EB;' : 'color:#6B7280;' ?>"><?= e($pool['name_ar']) ?></a>
<?php endforeach; ?>
</div>
......
......@@ -295,6 +295,16 @@
<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">
<div id="regMemberLookup" style="display:none;margin-top:8px;padding:10px 12px;border-radius:8px;font-size:13px;"></div>
</div>
<div style="text-align:center;color:#9CA3AF;font-size:12px;position:relative;">
<span style="background:white;padding:0 12px;position:relative;z-index:1;">أو أدخل الرقم القومي</span>
<div style="position:absolute;top:50%;left:0;right:0;height:1px;background:#E5E7EB;z-index:0;"></div>
</div>
<div>
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" class="form-input" style="padding:14px;font-size:18px;border-radius:10px;direction:ltr;text-align:right;letter-spacing:1px;" maxlength="14" inputmode="numeric" pattern="[0-9]*" placeholder="أدخل 14 رقم" id="regNid">
......@@ -307,7 +317,7 @@
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label class="form-label">الهاتف</label>
<input type="text" name="phone" class="form-input" style="padding:14px;font-size:15px;border-radius:10px;" dir="ltr" inputmode="tel">
<input type="text" name="phone" class="form-input" style="padding:14px;font-size:15px;border-radius:10px;" dir="ltr" inputmode="tel" id="regPhone">
</div>
<div>
<label class="form-label">هاتف ولي الأمر</label>
......@@ -375,13 +385,58 @@ document.addEventListener('DOMContentLoaded', function() {
var startForm = document.getElementById('startForm');
if (startForm) {
var regNid = document.getElementById('regNid');
var regMembershipNo = document.getElementById('regMembershipNo');
var memberBadge = document.getElementById('regMemberBadge');
var memberLookupBox = document.getElementById('regMemberLookup');
var regNameAr = document.getElementById('regNameAr');
var regPhone = document.getElementById('regPhone');
function fillMemberData(data) {
if (data.name) regNameAr.value = data.name;
if (data.phone && regPhone && !regPhone.value.trim()) regPhone.value = data.phone;
if (data.national_id && !regNid.value) regNid.value = data.national_id;
if (data.membership_number && !regMembershipNo.value) regMembershipNo.value = data.membership_number;
}
// Membership number lookup
var regMemberTimer = null;
if (regMembershipNo) {
regMembershipNo.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
if (val.length < 1) { memberLookupBox.style.display = 'none'; return; }
clearTimeout(regMemberTimer);
regMemberTimer = setTimeout(function() {
memberLookupBox.style.display = '';
memberLookupBox.style.background = '#F3F4F6';
memberLookupBox.style.color = '#6B7280';
memberLookupBox.textContent = 'جاري البحث...';
fetch('/api/sa/member-lookup', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({membership_number: val, _csrf_token: csrfToken})
}).then(function(r){return r.json();}).then(function(data) {
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>');
fillMemberData(data);
} else {
memberLookupBox.style.background = '#FEF2F2';
memberLookupBox.style.color = '#DC2626';
memberLookupBox.textContent = data.reason || 'رقم العضوية غير موجود';
}
});
}, 400);
});
}
// NID lookup
if (regNid) {
regNid.addEventListener('input', function() {
this.value = this.value.replace(/\D/g, '');
if (this.value.length === 14) {
fetch('/sa/booking-wizard/lookup', {
fetch('/api/sa/member-lookup', {
method: 'POST',
headers: {'Content-Type':'application/json','X-Requested-With':'XMLHttpRequest','X-CSRF-TOKEN': csrfToken},
body: JSON.stringify({national_id: this.value, _csrf_token: csrfToken})
......@@ -401,13 +456,7 @@ document.addEventListener('DOMContentLoaded', function() {
memberBadge.innerHTML = 'غير عضو';
}
memberBadge.style.display = '';
if (data.name && !document.getElementById('regNameAr').value.trim()) {
document.getElementById('regNameAr').value = data.name;
}
if (data.phone) {
var phoneInput = startForm.querySelector('[name="phone"]');
if (phoneInput && !phoneInput.value.trim()) phoneInput.value = data.phone;
}
fillMemberData(data);
}
});
} else {
......
......@@ -21,7 +21,6 @@ MenuRegistry::register('sports_activity', [
['label_ar' => 'متصفح الأنشطة', 'label_en' => 'Activity Browser','route' => '/sa/activities', 'permission' => 'sa.discipline.view', 'order' => 2.5],
['label_ar' => 'البوابة', 'label_en' => 'Gate', 'route' => '/sa/gate', 'permission' => 'sa.gate.view', 'order' => 3],
['label_ar' => 'المراية', 'label_en' => 'Mirror', 'route' => '/sa/mirror', 'permission' => 'sa.mirror.view', 'order' => 4],
['label_ar' => 'شبكة حمام السباحة', 'label_en' => 'Pool Grid', 'route' => '/sa/pool-grid', 'permission' => 'sa.pool-grid.manage', 'order' => 5],
['label_ar' => 'الألعاب', 'label_en' => 'Disciplines', 'route' => '/sa/disciplines', 'permission' => 'sa.discipline.view', 'order' => 6],
['label_ar' => 'المرافق', 'label_en' => 'Facilities', 'route' => '/sa/facilities', 'permission' => 'sa.facility.view', 'order' => 7],
['label_ar' => 'المدربين', 'label_en' => 'Coaches', 'route' => '/sa/coaches', 'permission' => 'sa.coach.view', 'order' => 8],
......
......@@ -124,7 +124,7 @@ $genderValue = $requiredGender;
<small style="color:#6B7280;">صورة واضحة للوجه — مطلوبة لإصدار الكارنيه</small>
</div>
<div style="padding:20px;">
<?php $currentPhoto = null; $required = true; $fieldName = 'photo'; $label = 'صورة ' . $spouseLabel; include App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
<?php $currentPhoto = null; $required = true; $fieldName = 'photo'; $label = 'صورة ' . $spouseLabel; include \App\Core\App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
</div>
</div>
......
......@@ -36,7 +36,7 @@
<!-- Profile Photo -->
<div class="card" style="padding:20px;margin-bottom:20px;border-right:4px solid #7C3AED;">
<h3 style="color:#7C3AED;margin-bottom:15px;">الصورة الشخصية</h3>
<?php $currentPhoto = $spouse->photo_path ?? null; $required = false; $fieldName = 'photo'; $label = 'الصورة الشخصية'; include App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
<?php $currentPhoto = $spouse->photo_path ?? null; $required = false; $fieldName = 'photo'; $label = 'الصورة الشخصية'; include \App\Core\App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
</div>
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
......
......@@ -78,7 +78,7 @@
<small style="color:#6B7280;">صورة واضحة للوجه — مطلوبة لإصدار الكارنيه</small>
</div>
<div style="padding:20px;">
<?php $currentPhoto = null; $required = true; $fieldName = 'photo'; $label = 'صورة العضو المؤقت'; include App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
<?php $currentPhoto = null; $required = true; $fieldName = 'photo'; $label = 'صورة العضو المؤقت'; include \App\Core\App::getInstance()->basePath() . '/app/Shared/Components/photo-upload-field.php'; ?>
</div>
</div>
......
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