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 ...@@ -73,10 +73,11 @@ class TransferController extends Controller
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب اختيار الابن/الابنة'); 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) { if ($transferType === 'child_separation' && $childId) {
$child = $db->selectOne("SELECT * FROM children WHERE id = ? AND is_archived = 0", [$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']); $dob = new \DateTime($child['date_of_birth']);
$now = new \DateTime(); $now = new \DateTime();
$age = (int) $now->diff($dob)->y; $age = (int) $now->diff($dob)->y;
...@@ -98,41 +99,54 @@ class TransferController extends Controller ...@@ -98,41 +99,54 @@ class TransferController extends Controller
$workDate = null; $workDate = null;
$dateTurned25 = null; $dateTurned25 = null;
$effectiveTransferDate = null; $effectiveTransferDate = null;
$marriageDate = null;
if ($transferType === 'child_separation' && $child) { if ($transferType === 'child_separation' && $child) {
$isEmployedPost = $request->post('is_employed', ''); $childGender = $child['gender'] ?? 'male';
if ($isEmployedPost === '') {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب تحديد حالة التوظيف');
}
$isEmployed = (bool) (int) $isEmployedPost;
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
if ($isEmployed) { if ($childGender === 'female') {
$workDate = trim($request->post('work_date', '')) ?: null; // Female path: effective date = marriage_date only
if (!$workDate) { $marriageDate = trim($request->post('marriage_date', '')) ?: null;
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ مباشرة العمل عند اختيار موظف'); if (!$marriageDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ الزواج لفصل الإناث');
} }
if (!$graduationDate) { $effectiveTransferDate = $marriageDate;
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ التخرج عند اختيار موظف'); } 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'])) { if (empty($child['date_of_birth'])) {
return $this->redirect("/transfers/create/{$memberId}")->withError('لا يوجد تاريخ ميلاد للابن — لا يمكن احتساب تاريخ الفصل'); return $this->redirect("/transfers/create/{$memberId}")->withError('لا يوجد تاريخ ميلاد للابن — لا يمكن احتساب تاريخ الفصل');
} }
$dates = SeparationFeeCalculator::computeChildSeparationDates( $dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'], $child['date_of_birth'],
$isEmployed, $isEmployed,
$workDate, $workDate,
$date25Override, $date25Override,
$graduationDate $graduationDate
); );
$dateTurned25 = $dates['date_turned_25']; $dateTurned25 = $dates['date_turned_25'];
$effectiveTransferDate = $dates['effective_transfer_date']; $effectiveTransferDate = $dates['effective_transfer_date'];
}
} }
// Read dependent counts for new membership (child_separation) // Read dependent counts for new membership (child_separation)
...@@ -262,6 +276,7 @@ class TransferController extends Controller ...@@ -262,6 +276,7 @@ class TransferController extends Controller
'is_employed' => $isEmployed !== null ? ($isEmployed ? 1 : 0) : null, 'is_employed' => $isEmployed !== null ? ($isEmployed ? 1 : 0) : null,
'graduation_date' => $graduationDate, 'graduation_date' => $graduationDate,
'work_date' => $workDate, 'work_date' => $workDate,
'marriage_date' => $marriageDate,
'date_turned_25' => $dateTurned25, 'date_turned_25' => $dateTurned25,
'effective_transfer_date' => $effectiveTransferDate, 'effective_transfer_date' => $effectiveTransferDate,
'target_spouses_count' => $depSpouses > 0 ? $depSpouses : null, 'target_spouses_count' => $depSpouses > 0 ? $depSpouses : null,
...@@ -441,18 +456,32 @@ class TransferController extends Controller ...@@ -441,18 +456,32 @@ class TransferController extends Controller
if ($transferType === 'child_separation' && $childId) { if ($transferType === 'child_separation' && $childId) {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$child = $db->selectOne("SELECT date_of_birth FROM children WHERE id = ?", [$childId]); $child = $db->selectOne("SELECT date_of_birth, gender FROM children WHERE id = ?", [$childId]);
$childGender = $request->post('child_gender', $child['gender'] ?? 'male');
$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;
$depSpouses = (int) $request->post('dep_spouses_count', 0); $depSpouses = (int) $request->post('dep_spouses_count', 0);
$depChildren = (int) $request->post('dep_children_count', 0); $depChildren = (int) $request->post('dep_children_count', 0);
$depTemps = (int) $request->post('dep_temps_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( $dates = SeparationFeeCalculator::computeChildSeparationDates(
$child['date_of_birth'], $child['date_of_birth'],
$isEmployed, $isEmployed,
...@@ -465,9 +494,7 @@ class TransferController extends Controller ...@@ -465,9 +494,7 @@ class TransferController extends Controller
$isEmployed ? $workDate : null, $isEmployed ? $workDate : null,
$dates['effective_transfer_date'], $dates['effective_transfer_date'],
$qualCode, $qualCode,
$depSpouses, $depSpouses, $depChildren, $depTemps
$depChildren,
$depTemps
); );
$result['date_turned_25'] = $dates['date_turned_25']; $result['date_turned_25'] = $dates['date_turned_25'];
$result['effective_transfer_date'] = $dates['effective_transfer_date']; $result['effective_transfer_date'] = $dates['effective_transfer_date'];
......
...@@ -27,7 +27,7 @@ class TransferRequest extends Model ...@@ -27,7 +27,7 @@ class TransferRequest extends Model
'archive_snapshot_id', 'workflow_instance_id', 'archive_snapshot_id', 'workflow_instance_id',
'board_decision_notes', 'approved_by', 'approved_at', 'board_decision_notes', 'approved_by', 'approved_at',
'completed_at', 'status', 'notes', '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', 'target_spouses_count', 'target_children_count', 'target_temps_count',
]; ];
......
This diff is collapsed.
...@@ -8,6 +8,7 @@ return function (Database $db): void { ...@@ -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_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_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", '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) { foreach ($cols as $col => $sql) {
......
...@@ -85,7 +85,8 @@ app/Modules/Transfers/ ...@@ -85,7 +85,8 @@ app/Modules/Transfers/
| status | varchar(50) | NO | MUL | requested | requested/approved/fee_paid/completed/rejected | | status | varchar(50) | NO | MUL | requested | requested/approved/fee_paid/completed/rejected |
| is_employed | tinyint(1) | YES | | | Whether child is employed (child_separation) | | is_employed | tinyint(1) | YES | | | Whether child is employed (child_separation) |
| graduation_date | date | YES | | | Graduation date (required when employed) | | 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 | | date_turned_25 | date | YES | | | Calculated: child's DOB + 25 years |
| effective_transfer_date | date | YES | | | Computed: min(max(work,grad), date25) | | effective_transfer_date | date | YES | | | Computed: min(max(work,grad), date25) |
| target_spouses_count | int unsigned | YES | | | Dependents joining new membership (spouses) | | target_spouses_count | int unsigned | YES | | | Dependents joining new membership (spouses) |
...@@ -127,9 +128,11 @@ requested → approved → fee_paid → completed ...@@ -127,9 +128,11 @@ requested → approved → fee_paid → completed
- Show dependents section (spouses/children/temps joining new membership) - Show dependents section (spouses/children/temps joining new membership)
2. POST /transfers/store/{memberId} 2. POST /transfers/store/{memberId}
- Validate: transfer_type, child_id, is_employed - Validate: transfer_type, child_id
- If employed: graduation_date AND work_date required - Gender-based logic:
- Compute effective_transfer_date = min(max(work_date, graduation_date), date_turned_25) - 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 - Read dep_spouses_count, dep_children_count, dep_temps_count
- Calculate fees via SeparationFeeCalculator::calculateForChildSeparation() - Calculate fees via SeparationFeeCalculator::calculateForChildSeparation()
(annual subscription includes member + specified dependents) (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