Commit 014320e1 authored by Fares's avatar Fares

feat(dependents): add non-Egyptian checkbox + passport field for foreign member dependents

Foreign membership dependents (spouses, children, temporary) are included
in the membership fee — zero additional charges. Add a "not Egyptian"
checkbox that toggles between NID and passport number fields on all three
dependent creation forms. Server-side validation skips NID requirement
when non-Egyptian is checked and requires passport instead.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 4733feb0
......@@ -89,9 +89,16 @@ class ChildController extends Controller
elseif (!in_array($data['gender'], ['male', 'female'], true)) $errors[] = 'النوع يجب أن يكون ذكر أو أنثى';
if (empty($data['relationship'] ?? '')) $errors[] = 'نوع القرابة مطلوب';
// Parse NID if provided
// Non-Egyptian: require passport instead of NID
$isNonEgyptian = !empty($data['is_non_egyptian']);
$passportNumber = trim($data['passport_number'] ?? '');
if ($isNonEgyptian && $passportNumber === '') {
$errors[] = 'رقم جواز السفر مطلوب لغير المصريين';
}
// Parse NID if provided (only for Egyptians)
$nid = trim($data['national_id'] ?? '');
if ($nid !== '') {
if (!$isNonEgyptian && $nid !== '') {
$parsed = NationalIdParser::parse($nid);
if (!$parsed['is_valid']) {
$errors = array_merge($errors, $parsed['errors']);
......@@ -122,8 +129,9 @@ class ChildController extends Controller
$childAge = (int) ($data['age_years'] ?? 0);
// NID required for Egyptian children over age threshold
$nidRequiredAge = (int) (RuleEngine::getValue('CHILD_NID_REQUIRED_AGE', 'value') ?? 16);
if ($childAge >= $nidRequiredAge && $nid === '') {
if (!$isNonEgyptian && $childAge >= $nidRequiredAge && $nid === '') {
$errors[] = 'الأبناء فوق ' . $nidRequiredAge . ' سنة يجب أن يكون لديهم رقم قومي';
}
......@@ -189,6 +197,7 @@ class ChildController extends Controller
'full_name_ar' => trim($data['full_name_ar']),
'full_name_en' => trim($data['full_name_en'] ?? ''),
'national_id' => $nid ?: null,
'passport_number' => $isNonEgyptian ? $passportNumber : null,
'birth_certificate_number' => $data['birth_certificate_number'] ?? null,
'date_of_birth' => $data['date_of_birth'],
'age_years' => (int) ($data['age_years'] ?? 0),
......
......@@ -17,7 +17,7 @@ class Child extends Model
protected static array $fillable = [
'member_id', 'child_order', 'full_name_ar', 'full_name_en',
'national_id', 'birth_certificate_number', 'date_of_birth',
'national_id', 'passport_number', 'birth_certificate_number', 'date_of_birth',
'age_years', 'age_months', 'gender', 'relationship',
'school_faculty', 'nationality', 'classification',
'addition_fee', 'fee_breakdown_json', 'fee_receipt_number', 'status',
......
......@@ -23,6 +23,29 @@ final class ChildFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
// Foreign membership: spouse + 3 children included in the $10k fee — no extra charges
if (($member['membership_type'] ?? 'working') === 'foreign' && $isOnInitialForm) {
$totalChildren = Child::countActiveForMember($memberId);
$childOrder = $totalChildren + 1;
$childDob = $childData['date_of_birth'] ?? null;
$childAge = $childDob ? age_from_dob($childDob)['years'] : 0;
return [
'child_order' => $childOrder,
'child_age' => $childAge,
'children_under_18' => Child::countActiveUnder18ForMember($memberId),
'membership_value' => '0.00',
'classification' => 'included',
'fee' => '0.00',
'percentage' => '0.00',
'form_fee' => '0.00',
'annual_subscription' => '0.00',
'total_fee' => '0.00',
'rule_applied' => 'العضوية الأجنبية — الأبناء مشمولون في رسوم العضوية',
'error' => null,
'breakdown' => ['✅ مشمول/ة في رسوم العضوية الأجنبية — بدون رسوم إضافية'],
];
}
// Always use current membership value from pricing_configs
$qualId = !empty($member['qualification_id']) ? (int) $member['qualification_id'] : null;
$branchId = (int) ($member['branch_id'] ?? 1);
......
......@@ -33,12 +33,23 @@
<option value="daughter" <?= old('relationship') === 'daughter' ? 'selected' : '' ?>>ابنة</option>
</select>
</div>
<div class="form-group">
<div class="form-group" style="grid-column:1/-1;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:8px 12px;background:#FEF3C7;border-radius:8px;border:1px solid #F59E0B;">
<input type="checkbox" id="child_not_egyptian_check" name="is_non_egyptian" value="1" <?= old('is_non_egyptian') ? 'checked' : '' ?> style="width:18px;height:18px;">
<span style="font-weight:600;color:#92400E;">🛂 غير مصري/ة (يحمل جواز سفر أجنبي)</span>
</label>
</div>
<div class="form-group" id="child_nid_group">
<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">
<div class="form-group" id="child_passport_group" style="display:none;">
<label class="form-label">رقم جواز السفر <span style="color:#DC2626;">*</span></label>
<input type="text" name="passport_number" id="child_passport_input" value="<?= e(old('passport_number')) ?>" class="form-input" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="رقم الجواز">
</div>
<div class="form-group" id="child_birth_cert_group">
<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>
......@@ -117,6 +128,31 @@ document.addEventListener('DOMContentLoaded', function() {
var genderSourceEl = document.getElementById('child_gender_source');
var nidLocked = false;
// Non-Egyptian toggle
var notEgyptianCheck = document.getElementById('child_not_egyptian_check');
var nidGroup = document.getElementById('child_nid_group');
var passportGroup = document.getElementById('child_passport_group');
var passportInput = document.getElementById('child_passport_input');
var birthCertGroup = document.getElementById('child_birth_cert_group');
function toggleChildNationality() {
if (notEgyptianCheck.checked) {
nidGroup.style.display = 'none';
nidInput.value = '';
birthCertGroup.style.display = 'none';
passportGroup.style.display = '';
passportInput.required = true;
} else {
nidGroup.style.display = '';
birthCertGroup.style.display = '';
passportGroup.style.display = 'none';
passportInput.required = false;
passportInput.value = '';
}
}
notEgyptianCheck.addEventListener('change', toggleChildNationality);
toggleChildNationality();
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
......
......@@ -123,9 +123,15 @@ class SpouseController extends Controller
// Force the correct gender regardless of what was submitted
$data['gender'] = $requiredGender;
// ── NID parsing ──
// ── Non-Egyptian: require passport ──
$isNonEgyptian = !empty($data['is_non_egyptian']);
if ($isNonEgyptian && empty(trim($data['passport_number'] ?? ''))) {
$errors[] = 'رقم جواز السفر مطلوب لغير المصريين';
}
// ── NID parsing (only for Egyptians) ──
$nid = trim($data['national_id'] ?? '');
if ($nid !== '') {
if (!$isNonEgyptian && $nid !== '') {
$parsed = NationalIdParser::parse($nid);
if (!$parsed['is_valid']) {
$errors = array_merge($errors, $parsed['errors']);
......
......@@ -35,6 +35,32 @@ final class SpouseFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
// Foreign membership: spouse + 3 children included in the $10k fee — no extra charges
if (($member['membership_type'] ?? 'working') === 'foreign' && $isOnInitialForm) {
$existingCount = Spouse::countActiveForMember($memberId);
$spouseOrder = $existingCount + 1;
return [
'spouse_order' => $spouseOrder,
'membership_value' => '0.00',
'is_initial' => true,
'is_late_addition' => false,
'is_acquired' => false,
'is_foreign' => false,
'percentage' => '0.00',
'percentage_fee' => '0.00',
'annual_per_year' => '0.00',
'year_count' => 0,
'yearly_total' => '0.00',
'addition_fee' => '0.00',
'form_fee' => '0.00',
'annual_subscription' => '0.00',
'total_fee' => '0.00',
'rule_applied' => 'العضوية الأجنبية — الزوج/ة مشمول/ة في رسوم العضوية',
'error' => null,
'breakdown' => ['✅ مشمول/ة في رسوم العضوية الأجنبية — بدون رسوم إضافية'],
];
}
// Always use current membership value from pricing_configs
$qualId = !empty($member['qualification_id']) ? (int) $member['qualification_id'] : null;
$branchId = (int) ($member['branch_id'] ?? 1);
......
......@@ -49,12 +49,24 @@ $genderValue = $requiredGender;
<small style="color:#6B7280;font-size:11px;">يتم تحديده تلقائياً بناءً على نوع العضو/ة</small>
</div>
<div class="form-group">
<div class="form-group" id="nid_group">
<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" style="grid-column:1/-1;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:8px 12px;background:#FEF3C7;border-radius:8px;border:1px solid #F59E0B;">
<input type="checkbox" id="not_egyptian_check" name="is_non_egyptian" value="1" <?= old('is_non_egyptian') ? 'checked' : '' ?> style="width:18px;height:18px;">
<span style="font-weight:600;color:#92400E;">🛂 غير مصري/ة (يحمل جواز سفر أجنبي)</span>
</label>
</div>
<div class="form-group" id="passport_group" style="display:none;">
<label class="form-label">رقم جواز السفر <span style="color:#DC2626;">*</span></label>
<input type="text" name="passport_number" id="passport_number_input" value="<?= e(old('passport_number')) ?>" class="form-input" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="رقم الجواز">
</div>
<div class="form-group">
<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>
......@@ -151,6 +163,28 @@ document.addEventListener('DOMContentLoaded', function() {
var ageSource = document.getElementById('age_source');
var nidLocked = false;
// Non-Egyptian toggle
var notEgyptianCheck = document.getElementById('not_egyptian_check');
var nidGroup = document.getElementById('nid_group');
var passportGroup = document.getElementById('passport_group');
var passportInput = document.getElementById('passport_number_input');
function toggleNationality() {
if (notEgyptianCheck.checked) {
nidGroup.style.display = 'none';
nidInput.value = '';
passportGroup.style.display = '';
passportInput.required = true;
} else {
nidGroup.style.display = '';
passportGroup.style.display = 'none';
passportInput.required = false;
passportInput.value = '';
}
}
notEgyptianCheck.addEventListener('change', toggleNationality);
toggleNationality();
nidInput.addEventListener('input', function() {
var val = this.value.replace(/\D/g, '');
this.value = val;
......
......@@ -126,8 +126,14 @@ class TemporaryController extends Controller
if (empty($data['gender'] ?? '')) $errors[] = 'النوع مطلوب';
elseif (!in_array($data['gender'], ['male', 'female'], true)) $errors[] = 'النوع يجب أن يكون ذكر أو أنثى';
// Non-Egyptian: require passport instead of NID
$isNonEgyptian = !empty($data['is_non_egyptian']);
if ($isNonEgyptian && empty(trim($data['passport_number'] ?? ''))) {
$errors[] = 'رقم جواز السفر مطلوب لغير المصريين';
}
$nid = trim($data['national_id'] ?? '');
if ($nid !== '') {
if (!$isNonEgyptian && $nid !== '') {
$parsed = NationalIdParser::parse($nid);
if (!$parsed['is_valid']) {
$errors = array_merge($errors, $parsed['errors']);
......
......@@ -20,6 +20,21 @@ final class TemporaryFeeCalculator
$membershipValue = $member['membership_value'] ?? '0.00';
$isOnInitialForm = FormFeeService::isOnInitialForm($member);
// Foreign membership: dependents included in the $10k fee — no extra charges
if (($member['membership_type'] ?? 'working') === 'foreign' && $isOnInitialForm) {
return [
'membership_value' => '0.00',
'fee' => '0.00',
'percentage' => '0.00',
'form_fee' => '0.00',
'total_fee' => '0.00',
'rule_applied' => 'العضوية الأجنبية — مشمول في رسوم العضوية',
'exempt' => true,
'error' => null,
'breakdown' => ['✅ مشمول في رسوم العضوية الأجنبية — بدون رسوم إضافية'],
];
}
// Always use current pricing_configs price (same logic as SpouseFeeCalculator / ChildFeeCalculator)
$qualId = !empty($member['qualification_id']) ? (int) $member['qualification_id'] : null;
$branchId = (int) ($member['branch_id'] ?? 1);
......
......@@ -29,11 +29,22 @@
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<div class="form-group" style="grid-column:1/-1;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;padding:8px 12px;background:#FEF3C7;border-radius:8px;border:1px solid #F59E0B;">
<input type="checkbox" id="temp_not_egyptian_check" name="is_non_egyptian" value="1" <?= old('is_non_egyptian') ? 'checked' : '' ?> style="width:18px;height:18px;">
<span style="font-weight:600;color:#92400E;">🛂 غير مصري/ة (يحمل جواز سفر أجنبي)</span>
</label>
</div>
<div class="form-group" id="temp_nid_group">
<label class="form-label">الرقم القومي</label>
<input type="text" name="national_id" id="nidInput" value="<?= e(old('national_id')) ?>" class="form-input" maxlength="14" style="direction:ltr;text-align:left;">
<small id="nidStatus" style="display:none;margin-top:4px;font-size:11px;"></small>
</div>
<div class="form-group" id="temp_passport_group" style="display:none;">
<label class="form-label">رقم جواز السفر <span style="color:#DC2626;">*</span></label>
<input type="text" name="passport_number" id="temp_passport_input" value="<?= e(old('passport_number')) ?>" class="form-input" style="direction:ltr;text-align:left;font-size:16px;letter-spacing:1px;" placeholder="رقم الجواز">
</div>
<div class="form-group">
<label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="date_of_birth" id="dobInput" value="<?= e(old('date_of_birth')) ?>" class="form-input" required readonly style="background:#F9FAFB;">
......@@ -97,6 +108,31 @@
var status = document.getElementById('nidStatus');
if (!nid) return;
// Non-Egyptian toggle
var notEgyptianCheck = document.getElementById('temp_not_egyptian_check');
var nidGroup = document.getElementById('temp_nid_group');
var passportGroup = document.getElementById('temp_passport_group');
var passportInput = document.getElementById('temp_passport_input');
function toggleTempNationality() {
if (notEgyptianCheck.checked) {
nidGroup.style.display = 'none';
nid.value = '';
passportGroup.style.display = '';
passportInput.required = true;
dob.readOnly = false;
dob.style.background = '';
genderSelect.disabled = false;
} else {
nidGroup.style.display = '';
passportGroup.style.display = 'none';
passportInput.required = false;
passportInput.value = '';
}
}
notEgyptianCheck.addEventListener('change', toggleTempNationality);
toggleTempNationality();
function parseNid(val) {
if (val.length !== 14) { status.style.display = 'none'; dob.readOnly = false; dob.style.background = ''; genderSelect.disabled = false; return; }
var formData = new FormData();
......
<?php
declare(strict_types=1);
return [
'up' => "ALTER TABLE children ADD COLUMN passport_number VARCHAR(50) NULL DEFAULT NULL AFTER national_id",
'down' => "ALTER TABLE children DROP COLUMN passport_number",
];
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