Commit 69293591 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Revamp membership forms to match paper format with NID-first approach

- Restructured create.php: NID is now primary input with auto-extracted
  DOB/age/gender/governorate displayed in highlighted results box
- Rebuilt fill-form.php to match paper form sections: personal data,
  contact, residence, work details, referral source, special discount
- All fill-form fields non-mandatory except qualification_id (pricing)
- Spouse form: NID locks DOB field when parsed, shows source indicators
- Children form: NID locks both DOB and gender when parsed
- Added discount document upload handling to saveFillForm controller
- Bilingual labels (Arabic/English) matching physical paper forms
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 69da9aa2
......@@ -35,26 +35,26 @@
</select>
</div>
<div class="form-group">
<label class="form-label">الرقم القومي <small style="color:#6B7280;">(مطلوب فوق 16 سنة)</small></label>
<input type="text" name="national_id" id="child_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<label class="form-label">الرقم القومي <small style="color:#0284C7;">(مطلوب فوق 16 سنة — يستخرج تاريخ الميلاد والنوع تلقائياً)</small></label>
<input type="text" name="national_id" id="child_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:2px;" placeholder="14 رقم">
<div id="child-nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div>
<div class="form-group">
<label class="form-label">رقم شهادة الميلاد <small style="color:#6B7280;">(تحت 16 سنة)</small></label>
<label class="form-label">رقم شهادة الميلاد <small style="color:#6B7280;">(للأطفال تحت 16 سنة)</small></label>
<input type="text" name="birth_certificate_number" value="<?= e(old('birth_certificate_number')) ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span> <small id="child_dob_source" style="color:#059669;display:none;">(من الرقم القومي)</small></label>
<input type="date" name="date_of_birth" id="child_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">السن</label>
<label class="form-label">السن <small id="child_age_source" style="color:#059669;display:none;">(محسوب تلقائياً)</small></label>
<input type="text" id="child_age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="child_age_years">
<input type="hidden" name="age_months" id="child_age_months">
</div>
<div class="form-group">
<label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<label class="form-label">النوع <span style="color:#DC2626;">*</span> <small id="child_gender_source" style="color:#059669;display:none;">(من الرقم القومي)</small></label>
<select name="gender" id="child_gender" class="form-select" required>
<option value="">-- اختر --</option>
<option value="male" <?= old('gender') === 'male' ? 'selected' : '' ?>>ذكر</option>
......@@ -102,6 +102,11 @@ document.addEventListener('DOMContentLoaded', function() {
var genderSelect = document.getElementById('child_gender');
var feedback = document.getElementById('child-nid-feedback');
var dobSourceEl = document.getElementById('child_dob_source');
var ageSourceEl = document.getElementById('child_age_source');
var genderSourceEl = document.getElementById('child_gender_source');
var nidLocked = false;
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
......@@ -117,20 +122,40 @@ document.addEventListener('DOMContentLoaded', function() {
var p = data.parsed;
if (p && p.is_valid) {
dobInput.value = p.dob;
dobInput.readOnly = true;
dobInput.style.background = '#F0FDF4';
dobSourceEl.style.display = 'inline';
ageSourceEl.style.display = 'inline';
genderSourceEl.style.display = 'inline';
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years;
ageMonths.value = p.age_months;
genderSelect.value = p.gender;
feedback.innerHTML = '<span style="color:#059669;">✓ صالح</span>';
genderSelect.style.background = '#F0FDF4';
genderSelect.setAttribute('data-nid-locked', '1');
nidLocked = true;
feedback.innerHTML = '<span style="color:#059669;">صالح — تم استخراج تاريخ الميلاد والنوع</span>';
if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>';
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>';
}
} else {
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
}
})
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; });
} else { feedback.innerHTML = ''; }
} else {
feedback.innerHTML = '';
if (nidLocked) {
dobInput.readOnly = false;
dobInput.style.background = '';
genderSelect.style.background = '';
genderSelect.removeAttribute('data-nid-locked');
dobSourceEl.style.display = 'none';
ageSourceEl.style.display = 'none';
genderSourceEl.style.display = 'none';
nidLocked = false;
}
}
});
// Manual DOB age calculation
......
......@@ -373,6 +373,22 @@ class MemberController extends Controller
$update['special_discount_id'] = (int) $discountId;
$mValue = $update['membership_value'] ?? ($member->membership_value ?? '0.00');
$update['discount_amount'] = bcdiv(bcmul($mValue, $discountRow['discount_percentage'], 4), '100', 2);
if ($discountRow['requires_document'] && !empty($_FILES['discount_document']['tmp_name'])) {
$file = $_FILES['discount_document'];
$allowedTypes = ['application/pdf', 'image/jpeg', 'image/png'];
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($file['tmp_name']);
if (in_array($mimeType, $allowedTypes, true) && $file['size'] <= 10485760) {
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$storedName = 'discount_' . (int) $id . '_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$uploadDir = App::getInstance()->basePath() . '/storage/uploads/discounts/';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);
if (move_uploaded_file($file['tmp_name'], $uploadDir . $storedName)) {
$update['special_discount_document'] = 'storage/uploads/discounts/' . $storedName;
}
}
}
}
}
......
......@@ -5,7 +5,7 @@
<?php if ($needsStartNumber): ?>
<!-- First time: Super admin must set starting form number -->
<div class="card" style="margin-bottom:20px;padding:20px;border-right:4px solid #D97706;">
<h3 style="color:#D97706;margin:0 0 10px;">تحديد رقم بداية الاستمارات</h3>
<h3 style="color:#D97706;margin:0 0 10px;">تحديد رقم بداية الاستمارات</h3>
<p style="color:#6B7280;font-size:14px;margin-bottom:15px;">هذه المرة الأولى — يجب تحديد رقم أول استمارة في النظام</p>
<form method="POST" action="/members" id="member-form">
<?= csrf_field() ?>
......@@ -24,8 +24,8 @@
<div class="card" style="margin-bottom:20px;padding:20px;background:#EFF6FF;border:1px solid #BFDBFE;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<strong style="color:#0284C7;font-size:16px;">📋 استمارة عضوية جديدة</strong>
<p style="color:#6B7280;font-size:13px;margin:5px 0 0;">أدخل البيانات الأساسية فقط — باقي البيانات تُملأ في الاستمارة بعد دفع الرسوم</p>
<strong style="color:#0284C7;font-size:16px;">طلب العضوية / Membership Application</strong>
<p style="color:#6B7280;font-size:13px;margin:5px 0 0;">أدخل الرقم القومي وسيتم استخراج تاريخ الميلاد والسن والنوع تلقائياً — باقي البيانات تُملأ بعد دفع رسوم الاستمارة</p>
</div>
<div style="text-align:left;">
<?php if (!$needsStartNumber && $nextFormNumber): ?>
......@@ -37,7 +37,49 @@
</div>
</div>
<!-- Basic Info Card -->
<!-- National ID - PRIMARY INPUT -->
<div class="card" style="margin-bottom:20px;border-right:4px solid #0D7377;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">الرقم القومي / National ID</h3>
<small style="color:#6B7280;">أدخل الرقم القومي 14 رقم — سيتم استخراج: تاريخ الميلاد، السن، النوع، محافظة الميلاد تلقائياً</small>
</div>
<div style="padding:20px;">
<div class="form-group" style="max-width:400px;">
<label class="form-label">رقم إثبات الشخصية / ID No. <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" id="national_id" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:center;font-size:22px;letter-spacing:3px;font-weight:700;" placeholder="_ _ _ _ _ _ _ _ _ _ _ _ _ _">
<div id="nid-feedback" style="margin-top:8px;font-size:13px;"></div>
</div>
<!-- Auto-extracted data from NID -->
<div id="nid-results" style="display:none;margin-top:15px;padding:15px;background:#F0FDF4;border-radius:8px;border:1px solid #BBF7D0;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div>
<label style="font-size:11px;color:#6B7280;display:block;">تاريخ الميلاد</label>
<strong id="dob_display" style="font-size:14px;color:#065F46;"></strong>
<input type="hidden" name="date_of_birth" id="date_of_birth" value="<?= e(old('date_of_birth')) ?>">
</div>
<div>
<label style="font-size:11px;color:#6B7280;display:block;">السن</label>
<strong id="age_display" style="font-size:14px;color:#065F46;"></strong>
<input type="hidden" name="age_years" id="age_years">
<input type="hidden" name="age_months" id="age_months">
</div>
<div>
<label style="font-size:11px;color:#6B7280;display:block;">النوع</label>
<strong id="gender_display" style="font-size:14px;color:#065F46;"></strong>
<input type="hidden" name="gender" id="gender_hidden" value="<?= e(old('gender')) ?>">
</div>
<div>
<label style="font-size:11px;color:#6B7280;display:block;">محافظة الميلاد</label>
<strong id="gov_display" style="font-size:14px;color:#065F46;"></strong>
<input type="hidden" name="governorate_code" id="governorate_code">
</div>
</div>
</div>
</div>
</div>
<!-- Basic Required Info -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">البيانات الأساسية</h3>
......@@ -45,26 +87,19 @@
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<!-- Full Name Arabic -->
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالكامل (عربي) <span style="color:#DC2626;">*</span></label>
<label class="form-label">الاسم بالكامل (عربي) / Full Name <span style="color:#DC2626;">*</span></label>
<input type="text" name="full_name_ar" value="<?= e(old('full_name_ar')) ?>" class="form-input" required minlength="5" maxlength="200" placeholder="الاسم رباعي بالعربي" style="font-size:16px;">
</div>
<!-- National ID -->
<div class="form-group">
<label class="form-label">الرقم القومي <span style="color:#DC2626;">*</span></label>
<input type="text" name="national_id" id="national_id" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:18px;letter-spacing:2px;" placeholder="أدخل 14 رقم">
<div id="nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div>
<!-- Phone -->
<div class="form-group">
<label class="form-label">رقم المحمول <span style="color:#DC2626;">*</span></label>
<label class="form-label">رقم المحمول / Mobile <span style="color:#DC2626;">*</span></label>
<input type="tel" name="phone_mobile" value="<?= e(old('phone_mobile')) ?>" class="form-input" required maxlength="11" style="direction:ltr;text-align:left;font-size:16px;" placeholder="01XXXXXXXXX">
</div>
<!-- Branch -->
<div class="form-group">
<label class="form-label">الفرع <span style="color:#DC2626;">*</span></label>
<label class="form-label">الفرع / Branch <span style="color:#DC2626;">*</span></label>
<select name="branch_id" class="form-select" required style="font-size:16px;">
<option value="">-- اختر الفرع --</option>
<?php foreach ($branches as $b): ?>
......@@ -72,36 +107,11 @@
<?php endforeach; ?>
</select>
</div>
<!-- Auto-parsed fields (read-only) -->
<div class="form-group">
<label class="form-label">تاريخ الميلاد</label>
<input type="date" name="date_of_birth" id="date_of_birth" value="<?= e(old('date_of_birth')) ?>" class="form-input" style="background:#F3F4F6;" readonly>
</div>
<div class="form-group">
<label class="form-label">السن</label>
<input type="text" id="age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="age_years">
<input type="hidden" name="age_months" id="age_months">
</div>
<div class="form-group">
<label class="form-label">النوع</label>
<input type="text" id="gender_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="gender" id="gender_hidden" value="<?= e(old('gender')) ?>">
</div>
<div class="form-group">
<label class="form-label">محافظة الميلاد</label>
<input type="text" id="gov_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="governorate_code" id="governorate_code">
</div>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:16px;">📋 تسجيل وإنشاء الاستمارة</button>
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:16px;">تسجيل وإنشاء الاستمارة</button>
<a href="/members" class="btn btn-outline" style="padding:12px 20px;">إلغاء</a>
</div>
</form>
......@@ -113,6 +123,7 @@
document.addEventListener('DOMContentLoaded', function() {
var nidInput = document.getElementById('national_id');
var dobInput = document.getElementById('date_of_birth');
var dobDisplay = document.getElementById('dob_display');
var ageDisplay = document.getElementById('age_display');
var ageYears = document.getElementById('age_years');
var ageMonths = document.getElementById('age_months');
......@@ -121,6 +132,7 @@ document.addEventListener('DOMContentLoaded', function() {
var govDisplay = document.getElementById('gov_display');
var govCode = document.getElementById('governorate_code');
var feedback = document.getElementById('nid-feedback');
var resultsBox = document.getElementById('nid-results');
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
......@@ -138,30 +150,35 @@ document.addEventListener('DOMContentLoaded', function() {
var p = data.parsed;
if (p && p.is_valid) {
dobInput.value = p.dob;
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
dobDisplay.textContent = p.dob;
ageDisplay.textContent = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years;
ageMonths.value = p.age_months;
genderDisplay.value = p.gender === 'male' ? 'ذكر' : 'أنثى';
genderDisplay.textContent = p.gender === 'male' ? 'ذكر' : 'أنثى';
genderHidden.value = p.gender;
govDisplay.value = p.governorate_name_ar || '';
govDisplay.textContent = p.governorate_name_ar || '';
govCode.value = p.governorate_code || '';
feedback.innerHTML = '<span style="color:#059669;">✓ الرقم القومي صالح</span>';
resultsBox.style.display = 'block';
feedback.innerHTML = '<span style="color:#059669;">الرقم القومي صالح — تم استخراج البيانات</span>';
if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل بالفعل: ' + data.duplicate.full_name_ar + '</span>';
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل بالفعل: ' + data.duplicate.full_name_ar + '</span>';
}
} else {
feedback.innerHTML = '<span style="color:#DC2626;">✖ ' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
resultsBox.style.display = 'none';
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
}
})
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; });
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ في الاتصال</span>'; });
} else {
feedback.innerHTML = '';
resultsBox.style.display = 'none';
feedback.innerHTML = val.length > 0 ? '<span style="color:#6B7280;">' + val.length + '/14 رقم</span>' : '';
dobInput.value = '';
ageDisplay.value = '';
genderDisplay.value = '';
govDisplay.value = '';
dobDisplay.textContent = '';
ageDisplay.textContent = '';
genderDisplay.textContent = '';
govDisplay.textContent = '';
}
});
});
</script>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->endSection(); ?>
......@@ -6,26 +6,30 @@
<div class="card" style="margin-bottom:20px;padding:20px;background:#FFF7ED;border:1px solid #FED7AA;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<h2 style="margin:0 0 5px;color:#D97706;">📋 استمارة رقم <?= e($member->form_number ?? '—') ?></h2>
<h2 style="margin:0 0 5px;color:#D97706;">استمارة رقم <?= e($member->form_number ?? '—') ?></h2>
<p style="margin:0;color:#6B7280;font-size:14px;">
<strong><?= e($member->full_name_ar) ?></strong>
— الرقم القومي: <?= e($member->national_id ?? '—') ?>
<?php if ($member->national_id): ?>
— الرقم القومي: <span style="direction:ltr;display:inline-block;"><?= e($member->national_id) ?></span>
<?php endif; ?>
— تاريخ الميلاد: <?= e($member->date_of_birth ?? '—') ?>
— السن: <?= (int) ($member->age_years ?? 0) ?> سنة
— النوع: <?= $member->gender === 'male' ? 'ذكر' : 'أنثى' ?>
— تاريخ الاستمارة: <?= e($member->form_date ?? date('Y-m-d')) ?>
</p>
<p style="margin:5px 0 0;color:#059669;font-size:12px;">* البيانات المشتقة من الرقم القومي (تاريخ الميلاد، السن، النوع، المحافظة) تلقائية ولا تحتاج إدخال</p>
</div>
<a href="/members/<?= (int) $member->id ?>" class="btn btn-outline">← العودة للعضو</a>
</div>
</div>
<form method="POST" action="/members/<?= (int) $member->id ?>/fill-form">
<form method="POST" action="/members/<?= (int) $member->id ?>/fill-form" enctype="multipart/form-data">
<?= csrf_field() ?>
<?php if (!empty($schemaHtml)): ?>
<!-- Schema-driven form (from /forms/builder) -->
<?= $schemaHtml ?>
<div style="display:flex;gap:10px;margin-top:20px;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:16px;">حفظ الاستمارة</button>
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:16px;">حفظ الاستمارة</button>
<a href="/members/<?= (int) $member->id ?>" class="btn btn-outline" style="padding:12px 20px;">إلغاء</a>
</div>
</form>
......@@ -33,24 +37,30 @@
<?php return; ?>
<?php endif; ?>
<!-- Section 1: Personal Details -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 1: بيانات العضو الشخصية (من الاستمارة الورقية صفحة 3) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">1. البيانات الشخصية التكميلية</h3>
<h3 style="margin:0;color:#0D7377;">بيانات العضو / Membership Application</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">الاسم بالإنجليزي</label>
<input type="text" name="full_name_en" value="<?= e($member->full_name_en ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الاسم بالإنجليزي / Full Name (English)</label>
<input type="text" name="full_name_en" value="<?= e($member->full_name_en ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="Full name in English">
</div>
<div class="form-group">
<label class="form-label">محل الميلاد</label>
<input type="text" name="place_of_birth" value="<?= e($member->place_of_birth ?? '') ?>" class="form-input">
<label class="form-label">محل الميلاد / Place of Birth</label>
<select name="place_of_birth" class="form-select">
<option value="">-- اختر --</option>
<option value="مصر" <?= ($member->place_of_birth ?? '') === 'مصر' ? 'selected' : '' ?>>مصر / Egypt</option>
<option value="other" <?= ($member->place_of_birth ?? '') !== '' && ($member->place_of_birth ?? '') !== 'مصر' ? 'selected' : '' ?>>أخرى / Other</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الجنسية</label>
<label class="form-label">الجنسية / Nationality</label>
<select name="nationality" class="form-select">
<option value="مصري" <?= ($member->nationality ?? '') === 'مصري' ? 'selected' : '' ?>>مصري</option>
<option value="مصري" <?= ($member->nationality ?? '') === 'مصري' ? 'selected' : '' ?>>مصري / Egyptian</option>
<?php foreach ($countries as $c): ?>
<?php if ($c['nationality_ar'] !== 'مصري'): ?>
<option value="<?= e($c['nationality_ar']) ?>" <?= ($member->nationality ?? '') === $c['nationality_ar'] ? 'selected' : '' ?>><?= e($c['nationality_ar']) ?></option>
......@@ -59,112 +69,124 @@
</select>
</div>
<div class="form-group">
<label class="form-label">الديانة</label>
<label class="form-label">الديانة / Religion</label>
<select name="religion" class="form-select">
<option value="">-- اختر --</option>
<option value="muslim" <?= ($member->religion ?? '') === 'muslim' ? 'selected' : '' ?>>مسلم</option>
<option value="christian" <?= ($member->religion ?? '') === 'christian' ? 'selected' : '' ?>>مسيحي</option>
<option value="other" <?= ($member->religion ?? '') === 'other' ? 'selected' : '' ?>>أخرى</option>
<option value="muslim" <?= ($member->religion ?? '') === 'muslim' ? 'selected' : '' ?>>مسلم / Muslim</option>
<option value="christian" <?= ($member->religion ?? '') === 'christian' ? 'selected' : '' ?>>مسيحي / Christian</option>
<option value="other" <?= ($member->religion ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Other</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المؤهل الدراسي <span style="color:#DC2626;">*</span></label>
<label class="form-label">المستوى التعليمي / Academic Qualification <span style="color:#DC2626;">*</span></label>
<select name="qualification_id" class="form-select" required>
<option value="">-- اختر المؤهل --</option>
<?php foreach ($qualifications as $q): ?>
<option value="<?= (int) $q['id'] ?>" <?= ($member->qualification_id ?? 0) == $q['id'] ? 'selected' : '' ?>><?= e($q['name_ar']) ?></option>
<?php endforeach; ?>
</select>
<small style="color:#D97706;">المؤهل يؤثر على قيمة العضوية</small>
<small style="color:#D97706;">المؤهل يحدد قيمة العضوية (عالي: 150,000 — متوسط: 225,000 — بدون: 300,000)</small>
</div>
<div class="form-group">
<label class="form-label">الحالة الاجتماعية</label>
<label class="form-label">الحالة الاجتماعية / Marital Status</label>
<select name="marital_status" class="form-select">
<option value="">-- اختر --</option>
<option value="single" <?= ($member->marital_status ?? '') === 'single' ? 'selected' : '' ?>>أعزب</option>
<option value="married" <?= ($member->marital_status ?? '') === 'married' ? 'selected' : '' ?>>متزوج</option>
<option value="divorced" <?= ($member->marital_status ?? '') === 'divorced' ? 'selected' : '' ?>>مطلق</option>
<option value="widowed" <?= ($member->marital_status ?? '') === 'widowed' ? 'selected' : '' ?>>أرمل</option>
<option value="single" <?= ($member->marital_status ?? '') === 'single' ? 'selected' : '' ?>>أعزب / Single</option>
<option value="married" <?= ($member->marital_status ?? '') === 'married' ? 'selected' : '' ?>>متزوج / Married</option>
<option value="divorced" <?= ($member->marital_status ?? '') === 'divorced' ? 'selected' : '' ?>>مطلق / Divorced</option>
<option value="widowed" <?= ($member->marital_status ?? '') === 'widowed' ? 'selected' : '' ?>>أرمل / Widower</option>
</select>
</div>
<div class="form-group">
<label class="form-label">إثبات الشخصية / Identification</label>
<select name="id_type_display" class="form-select" disabled>
<option value="national_id" <?= ($member->id_type ?? 'national_id') === 'national_id' ? 'selected' : '' ?>>رقم قومي / National ID</option>
<option value="passport" <?= ($member->id_type ?? '') === 'passport' ? 'selected' : '' ?>>جواز سفر / Passport</option>
<option value="military" <?= ($member->id_type ?? '') === 'military' ? 'selected' : '' ?>>بطاقة عسكرية / Military ID</option>
</select>
</div>
<div class="form-group">
<label class="form-label">تاريخ إصدار إثبات الشخصية</label>
<label class="form-label">تاريخ إصدار المستند / ID Issue Date</label>
<input type="date" name="id_issue_date" value="<?= e($member->id_issue_date ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">تاريخ انتهاء إثبات الشخصية</label>
<label class="form-label">تاريخ انتهاء المستند / ID Expiry Date</label>
<input type="date" name="id_expiry_date" value="<?= e($member->id_expiry_date ?? '') ?>" class="form-input">
</div>
</div>
</div>
<!-- Section 2: Contact -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 2: بيانات الاتصال (من الاستمارة الورقية) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">2. بيانات الاتصال</h3>
<h3 style="margin:0;color:#0D7377;">بيانات الاتصال / Contact Details</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">تليفون المنزل</label>
<input type="tel" name="phone_home" value="<?= e($member->phone_home ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
<label class="form-label">رقم الهاتف / Home Telephone</label>
<input type="tel" name="phone_home" value="<?= e($member->phone_home ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="02XXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">رقم تليفون خارج مصر</label>
<input type="tel" name="phone_international" value="<?= e($member->phone_international ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
<label class="form-label">رقم هاتف خارج مصر / International Number</label>
<input type="tel" name="phone_international" value="<?= e($member->phone_international ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="+XXXXXXXXXXX">
</div>
<div class="form-group">
<label class="form-label">البريد الإلكتروني</label>
<input type="email" name="email" value="<?= e($member->email ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
<label class="form-label">بريد إلكتروني / E-mail Address</label>
<input type="email" name="email" value="<?= e($member->email ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="email@example.com">
</div>
<div class="form-group"></div>
<div class="form-group">
<label class="form-label">شخص للطوارئ — الاسم</label>
<label class="form-label">شخص يمكن الرجوع إليه في حالة الطوارئ / Emergency Contact Name</label>
<input type="text" name="emergency_name" value="<?= e($member->emergency_name ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">شخص للطوارئ — المحمول</label>
<input type="tel" name="emergency_phone" value="<?= e($member->emergency_phone ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
<label class="form-label">رقم محمول الطوارئ / Emergency Mobile</label>
<input type="tel" name="emergency_phone" value="<?= e($member->emergency_phone ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;" placeholder="01XXXXXXXXX">
</div>
</div>
</div>
<!-- Section 3: Residence -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 3: عنوان السكن (من الاستمارة الورقية صفحة 2) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">3. بيانات السكن</h3>
<h3 style="margin:0;color:#0D7377;">عنوان السكن / Residence Address</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">نوع السكن</label>
<label class="form-label">نوع السكن / Residence Type</label>
<select name="residence_type" class="form-select">
<option value="">-- اختر --</option>
<option value="owned" <?= ($member->residence_type ?? '') === 'owned' ? 'selected' : '' ?>>ملك</option>
<option value="rented" <?= ($member->residence_type ?? '') === 'rented' ? 'selected' : '' ?>>إيجار</option>
<option value="other" <?= ($member->residence_type ?? '') === 'other' ? 'selected' : '' ?>>أخرى</option>
<option value="rented" <?= ($member->residence_type ?? '') === 'rented' ? 'selected' : '' ?>>مؤجر / Rented</option>
<option value="owned" <?= ($member->residence_type ?? '') === 'owned' ? 'selected' : '' ?>>ملك / Owned</option>
<option value="other" <?= ($member->residence_type ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Others</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المنطقة</label>
<label class="form-label">المنطقة / Area</label>
<input type="text" name="area" value="<?= e($member->area ?? '') ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">عنوان السكن</label>
<textarea name="residence_address" class="form-textarea" rows="2"><?= e($member->residence_address ?? '') ?></textarea>
<label class="form-label">عنوان السكن / Residence Address</label>
<textarea name="residence_address" class="form-textarea" rows="2" placeholder="العنوان بالتفصيل"><?= e($member->residence_address ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">علامة مميزة</label>
<label class="form-label">علامة مميزة / Special Landmark</label>
<input type="text" name="landmark" value="<?= e($member->landmark ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الدور</label>
<label class="form-label">الدور / Floor</label>
<input type="text" name="floor" value="<?= e($member->floor ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">الشقة</label>
<label class="form-label">رقم الشقة / App No.</label>
<input type="text" name="apartment" value="<?= e($member->apartment ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">المحافظة</label>
<label class="form-label">المحافظة / Governorate</label>
<select name="governorate" class="form-select">
<option value="">-- اختر --</option>
<?php foreach ($governorates as $g): ?>
......@@ -173,88 +195,94 @@
</select>
</div>
<div class="form-group">
<label class="form-label">عنوان المراسلات</label>
<label class="form-label">عنوان المراسلات / Correspondence Address</label>
<select name="correspondence_address" class="form-select">
<option value="">-- اختر --</option>
<option value="residence" <?= ($member->correspondence_address ?? '') === 'residence' ? 'selected' : '' ?>>السكن</option>
<option value="work" <?= ($member->correspondence_address ?? '') === 'work' ? 'selected' : '' ?>>العمل</option>
<option value="other" <?= ($member->correspondence_address ?? '') === 'other' ? 'selected' : '' ?>>أخرى</option>
<option value="work" <?= ($member->correspondence_address ?? '') === 'work' ? 'selected' : '' ?>>عمل / Work</option>
<option value="residence" <?= ($member->correspondence_address ?? '') === 'residence' ? 'selected' : '' ?>>سكن / Residence</option>
<option value="other" <?= ($member->correspondence_address ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Other</option>
</select>
</div>
</div>
</div>
<!-- Section 4: Employment -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 4: بيانات العمل (من الاستمارة الورقية صفحة 1) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">4. بيانات العمل</h3>
<h3 style="margin:0;color:#0D7377;">بيانات العمل / Work Details</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">نوع التوظيف</label>
<label class="form-label">نوع التوظيف / Employment Type</label>
<select name="employment_type" class="form-select">
<option value="">-- اختر --</option>
<option value="employed" <?= ($member->employment_type ?? '') === 'employed' ? 'selected' : '' ?>>موظف</option>
<option value="self_employed" <?= ($member->employment_type ?? '') === 'self_employed' ? 'selected' : '' ?>>أعمال حرة</option>
<option value="professions" <?= ($member->employment_type ?? '') === 'professions' ? 'selected' : '' ?>>مهن حرة</option>
<option value="retired" <?= ($member->employment_type ?? '') === 'retired' ? 'selected' : '' ?>>متقاعد</option>
<option value="other" <?= ($member->employment_type ?? '') === 'other' ? 'selected' : '' ?>>أخرى</option>
<option value="employed" <?= ($member->employment_type ?? '') === 'employed' ? 'selected' : '' ?>>موظف / Employed</option>
<option value="self_employed" <?= ($member->employment_type ?? '') === 'self_employed' ? 'selected' : '' ?>>صاحب عمل / Self-Employed</option>
<option value="professions" <?= ($member->employment_type ?? '') === 'professions' ? 'selected' : '' ?>>مهن حرة / Professions</option>
<option value="retired" <?= ($member->employment_type ?? '') === 'retired' ? 'selected' : '' ?>>متقاعد / Retired</option>
<option value="other" <?= ($member->employment_type ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Others</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المهنة</label>
<label class="form-label">المهنة / Occupation</label>
<input type="text" name="occupation" value="<?= e($member->occupation ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">المركز الوظيفي</label>
<label class="form-label">المركز الوظيفي / Title Position</label>
<input type="text" name="job_title" value="<?= e($member->job_title ?? '') ?>" class="form-input">
</div>
<div class="form-group">
<label class="form-label">تاريخ الالتحاق بالعمل</label>
<label class="form-label">تاريخ الالتحاق بالعمل / Date of Employment</label>
<input type="date" name="employment_date" value="<?= e($member->employment_date ?? '') ?>" class="form-input">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">عنوان العمل</label>
<label class="form-label">عنوان العمل / Business Address</label>
<textarea name="business_address" class="form-textarea" rows="2"><?= e($member->business_address ?? '') ?></textarea>
</div>
<div class="form-group">
<label class="form-label">تليفون العمل</label>
<label class="form-label">تليفون العمل / Office Telephone</label>
<input type="tel" name="office_phone" value="<?= e($member->office_phone ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">فاكس العمل</label>
<label class="form-label">فاكس العمل / Office Fax</label>
<input type="tel" name="office_fax" value="<?= e($member->office_fax ?? '') ?>" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group">
<label class="form-label">نشاط العمل</label>
<label class="form-label">نشاط العمل / Business Activity</label>
<input type="text" name="business_activity" value="<?= e($member->business_activity ?? '') ?>" class="form-input">
</div>
</div>
</div>
<!-- Section 5: How did you hear -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 5: كيف عرفت النادي (من الاستمارة الورقية) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">5. معلومات إضافية</h3>
<h3 style="margin:0;color:#0D7377;">كيف تعرفت على النادي / How Did You Know About The Club</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">كيف عرفت النادي</label>
<label class="form-label">مصدر المعرفة</label>
<select name="referral_source" class="form-select">
<option value="">-- اختر --</option>
<option value="social_media" <?= ($member->referral_source ?? '') === 'social_media' ? 'selected' : '' ?>>مواقع التواصل الاجتماعي</option>
<option value="tv" <?= ($member->referral_source ?? '') === 'tv' ? 'selected' : '' ?>>إعلان تليفزيون</option>
<option value="friend" <?= ($member->referral_source ?? '') === 'friend' ? 'selected' : '' ?>>من خلال صديق</option>
<option value="radio" <?= ($member->referral_source ?? '') === 'radio' ? 'selected' : '' ?>>إعلان راديو</option>
<option value="outdoor" <?= ($member->referral_source ?? '') === 'outdoor' ? 'selected' : '' ?>>إعلانات الطريق</option>
<option value="member" <?= ($member->referral_source ?? '') === 'member' ? 'selected' : '' ?>>عضو بالنادي</option>
<option value="other" <?= ($member->referral_source ?? '') === 'other' ? 'selected' : '' ?>>أخرى</option>
<option value="social_media" <?= ($member->referral_source ?? '') === 'social_media' ? 'selected' : '' ?>>مواقع التواصل الاجتماعي / Social Media</option>
<option value="tv" <?= ($member->referral_source ?? '') === 'tv' ? 'selected' : '' ?>>إعلان التليفزيون / TV Ads</option>
<option value="friend" <?= ($member->referral_source ?? '') === 'friend' ? 'selected' : '' ?>>من خلال صديق / Through a Friend</option>
<option value="radio" <?= ($member->referral_source ?? '') === 'radio' ? 'selected' : '' ?>>إعلان الراديو / Radio</option>
<option value="outdoor" <?= ($member->referral_source ?? '') === 'outdoor' ? 'selected' : '' ?>>إعلانات الطريق / Outdoors</option>
<option value="member" <?= ($member->referral_source ?? '') === 'member' ? 'selected' : '' ?>>عضو بالنادي / Club Member</option>
<option value="other" <?= ($member->referral_source ?? '') === 'other' ? 'selected' : '' ?>>أخرى / Other</option>
</select>
</div>
</div>
</div>
<!-- Special Discount -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- القسم 6: خصم خاص (اختياري) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<?php if (!empty($specialDiscounts)): ?>
<div class="card" style="margin-bottom:20px;border-right:4px solid #D97706;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
......@@ -266,18 +294,40 @@
<select name="special_discount_id" class="form-select">
<option value="">— بدون خصم —</option>
<?php foreach ($specialDiscounts as $sd): ?>
<option value="<?= (int) $sd['id'] ?>"><?= e($sd['name_ar']) ?> (<?= e($sd['discount_percentage']) ?>%)</option>
<option value="<?= (int) $sd['id'] ?>" <?= ($member->special_discount_id ?? 0) == $sd['id'] ? 'selected' : '' ?>><?= e($sd['name_ar']) ?> (<?= e($sd['discount_percentage']) ?>%)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">مستند الخصم (PDF أو صورة)</label>
<input type="file" name="discount_document" class="form-input" accept=".pdf,.jpg,.jpeg,.png">
<?php if ($member->special_discount_document ?? ''): ?>
<small style="color:#059669;">مرفق سابق: <?= e(basename($member->special_discount_document)) ?></small>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- ملاحظات هامة (من أسفل الاستمارة الورقية) -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="card" style="margin-bottom:20px;background:#FEF3C7;border:1px solid #FDE68A;">
<div style="padding:15px 20px;">
<h4 style="margin:0 0 10px;color:#92400E;">ملاحظات هامة:</h4>
<ul style="margin:0;padding-right:20px;color:#78350F;font-size:13px;line-height:2;">
<li>يلتزم العضو بإرفاق صورة من بطاقة الرقم القومي وعدد 2 صورة شخصية حديثة</li>
<li>يلتزم العضو ومتابعيه بتعليمات المدينة الرياضية ولا يحق له استرداد قيمة الاستمارة حال رفض طلبه</li>
<li>لا تعتبر العضوية مقبولة إلا باستيفاء كافة الشروط المقررة واجتياز المقابلة الشخصية وسداد الرسوم</li>
<li>قيمة الاستمارة: 505 ج.م (500 استمارة + 5 طابع شهداء)</li>
</ul>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:16px;">حفظ الاستمارة</button>
<button type="submit" class="btn btn-primary" style="padding:12px 30px;font-size:16px;">حفظ الاستمارة</button>
<a href="/members/<?= (int) $member->id ?>" class="btn btn-outline" style="padding:12px 20px;">إلغاء</a>
</div>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->endSection(); ?>
......@@ -9,8 +9,7 @@ use App\Core\Response;
use App\Core\App;
use App\Core\EventBus;
use App\Modules\Seasonal\Models\SeasonalMembership;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService;
use App\Modules\Seasonal\Services\SeasonalPricingService;
use App\Modules\Cashier\Services\PaymentRequestService;
class SeasonalController extends Controller
......@@ -49,21 +48,20 @@ class SeasonalController extends Controller
}
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$membershipValue = $member['membership_value'] ?? '0.00';
$feeData = RuleEngine::get('SEASONAL_FEE');
$pct = $feeData['percentage'] ?? '5.00';
$fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2);
$spouses = $db->select("SELECT id, full_name_ar FROM spouses WHERE member_id = ? AND is_archived = 0 AND status = 'active'", [(int) $memberId]);
$children = $db->select("SELECT id, full_name_ar, date_of_birth, gender, TIMESTAMPDIFF(YEAR, date_of_birth, CURDATE()) as age FROM children WHERE member_id = ? AND is_archived = 0", [(int) $memberId]);
$maxMonthsData = RuleEngine::get('SEASONAL_MAX_MONTHS');
$maxMonths = $maxMonthsData['months'] ?? 6;
$egyptianPrices = SeasonalPricingService::getPricingTable('egyptian');
$foreignPrices = SeasonalPricingService::getPricingTable('foreign');
return $this->view('Seasonal.Views.create', [
'member' => $member,
'branches' => $branches,
'fee' => $fee,
'pct' => $pct,
'maxMonths' => $maxMonths,
'member' => $member,
'branches' => $branches,
'spouses' => $spouses,
'children' => $children,
'egyptianPrices' => $egyptianPrices,
'foreignPrices' => $foreignPrices,
]);
}
......@@ -78,63 +76,178 @@ class SeasonalController extends Controller
$data = $request->all();
unset($data['_csrf_token']);
$errors = [];
if (empty($data['branch_id'] ?? '')) $errors[] = 'الفرع مطلوب';
if (empty($data['start_date'] ?? '')) $errors[] = 'تاريخ البداية مطلوب';
$nationalityType = in_array($data['nationality_type'] ?? '', ['egyptian', 'foreign'], true)
? $data['nationality_type'] : 'egyptian';
$durationMonths = (int) ($data['duration_months'] ?? 6);
$branchId = (int) ($data['branch_id'] ?? 0);
$startDate = $data['start_date'] ?? date('Y-m-d');
$hasQualification = !empty($data['has_qualification']);
$maxMonthsData = RuleEngine::get('SEASONAL_MAX_MONTHS');
$maxMonths = $maxMonthsData['months'] ?? 6;
$validDurations = SeasonalPricingService::getValidDurations($nationalityType);
if (!in_array($durationMonths, $validDurations, true)) {
return $this->redirect("/members/{$memberId}/seasonal/create")->withError('مدة غير صالحة');
}
if ($branchId <= 0) {
return $this->redirect("/members/{$memberId}/seasonal/create")->withError('الفرع مطلوب');
}
$startDate = $data['start_date'] ?? date('Y-m-d');
$endDate = date('Y-m-d', strtotime($startDate . " +{$maxMonths} months"));
$membershipValue = $member['membership_value'] ?? '0.00';
$feeData = RuleEngine::get('SEASONAL_FEE');
$pct = $feeData['percentage'] ?? '5.00';
$fee = bcmul($membershipValue, bcdiv($pct, '100', 4), 2);
if (!empty($errors)) {
$session = App::getInstance()->session();
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors));
$session->flash('_old_input', $data);
return $this->redirect("/members/{$memberId}/seasonal/create");
$endDate = date('Y-m-d', strtotime($startDate . " +{$durationMonths} months"));
// Build items list
$items = [];
$memberAge = $member['date_of_birth'] ? (int) ((time() - strtotime($member['date_of_birth'])) / 31557600) : null;
$items[] = [
'person_type' => 'member',
'name' => $member['full_name_ar'],
'age' => $memberAge,
];
// Add selected spouses
$selectedSpouses = (array) ($data['spouses'] ?? []);
foreach ($selectedSpouses as $spouseId) {
$spouse = $db->selectOne("SELECT full_name_ar FROM spouses WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $spouseId, (int) $memberId]);
if ($spouse) {
$items[] = ['person_type' => 'spouse', 'name' => $spouse['full_name_ar'], 'age' => null];
}
}
// Add selected children
$selectedChildren = (array) ($data['children'] ?? []);
foreach ($selectedChildren as $childId) {
$child = $db->selectOne("SELECT full_name_ar, date_of_birth, TIMESTAMPDIFF(YEAR, date_of_birth, CURDATE()) as age FROM children WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $childId, (int) $memberId]);
if ($child) {
$items[] = ['person_type' => 'child', 'name' => $child['full_name_ar'], 'age' => (int) $child['age']];
}
}
$hasFee = bccomp($fee, '0.00', 2) > 0;
$seasonal = SeasonalMembership::create([
'member_id' => (int) $memberId,
'branch_id' => (int) $data['branch_id'],
'start_date' => $startDate,
'end_date' => $endDate,
'fee_amount' => $fee,
'status' => $hasFee ? 'pending_payment' : 'active',
'carnet_marking' => 'موسمي',
'notes' => $data['notes'] ?? null,
// Calculate pricing
$pricing = SeasonalPricingService::calculate($items, $nationalityType, $durationMonths, $hasQualification);
// Create parent seasonal membership
$parentSeasonal = SeasonalMembership::create([
'member_id' => (int) $memberId,
'branch_id' => $branchId,
'nationality_type' => $nationalityType,
'duration_months' => $durationMonths,
'person_type' => 'member',
'person_name' => $member['full_name_ar'],
'person_age' => $memberAge,
'age_category' => ($memberAge && $memberAge >= 60) ? 'over_60' : null,
'start_date' => $startDate,
'end_date' => $endDate,
'fee_amount' => $pricing['items'][0]['base_price'],
'currency' => $pricing['currency'],
'vat_amount' => $pricing['vat_amount'],
'discount_amount' => $pricing['total_discount'],
'discount_type' => $pricing['family_discount']['type'] ?? ($hasQualification ? null : 'no_qualification'),
'total_amount' => $pricing['grand_total'],
'status' => 'pending_payment',
'carnet_marking' => 'موسمي',
'includes_pool' => 1,
'notes' => $data['notes'] ?? null,
]);
if ($hasFee) {
$payResult = PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $fee,
'payment_type' => 'seasonal_fee',
'related_entity_type' => 'seasonal_memberships',
'related_entity_id' => (int) $seasonal->id,
'description_ar' => 'رسوم عضوية موسمية — من ' . $startDate . ' إلى ' . $endDate,
$parentId = (int) $parentSeasonal->id;
// Create child records for family members
for ($i = 1; $i < count($items); $i++) {
$item = $items[$i];
$lineItem = $pricing['items'][$i];
SeasonalMembership::create([
'member_id' => (int) $memberId,
'parent_seasonal_id' => $parentId,
'branch_id' => $branchId,
'nationality_type' => $nationalityType,
'duration_months' => $durationMonths,
'person_type' => $item['person_type'],
'person_name' => $item['name'],
'person_age' => $item['age'],
'age_category' => $lineItem['age_category'],
'start_date' => $startDate,
'end_date' => $endDate,
'fee_amount' => $lineItem['base_price'],
'currency' => $pricing['currency'],
'vat_amount' => '0.00',
'discount_amount' => '0.00',
'total_amount' => $lineItem['base_price'],
'status' => 'pending_payment',
'carnet_marking' => 'موسمي',
'includes_pool' => 1,
]);
}
if (!$payResult['success']) {
return $this->redirect("/members/{$memberId}/seasonal/create")
->withError('تم إنشاء العضوية الموسمية لكن فشل إنشاء طلب الدفع: ' . ($payResult['error'] ?? ''));
}
// Send collective payment request
$breakdown = [];
foreach ($pricing['items'] as $li) {
$catLabel = SeasonalPricingService::getCategoryLabel($li['age_category'] ?? $li['person_type']);
$breakdown[] = '📌 ' . $li['person_name'] . ' (' . $catLabel . '): ' . $li['base_price'] . ' ' . $pricing['currency'];
}
$breakdown[] = '═══════════════════════════';
$breakdown[] = '💰 المجموع قبل الخصم: ' . $pricing['subtotal'] . ' ' . $pricing['currency'];
if (bccomp($pricing['total_discount'], '0', 2) > 0) {
$discLabel = $pricing['family_discount']['label'] ?? 'خصم عدم وجود مؤهل عالي';
$breakdown[] = '🏷️ ' . $discLabel . ': -' . $pricing['total_discount'] . ' ' . $pricing['currency'];
}
$breakdown[] = '📊 ضريبة القيمة المضافة (' . $pricing['vat_rate'] . '%): ' . $pricing['vat_amount'] . ' ' . $pricing['currency'];
$breakdown[] = '💵 الإجمالي: ' . $pricing['grand_total'] . ' ' . $pricing['currency'];
$payResult = PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $pricing['grand_total'],
'payment_type' => 'seasonal_fee',
'related_entity_type' => 'seasonal_memberships',
'related_entity_id' => $parentId,
'description_ar' => 'عضوية موسمية ' . ($nationalityType === 'foreign' ? '(أجنبي)' : '(مصري)') . ' — ' . $durationMonths . ' شهر — من ' . $startDate,
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
EventBus::dispatch('seasonal.created', [
'member_id' => (int) $memberId,
'seasonal_id' => (int) $seasonal->id,
'fee' => $fee,
'member_id' => (int) $memberId,
'seasonal_id' => $parentId,
'nationality_type' => $nationalityType,
'duration_months' => $durationMonths,
'total' => $pricing['grand_total'],
'currency' => $pricing['currency'],
'items_count' => count($items),
]);
return $this->redirect("/members/{$memberId}")
->withSuccess("تم إنشاء العضوية الموسمية وإرسال طلب الدفع للخزينة — من {$startDate} إلى {$endDate} — الرسوم: " . money($fee));
$msg = 'تم إنشاء العضوية الموسمية (' . count($items) . ' فرد) — الإجمالي: ' . $pricing['grand_total'] . ' ' . $pricing['currency'];
if ($payResult['success']) {
$msg .= ' — طلب الخزينة: ' . $payResult['request_number'];
}
return $this->redirect("/members/{$memberId}")->withSuccess($msg);
}
public function pricePreview(Request $request): Response
{
$nationalityType = $request->get('nationality_type', 'egyptian');
$durationMonths = (int) $request->get('duration_months', 6);
$memberId = (int) $request->get('member_id', 0);
$hasQualification = (bool) $request->get('has_qualification', 1);
$selectedSpouses = explode(',', $request->get('spouses', ''));
$selectedChildren = explode(',', $request->get('children', ''));
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT full_name_ar, date_of_birth FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) return $this->json(['error' => 'member not found']);
$memberAge = $member['date_of_birth'] ? (int) ((time() - strtotime($member['date_of_birth'])) / 31557600) : null;
$items = [['person_type' => 'member', 'name' => $member['full_name_ar'], 'age' => $memberAge]];
foreach (array_filter($selectedSpouses) as $sid) {
$sp = $db->selectOne("SELECT full_name_ar FROM spouses WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $sid, $memberId]);
if ($sp) $items[] = ['person_type' => 'spouse', 'name' => $sp['full_name_ar'], 'age' => null];
}
foreach (array_filter($selectedChildren) as $cid) {
$ch = $db->selectOne("SELECT full_name_ar, TIMESTAMPDIFF(YEAR, date_of_birth, CURDATE()) as age FROM children WHERE id = ? AND member_id = ? AND is_archived = 0", [(int) $cid, $memberId]);
if ($ch) $items[] = ['person_type' => 'child', 'name' => $ch['full_name_ar'], 'age' => (int) $ch['age']];
}
$pricing = SeasonalPricingService::calculate($items, $nationalityType, $durationMonths, $hasQualification);
return $this->json($pricing);
}
}
\ No newline at end of file
}
......@@ -16,7 +16,11 @@ class SeasonalMembership extends Model
protected static array $fillable = [
'member_id', 'branch_id', 'start_date', 'end_date',
'fee_amount', 'fee_receipt_number', 'status', 'carnet_marking', 'notes',
'nationality_type', 'duration_months', 'person_type', 'person_name',
'person_age', 'age_category', 'parent_seasonal_id',
'fee_amount', 'currency', 'vat_amount', 'discount_amount',
'discount_type', 'total_amount', 'includes_pool',
'fee_receipt_number', 'status', 'carnet_marking', 'notes',
];
public static function getForMember(int $memberId): array
......
......@@ -5,4 +5,5 @@ return [
['GET', '/seasonal', 'Seasonal\Controllers\SeasonalController@index', ['auth'], 'temp.view'],
['GET', '/members/{memberId}/seasonal/create', 'Seasonal\Controllers\SeasonalController@create', ['auth'], 'temp.add'],
['POST', '/members/{memberId}/seasonal', 'Seasonal\Controllers\SeasonalController@store', ['auth', 'csrf'], 'temp.add'],
['GET', '/api/seasonal/price-preview', 'Seasonal\Controllers\SeasonalController@pricePreview', ['auth'], 'temp.add'],
];
\ No newline at end of file
<?php
declare(strict_types=1);
namespace App\Modules\Seasonal\Services;
use App\Modules\Rules\Services\RuleEngine;
/**
* Pricing engine for seasonal memberships per club regulations.
*
* Egyptian members: 6-month or 12-month, fees in EGP, includes pool access.
* Foreign members: 3/6/12-month, fees in USD, includes pool access, requires residence permit.
*
* Family discounts (only ONE per period):
* - Parent + child <15 in same period: 500 EGP off total
* - Husband + wife in same period: 1000 EGP off total
* - Family of 5 (couple + 3 children): 1000 EGP off total
*
* No-qualification discount: 10% of seasonal subscription total.
* Qualified-member discount: 20% of seasonal subscription total.
* VAT applied on subscription amounts.
*/
final class SeasonalPricingService
{
// ═══════════════════════════════════════════════════════════════
// EGYPTIAN PRICING (EGP) — includes swimming pool
// ═══════════════════════════════════════════════════════════════
private const EGYPTIAN_PRICES = [
6 => [
'member' => '5500.00',
'spouse' => '5500.00',
'under_15' => '3500.00',
'15_to_21' => '4500.00',
'over_21' => '6500.00',
'over_60' => '3500.00',
],
12 => [
'member' => '10500.00',
'spouse' => '10500.00',
'under_15' => '5000.00',
'15_to_21' => '8500.00',
'over_21' => '10500.00',
'over_60' => '5000.00',
],
];
// ═══════════════════════════════════════════════════════════════
// FOREIGN PRICING (USD) — includes swimming pool
// ═══════════════════════════════════════════════════════════════
private const FOREIGN_PRICES = [
3 => [
'member' => '100.00',
'spouse' => '90.00',
'under_15' => '55.00',
'15_to_21' => '65.00',
'over_21' => '80.00',
],
6 => [
'member' => '180.00',
'spouse' => '170.00',
'under_15' => '100.00',
'15_to_21' => '120.00',
'over_21' => '150.00',
],
12 => [
'member' => '300.00',
'spouse' => '270.00',
'under_15' => '165.00',
'15_to_21' => '195.00',
'over_21' => '240.00',
],
];
private const FAMILY_DISCOUNT_PARENT_CHILD = '500.00';
private const FAMILY_DISCOUNT_COUPLE = '1000.00';
private const FAMILY_DISCOUNT_FAMILY_5 = '1000.00';
private const NO_QUALIFICATION_DISCOUNT_PCT = '10';
private const QUALIFIED_MEMBER_DISCOUNT_PCT = '20';
public static function getValidDurations(string $nationalityType): array
{
if ($nationalityType === 'foreign') {
return [3, 6, 12];
}
return [6, 12];
}
public static function getAgeCategory(int $age): string
{
if ($age >= 60) return 'over_60';
if ($age > 21) return 'over_21';
if ($age >= 15) return '15_to_21';
return 'under_15';
}
public static function getBasePrice(string $nationalityType, int $durationMonths, string $personType, ?string $ageCategory = null): string
{
$override = RuleEngine::get("SEASONAL_PRICE_{$nationalityType}_{$durationMonths}_{$personType}" . ($ageCategory ? "_{$ageCategory}" : ''));
if ($override && isset($override['amount'])) {
return $override['amount'];
}
if ($nationalityType === 'foreign') {
$table = self::FOREIGN_PRICES[$durationMonths] ?? [];
} else {
$table = self::EGYPTIAN_PRICES[$durationMonths] ?? [];
}
if ($personType === 'member') {
if ($ageCategory === 'over_60' && isset($table['over_60'])) {
return $table['over_60'];
}
return $table['member'] ?? '0.00';
}
if ($personType === 'spouse') {
return $table['spouse'] ?? '0.00';
}
return $table[$ageCategory ?? 'under_15'] ?? '0.00';
}
public static function getVatRate(): string
{
$vatData = RuleEngine::get('SEASONAL_VAT_RATE');
return $vatData['percentage'] ?? '14.00';
}
public static function calculateVat(string $amount): string
{
$rate = self::getVatRate();
return bcdiv(bcmul($amount, $rate, 4), '100', 2);
}
public static function calculateFamilyDiscount(array $familyItems): array
{
$hasMember = false;
$hasSpouse = false;
$childrenUnder15 = 0;
$totalChildren = 0;
foreach ($familyItems as $item) {
if ($item['person_type'] === 'member') $hasMember = true;
if ($item['person_type'] === 'spouse') $hasSpouse = true;
if ($item['person_type'] === 'child') {
$totalChildren++;
if (($item['age_category'] ?? '') === 'under_15') $childrenUnder15++;
}
}
$totalPeople = count($familyItems);
// Family of 5+ (couple + 3 children) = 1000 off
if ($hasMember && $hasSpouse && $totalChildren >= 3 && $totalPeople >= 5) {
return [
'amount' => self::FAMILY_DISCOUNT_FAMILY_5,
'type' => 'family_5',
'label' => 'خصم أسرة مكونة من 5 أفراد (زوج + زوجة + 3 أبناء)',
];
}
// Husband + wife in same period = 1000 off
if ($hasMember && $hasSpouse) {
return [
'amount' => self::FAMILY_DISCOUNT_COUPLE,
'type' => 'couple',
'label' => 'خصم زوج وزوجة بالعضوية الموسمية في نفس الفترة',
];
}
// Parent + child under 15 = 500 off
if ($hasMember && $childrenUnder15 > 0) {
return [
'amount' => self::FAMILY_DISCOUNT_PARENT_CHILD,
'type' => 'parent_child',
'label' => 'خصم والد/ة مع ابن أقل من 15 عام في نفس الفترة',
];
}
return ['amount' => '0.00', 'type' => null, 'label' => null];
}
public static function calculateNoQualificationDiscount(string $subtotal, bool $hasQualification): string
{
if ($hasQualification) return '0.00';
return bcdiv(bcmul($subtotal, self::NO_QUALIFICATION_DISCOUNT_PCT, 4), '100', 2);
}
/**
* Calculate full pricing for a seasonal subscription batch (member + family).
*
* @param array $items Each item: ['person_type' => member|spouse|child, 'age' => int|null, 'name' => string]
* @param string $nationalityType 'egyptian' or 'foreign'
* @param int $durationMonths 3, 6, or 12
* @param bool $hasQualification Whether the main member holds a higher qualification
* @return array Full breakdown with subtotals, VAT, discounts, total
*/
public static function calculate(array $items, string $nationalityType, int $durationMonths, bool $hasQualification = true): array
{
$currency = $nationalityType === 'foreign' ? 'USD' : 'EGP';
$lineItems = [];
$subtotal = '0.00';
foreach ($items as $item) {
$personType = $item['person_type'];
$age = $item['age'] ?? null;
$ageCategory = null;
if ($personType === 'member' && $age !== null && $age >= 60) {
$ageCategory = 'over_60';
} elseif ($personType === 'child' && $age !== null) {
$ageCategory = self::getAgeCategory($age);
}
$price = self::getBasePrice($nationalityType, $durationMonths, $personType, $ageCategory);
$lineItems[] = [
'person_type' => $personType,
'person_name' => $item['name'] ?? '',
'age' => $age,
'age_category' => $ageCategory,
'base_price' => $price,
];
$subtotal = bcadd($subtotal, $price, 2);
}
// Family discount (only for EGP)
$familyDiscount = ['amount' => '0.00', 'type' => null, 'label' => null];
if ($nationalityType === 'egyptian') {
$familyDiscount = self::calculateFamilyDiscount($lineItems);
}
// No-qualification discount (only for EGP, only if no family discount already applied)
$qualDiscount = '0.00';
if ($nationalityType === 'egyptian' && $familyDiscount['type'] === null && !$hasQualification) {
$qualDiscount = self::calculateNoQualificationDiscount($subtotal, $hasQualification);
}
$totalDiscount = bcadd($familyDiscount['amount'], $qualDiscount, 2);
$afterDiscount = bcsub($subtotal, $totalDiscount, 2);
if (bccomp($afterDiscount, '0', 2) < 0) $afterDiscount = '0.00';
// VAT
$vatAmount = self::calculateVat($afterDiscount);
$grandTotal = bcadd($afterDiscount, $vatAmount, 2);
return [
'items' => $lineItems,
'subtotal' => $subtotal,
'family_discount' => $familyDiscount,
'qual_discount' => $qualDiscount,
'total_discount' => $totalDiscount,
'after_discount' => $afterDiscount,
'vat_rate' => self::getVatRate(),
'vat_amount' => $vatAmount,
'grand_total' => $grandTotal,
'currency' => $currency,
'duration_months' => $durationMonths,
'nationality_type' => $nationalityType,
'includes_pool' => true,
];
}
public static function getPricingTable(string $nationalityType): array
{
if ($nationalityType === 'foreign') {
return self::FOREIGN_PRICES;
}
return self::EGYPTIAN_PRICES;
}
public static function getCategoryLabel(string $category): string
{
return match ($category) {
'member' => 'العضو',
'spouse' => 'الزوجة',
'under_15' => 'أبناء أقل من 15 عام',
'15_to_21' => 'أبناء أكثر من 15 عام وأقل من 21 عام',
'over_21' => 'أبناء أكثر من 21 عام',
'over_60' => 'العضو أكثر من 60 عام',
default => $category,
};
}
}
......@@ -2,13 +2,30 @@
<?php $__template->section('title'); ?>عضوية موسمية — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:15px;padding:15px;display:flex;justify-content:space-between;align-items:center;">
<div><strong>العضو:</strong> <?= e($member['full_name_ar']) ?> &nbsp;|&nbsp; <strong>قيمة العضوية:</strong> <?= money($member['membership_value'] ?? '0') ?></div>
<div><strong>العضو:</strong> <?= e($member['full_name_ar']) ?> &nbsp;|&nbsp; <strong>رقم العضوية:</strong> <?= e($member['membership_number'] ?? '—') ?></div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة</a>
</div>
<form method="POST" action="/members/<?= (int) $member['id'] ?>/seasonal">
<form method="POST" action="/members/<?= (int) $member['id'] ?>/seasonal" id="seasonalForm">
<?= csrf_field() ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<h3 style="margin:0 0 15px;color:#0D7377;">1. نوع العضوية والمدة</h3>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">نوع الجنسية <span style="color:#DC2626;">*</span></label>
<select name="nationality_type" id="nationalityType" class="form-select" required onchange="updatePricing()">
<option value="egyptian">مصري (جنيه مصري)</option>
<option value="foreign">أجنبي (دولار أمريكي)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">المدة <span style="color:#DC2626;">*</span></label>
<select name="duration_months" id="durationMonths" class="form-select" required onchange="updatePricing()">
<option value="6">6 أشهر</option>
<option value="12">سنة</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الفرع <span style="color:#DC2626;">*</span></label>
<select name="branch_id" class="form-select" required>
......@@ -20,28 +37,192 @@
<label class="form-label">تاريخ البداية <span style="color:#DC2626;">*</span></label>
<input type="date" name="start_date" value="<?= e(date('Y-m-d')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">المدة</label>
<input type="text" value="<?= (int) $maxMonths ?> أشهر" class="form-input" disabled style="background:#F3F4F6;">
</div>
<div class="form-group">
<label class="form-label">الرسوم (<?= e($pct) ?>% من قيمة العضوية)</label>
<input type="text" value="<?= money($fee) ?>" class="form-input" disabled style="background:#F3F4F6;font-weight:700;color:#0D7377;">
</div>
<div style="margin-top:10px;">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;color:#6B7280;">
<input type="checkbox" name="has_qualification" value="1" checked onchange="updatePricing()">
العضو حاصل على مؤهل عالي / متوسط (عدم التفعيل = خصم 10%)
</label>
</div>
</div>
<!-- Family Members Selection -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<h3 style="margin:0 0 15px;color:#0D7377;">2. أفراد الأسرة المشتركون</h3>
<p style="color:#6B7280;font-size:13px;margin:0 0 15px;">العضو نفسه مشمول تلقائياً. اختر من أفراد الأسرة للاشتراك معه.</p>
<?php if (!empty($spouses)): ?>
<div style="margin-bottom:15px;">
<div style="font-size:12px;color:#0D7377;font-weight:700;margin-bottom:8px;">الزوجات</div>
<?php foreach ($spouses as $sp): ?>
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#F9FAFB;border-radius:8px;margin-bottom:4px;cursor:pointer;">
<input type="checkbox" name="spouses[]" value="<?= (int) $sp['id'] ?>" onchange="updatePricing()">
<span><?= e($sp['full_name_ar']) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($children)): ?>
<div style="margin-bottom:15px;">
<div style="font-size:12px;color:#0D7377;font-weight:700;margin-bottom:8px;">الأبناء</div>
<?php foreach ($children as $ch): ?>
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:#F9FAFB;border-radius:8px;margin-bottom:4px;cursor:pointer;">
<input type="checkbox" name="children[]" value="<?= (int) $ch['id'] ?>" onchange="updatePricing()">
<span><?= e($ch['full_name_ar']) ?> (<?= (int) $ch['age'] ?> سنة — <?= $ch['gender'] === 'male' ? 'ذكر' : 'أنثى' ?>)</span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (empty($spouses) && empty($children)): ?>
<p style="color:#9CA3AF;font-size:13px;">لا يوجد أفراد أسرة مسجلون.</p>
<?php endif; ?>
</div>
<!-- Pricing Preview -->
<div class="card" style="padding:20px;margin-bottom:20px;border:2px solid #0D7377;" id="pricingPreview">
<h3 style="margin:0 0 15px;color:#0D7377;">3. ملخص الرسوم</h3>
<div id="pricingBody">
<p style="color:#6B7280;text-align:center;">جاري حساب الرسوم...</p>
</div>
</div>
<!-- Pricing Tables Reference -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<h3 style="margin:0 0 15px;color:#6B7280;font-size:14px;">جدول الأسعار المرجعي</h3>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<!-- Egyptian -->
<div>
<h4 style="margin:0 0 10px;color:#0D7377;font-size:13px;">مصريين (ج.م) — شامل حمام السباحة</h4>
<table style="width:100%;font-size:12px;border-collapse:collapse;">
<thead><tr style="background:#F3F4F6;"><th style="padding:6px;text-align:right;">الفئة</th><th style="padding:6px;">6 أشهر</th><th style="padding:6px;">سنة</th></tr></thead>
<tbody>
<?php
$catOrder = ['member', 'spouse', 'under_15', '15_to_21', 'over_21', 'over_60'];
foreach ($catOrder as $cat):
$label = \App\Modules\Seasonal\Services\SeasonalPricingService::getCategoryLabel($cat);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:5px;"><?= e($label) ?></td>
<td style="padding:5px;text-align:center;"><?= number_format((float)($egyptianPrices[6][$cat] ?? 0)) ?></td>
<td style="padding:5px;text-align:center;"><?= number_format((float)($egyptianPrices[12][$cat] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea>
<!-- Foreign -->
<div>
<h4 style="margin:0 0 10px;color:#0D7377;font-size:13px;">أجانب (دولار) — شامل حمام السباحة</h4>
<table style="width:100%;font-size:12px;border-collapse:collapse;">
<thead><tr style="background:#F3F4F6;"><th style="padding:6px;text-align:right;">الفئة</th><th style="padding:6px;">3 شهور</th><th style="padding:6px;">6 أشهر</th><th style="padding:6px;">سنة</th></tr></thead>
<tbody>
<?php
$foreignCats = ['member', 'spouse', 'under_15', '15_to_21', 'over_21'];
foreach ($foreignCats as $cat):
$label = \App\Modules\Seasonal\Services\SeasonalPricingService::getCategoryLabel($cat);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:5px;"><?= e($label) ?></td>
<td style="padding:5px;text-align:center;">$<?= number_format((float)($foreignPrices[3][$cat] ?? 0)) ?></td>
<td style="padding:5px;text-align:center;">$<?= number_format((float)($foreignPrices[6][$cat] ?? 0)) ?></td>
<td style="padding:5px;text-align:center;">$<?= number_format((float)($foreignPrices[12][$cat] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div style="margin-top:15px;padding:15px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:8px;font-size:13px;">
<strong style="color:#D97706;">⚠ تنبيهات:</strong><br>
• العضوية الموسمية مدتها <?= (int) $maxMonths ?> أشهر<br>
• لا يحق للعضو الموسمي المشاركة في فرق النادي<br>
• الكارنيه مميز بعلامة خاصة<br>
• يمكن للنادي إلغاء العضوية في أي وقت للمخالفات بدون استرداد
<div style="margin-top:15px;padding:10px;background:#FFF7ED;border:1px solid #FED7AA;border-radius:8px;font-size:12px;color:#92400E;">
<strong>الخصومات المتاحة (خصم واحد فقط):</strong><br>
• والد/ة مع ابن أقل من 15 سنة في نفس الفترة: خصم 500 ج.م<br>
• زوج وزوجة في نفس الفترة: خصم 1000 ج.م<br>
• أسرة 5 أفراد (زوج + زوجة + 3 أبناء): خصم 1000 ج.م<br>
• بدون مؤهل عالي/متوسط: خصم 10% من قيمة الاشتراك<br>
<strong style="color:#DC2626;">• يُضاف ضريبة القيمة المضافة على جميع المبالغ</strong>
</div>
</div>
<button type="submit" class="btn btn-primary">إنشاء العضوية الموسمية</button>
<div class="form-group" style="margin-bottom:20px;">
<label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea>
</div>
<button type="submit" class="btn btn-primary" style="padding:12px 30px;">إنشاء العضوية الموسمية وإرسال للخزينة</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</form>
<?php $__template->endSection(); ?>
\ No newline at end of file
<script>
const memberId = <?= (int) $member['id'] ?>;
let debounceTimer = null;
function updatePricing() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(fetchPricing, 300);
updateDurationOptions();
}
function updateDurationOptions() {
const nat = document.getElementById('nationalityType').value;
const sel = document.getElementById('durationMonths');
const current = sel.value;
sel.innerHTML = '';
if (nat === 'foreign') {
sel.innerHTML = '<option value="3">3 شهور</option><option value="6">6 أشهر</option><option value="12">سنة</option>';
} else {
sel.innerHTML = '<option value="6">6 أشهر</option><option value="12">سنة</option>';
}
if ([...sel.options].some(o => o.value === current)) sel.value = current;
}
function fetchPricing() {
const nat = document.getElementById('nationalityType').value;
const dur = document.getElementById('durationMonths').value;
const hasQual = document.querySelector('[name="has_qualification"]').checked ? '1' : '0';
const spouses = [...document.querySelectorAll('[name="spouses[]"]:checked')].map(e => e.value).join(',');
const children = [...document.querySelectorAll('[name="children[]"]:checked')].map(e => e.value).join(',');
const url = '/api/seasonal/price-preview?nationality_type=' + nat + '&duration_months=' + dur
+ '&member_id=' + memberId + '&has_qualification=' + hasQual
+ '&spouses=' + spouses + '&children=' + children;
fetch(url, {credentials: 'same-origin'})
.then(r => r.json())
.then(data => renderPricing(data))
.catch(() => {
document.getElementById('pricingBody').innerHTML = '<p style="color:#DC2626;">خطأ في حساب الرسوم</p>';
});
}
function renderPricing(p) {
if (p.error) {
document.getElementById('pricingBody').innerHTML = '<p style="color:#DC2626;">' + p.error + '</p>';
return;
}
const cur = p.currency;
let html = '<table style="width:100%;font-size:14px;border-collapse:collapse;">';
html += '<thead><tr style="border-bottom:2px solid #E5E7EB;"><th style="padding:8px;text-align:right;">الفرد</th><th style="padding:8px;text-align:left;">المبلغ</th></tr></thead><tbody>';
p.items.forEach(item => {
html += '<tr style="border-bottom:1px solid #F3F4F6;"><td style="padding:8px;">' + item.person_name + '</td><td style="padding:8px;text-align:left;font-weight:600;">' + parseFloat(item.base_price).toLocaleString() + ' ' + cur + '</td></tr>';
});
html += '</tbody></table>';
html += '<div style="margin-top:15px;padding:12px;background:#F9FAFB;border-radius:8px;">';
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;"><span>المجموع</span><strong>' + parseFloat(p.subtotal).toLocaleString() + ' ' + cur + '</strong></div>';
if (parseFloat(p.total_discount) > 0) {
const discLabel = p.family_discount.label || 'خصم عدم وجود مؤهل';
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;color:#059669;"><span>🏷️ ' + discLabel + '</span><strong>-' + parseFloat(p.total_discount).toLocaleString() + ' ' + cur + '</strong></div>';
}
html += '<div style="display:flex;justify-content:space-between;padding:4px 0;color:#6B7280;"><span>ضريبة القيمة المضافة (' + p.vat_rate + '%)</span><span>' + parseFloat(p.vat_amount).toLocaleString() + ' ' + cur + '</span></div>';
html += '<div style="display:flex;justify-content:space-between;padding:8px 0;border-top:2px solid #0D7377;margin-top:8px;font-size:18px;"><span style="font-weight:700;color:#0D7377;">الإجمالي</span><strong style="color:#0D7377;">' + parseFloat(p.grand_total).toLocaleString() + ' ' + cur + '</strong></div>';
html += '</div>';
document.getElementById('pricingBody').innerHTML = html;
}
document.addEventListener('DOMContentLoaded', fetchPricing);
</script>
<?php $__template->endSection(); ?>
......@@ -50,18 +50,18 @@ $genderValue = $requiredGender;
</div>
<div class="form-group">
<label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" id="spouse_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<label class="form-label">الرقم القومي <small style="color:#0284C7;">(يستخرج تاريخ الميلاد والسن تلقائياً)</small></label>
<input type="text" name="national_id" id="spouse_nid" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:2px;" placeholder="14 رقم">
<div id="spouse-nid-feedback" style="margin-top:5px;font-size:12px;"></div>
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span> <small id="dob_source" style="color:#059669;display:none;">(من الرقم القومي)</small></label>
<input type="date" name="date_of_birth" id="spouse_dob" value="<?= e(old('date_of_birth')) ?>" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">السن</label>
<label class="form-label">السن <small id="age_source" style="color:#059669;display:none;">(محسوب تلقائياً)</small></label>
<input type="text" id="spouse_age_display" class="form-input" style="background:#F3F4F6;" readonly>
<input type="hidden" name="age_years" id="spouse_age_years">
<input type="hidden" name="age_months" id="spouse_age_months">
......@@ -136,6 +136,10 @@ document.addEventListener('DOMContentLoaded', function() {
var feedback = document.getElementById('spouse-nid-feedback');
var requiredGender = '<?= e($genderValue) ?>';
var dobSource = document.getElementById('dob_source');
var ageSource = document.getElementById('age_source');
var nidLocked = false;
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
......@@ -151,26 +155,39 @@ document.addEventListener('DOMContentLoaded', function() {
var p = data.parsed;
if (p && p.is_valid) {
dobInput.value = p.dob;
dobInput.readOnly = true;
dobInput.style.background = '#F0FDF4';
dobSource.style.display = 'inline';
ageSource.style.display = 'inline';
ageDisplay.value = p.age_years + ' سنة و ' + p.age_months + ' شهر';
ageYears.value = p.age_years;
ageMonths.value = p.age_months;
feedback.innerHTML = '<span style="color:#059669;">✓ صالح</span>';
nidLocked = true;
feedback.innerHTML = '<span style="color:#059669;">صالح — تم استخراج تاريخ الميلاد والسن</span>';
// Check gender mismatch
if (p.gender !== requiredGender) {
var needed = requiredGender === 'male' ? 'ذكر' : 'أنثى';
feedback.innerHTML += '<br><span style="color:#DC2626;">الرقم القومي يشير إلى نوع مختلف — المطلوب: ' + needed + '</span>';
feedback.innerHTML += '<br><span style="color:#DC2626;">الرقم القومي يشير إلى نوع مختلف — المطلوب: ' + needed + '</span>';
}
if (data.duplicate) {
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>';
feedback.innerHTML += '<br><span style="color:#DC2626;">مسجل: ' + data.duplicate.full_name_ar + '</span>';
}
} else {
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
feedback.innerHTML = '<span style="color:#DC2626;">' + (p.errors ? p.errors.join(' | ') : 'غير صالح') + '</span>';
}
})
.catch(function() { feedback.innerHTML = '<span style="color:#DC2626;">خطأ</span>'; });
} else { feedback.innerHTML = ''; }
} else {
feedback.innerHTML = '';
if (nidLocked) {
dobInput.readOnly = false;
dobInput.style.background = '';
dobSource.style.display = 'none';
ageSource.style.display = 'none';
nidLocked = false;
}
}
});
dobInput.addEventListener('change', function() {
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE `seasonal_memberships` ADD COLUMN `nationality_type` ENUM('egyptian','foreign') NOT NULL DEFAULT 'egyptian' AFTER `branch_id`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `duration_months` TINYINT UNSIGNED NOT NULL DEFAULT 6 AFTER `nationality_type`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `person_type` ENUM('member','spouse','child') NOT NULL DEFAULT 'member' AFTER `duration_months`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `person_name` VARCHAR(200) NULL AFTER `person_type`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `person_age` TINYINT UNSIGNED NULL AFTER `person_name`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `age_category` ENUM('under_15','15_to_21','over_21','over_60') NULL AFTER `person_age`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `currency` ENUM('EGP','USD') NOT NULL DEFAULT 'EGP' AFTER `fee_amount`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `vat_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `currency`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `discount_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `vat_amount`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `discount_type` VARCHAR(100) NULL AFTER `discount_amount`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `total_amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00 AFTER `discount_type`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `parent_seasonal_id` BIGINT UNSIGNED NULL AFTER `member_id`;
ALTER TABLE `seasonal_memberships` ADD COLUMN `includes_pool` TINYINT(1) NOT NULL DEFAULT 0 AFTER `carnet_marking`;
ALTER TABLE `seasonal_memberships` ADD INDEX `idx_seasonal_nationality` (`nationality_type`);
ALTER TABLE `seasonal_memberships` ADD INDEX `idx_seasonal_parent` (`parent_seasonal_id`)",
'down' => "
ALTER TABLE `seasonal_memberships` DROP INDEX IF EXISTS `idx_seasonal_parent`;
ALTER TABLE `seasonal_memberships` DROP INDEX IF EXISTS `idx_seasonal_nationality`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `includes_pool`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `parent_seasonal_id`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `total_amount`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `discount_type`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `discount_amount`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `vat_amount`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `currency`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `age_category`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `person_age`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `person_name`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `person_type`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `duration_months`;
ALTER TABLE `seasonal_memberships` DROP COLUMN IF EXISTS `nationality_type`",
];
<?php
declare(strict_types=1);
use App\Core\Database;
return function (Database $db): void {
$rules = [
['key' => 'SEASONAL_VAT_RATE', 'value' => json_encode(['percentage' => '14.00']), 'description' => 'نسبة ضريبة القيمة المضافة على الاشتراكات الموسمية'],
['key' => 'SEASONAL_FAMILY_DISCOUNT_PARENT_CHILD', 'value' => json_encode(['amount' => '500.00']), 'description' => 'خصم والد مع ابن أقل من 15 سنة'],
['key' => 'SEASONAL_FAMILY_DISCOUNT_COUPLE', 'value' => json_encode(['amount' => '1000.00']), 'description' => 'خصم زوج وزوجة في نفس الفترة'],
['key' => 'SEASONAL_FAMILY_DISCOUNT_FAMILY_5', 'value' => json_encode(['amount' => '1000.00']), 'description' => 'خصم أسرة 5 أفراد'],
['key' => 'SEASONAL_NO_QUALIFICATION_DISCOUNT', 'value' => json_encode(['percentage' => '10.00']), 'description' => 'خصم عدم وجود مؤهل عالي/متوسط'],
];
foreach ($rules as $rule) {
$exists = $db->selectOne("SELECT id FROM system_config WHERE config_key = ?", [$rule['key']]);
if (!$exists) {
$db->insert('system_config', [
'config_key' => $rule['key'],
'config_value' => $rule['value'],
'description' => $rule['description'],
'is_active' => 1,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
};
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