Commit a1bc3213 authored by Fares's avatar Fares

feat(seasonal): collect full data for inline spouses, children, and temporary members

Inline family member forms now collect all fields matching standalone creation
forms (NID with auto-parsing, passport, DOB, gender, nationality, qualification,
contact info, etc.) instead of just names. Also adds temporary member inline creation.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 53d68f25
......@@ -54,6 +54,7 @@ class SeasonalController extends Controller
if ($existing) return $this->redirect("/members/{$memberId}")->withError('بيانات العضوية الموسمية مسجلة بالفعل');
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
$qualifications = $db->select("SELECT id, name_ar FROM qualifications WHERE is_active = 1 ORDER BY sort_order");
$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]);
......@@ -64,6 +65,7 @@ class SeasonalController extends Controller
return $this->view('Seasonal.Views.create', [
'member' => $member,
'branches' => $branches,
'qualifications' => $qualifications,
'spouses' => $spouses,
'children' => $children,
'egyptianPrices' => $egyptianPrices,
......@@ -134,14 +136,31 @@ class SeasonalController extends Controller
$newSpouses = (array) ($data['new_spouses'] ?? []);
$createdSpouseIds = [];
foreach ($newSpouses as $ns) {
$spName = trim($ns['name'] ?? '');
$spName = trim($ns['full_name_ar'] ?? '');
if ($spName === '') continue;
$spDob = (!empty($ns['date_of_birth']) && strtotime($ns['date_of_birth'])) ? $ns['date_of_birth'] : null;
$spAgeYears = $spDob ? (int) ((time() - strtotime($spDob)) / 31557600) : null;
$spAgeMonths = $spDob ? (int) (((time() - strtotime($spDob)) % 31557600) / 2629800) : null;
$sp = \App\Modules\Spouses\Models\Spouse::create([
'member_id' => (int) $memberId,
'full_name_ar' => $spName,
'spouse_order' => \App\Modules\Spouses\Models\Spouse::countActiveForMember((int) $memberId) + 1,
'status' => 'active',
'join_date' => date('Y-m-d'),
'member_id' => (int) $memberId,
'full_name_ar' => $spName,
'full_name_en' => trim($ns['full_name_en'] ?? '') ?: null,
'national_id' => trim($ns['national_id'] ?? '') ?: null,
'passport_number' => trim($ns['passport_number'] ?? '') ?: null,
'date_of_birth' => $spDob,
'age_years' => $spAgeYears,
'age_months' => $spAgeMonths,
'gender' => $ns['gender'] ?? 'female',
'nationality' => $ns['nationality'] ?? 'egyptian',
'marriage_date' => (!empty($ns['marriage_date']) && strtotime($ns['marriage_date'])) ? $ns['marriage_date'] : null,
'qualification_id' => !empty($ns['qualification_id']) ? (int) $ns['qualification_id'] : null,
'occupation' => trim($ns['occupation'] ?? '') ?: null,
'mobile' => trim($ns['mobile'] ?? '') ?: null,
'work_phone' => trim($ns['work_phone'] ?? '') ?: null,
'work_address' => trim($ns['work_address'] ?? '') ?: null,
'spouse_order' => \App\Modules\Spouses\Models\Spouse::countActiveForMember((int) $memberId) + 1,
'status' => 'active',
'join_date' => date('Y-m-d'),
]);
$createdSpouseIds[] = (int) $sp->id;
$items[] = ['person_type' => 'spouse', 'name' => $spName, 'age' => null];
......@@ -151,28 +170,70 @@ class SeasonalController extends Controller
$newChildren = (array) ($data['new_children'] ?? []);
$createdChildIds = [];
foreach ($newChildren as $nc) {
$chName = trim($nc['name'] ?? '');
$chDob = $nc['dob'] ?? '';
if ($chName === '' || !$chDob || !strtotime($chDob)) continue;
$chName = trim($nc['full_name_ar'] ?? '');
$chDob = (!empty($nc['date_of_birth']) && strtotime($nc['date_of_birth'])) ? $nc['date_of_birth'] : '';
if ($chName === '' || $chDob === '') continue;
$childAge = (int) ((time() - strtotime($chDob)) / 31557600);
$childMonths = (int) (((time() - strtotime($chDob)) % 31557600) / 2629800);
$chGender = in_array($nc['gender'] ?? '', ['male', 'female'], true) ? $nc['gender'] : 'male';
$chRelation = in_array($nc['relationship'] ?? '', ['son', 'daughter'], true) ? $nc['relationship'] : ($chGender === 'female' ? 'daughter' : 'son');
$ch = \App\Modules\Children\Models\Child::create([
'member_id' => (int) $memberId,
'child_order' => \App\Modules\Children\Models\Child::countActiveForMember((int) $memberId) + 1,
'full_name_ar' => $chName,
'date_of_birth' => $chDob,
'age_years' => $childAge,
'age_months' => $childMonths,
'gender' => 'male',
'relationship' => 'son',
'classification' => 'included',
'status' => 'active',
'join_date' => date('Y-m-d'),
'member_id' => (int) $memberId,
'child_order' => \App\Modules\Children\Models\Child::countActiveForMember((int) $memberId) + 1,
'full_name_ar' => $chName,
'full_name_en' => trim($nc['full_name_en'] ?? '') ?: null,
'national_id' => trim($nc['national_id'] ?? '') ?: null,
'passport_number' => trim($nc['passport_number'] ?? '') ?: null,
'birth_certificate_number' => trim($nc['birth_certificate_number'] ?? '') ?: null,
'date_of_birth' => $chDob,
'age_years' => $childAge,
'age_months' => $childMonths,
'gender' => $chGender,
'relationship' => $chRelation,
'nationality' => $nc['nationality'] ?? 'egyptian',
'school_faculty' => trim($nc['school_faculty'] ?? '') ?: null,
'classification' => 'included',
'status' => 'active',
'join_date' => date('Y-m-d'),
]);
$createdChildIds[] = (int) $ch->id;
$items[] = ['person_type' => 'child', 'name' => $chName, 'age' => $childAge];
}
// Add new inline temporary members
$newTemps = (array) ($data['new_temps'] ?? []);
$createdTempIds = [];
foreach ($newTemps as $nt) {
$tName = trim($nt['full_name_ar'] ?? '');
if ($tName === '') continue;
$tDob = (!empty($nt['date_of_birth']) && strtotime($nt['date_of_birth'])) ? $nt['date_of_birth'] : null;
$tAgeYears = $tDob ? (int) ((time() - strtotime($tDob)) / 31557600) : null;
$tAgeMonths = $tDob ? (int) (((time() - strtotime($tDob)) % 31557600) / 2629800) : null;
$tGender = in_array($nt['gender'] ?? '', ['male', 'female'], true) ? $nt['gender'] : 'male';
$tm = \App\Modules\Temporary\Models\TemporaryMember::create([
'member_id' => (int) $memberId,
'full_name_ar' => $tName,
'full_name_en' => trim($nt['full_name_en'] ?? '') ?: null,
'category' => $nt['category'] ?? 'relative',
'national_id' => trim($nt['national_id'] ?? '') ?: null,
'passport_number' => trim($nt['passport_number'] ?? '') ?: null,
'date_of_birth' => $tDob,
'age_years' => $tAgeYears,
'age_months' => $tAgeMonths,
'gender' => $tGender,
'nationality' => $nt['nationality'] ?? 'egyptian',
'relationship_to_member' => trim($nt['relationship_to_member'] ?? '') ?: null,
'has_championship' => !empty($nt['has_championship']) ? 1 : 0,
'disability_documentation' => !empty($nt['disability_documentation']) ? 1 : 0,
'notes' => trim($nt['notes'] ?? '') ?: null,
'status' => 'active',
'join_date' => date('Y-m-d'),
]);
$createdTempIds[] = (int) $tm->id;
$tAge = $tDob ? $tAgeYears : null;
$items[] = ['person_type' => 'child', 'name' => $tName, 'age' => $tAge ?? 20];
}
// Calculate pricing
$pricing = SeasonalPricingService::calculate($items, $nationalityType, $durationMonths, $hasQualification);
......
......@@ -78,20 +78,29 @@
<!-- Add New Spouses -->
<div style="margin-bottom:15px;border-top:1px solid #E5E7EB;padding-top:15px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<div style="font-size:12px;color:#0D7377;font-weight:700;">إضافة زوجة</div>
<button type="button" onclick="addNewSpouse()" class="btn btn-outline" style="font-size:12px;padding:4px 12px;">+ إضافة</button>
<div style="font-size:12px;color:#0D7377;font-weight:700;">إضافة زوجة جديدة</div>
<button type="button" onclick="addNewSpouse()" class="btn btn-outline" style="font-size:12px;padding:4px 12px;">+ إضافة زوجة</button>
</div>
<div id="newSpousesContainer"></div>
</div>
<!-- Add New Children -->
<div style="border-top:1px solid #E5E7EB;padding-top:15px;">
<div style="margin-bottom:15px;border-top:1px solid #E5E7EB;padding-top:15px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<div style="font-size:12px;color:#0D7377;font-weight:700;">إضافة أبناء</div>
<button type="button" onclick="addNewChild()" class="btn btn-outline" style="font-size:12px;padding:4px 12px;">+ إضافة</button>
<div style="font-size:12px;color:#0D7377;font-weight:700;">إضافة أبناء جدد</div>
<button type="button" onclick="addNewChild()" class="btn btn-outline" style="font-size:12px;padding:4px 12px;">+ إضافة ابن/ة</button>
</div>
<div id="newChildrenContainer"></div>
</div>
<!-- Add New Temporary Members -->
<div style="border-top:1px solid #E5E7EB;padding-top:15px;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<div style="font-size:12px;color:#0D7377;font-weight:700;">إضافة أعضاء مؤقتين</div>
<button type="button" onclick="addNewTemp()" class="btn btn-outline" style="font-size:12px;padding:4px 12px;">+ إضافة مؤقت</button>
</div>
<div id="newTempsContainer"></div>
</div>
</div>
<!-- Pricing Preview -->
......@@ -169,36 +178,173 @@
<script>
const memberId = <?= (int) $member['id'] ?>;
const qualificationsJson = <?= json_encode($qualifications ?? [], JSON_UNESCAPED_UNICODE) ?>;
let debounceTimer = null;
let spouseCount = 0;
let childCount = 0;
let tempCount = 0;
function qualOptions() {
let opts = '<option value="">-- اختر --</option>';
qualificationsJson.forEach(q => { opts += '<option value="' + q.id + '">' + q.name_ar + '</option>'; });
return opts;
}
function addNewSpouse() {
spouseCount++;
const html = '<div style="display:grid;grid-template-columns:1fr auto;gap:8px;padding:8px 12px;background:#F0FDF4;border-radius:8px;margin-bottom:6px;" id="new-spouse-' + spouseCount + '">'
+ '<input type="text" name="new_spouses[' + spouseCount + '][name]" class="form-input" placeholder="اسم الزوجة بالكامل" required style="font-size:14px;">'
+ '<button type="button" onclick="removeRow(\'new-spouse-' + spouseCount + '\')" style="background:#FEE2E2;border:1px solid #FECACA;border-radius:6px;padding:6px 10px;cursor:pointer;color:#DC2626;font-weight:700;">✕</button>'
+ '</div>';
const i = spouseCount;
const html = `<div class="card" style="padding:15px;margin-bottom:10px;background:#F0FDF4;border:1px solid #BBF7D0;" id="new-spouse-${i}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<strong style="color:#0D7377;">زوجة جديدة #${i}</strong>
<button type="button" onclick="removeRow('new-spouse-${i}')" style="background:#FEE2E2;border:1px solid #FECACA;border-radius:6px;padding:4px 10px;cursor:pointer;color:#DC2626;font-weight:700;">✕ حذف</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">الاسم بالعربية <span style="color:#DC2626;">*</span></label>
<input type="text" name="new_spouses[${i}][full_name_ar]" class="form-input" required></div>
<div class="form-group"><label class="form-label">الاسم بالإنجليزية</label>
<input type="text" name="new_spouses[${i}][full_name_en]" class="form-input"></div>
<div class="form-group"><label class="form-label">الرقم القومي</label>
<input type="text" name="new_spouses[${i}][national_id]" class="form-input nid-field" maxlength="14" style="direction:ltr;text-align:left;letter-spacing:2px;" placeholder="14 رقم" data-target="spouse-${i}" oninput="parseNID(this,'spouse-${i}')"></div>
<div class="form-group"><label class="form-label"><input type="checkbox" name="new_spouses[${i}][is_non_egyptian]" value="1" onchange="togglePassport(this,'sp-passport-${i}')"> غير مصرية</label>
<input type="text" name="new_spouses[${i}][passport_number]" id="sp-passport-${i}" class="form-input" style="display:none;direction:ltr;margin-top:6px;" placeholder="رقم جواز السفر"></div>
<div class="form-group"><label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="new_spouses[${i}][date_of_birth]" id="spouse-${i}-dob" class="form-input" required></div>
<div class="form-group"><label class="form-label">تاريخ الزواج</label>
<input type="date" name="new_spouses[${i}][marriage_date]" class="form-input"></div>
<div class="form-group"><label class="form-label">الجنسية</label>
<select name="new_spouses[${i}][nationality]" class="form-select"><option value="egyptian">مصرية</option><option value="foreign">أجنبية</option></select></div>
<div class="form-group"><label class="form-label">النوع</label>
<select name="new_spouses[${i}][gender]" id="spouse-${i}-gender" class="form-select"><option value="female">أنثى</option><option value="male">ذكر</option></select></div>
<div class="form-group"><label class="form-label">المؤهل</label>
<select name="new_spouses[${i}][qualification_id]" class="form-select">${qualOptions()}</select></div>
<div class="form-group"><label class="form-label">الوظيفة</label>
<input type="text" name="new_spouses[${i}][occupation]" class="form-input"></div>
<div class="form-group"><label class="form-label">الموبايل</label>
<input type="tel" name="new_spouses[${i}][mobile]" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">تليفون العمل</label>
<input type="tel" name="new_spouses[${i}][work_phone]" class="form-input" style="direction:ltr;text-align:left;"></div>
</div>
<div class="form-group" style="margin-top:10px;"><label class="form-label">عنوان العمل</label>
<textarea name="new_spouses[${i}][work_address]" class="form-textarea" rows="1"></textarea></div>
</div>`;
document.getElementById('newSpousesContainer').insertAdjacentHTML('beforeend', html);
updatePricing();
}
function addNewChild() {
childCount++;
const html = '<div style="display:grid;grid-template-columns:1fr 140px auto;gap:8px;padding:8px 12px;background:#F0FDF4;border-radius:8px;margin-bottom:6px;" id="new-child-' + childCount + '">'
+ '<input type="text" name="new_children[' + childCount + '][name]" class="form-input" placeholder="اسم الابن/ة بالكامل" required style="font-size:14px;">'
+ '<input type="date" name="new_children[' + childCount + '][dob]" class="form-input" required style="font-size:13px;" onchange="updatePricing()">'
+ '<button type="button" onclick="removeRow(\'new-child-' + childCount + '\')" style="background:#FEE2E2;border:1px solid #FECACA;border-radius:6px;padding:6px 10px;cursor:pointer;color:#DC2626;font-weight:700;">✕</button>'
+ '</div>';
const i = childCount;
const html = `<div class="card" style="padding:15px;margin-bottom:10px;background:#EFF6FF;border:1px solid #BFDBFE;" id="new-child-${i}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<strong style="color:#0D7377;">ابن/ة جديد/ة #${i}</strong>
<button type="button" onclick="removeRow('new-child-${i}')" style="background:#FEE2E2;border:1px solid #FECACA;border-radius:6px;padding:4px 10px;cursor:pointer;color:#DC2626;font-weight:700;">✕ حذف</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">الاسم بالعربية <span style="color:#DC2626;">*</span></label>
<input type="text" name="new_children[${i}][full_name_ar]" class="form-input" required></div>
<div class="form-group"><label class="form-label">الاسم بالإنجليزية</label>
<input type="text" name="new_children[${i}][full_name_en]" class="form-input"></div>
<div class="form-group"><label class="form-label">الرقم القومي</label>
<input type="text" name="new_children[${i}][national_id]" class="form-input nid-field" maxlength="14" style="direction:ltr;text-align:left;letter-spacing:2px;" placeholder="14 رقم" oninput="parseNID(this,'child-${i}')"></div>
<div class="form-group"><label class="form-label"><input type="checkbox" name="new_children[${i}][is_non_egyptian]" value="1" onchange="togglePassport(this,'ch-passport-${i}')"> غير مصري/ة</label>
<input type="text" name="new_children[${i}][passport_number]" id="ch-passport-${i}" class="form-input" style="display:none;direction:ltr;margin-top:6px;" placeholder="رقم جواز السفر"></div>
<div class="form-group"><label class="form-label">رقم شهادة الميلاد</label>
<input type="text" name="new_children[${i}][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>
<input type="date" name="new_children[${i}][date_of_birth]" id="child-${i}-dob" class="form-input" required onchange="updatePricing()"></div>
<div class="form-group"><label class="form-label">النوع <span style="color:#DC2626;">*</span></label>
<select name="new_children[${i}][gender]" id="child-${i}-gender" class="form-select" required onchange="syncRelationship(this, 'child-${i}-rel')">
<option value="male">ذكر</option><option value="female">أنثى</option></select></div>
<div class="form-group"><label class="form-label">صلة القرابة</label>
<select name="new_children[${i}][relationship]" id="child-${i}-rel" class="form-select">
<option value="son">ابن</option><option value="daughter">ابنة</option></select></div>
<div class="form-group"><label class="form-label">الجنسية</label>
<select name="new_children[${i}][nationality]" class="form-select"><option value="egyptian">مصري/ة</option><option value="foreign">أجنبي/ة</option></select></div>
<div class="form-group"><label class="form-label">المدرسة / الكلية</label>
<input type="text" name="new_children[${i}][school_faculty]" class="form-input"></div>
</div>
</div>`;
document.getElementById('newChildrenContainer').insertAdjacentHTML('beforeend', html);
updatePricing();
}
function addNewTemp() {
tempCount++;
const i = tempCount;
const html = `<div class="card" style="padding:15px;margin-bottom:10px;background:#FFF7ED;border:1px solid #FED7AA;" id="new-temp-${i}">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<strong style="color:#92400E;">عضو مؤقت جديد #${i}</strong>
<button type="button" onclick="removeRow('new-temp-${i}')" style="background:#FEE2E2;border:1px solid #FECACA;border-radius:6px;padding:4px 10px;cursor:pointer;color:#DC2626;font-weight:700;">✕ حذف</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">الاسم بالعربية <span style="color:#DC2626;">*</span></label>
<input type="text" name="new_temps[${i}][full_name_ar]" class="form-input" required></div>
<div class="form-group"><label class="form-label">الاسم بالإنجليزية</label>
<input type="text" name="new_temps[${i}][full_name_en]" class="form-input"></div>
<div class="form-group"><label class="form-label">التصنيف <span style="color:#DC2626;">*</span></label>
<select name="new_temps[${i}][category]" class="form-select" required>
<option value="relative">قريب</option><option value="nanny">مربية</option><option value="driver">سائق</option><option value="other">أخرى</option></select></div>
<div class="form-group"><label class="form-label">الرقم القومي</label>
<input type="text" name="new_temps[${i}][national_id]" class="form-input nid-field" maxlength="14" style="direction:ltr;text-align:left;letter-spacing:2px;" placeholder="14 رقم" oninput="parseNID(this,'temp-${i}')"></div>
<div class="form-group"><label class="form-label"><input type="checkbox" name="new_temps[${i}][is_non_egyptian]" value="1" onchange="togglePassport(this,'tm-passport-${i}')"> غير مصري/ة</label>
<input type="text" name="new_temps[${i}][passport_number]" id="tm-passport-${i}" class="form-input" style="display:none;direction:ltr;margin-top:6px;" placeholder="رقم جواز السفر"></div>
<div class="form-group"><label class="form-label">تاريخ الميلاد <span style="color:#DC2626;">*</span></label>
<input type="date" name="new_temps[${i}][date_of_birth]" id="temp-${i}-dob" class="form-input" required onchange="updatePricing()"></div>
<div class="form-group"><label class="form-label">النوع</label>
<select name="new_temps[${i}][gender]" id="temp-${i}-gender" class="form-select">
<option value="male">ذكر</option><option value="female">أنثى</option></select></div>
<div class="form-group"><label class="form-label">الجنسية</label>
<select name="new_temps[${i}][nationality]" class="form-select"><option value="egyptian">مصري/ة</option><option value="foreign">أجنبي/ة</option></select></div>
<div class="form-group"><label class="form-label">صلة القرابة بالعضو</label>
<input type="text" name="new_temps[${i}][relationship_to_member]" class="form-input"></div>
<div class="form-group" style="display:flex;gap:15px;align-items:center;padding-top:20px;">
<label><input type="checkbox" name="new_temps[${i}][has_championship]" value="1"> بطولة رياضية</label>
<label><input type="checkbox" name="new_temps[${i}][disability_documentation]" value="1"> إعاقة موثقة</label>
</div>
</div>
<div class="form-group" style="margin-top:10px;"><label class="form-label">ملاحظات</label>
<textarea name="new_temps[${i}][notes]" class="form-textarea" rows="1"></textarea></div>
</div>`;
document.getElementById('newTempsContainer').insertAdjacentHTML('beforeend', html);
updatePricing();
}
function removeRow(id) {
document.getElementById(id).remove();
updatePricing();
}
function togglePassport(checkbox, passportId) {
document.getElementById(passportId).style.display = checkbox.checked ? 'block' : 'none';
}
function syncRelationship(genderSelect, relId) {
const rel = document.getElementById(relId);
if (rel) rel.value = genderSelect.value === 'female' ? 'daughter' : 'son';
}
function parseNID(input, prefix) {
const nid = input.value.replace(/\D/g, '');
if (nid.length !== 14) return;
const century = nid[0] === '2' ? '19' : '20';
const year = century + nid.substring(1, 3);
const month = nid.substring(3, 5);
const day = nid.substring(5, 7);
const dob = year + '-' + month + '-' + day;
const dobField = document.getElementById(prefix + '-dob');
if (dobField && !isNaN(Date.parse(dob))) dobField.value = dob;
const genderDigit = parseInt(nid[12]);
const gender = (genderDigit % 2 === 0) ? 'female' : 'male';
const genderField = document.getElementById(prefix + '-gender');
if (genderField) {
genderField.value = gender;
const relId = prefix.replace('child-', 'child-') + '-rel';
const relField = document.getElementById(relId);
if (relField) relField.value = gender === 'female' ? 'daughter' : 'son';
}
updatePricing();
}
function updatePricing() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(fetchPricing, 300);
......@@ -226,15 +372,24 @@ function fetchPricing() {
const children = [...document.querySelectorAll('[name="children[]"]:checked')].map(e => e.value).join(',');
// Collect new inline spouses count
const newSpouseNames = [...document.querySelectorAll('[name^="new_spouses"][name$="[name]"]')].map(e => e.value.trim()).filter(n => n);
const newSpouseInputs = [...document.querySelectorAll('[name$="[full_name_ar]"]')].filter(e => e.name.startsWith('new_spouses'));
const newSpousesCount = newSpouseInputs.filter(e => e.value.trim()).length;
// Collect new inline children DOBs
const newChildDobs = [...document.querySelectorAll('[name^="new_children"][name$="[dob]"]')].map(e => e.value).filter(d => d);
const newChildDobInputs = [...document.querySelectorAll('[name$="[date_of_birth]"]')].filter(e => e.name.startsWith('new_children'));
const newChildDobs = newChildDobInputs.map(e => e.value).filter(d => d);
// Collect new temp DOBs (treated as children for pricing)
const newTempDobInputs = [...document.querySelectorAll('[name$="[date_of_birth]"]')].filter(e => e.name.startsWith('new_temps'));
const newTempDobs = newTempDobInputs.map(e => e.value).filter(d => d);
const allChildDobs = newChildDobs.concat(newTempDobs);
const url = '/api/seasonal/price-preview?nationality_type=' + nat + '&duration_months=' + dur
+ '&member_id=' + memberId + '&has_qualification=' + hasQual
+ '&spouses=' + spouses + '&children=' + children
+ '&new_spouses_count=' + newSpouseNames.length
+ '&new_children_dobs=' + encodeURIComponent(newChildDobs.join(','));
+ '&new_spouses_count=' + newSpousesCount
+ '&new_children_dobs=' + encodeURIComponent(allChildDobs.join(','));
fetch(url, {credentials: 'same-origin'})
.then(r => r.json())
......
# Seasonal Module — Architecture Map
> **Last updated:** 2026-06-11
> **Last updated:** 2026-07-22
> **Status:** Living document — incrementally updated as new information is discovered
---
......@@ -9,7 +9,8 @@
The Seasonal module manages **time-limited seasonal memberships** (pool access, facility use) for members typed as 'seasonal'. As of 2026-06-11, seasonal membership is a **primary membership type** selected at creation (not a working member add-on). It:
- Handles dual-nationality pricing (Egyptian in EGP, Foreign in USD)
- Supports family group subscriptions (member + spouses + children in one batch)
- Supports family group subscriptions (member + spouses + children + temporary members in one batch)
- Allows inline creation of NEW spouses, children, and temporary members with full data collection (same fields as standalone forms)
- Calculates age-category-based pricing for children
- Applies family discounts (couple, parent+child, family of 5+)
- Applies no-qualification and qualified-member discounts
......@@ -97,8 +98,10 @@ A family seasonal subscription creates **multiple rows**:
|-------|--------------|--------------|
| members | Members | member_id FK |
| branches | Branches | branch_id FK |
| spouses | Spouses | Referenced during creation to list available spouses |
| children | Children | Referenced during creation to list available children |
| qualifications | Members | Loaded for spouse qualification dropdown in inline form |
| spouses | Spouses | Referenced during creation to list available spouses; new spouses created inline |
| children | Children | Referenced during creation to list available children; new children created inline |
| temporary_members | Temporary | New temporary members created inline during seasonal registration |
| payment_requests | Cashier | Created on store, linked via entity_type/entity_id |
---
......@@ -247,7 +250,10 @@ No dedicated permissions are registered in bootstrap.php.
1. **Menu registered externally**: The Seasonal sidebar menu item is registered by Members/bootstrap.php
2. **EventBus injection**: The bootstrap.php listener injects seasonal data into `member.profile_data` for display on member profile pages
3. **Family batch creation**: A single form submission creates N+1 records (1 parent + N family members)
4. **Payment request description includes emoji**: The breakdown stored in payment notes uses emoji characters for formatting
5. **Index limited to 100 rows**: The index query has `LIMIT 100` — large datasets will be truncated
6. **No over_60 for foreign**: Foreign pricing table does not include an over_60 category (member price applies)
7. **Status filter on index**: Supports filtering by status via query parameter `?status=active`
4. **Inline member creation**: The form creates actual Spouse/Child/TemporaryMember records in their respective tables (full data: NID parsing, passport, DOB, gender, nationality, qualification, contact info, etc.) — not just seasonal_memberships entries
5. **Payment request description includes emoji**: The breakdown stored in payment notes uses emoji characters for formatting
6. **Index limited to 100 rows**: The index query has `LIMIT 100` — large datasets will be truncated
7. **No over_60 for foreign**: Foreign pricing table does not include an over_60 category (member price applies)
8. **Status filter on index**: Supports filtering by status via query parameter `?status=active`
9. **Temporary members priced as children**: Inline temp members use `person_type='child'` for pricing with their age determining the tier
10. **NID auto-parsing in JS**: The inline forms parse Egyptian 14-digit national IDs to extract date_of_birth and gender automatically
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