Commit 316c58eb authored by Fares's avatar Fares

feat(transfers): gender-based separation logic — males use work/grad/age25,...

feat(transfers): gender-based separation logic — males use work/grad/age25, females use marriage date

- Males: effective_date = min(max(work_date, graduation_date), date_turned_25) — unchanged
- Females: effective_date = marriage_date only (no work/graduation/age25 fields)
- Add data-gender attribute to child select options for JS detection
- Show male-specific fields (employment, graduation, work_date, date25) only for males
- Show marriage_date field only for females
- Females bypass age >= 25 filter in dropdown (eligible at any age via marriage)
- Server-side age validation skipped for female children
- Add marriage_date column to migration and model fillable
- Frontend dynamically switches between male/female form based on selected child's gender
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1c8ee16d
......@@ -73,10 +73,11 @@ class TransferController extends Controller
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب اختيار الابن/الابنة');
}
// Age validation: dependents under separation age cannot self-separate
// Age validation: male dependents under separation age cannot self-separate
// Females are separated based on marriage date, not age
if ($transferType === 'child_separation' && $childId) {
$child = $db->selectOne("SELECT * FROM children WHERE id = ? AND is_archived = 0", [$childId]);
if ($child && !empty($child['date_of_birth'])) {
if ($child && ($child['gender'] ?? 'male') === 'male' && !empty($child['date_of_birth'])) {
$dob = new \DateTime($child['date_of_birth']);
$now = new \DateTime();
$age = (int) $now->diff($dob)->y;
......@@ -98,41 +99,54 @@ class TransferController extends Controller
$workDate = null;
$dateTurned25 = null;
$effectiveTransferDate = null;
$marriageDate = null;
if ($transferType === 'child_separation' && $child) {
$isEmployedPost = $request->post('is_employed', '');
if ($isEmployedPost === '') {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب تحديد حالة التوظيف');
}
$isEmployed = (bool) (int) $isEmployedPost;
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
$childGender = $child['gender'] ?? 'male';
if ($isEmployed) {
$workDate = trim($request->post('work_date', '')) ?: null;
if (!$workDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ مباشرة العمل عند اختيار موظف');
if ($childGender === 'female') {
// Female path: effective date = marriage_date only
$marriageDate = trim($request->post('marriage_date', '')) ?: null;
if (!$marriageDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ الزواج لفصل الإناث');
}
if (!$graduationDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ التخرج عند اختيار موظف');
$effectiveTransferDate = $marriageDate;
} else {
// Male path: work + graduation + date_turned_25
$isEmployedPost = $request->post('is_employed', '');
if ($isEmployedPost === '') {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب تحديد حالة التوظيف');
}
$isEmployed = (bool) (int) $isEmployedPost;
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
if ($isEmployed) {
$workDate = trim($request->post('work_date', '')) ?: null;
if (!$workDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ مباشرة العمل عند اختيار موظف');
}
if (!$graduationDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ التخرج عند اختيار موظف');
}
}
}
$date25Override = trim($request->post('date_turned_25', '')) ?: null;
$date25Override = trim($request->post('date_turned_25', '')) ?: null;
if (empty($child['date_of_birth'])) {
return $this->redirect("/transfers/create/{$memberId}")->withError('لا يوجد تاريخ ميلاد للابن — لا يمكن احتساب تاريخ الفصل');
}
if (empty($child['date_of_birth'])) {
return $this->redirect("/transfers/create/{$memberId}")->withError('لا يوجد تاريخ ميلاد للابن — لا يمكن احتساب تاريخ الفصل');
}
$dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'],
$isEmployed,
$workDate,
$date25Override,
$graduationDate
);
$dateTurned25 = $dates['date_turned_25'];
$effectiveTransferDate = $dates['effective_transfer_date'];
$dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'],
$isEmployed,
$workDate,
$date25Override,
$graduationDate
);
$dateTurned25 = $dates['date_turned_25'];
$effectiveTransferDate = $dates['effective_transfer_date'];
}
}
// Read dependent counts for new membership (child_separation)
......@@ -262,6 +276,7 @@ class TransferController extends Controller
'is_employed' => $isEmployed !== null ? ($isEmployed ? 1 : 0) : null,
'graduation_date' => $graduationDate,
'work_date' => $workDate,
'marriage_date' => $marriageDate,
'date_turned_25' => $dateTurned25,
'effective_transfer_date' => $effectiveTransferDate,
'target_spouses_count' => $depSpouses > 0 ? $depSpouses : null,
......@@ -441,18 +456,32 @@ class TransferController extends Controller
if ($transferType === 'child_separation' && $childId) {
$db = App::getInstance()->db();
$child = $db->selectOne("SELECT date_of_birth FROM children WHERE id = ?", [$childId]);
$isEmployedPost = $request->post('is_employed', '');
$isEmployed = $isEmployedPost !== '' ? (bool)(int)$isEmployedPost : false;
$workDate = trim($request->post('work_date', '')) ?: null;
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
$date25Override = trim($request->post('date_turned_25', '')) ?: null;
$child = $db->selectOne("SELECT date_of_birth, gender FROM children WHERE id = ?", [$childId]);
$childGender = $request->post('child_gender', $child['gender'] ?? 'male');
$depSpouses = (int) $request->post('dep_spouses_count', 0);
$depChildren = (int) $request->post('dep_children_count', 0);
$depTemps = (int) $request->post('dep_temps_count', 0);
if ($child && !empty($child['date_of_birth'])) {
if ($childGender === 'female') {
// Female path: effective date = marriage_date
$marriageDate = trim($request->post('marriage_date', '')) ?: null;
if ($marriageDate) {
$result = SeparationFeeCalculator::calculateForChildSeparation(
$memberId, $childId, false, null,
$marriageDate, $qualCode,
$depSpouses, $depChildren, $depTemps
);
$result['effective_transfer_date'] = $marriageDate;
return $this->json($result);
}
} elseif ($child && !empty($child['date_of_birth'])) {
// Male path: work + graduation + date_turned_25
$isEmployedPost = $request->post('is_employed', '');
$isEmployed = $isEmployedPost !== '' ? (bool)(int)$isEmployedPost : false;
$workDate = trim($request->post('work_date', '')) ?: null;
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
$date25Override = trim($request->post('date_turned_25', '')) ?: null;
$dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'],
$isEmployed,
......@@ -465,9 +494,7 @@ class TransferController extends Controller
$isEmployed ? $workDate : null,
$dates['effective_transfer_date'],
$qualCode,
$depSpouses,
$depChildren,
$depTemps
$depSpouses, $depChildren, $depTemps
);
$result['date_turned_25'] = $dates['date_turned_25'];
$result['effective_transfer_date'] = $dates['effective_transfer_date'];
......
......@@ -27,7 +27,7 @@ class TransferRequest extends Model
'archive_snapshot_id', 'workflow_instance_id',
'board_decision_notes', 'approved_by', 'approved_at',
'completed_at', 'status', 'notes',
'is_employed', 'graduation_date', 'work_date', 'date_turned_25', 'effective_transfer_date',
'is_employed', 'graduation_date', 'work_date', 'marriage_date', 'date_turned_25', 'effective_transfer_date',
'target_spouses_count', 'target_children_count', 'target_temps_count',
];
......
......@@ -31,7 +31,7 @@
$childAge = (int) (new \DateTime($dob))->diff(new \DateTime())->y;
}
?>
<option value="<?= (int) $c['id'] ?>" data-age="<?= $childAge ?>"><?= e($c['full_name_ar']) ?><?= $childAge ?> سنة (<?= $c['gender'] === 'male' ? 'ذكر' : 'أنثى' ?>)</option>
<option value="<?= (int) $c['id'] ?>" data-age="<?= $childAge ?>" data-gender="<?= e($c['gender']) ?>"><?= e($c['full_name_ar']) ?><?= $childAge ?> سنة (<?= $c['gender'] === 'male' ? 'ذكر' : 'أنثى' ?>)</option>
<?php endforeach; ?>
</select>
<div id="age-warning" style="display:none;margin-top:6px;padding:8px 12px;background:#FEE2E2;border:1px solid #FECACA;border-radius:6px;font-size:13px;color:#DC2626;">
......@@ -41,47 +41,60 @@
</div>
</div>
<!-- فصل أبناء — Employment & Date Fields -->
<!-- فصل أبناء — Employment & Date Fields (MALE path) -->
<div id="child-separation-section" style="display:none;">
<div class="card" style="margin-bottom:20px;border:2px solid #0D9488;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;background:#F0FDF4;">
<h3 style="margin:0;color:#0D9488;font-size:15px;">بيانات فصل الأبناء</h3>
<p style="margin:5px 0 0;font-size:12px;color:#6B7280;">يتم احتساب تاريخ الفصل الفعلي تلقائياً بناءً على حالة التوظيف وتاريخ بلوغ الـ 25</p>
<p style="margin:5px 0 0;font-size:12px;color:#6B7280;" id="child-sep-subtitle">يتم احتساب تاريخ الفصل الفعلي تلقائياً بناءً على حالة التوظيف وتاريخ بلوغ الـ 25</p>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<!-- Employment Status -->
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">الحالة الوظيفية <span style="color:#DC2626;">*</span></label>
<div style="display:flex;gap:20px;margin-top:6px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="radio" name="is_employed" id="is_employed_no" value="0">
<span>غير موظف</span>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="radio" name="is_employed" id="is_employed_yes" value="1">
<span>موظف</span>
</label>
<!-- MALE-only fields -->
<div id="male-separation-fields">
<!-- Employment Status -->
<div class="form-group" style="grid-column:1/-1;margin-bottom:15px;">
<label class="form-label">الحالة الوظيفية <span style="color:#DC2626;">*</span></label>
<div style="display:flex;gap:20px;margin-top:6px;">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="radio" name="is_employed" id="is_employed_no" value="0">
<span>غير موظف</span>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;">
<input type="radio" name="is_employed" id="is_employed_yes" value="1">
<span>موظف</span>
</label>
</div>
</div>
</div>
<!-- Graduation Date (required when employed) -->
<div class="form-group" id="graduation-date-group">
<label class="form-label">تاريخ التخرج <span id="grad-required-mark" style="color:#DC2626;display:none;">*</span></label>
<input type="date" name="graduation_date" id="graduation_date" class="form-input">
</div>
<!-- Graduation Date (required when employed) -->
<div class="form-group" id="graduation-date-group" style="margin-bottom:15px;">
<label class="form-label">تاريخ التخرج <span id="grad-required-mark" style="color:#DC2626;display:none;">*</span></label>
<input type="date" name="graduation_date" id="graduation_date" class="form-input">
</div>
<!-- Work Date (shown only when employed) -->
<div class="form-group" id="work-date-group" style="display:none;margin-bottom:15px;">
<label class="form-label">تاريخ مباشرة العمل <span style="color:#DC2626;">*</span></label>
<input type="date" name="work_date" id="work_date" class="form-input">
<small style="color:#6B7280;font-size:11px;">الفصل يتم عند أبكر التاريخين: اكتمال العمل والتخرج أو بلوغ الـ 25</small>
</div>
<!-- Work Date (shown only when employed) -->
<div class="form-group" id="work-date-group" style="display:none;">
<label class="form-label">تاريخ مباشرة العمل <span style="color:#DC2626;">*</span></label>
<input type="date" name="work_date" id="work_date" class="form-input">
<small style="color:#6B7280;font-size:11px;">الفصل يتم عند أبكر التاريخين: اكتمال العمل والتخرج أو بلوغ الـ 25</small>
<!-- date_turned_25 — auto-computed, editable override -->
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">تاريخ بلوغ الـ 25 <span style="font-size:11px;color:#6B7280;">(محسوب تلقائياً — قابل للتعديل)</span></label>
<input type="date" name="date_turned_25" id="date_turned_25" class="form-input" style="background:#F9FAFB;">
<small id="date25-auto-note" style="color:#059669;font-size:11px;display:none;">محسوب تلقائياً من تاريخ الميلاد</small>
</div>
</div>
<!-- date_turned_25 — auto-computed, editable override -->
<div class="form-group">
<label class="form-label">تاريخ بلوغ الـ 25 <span style="font-size:11px;color:#6B7280;">(محسوب تلقائياً — قابل للتعديل)</span></label>
<input type="date" name="date_turned_25" id="date_turned_25" class="form-input" style="background:#F9FAFB;">
<small id="date25-auto-note" style="color:#059669;font-size:11px;display:none;">محسوب تلقائياً من تاريخ الميلاد</small>
<!-- FEMALE-only fields -->
<div id="female-separation-fields" style="display:none;">
<div class="form-group" style="margin-bottom:15px;">
<label class="form-label">تاريخ الزواج <span style="color:#DC2626;">*</span></label>
<input type="date" name="marriage_date" id="marriage_date" class="form-input">
<small style="color:#6B7280;font-size:11px;">يتم احتساب سنوات الاكتساب من تاريخ الزواج</small>
</div>
</div>
<!-- Effective Transfer Date (read-only display) -->
......@@ -278,6 +291,10 @@
var effectiveBasis = document.getElementById('effective-basis-note');
var elapsedGroup = document.getElementById('elapsed-years-group');
var elapsedDisplay = document.getElementById('elapsed-years-display');
var maleFields = document.getElementById('male-separation-fields');
var femaleFields = document.getElementById('female-separation-fields');
var marriageDateInput = document.getElementById('marriage_date');
var childSepSubtitle = document.getElementById('child-sep-subtitle');
// Dependents for new membership (child_separation)
var depSpousesCount = document.getElementById('dep_spouses_count');
......@@ -301,6 +318,27 @@
return isEmployedYes && isEmployedYes.checked;
}
function getSelectedChildGender() {
var sel = childSelect.options[childSelect.selectedIndex];
if (!sel || !sel.value) return null;
return sel.getAttribute('data-gender') || null;
}
function isFemaleChild() {
return getSelectedChildGender() === 'female';
}
function updateGenderFieldsVisibility() {
var female = isFemaleChild();
maleFields.style.display = female ? 'none' : '';
femaleFields.style.display = female ? '' : 'none';
if (female) {
childSepSubtitle.textContent = 'للإناث: يتم احتساب تاريخ الفصل بناءً على تاريخ الزواج فقط';
} else {
childSepSubtitle.textContent = 'يتم احتساب تاريخ الفصل الفعلي تلقائياً بناءً على حالة التوظيف وتاريخ بلوغ الـ 25';
}
}
function updateDate25FromChild() {
var cid = parseInt(childSelect.value, 10);
var dob = childDobMap[cid] || null;
......@@ -310,10 +348,27 @@
date25Input.value = d25;
date25Note.style.display = 'block';
}
updateGenderFieldsVisibility();
updateEffectiveDate();
}
function updateEffectiveDate() {
// Female path: use marriage_date only
if (isFemaleChild()) {
var mDate = marriageDateInput.value;
if (!mDate) {
effectiveDisplay.value = '';
effectiveBasis.textContent = 'أدخل تاريخ الزواج لتحديد تاريخ الفصل';
elapsedGroup.style.display = 'none';
return;
}
effectiveDisplay.value = mDate;
effectiveBasis.textContent = 'الفصل يتم عند تاريخ الزواج';
fetchFeePreview();
return;
}
// Male path: work + graduation + date_turned_25
var d25 = date25Input.value;
var wDate = workDateInput.value;
var gDate = gradDateInput.value;
......@@ -363,16 +418,23 @@
if (isChildType) {
allOptions.forEach(function(opt) {
var age = parseInt(opt.getAttribute('data-age'), 10);
var gender = opt.getAttribute('data-gender') || 'male';
if (type === 'child_separation') {
opt.style.display = age >= 25 ? '' : 'none';
opt.disabled = age < 25;
// Males: must be >= 25; Females: eligible at any age (marriage-based)
var eligible = (gender === 'female') || (age >= 25);
opt.style.display = eligible ? '' : 'none';
opt.disabled = !eligible;
} else {
opt.style.display = '';
opt.disabled = false;
}
});
if (type === 'child_separation') {
var hasEligible = allOptions.some(function(opt) { return parseInt(opt.getAttribute('data-age'), 10) >= 25; });
var hasEligible = allOptions.some(function(opt) {
var age = parseInt(opt.getAttribute('data-age'), 10);
var gender = opt.getAttribute('data-gender') || 'male';
return (gender === 'female') || (age >= 25);
});
if (!hasEligible) ageWarning.style.display = 'block';
}
}
......@@ -440,11 +502,17 @@
var body = 'member_id=' + memberId + '&child_id=' + childId + '&transfer_type=' + encodeURIComponent(type);
if (type === 'child_separation') {
var emp = isEmployed() ? '1' : '0';
body += '&is_employed=' + emp;
if (workDateInput.value) body += '&work_date=' + encodeURIComponent(workDateInput.value);
if (gradDateInput.value) body += '&graduation_date=' + encodeURIComponent(gradDateInput.value);
if (date25Input.value) body += '&date_turned_25=' + encodeURIComponent(date25Input.value);
var childGender = getSelectedChildGender() || 'male';
body += '&child_gender=' + childGender;
if (childGender === 'female') {
if (marriageDateInput.value) body += '&marriage_date=' + encodeURIComponent(marriageDateInput.value);
} else {
var emp = isEmployed() ? '1' : '0';
body += '&is_employed=' + emp;
if (workDateInput.value) body += '&work_date=' + encodeURIComponent(workDateInput.value);
if (gradDateInput.value) body += '&graduation_date=' + encodeURIComponent(gradDateInput.value);
if (date25Input.value) body += '&date_turned_25=' + encodeURIComponent(date25Input.value);
}
body += '&dep_spouses_count=' + (depSpousesCount.value || '0');
body += '&dep_children_count=' + (depChildrenCount.value || '0');
body += '&dep_temps_count=' + (depTempsCount.value || '0');
......@@ -499,6 +567,7 @@
}
if (workDateInput) workDateInput.addEventListener('change', updateEffectiveDate);
if (gradDateInput) gradDateInput.addEventListener('change', updateEffectiveDate);
if (marriageDateInput) marriageDateInput.addEventListener('change', updateEffectiveDate);
if (date25Input) {
date25Input.addEventListener('change', function() {
this.dataset.userOverride = '1';
......
......@@ -8,6 +8,7 @@ return function (Database $db): void {
'target_spouses_count' => "ALTER TABLE transfer_requests ADD COLUMN target_spouses_count INT UNSIGNED NULL DEFAULT NULL AFTER target_companions_count",
'target_children_count' => "ALTER TABLE transfer_requests ADD COLUMN target_children_count INT UNSIGNED NULL DEFAULT NULL AFTER target_spouses_count",
'target_temps_count' => "ALTER TABLE transfer_requests ADD COLUMN target_temps_count INT UNSIGNED NULL DEFAULT NULL AFTER target_children_count",
'marriage_date' => "ALTER TABLE transfer_requests ADD COLUMN marriage_date DATE NULL DEFAULT NULL AFTER work_date",
];
foreach ($cols as $col => $sql) {
......
......@@ -85,7 +85,8 @@ app/Modules/Transfers/
| status | varchar(50) | NO | MUL | requested | requested/approved/fee_paid/completed/rejected |
| is_employed | tinyint(1) | YES | | | Whether child is employed (child_separation) |
| graduation_date | date | YES | | | Graduation date (required when employed) |
| work_date | date | YES | | | Employment start date |
| work_date | date | YES | | | Employment start date (males) |
| marriage_date | date | YES | | | Marriage date (females — used as effective date) |
| date_turned_25 | date | YES | | | Calculated: child's DOB + 25 years |
| effective_transfer_date | date | YES | | | Computed: min(max(work,grad), date25) |
| target_spouses_count | int unsigned | YES | | | Dependents joining new membership (spouses) |
......@@ -127,9 +128,11 @@ requested → approved → fee_paid → completed
- Show dependents section (spouses/children/temps joining new membership)
2. POST /transfers/store/{memberId}
- Validate: transfer_type, child_id, is_employed
- If employed: graduation_date AND work_date required
- Compute effective_transfer_date = min(max(work_date, graduation_date), date_turned_25)
- Validate: transfer_type, child_id
- Gender-based logic:
- MALE: require is_employed; if employed: graduation_date + work_date required
Compute effective_transfer_date = min(max(work_date, graduation_date), date_turned_25)
- FEMALE: require marriage_date only; effective_transfer_date = marriage_date
- Read dep_spouses_count, dep_children_count, dep_temps_count
- Calculate fees via SeparationFeeCalculator::calculateForChildSeparation()
(annual subscription includes member + specified dependents)
......
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