Commit 910e8606 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(death): full board-approval workflow, mandatory docs, trustee fee, children-transfer fix

- New status flow: board_review → board_approved → pending_form_fill → completed
- Mandatory document uploads at case creation (death certificate + inheritance notice)
- Board approval step: configurable trustee fee (% of membership_value or flat amount)
- Payment request created only after board approval (not at case creation)
- Cashier bootstrap fixed: death_fee for primary_member now sets pending_form_fill
- Pre-completion validations: board approval, payment, both docs, wife form filled
- Children transfer bug fixed: sweep remaining children to primary + renumber child_order
- Source tracking: transferred_from_death_id on new member rows
- Death-origin badge in member show page
- Migration Phase_94_001: new columns on death_cases + members
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent e6708441
......@@ -147,9 +147,19 @@ EventBus::listen('payment_request.completed', function (array $data) {
'separation_fee' => ['table' => 'transfer_requests','event' => 'transfer.fee_paid', 'key' => 'transfer_id'],
];
if (isset($tableMap[$paymentType])) {
$cfg = $tableMap[$paymentType];
$cfg = $tableMap[$paymentType];
$newStatus = 'fee_paid';
// Death fee for primary_member: skip to pending_form_fill (wife must fill form next)
if ($paymentType === 'death_fee') {
$deathCase = $db->selectOne("SELECT deceased_type FROM death_cases WHERE id = ?", [$entityId]);
if ($deathCase && $deathCase['deceased_type'] === 'primary_member') {
$newStatus = 'pending_form_fill';
}
}
$db->update($cfg['table'], [
'status' => 'fee_paid',
'status' => $newStatus,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$entityId]);
EventBus::dispatch($cfg['event'], [$cfg['key'] => $entityId, 'payment_id' => $paymentId]);
......
......@@ -15,7 +15,6 @@ use App\Modules\Payments\Services\PaymentService;
use App\Modules\Cashier\Services\PaymentRequestService;
use App\Modules\Members\Services\NationalIdParser;
use App\Modules\ServiceCatalog\Models\ServicePrice;
use App\Modules\Members\Models\Member;
class DeathController extends Controller
{
......@@ -30,6 +29,50 @@ class DeathController extends Controller
return compact('formFee', 'annualSubBase', 'devFee', 'annualSub', 'totalFee');
}
private static function uploadDocument(array $fileInfo, string $prefix, int $memberId): array
{
$allowedMimes = ['application/pdf', 'image/jpeg', 'image/png', 'image/gif'];
$allowedExts = ['pdf', 'jpg', 'jpeg', 'png', 'gif'];
if (empty($fileInfo['tmp_name']) || $fileInfo['error'] !== UPLOAD_ERR_OK) {
$errMap = [
UPLOAD_ERR_INI_SIZE => 'حجم الملف أكبر من المسموح',
UPLOAD_ERR_FORM_SIZE => 'حجم الملف أكبر من المسموح',
UPLOAD_ERR_PARTIAL => 'رُفع الملف جزئياً',
UPLOAD_ERR_NO_FILE => 'لم يتم اختيار ملف',
UPLOAD_ERR_NO_TMP_DIR => 'مجلد مؤقت غير موجود',
UPLOAD_ERR_CANT_WRITE => 'فشل كتابة الملف',
UPLOAD_ERR_EXTENSION => 'نوع الملف غير مسموح',
];
return ['success' => false, 'error' => $errMap[$fileInfo['error'] ?? UPLOAD_ERR_NO_FILE] ?? 'خطأ في رفع الملف'];
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($fileInfo['tmp_name']);
if (!in_array($mime, $allowedMimes, true)) {
return ['success' => false, 'error' => 'نوع الملف غير مسموح — يجب أن يكون PDF أو صورة (JPG/PNG)'];
}
$ext = strtolower(pathinfo($fileInfo['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedExts, true)) {
return ['success' => false, 'error' => 'امتداد الملف غير مسموح'];
}
$uploadDir = App::getInstance()->basePath() . '/storage/uploads/death/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$filename = $prefix . '_' . $memberId . '_' . time() . '_' . bin2hex(random_bytes(2)) . '.' . $ext;
$destPath = $uploadDir . $filename;
if (!move_uploaded_file($fileInfo['tmp_name'], $destPath)) {
return ['success' => false, 'error' => 'فشل حفظ الملف على الخادم'];
}
return ['success' => true, 'path' => 'storage/uploads/death/' . $filename];
}
public function index(Request $request): Response
{
$filters = ['search' => trim((string) $request->get('q', '')), 'status' => $request->get('status', '')];
......@@ -64,30 +107,60 @@ class DeathController extends Controller
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
$deceasedType = trim($request->post('deceased_type', ''));
$deathDate = trim($request->post('death_date', ''));
$certNumber = trim($request->post('death_certificate_number', ''));
$notes = trim($request->post('notes', ''));
$deathDate = trim($request->post('death_date', ''));
$certNumber = trim($request->post('death_certificate_number', ''));
$notes = trim($request->post('notes', ''));
if (!$deceasedType || !$deathDate) {
return $this->redirect("/death/create/{$memberId}")->withError('بيانات الوفاة غير مكتملة');
}
$fees = self::getFees();
// Mandatory document upload only for primary_member death
$certPath = null;
$inheritancePath = null;
if ($deceasedType === 'primary_member') {
if (empty($_FILES['death_certificate']['tmp_name'])) {
return $this->redirect("/death/create/{$memberId}")->withError('يجب رفع شهادة الوفاة (PDF أو صورة)');
}
if (empty($_FILES['inheritance_notice']['tmp_name'])) {
return $this->redirect("/death/create/{$memberId}")->withError('يجب رفع إعلام الوراثة (PDF أو صورة)');
}
$certUpload = self::uploadDocument($_FILES['death_certificate'], 'cert', (int) $memberId);
if (!$certUpload['success']) {
return $this->redirect("/death/create/{$memberId}")->withError('شهادة الوفاة: ' . $certUpload['error']);
}
$inheritanceUpload = self::uploadDocument($_FILES['inheritance_notice'], 'inh', (int) $memberId);
if (!$inheritanceUpload['success']) {
return $this->redirect("/death/create/{$memberId}")->withError('إعلام الوراثة: ' . $inheritanceUpload['error']);
}
$certPath = $certUpload['path'];
$inheritancePath = $inheritanceUpload['path'];
}
$fees = self::getFees();
$totalFee = $fees['totalFee'];
// Status: primary_member → board_review; spouse/child → recorded (no fee, no board)
$initialStatus = ($deceasedType === 'primary_member') ? 'board_review' : 'recorded';
$caseData = [
'member_id' => (int) $memberId,
'deceased_type' => $deceasedType,
'death_date' => $deathDate,
'death_certificate_number' => $certNumber ?: null,
'death_certificate_path' => $certPath,
'inheritance_notice_path' => $inheritancePath,
'same_membership_number' => ($deceasedType === 'primary_member') ? 1 : 0,
'fee_amount' => $totalFee,
'status' => 'recorded',
'status' => $initialStatus,
'notes' => $notes ?: null,
];
if ($deceasedType === 'primary_member') {
$primarySpouseId = $request->post('primary_spouse_id') ? (int) $request->post('primary_spouse_id') : null;
$primarySpouseId = $request->post('primary_spouse_id') ? (int) $request->post('primary_spouse_id') : null;
$secondarySpouseIds = $request->post('secondary_spouse_ids', []);
$childrenAssignment = $request->post('children_assignment', []);
......@@ -95,12 +168,11 @@ class DeathController extends Controller
return $this->redirect("/death/create/{$memberId}")->withError('يجب اختيار الزوجة الأساسية لنقل العضوية');
}
$caseData['spouse_id'] = $primarySpouseId;
$caseData['primary_spouse_id'] = $primarySpouseId;
$caseData['secondary_spouses_json'] = !empty($secondarySpouseIds) ? json_encode(array_map('intval', $secondarySpouseIds)) : null;
$caseData['spouse_id'] = $primarySpouseId;
$caseData['primary_spouse_id'] = $primarySpouseId;
$caseData['secondary_spouses_json'] = !empty($secondarySpouseIds) ? json_encode(array_map('intval', $secondarySpouseIds)) : null;
$caseData['children_assignment_json'] = !empty($childrenAssignment) ? json_encode($childrenAssignment) : null;
// Total fee: 570+annual for primary + 570+annual for each secondary
$secondaryCount = count($secondarySpouseIds);
if ($secondaryCount > 0) {
$totalFee = bcmul($fees['totalFee'], (string) (1 + $secondaryCount), 2);
......@@ -108,11 +180,11 @@ class DeathController extends Controller
}
} elseif ($deceasedType === 'spouse') {
$spouseId = $request->post('spouse_id') ? (int) $request->post('spouse_id') : null;
$caseData['spouse_id'] = $spouseId;
$caseData['spouse_id'] = $spouseId;
$caseData['fee_amount'] = '0.00';
} elseif ($deceasedType === 'child') {
$childId = $request->post('child_id') ? (int) $request->post('child_id') : null;
$caseData['child_id'] = $childId;
$caseData['child_id'] = $childId;
$caseData['fee_amount'] = '0.00';
}
......@@ -120,42 +192,11 @@ class DeathController extends Controller
EventBus::dispatch('death.recorded', ['case_id' => (int) $case->id, 'member_id' => (int) $memberId, 'type' => $deceasedType]);
// Send payment to cashier queue (only for primary_member death which requires transfer fee)
if (bccomp($totalFee, '0', 2) > 0) {
$secondaryCount = 0;
if (!empty($caseData['secondary_spouses_json'])) {
$secondaryCount = count(json_decode($caseData['secondary_spouses_json'], true));
}
$breakdown = [
'رسوم نقل العضوية للزوجة الأساسية:',
' رسوم استمارة (570): ' . money($fees['formFee']),
' اشتراك سنوي: ' . money($fees['annualSub']),
];
if ($secondaryCount > 0) {
$breakdown[] = '';
$breakdown[] = "رسوم عضويات منفصلة ({$secondaryCount} زوجة إضافية):";
$breakdown[] = " {$secondaryCount} × " . money($fees['totalFee']) . ' = ' . money(bcmul($fees['totalFee'], (string) $secondaryCount, 2));
}
$breakdown[] = '═══════════════════════════';
$breakdown[] = 'الإجمالي: ' . money($totalFee);
$result = PaymentRequestService::createRequest([
'member_id' => (int) $memberId,
'amount' => $totalFee,
'payment_type' => 'death_fee',
'related_entity_type' => 'death_cases',
'related_entity_id' => (int) $case->id,
'description_ar' => 'رسوم وفاة — نقل عضوية — حالة #' . $case->id,
'notes' => json_encode(['fee_breakdown' => $breakdown], JSON_UNESCAPED_UNICODE),
]);
if ($result['success']) {
return $this->redirect("/death/{$case->id}")->withSuccess(
'تم تسجيل حالة الوفاة وإرسال طلب الدفع للخزينة — ' . money($totalFee)
);
}
if ($deceasedType === 'primary_member') {
return $this->redirect("/death/{$case->id}")->withSuccess('تم تسجيل حالة الوفاة وإرسالها لمراجعة مجلس الإدارة');
}
// Non-primary: spouse/child death needs no payment, can complete directly
return $this->redirect("/death/{$case->id}")->withSuccess('تم تسجيل حالة الوفاة');
}
......@@ -163,16 +204,18 @@ class DeathController extends Controller
{
$db = App::getInstance()->db();
$case = $db->selectOne(
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number
"SELECT dc.*, m.full_name_ar as member_name, m.membership_number, m.membership_value as member_membership_value
FROM death_cases dc JOIN members m ON m.id = dc.member_id WHERE dc.id = ?",
[(int) $id]
);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
$fees = self::getFees();
$primarySpouse = null;
$primarySpouse = null;
$secondarySpouses = [];
$newMember = null;
$newMember = null;
$paymentRequest = null;
$boardApprover = null;
if ($case['primary_spouse_id']) {
$primarySpouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['primary_spouse_id']]);
......@@ -187,6 +230,12 @@ class DeathController extends Controller
if ($case['transferred_to_member_id']) {
$newMember = $db->selectOne("SELECT id, full_name_ar, membership_number, status FROM members WHERE id = ?", [(int) $case['transferred_to_member_id']]);
}
if ($case['payment_request_id']) {
$paymentRequest = $db->selectOne("SELECT * FROM payment_requests WHERE id = ?", [(int) $case['payment_request_id']]);
}
if ($case['board_approved_by']) {
$boardApprover = $db->selectOne("SELECT full_name_ar FROM employees WHERE id = ?", [(int) $case['board_approved_by']]);
}
return $this->view('Death.Views.show', [
'case' => $case,
......@@ -194,7 +243,176 @@ class DeathController extends Controller
'primarySpouse' => $primarySpouse,
'secondarySpouses' => $secondarySpouses,
'newMember' => $newMember,
'paymentRequest' => $paymentRequest,
'boardApprover' => $boardApprover,
]);
}
public function boardApprove(Request $request, string $id): Response
{
$this->authorize('transfer.approve');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
if ($case['status'] !== 'board_review') {
return $this->redirect("/death/{$id}")->withError('الحالة ليست في مرحلة مراجعة مجلس الإدارة');
}
$feeMethod = trim($request->post('fee_method', ''));
$feeValue = trim($request->post('fee_value', '0'));
$boardRef = trim($request->post('board_decision_reference', ''));
$boardDate = trim($request->post('board_decision_date', ''));
$boardNotes = trim($request->post('board_notes', ''));
if (!in_array($feeMethod, ['percentage', 'fixed'], true)) {
return $this->redirect("/death/{$id}")->withError('يجب تحديد طريقة احتساب رسوم مجلس الأمناء');
}
if (!is_numeric($feeValue) || bccomp($feeValue, '0', 4) < 0) {
return $this->redirect("/death/{$id}")->withError('قيمة رسوم مجلس الأمناء غير صحيحة');
}
// Calculate trustee fee (once regardless of secondary spouse count)
$boardFeeAmount = '0.00';
if ($feeMethod === 'percentage') {
$membershipValue = (string) ($case['member_membership_value'] ?? '0');
// Need to fetch membership_value from members table directly
$memberRow = $db->selectOne("SELECT membership_value FROM members WHERE id = ?", [(int) $case['member_id']]);
$membershipValue = (string) ($memberRow['membership_value'] ?? '0');
$boardFeeAmount = bcdiv(bcmul($membershipValue, $feeValue, 6), '100', 2);
} else {
$boardFeeAmount = bcadd($feeValue, '0', 2);
}
// Recalculate total: base fees × spouse multiplier + trustee fee (flat)
$fees = self::getFees();
$secondaryCount = 0;
if (!empty($case['secondary_spouses_json'])) {
$secondaryCount = count(json_decode($case['secondary_spouses_json'], true));
}
$baseFees = bcmul($fees['totalFee'], (string) (1 + $secondaryCount), 2);
$totalFee = bcadd($baseFees, $boardFeeAmount, 2);
$db->update('death_cases', [
'board_fee_method' => $feeMethod,
'board_fee_value' => $feeValue,
'board_fee_amount' => $boardFeeAmount,
'board_decision_reference' => $boardRef ?: null,
'board_decision_date' => $boardDate ?: null,
'board_notes' => $boardNotes ?: null,
'board_approved_by' => $employee ? (int) $employee->id : null,
'board_approved_at' => date('Y-m-d H:i:s'),
'fee_amount' => $totalFee,
'status' => 'board_approved',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
// Build fee breakdown for cashier notes
$multiplierLabel = $secondaryCount > 0 ? " (× " . (1 + $secondaryCount) . " زوجات)" : '';
$breakdown = [
'رسوم نقل العضوية' . $multiplierLabel . ':',
' رسوم استمارة: ' . money($fees['formFee']),
' اشتراك سنوي: ' . money($fees['annualSub']),
];
if ($secondaryCount > 0) {
$breakdown[] = ' مجموع الأساس: ' . money($baseFees);
}
$trusteeLabel = $feeMethod === 'percentage'
? 'رسوم مجلس الأمناء (' . $feeValue . '% من قيمة العضوية)'
: 'رسوم مجلس الأمناء (مبلغ ثابت)';
$breakdown[] = $trusteeLabel . ': ' . money($boardFeeAmount);
$breakdown[] = '═══════════════════════════';
$breakdown[] = 'الإجمالي: ' . money($totalFee);
$result = PaymentRequestService::createRequest([
'member_id' => (int) $case['member_id'],
'amount' => $totalFee,
'payment_type' => 'death_fee',
'related_entity_type' => 'death_cases',
'related_entity_id' => (int) $id,
'description_ar' => 'رسوم وفاة — نقل عضوية — حالة #' . $id,
'notes' => json_encode([
'fee_breakdown' => $breakdown,
'form_fee' => $fees['formFee'],
'annual_sub' => $fees['annualSub'],
'trustee_fee' => $boardFeeAmount,
'trustee_method' => $feeMethod,
'multiplier' => 1 + $secondaryCount,
'total' => $totalFee,
], JSON_UNESCAPED_UNICODE),
]);
if ($result['success']) {
$db->update('death_cases', [
'payment_request_id' => $result['request_id'],
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
}
return $this->redirect("/death/{$id}")->withSuccess(
'تم اعتماد القضية من مجلس الإدارة — رسوم مجلس الأمناء: ' . money($boardFeeAmount) . ' — الإجمالي: ' . money($totalFee)
);
}
public function boardReject(Request $request, string $id): Response
{
$this->authorize('transfer.approve');
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$case = $db->selectOne("SELECT id, status FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
if ($case['status'] !== 'board_review') {
return $this->redirect("/death/{$id}")->withError('الحالة ليست في مرحلة مراجعة مجلس الإدارة');
}
$boardNotes = trim($request->post('board_notes', ''));
$db->update('death_cases', [
'status' => 'rejected',
'board_notes' => $boardNotes ?: null,
'board_approved_by'=> $employee ? (int) $employee->id : null,
'board_approved_at'=> date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $id]);
return $this->redirect("/death/{$id}")->withWarning('تم رفض القضية من مجلس الإدارة');
}
public function downloadDocument(Request $request, string $id, string $type): Response
{
$this->authorize('transfer.view');
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT death_certificate_path, inheritance_notice_path FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case) return $this->redirect('/death')->withError('الحالة غير موجودة');
$column = match($type) {
'cert' => 'death_certificate_path',
'inheritance' => 'inheritance_notice_path',
default => null,
};
if (!$column || empty($case[$column])) {
return $this->redirect("/death/{$id}")->withError('المستند غير موجود');
}
$filePath = App::getInstance()->basePath() . '/' . $case[$column];
if (!file_exists($filePath)) {
return $this->redirect("/death/{$id}")->withError('ملف المستند غير موجود على الخادم');
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($filePath) ?: 'application/octet-stream';
$filename = basename($filePath);
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . $filename . '"');
header('Content-Length: ' . filesize($filePath));
header('Cache-Control: private, max-age=3600');
readfile($filePath);
exit;
}
public function pay(Request $request, string $id): Response
......@@ -225,7 +443,6 @@ class DeathController extends Controller
return $this->redirect("/death/{$id}")->withError($result['error']);
}
// For primary member death: move to pending_form_fill so wife fills full form
$newStatus = ($case['deceased_type'] === 'primary_member') ? 'pending_form_fill' : 'fee_paid';
$db->update('death_cases', [
......@@ -274,14 +491,13 @@ class DeathController extends Controller
$data = $request->all();
unset($data['_csrf_token']);
// Auto-extract DOB and gender from national ID (always authoritative)
$nid = trim((string) ($data['national_id'] ?? ''));
$data['national_id'] = $nid;
if ($nid !== '' && strlen($nid) === 14) {
$parsed = NationalIdParser::parse($nid);
if ($parsed['is_valid']) {
$data['date_of_birth'] = $parsed['dob'];
$data['gender'] = $parsed['gender'];
$data['gender'] = $parsed['gender'];
}
}
......@@ -292,7 +508,6 @@ class DeathController extends Controller
}
}
// Store form data in session for completion step
$db->update('death_cases', [
'primary_spouse_form_filled' => 1,
'notes' => json_encode(['form_data' => $data], JSON_UNESCAPED_UNICODE),
......@@ -304,12 +519,33 @@ class DeathController extends Controller
public function complete(Request $request, string $id): Response
{
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
$db = App::getInstance()->db();
$case = $db->selectOne("SELECT * FROM death_cases WHERE id = ?", [(int) $id]);
if (!$case || $case['status'] === 'completed') return $this->redirect('/death')->withError('الحالة غير صالحة');
$employee = App::getInstance()->currentEmployee();
// Pre-completion validations for primary_member death
if ($case['deceased_type'] === 'primary_member') {
if (!$case['board_approved_at']) {
return $this->redirect("/death/{$id}")->withError('لم يتم اعتماد القضية من مجلس الإدارة بعد');
}
if (!$case['death_certificate_path'] || !$case['inheritance_notice_path']) {
return $this->redirect("/death/{$id}")->withError('يجب رفع وثيقة الوفاة وإعلام الوراثة قبل الإتمام');
}
$deathPaymentCheck = $db->selectOne(
"SELECT id FROM payments WHERE member_id = ? AND payment_type = 'death_fee'
AND related_entity_type = 'death_cases' AND related_entity_id = ? AND is_voided = 0",
[(int) $case['member_id'], (int) $id]
);
if (!$deathPaymentCheck) {
return $this->redirect("/death/{$id}")->withError('لم يتم سداد رسوم الوفاة في الخزينة بعد');
}
if (!$case['primary_spouse_form_filled']) {
return $this->redirect("/death/{$id}")->withError('يجب ملء استمارة العضوية الجديدة للزوجة أولاً');
}
}
$db->beginTransaction();
try {
$snapshotId = ArchiveService::takeSnapshot('members', (int) $case['member_id'], 'death', 'وفاة — حالة #' . $id);
......@@ -327,30 +563,26 @@ class DeathController extends Controller
], '`id` = ?', [(int) $case['child_id']]);
} elseif ($case['deceased_type'] === 'primary_member') {
if (!$case['primary_spouse_form_filled']) {
$db->rollBack();
return $this->redirect("/death/{$id}")->withError('يجب ملء استمارة العضوية الجديدة أولاً');
}
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $case['member_id']]);
$spouse = $db->selectOne("SELECT * FROM spouses WHERE id = ?", [(int) $case['primary_spouse_id']]);
$formData = [];
if (!empty($case['notes'])) {
$notesData = json_decode($case['notes'], true);
$formData = $notesData['form_data'] ?? [];
$formData = $notesData['form_data'] ?? [];
}
$inheritedNumber = $member['membership_number'];
// Find the death_fee payment that was made for this case
$deathPayment = $db->selectOne(
"SELECT id FROM payments WHERE member_id = ? AND payment_type = 'death_fee' AND related_entity_type = 'death_cases' AND related_entity_id = ? AND is_voided = 0 ORDER BY id DESC LIMIT 1",
"SELECT id FROM payments WHERE member_id = ? AND payment_type = 'death_fee'
AND related_entity_type = 'death_cases' AND related_entity_id = ? AND is_voided = 0
ORDER BY id DESC LIMIT 1",
[(int) $case['member_id'], (int) $id]
);
$deathPaymentId = $deathPayment ? (int) $deathPayment['id'] : null;
// STEP 1: Archive the deceased member FIRST to release the membership_number
// STEP 1: Archive the deceased member and release their membership_number
$db->update('members', [
'is_archived' => 1,
'archived_at' => date('Y-m-d H:i:s'),
......@@ -358,27 +590,29 @@ class DeathController extends Controller
'membership_number' => null,
], '`id` = ?', [(int) $case['member_id']]);
// STEP 2: Now safe to insert the new member with the inherited number
// STEP 2: Insert new primary member with inherited number + source tracking
$newMemberData = [
'membership_number' => $inheritedNumber,
'full_name_ar' => $formData['full_name_ar'] ?? $spouse['full_name_ar'],
'full_name_en' => $formData['full_name_en'] ?? $spouse['full_name_en'] ?? null,
'national_id' => $formData['national_id'] ?? $spouse['national_id'],
'date_of_birth' => $formData['date_of_birth'] ?? $spouse['date_of_birth'],
'gender' => $formData['gender'] ?? $spouse['gender'],
'nationality' => $formData['nationality'] ?? $spouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => $member['membership_type'] ?? 'working',
'member_category' => 'working_member',
'status' => 'active',
'activated_at' => date('Y-m-d H:i:s'),
'activated_by_payment_id'=> $deathPaymentId,
'qualification_id' => !empty($formData['qualification_id']) ? (int) $formData['qualification_id'] : ($spouse['qualification_id'] ?? $member['qualification_id']),
'phone_mobile' => $formData['phone_mobile'] ?? $spouse['mobile'] ?? $member['phone_mobile'],
'membership_value' => $member['membership_value'],
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
'membership_number' => $inheritedNumber,
'full_name_ar' => $formData['full_name_ar'] ?? $spouse['full_name_ar'],
'full_name_en' => $formData['full_name_en'] ?? $spouse['full_name_en'] ?? null,
'national_id' => $formData['national_id'] ?? $spouse['national_id'],
'date_of_birth' => $formData['date_of_birth'] ?? $spouse['date_of_birth'],
'gender' => $formData['gender'] ?? $spouse['gender'],
'nationality' => $formData['nationality'] ?? $spouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => $member['membership_type'] ?? 'working',
'member_category' => 'working_member',
'status' => 'active',
'activated_at' => date('Y-m-d H:i:s'),
'activated_by_payment_id' => $deathPaymentId,
'qualification_id' => !empty($formData['qualification_id']) ? (int) $formData['qualification_id'] : ($spouse['qualification_id'] ?? $member['qualification_id']),
'phone_mobile' => $formData['phone_mobile'] ?? $spouse['mobile'] ?? $member['phone_mobile'],
'membership_value' => $member['membership_value'],
'transferred_from_death_id' => (int) $id,
'original_membership_number' => $inheritedNumber,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
];
$optionalFields = ['marital_status', 'religion', 'phone_home', 'email', 'emergency_name', 'emergency_phone', 'residence_address', 'area', 'governorate', 'occupation', 'job_title'];
......@@ -396,7 +630,7 @@ class DeathController extends Controller
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $case['primary_spouse_id']]);
// Handle children assignment
// Handle children assignment from JSON map
$childrenAssignment = !empty($case['children_assignment_json']) ? json_decode($case['children_assignment_json'], true) : [];
if (!empty($childrenAssignment)) {
foreach ($childrenAssignment as $childId => $assignTo) {
......@@ -406,6 +640,7 @@ class DeathController extends Controller
}
}
} else {
// No explicit assignment map: all children go to primary
$db->update('children', ['member_id' => $newMemberId, 'updated_at' => date('Y-m-d H:i:s')],
'`member_id` = ? AND `is_archived` = 0', [(int) $case['member_id']]);
}
......@@ -418,23 +653,24 @@ class DeathController extends Controller
if (!$secSpouse) continue;
$secMemberId = $db->insert('members', [
'full_name_ar' => $secSpouse['full_name_ar'],
'full_name_en' => $secSpouse['full_name_en'] ?? null,
'national_id' => $secSpouse['national_id'],
'date_of_birth' => $secSpouse['date_of_birth'],
'gender' => $secSpouse['gender'],
'nationality' => $secSpouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => $member['membership_type'] ?? 'working',
'member_category' => 'working_member',
'status' => 'active',
'activated_at' => date('Y-m-d H:i:s'),
'activated_by_payment_id'=> $deathPaymentId,
'phone_mobile' => $secSpouse['mobile'] ?? null,
'membership_value' => $member['membership_value'],
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
'full_name_ar' => $secSpouse['full_name_ar'],
'full_name_en' => $secSpouse['full_name_en'] ?? null,
'national_id' => $secSpouse['national_id'],
'date_of_birth' => $secSpouse['date_of_birth'],
'gender' => $secSpouse['gender'],
'nationality' => $secSpouse['nationality'] ?? 'مصري',
'branch_id' => (int) $member['branch_id'],
'membership_type' => $member['membership_type'] ?? 'working',
'member_category' => 'working_member',
'status' => 'active',
'activated_at' => date('Y-m-d H:i:s'),
'activated_by_payment_id' => $deathPaymentId,
'phone_mobile' => $secSpouse['mobile'] ?? null,
'membership_value' => $member['membership_value'],
'transferred_from_death_id' => (int) $id,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
\App\Modules\Members\Services\MemberNumberGenerator::assign($secMemberId);
......@@ -444,7 +680,7 @@ class DeathController extends Controller
'archived_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $secSpouseId]);
// Move children assigned to this secondary spouse
// Move explicitly-assigned children to this secondary spouse member
if (!empty($childrenAssignment)) {
foreach ($childrenAssignment as $childId => $assignTo) {
if ((int) $assignTo === (int) $secSpouseId) {
......@@ -456,6 +692,27 @@ class DeathController extends Controller
}
}
// FIX: Sweep any remaining children still on deceased member to new primary
// (catches children not in assignment map, e.g. added after case was created)
$db->query(
"UPDATE children SET member_id = ?, updated_at = ?
WHERE member_id = ? AND is_archived = 0",
[$newMemberId, date('Y-m-d H:i:s'), (int) $case['member_id']]
);
// Renumber child_order for all children of new primary (1..n, sequential)
$allChildren = $db->select(
"SELECT id FROM children WHERE member_id = ? AND is_archived = 0 ORDER BY child_order, id",
[$newMemberId]
);
foreach ($allChildren as $i => $c) {
$db->update('children', [
'child_order' => $i + 1,
'activated_by_payment_id' => $deathPaymentId,
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $c['id']]);
}
// Transfer temporary_members to new primary
$db->update('temporary_members', ['member_id' => $newMemberId, 'updated_at' => date('Y-m-d H:i:s')],
'`member_id` = ? AND `is_archived` = 0', [(int) $case['member_id']]);
......
......@@ -18,10 +18,14 @@ class DeathCase extends Model
protected static array $fillable = [
'member_id', 'deceased_type', 'death_date',
'death_certificate_number', 'death_certificate_path',
'inheritance_notice_path',
'spouse_id', 'primary_spouse_id', 'secondary_spouses_json',
'primary_spouse_form_filled', 'child_id',
'children_assignment_json', 'transferred_to_member_id',
'same_membership_number', 'fee_amount',
'board_fee_method', 'board_fee_value', 'board_fee_amount',
'board_decision_reference', 'board_decision_date', 'board_notes',
'board_approved_by', 'board_approved_at', 'payment_request_id',
'archive_snapshot_id', 'workflow_instance_id', 'status', 'notes',
];
......
......@@ -8,6 +8,9 @@ return [
['GET', '/death/{id}', 'Death\Controllers\DeathController@show', ['auth'], 'transfer.view'],
['GET', '/death/{id}/fill-form', 'Death\Controllers\DeathController@fillForm', ['auth'], 'transfer.initiate'],
['POST', '/death/{id}/fill-form', 'Death\Controllers\DeathController@saveFillForm', ['auth', 'csrf'], 'transfer.initiate'],
['POST', '/death/{id}/pay', 'Death\Controllers\DeathController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth', 'csrf'], 'transfer.approve'],
['POST', '/death/{id}/pay', 'Death\Controllers\DeathController@pay', ['auth', 'csrf'], 'payment.collect'],
['POST', '/death/{id}/complete', 'Death\Controllers\DeathController@complete', ['auth', 'csrf'], 'transfer.approve'],
['POST', '/death/{id}/board-approve', 'Death\Controllers\DeathController@boardApprove', ['auth', 'csrf'], 'transfer.approve'],
['POST', '/death/{id}/board-reject', 'Death\Controllers\DeathController@boardReject', ['auth', 'csrf'], 'transfer.approve'],
['GET', '/death/{id}/document/{type}', 'Death\Controllers\DeathController@downloadDocument', ['auth'], 'transfer.view'],
];
\ No newline at end of file
......@@ -10,9 +10,31 @@
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
</div>
<form method="POST" action="/death/store/<?= (int) $member['id'] ?>">
<form method="POST" action="/death/store/<?= (int) $member['id'] ?>" enctype="multipart/form-data">
<?= csrf_field() ?>
<!-- Mandatory documents — shown/required only when primary_member is selected -->
<div id="docs-section" style="display:none;">
<div class="card" style="margin-bottom:20px;border:2px solid #DC2626;">
<div style="padding:15px 20px;border-bottom:1px solid #FCA5A5;background:#FEF2F2;">
<h3 style="margin:0;color:#DC2626;font-size:15px;">المستندات المطلوبة — إلزامية لنقل العضوية</h3>
<p style="margin:4px 0 0;font-size:12px;color:#6B7280;">PDF أو صورة (JPG/PNG) — لن يتم تسجيل الحالة بدونهما</p>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">شهادة الوفاة <span style="color:#DC2626;">*</span></label>
<input type="file" name="death_certificate" id="death_certificate" class="form-input" accept=".pdf,.png,.jpg,.jpeg">
<p style="font-size:11px;color:#9CA3AF;margin:4px 0 0;">PDF أو JPG أو PNG</p>
</div>
<div class="form-group">
<label class="form-label">إعلام الوراثة <span style="color:#DC2626;">*</span></label>
<input type="file" name="inheritance_notice" id="inheritance_notice" class="form-input" accept=".pdf,.png,.jpg,.jpeg">
<p style="font-size:11px;color:#9CA3AF;margin:4px 0 0;">PDF أو JPG أو PNG</p>
</div>
</div>
</div>
</div>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#1F2937;">بيانات الوفاة</h3></div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
......@@ -133,6 +155,8 @@ document.addEventListener('DOMContentLoaded', function() {
var hiddenChild = document.getElementById('hidden_child_id');
var primarySelect = document.getElementById('primary_spouse_id');
var docsSection = document.getElementById('docs-section');
deceasedType.addEventListener('change', function() {
var selected = this.options[this.selectedIndex];
hiddenSpouse.value = selected.dataset.spouseId || '';
......@@ -141,9 +165,15 @@ document.addEventListener('DOMContentLoaded', function() {
if (this.value === 'primary_member') {
primarySection.style.display = 'block';
feeSection.style.display = 'block';
docsSection.style.display = 'block';
document.getElementById('death_certificate').required = true;
document.getElementById('inheritance_notice').required = true;
} else {
primarySection.style.display = 'none';
feeSection.style.display = (this.value === 'spouse' || this.value === 'child') ? 'none' : 'block';
feeSection.style.display = 'none';
docsSection.style.display = 'none';
document.getElementById('death_certificate').required = false;
document.getElementById('inheritance_notice').required = false;
}
});
......
......@@ -4,14 +4,20 @@
<?php
$statusLabels = [
'recorded' => 'مسجّل — في انتظار الدفع',
'recorded' => 'مسجّل — في انتظار مراجعة مجلس الإدارة',
'board_review' => 'قيد مراجعة مجلس الإدارة',
'board_approved' => 'معتمد من مجلس الإدارة — في انتظار الدفع',
'rejected' => 'مرفوض من مجلس الإدارة',
'fee_paid' => 'تم الدفع',
'pending_form_fill' => 'في انتظار ملء الاستمارة',
'completed' => 'مكتمل',
];
$statusColors = [
'recorded' => '#D97706',
'fee_paid' => '#2563EB',
'recorded' => '#9CA3AF',
'board_review' => '#D97706',
'board_approved' => '#2563EB',
'rejected' => '#DC2626',
'fee_paid' => '#0D7377',
'pending_form_fill' => '#7C3AED',
'completed' => '#059669',
];
......@@ -23,6 +29,7 @@ $deceasedLabel = match($case['deceased_type']) {
'child' => 'ابن/ابنة',
default => $case['deceased_type'],
};
$canApprove = can('transfer.approve');
?>
<!-- Case Info -->
......@@ -33,7 +40,7 @@ $deceasedLabel = match($case['deceased_type']) {
<tr><td style="padding:6px 0;color:#6B7280;">المتوفى</td><td style="padding:6px 0;font-weight:600;"><?= $deceasedLabel ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ الوفاة</td><td style="padding:6px 0;"><?= e($case['death_date']) ?></td></tr>
<?php if ($case['death_certificate_number']): ?>
<tr><td style="padding:6px 0;color:#6B7280;">شهادة الوفاة</td><td style="padding:6px 0;"><?= e($case['death_certificate_number']) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">رقم شهادة الوفاة</td><td style="padding:6px 0;"><?= e($case['death_certificate_number']) ?></td></tr>
<?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">الحالة</td><td style="padding:6px 0;font-weight:700;color:<?= $statusColor ?>;"><?= $statusLabel ?></td></tr>
<?php if ($primarySpouse): ?>
......@@ -56,49 +63,175 @@ $deceasedLabel = match($case['deceased_type']) {
</div>
<?php endif; ?>
<!-- Fee Breakdown -->
<?php if (bccomp($case['fee_amount'] ?? '0', '0', 2) > 0): ?>
<!-- Documents Section -->
<?php if ($case['deceased_type'] === 'primary_member'): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#D97706;font-size:15px;">تفصيل الرسوم</h3>
<h3 style="margin:0;color:#1F2937;font-size:15px;">المستندات المرفوعة</h3>
</div>
<div style="padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div>
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">شهادة الوفاة</div>
<?php if ($case['death_certificate_path']): ?>
<a href="/death/<?= (int) $case['id'] ?>/document/cert" target="_blank" class="btn btn-outline btn-sm" style="font-size:12px;">📄 عرض / تنزيل</a>
<?php else: ?>
<span style="color:#DC2626;font-size:13px;">لم يُرفع بعد</span>
<?php endif; ?>
</div>
<div>
<div style="font-size:13px;color:#6B7280;margin-bottom:6px;">إعلام الوراثة</div>
<?php if ($case['inheritance_notice_path']): ?>
<a href="/death/<?= (int) $case['id'] ?>/document/inheritance" target="_blank" class="btn btn-outline btn-sm" style="font-size:12px;">📄 عرض / تنزيل</a>
<?php else: ?>
<span style="color:#DC2626;font-size:13px;">لم يُرفع بعد</span>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Board Review Form (visible when status = board_review and user has approve permission) -->
<?php if ($case['status'] === 'board_review' && $canApprove): ?>
<div class="card" style="margin-bottom:20px;border:2px solid #D97706;">
<div style="padding:15px 20px;border-bottom:1px solid #FDE68A;background:#FFFBEB;">
<h3 style="margin:0;color:#D97706;font-size:15px;">مراجعة مجلس الإدارة</h3>
<p style="margin:4px 0 0;font-size:12px;color:#6B7280;">حدد رسوم مجلس الأمناء وقرار المجلس لإرسال طلب الدفع للخزينة</p>
</div>
<div style="padding:20px;">
<table style="width:100%;max-width:500px;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;">رسوم الاستمارة (570)</td><td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($fees['formFee']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">اشتراك سنوي</td><td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($fees['annualSubBase']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">رسوم تنمية</td><td style="padding:8px 0;font-weight:600;direction:ltr;text-align:left;"><?= money($fees['devFee']) ?></td></tr>
<?php
$secondaryCount = 0;
if (!empty($case['secondary_spouses_json'])) {
$secondaryCount = count(json_decode($case['secondary_spouses_json'], true));
}
if ($secondaryCount > 0): ?>
<tr><td style="padding:8px 0;color:#6B7280;">× <?= $secondaryCount + 1 ?> (أساسية + <?= $secondaryCount ?> إضافية)</td><td style="padding:8px 0;font-weight:600;"></td></tr>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/board-approve">
<?= csrf_field() ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px;">
<div class="form-group">
<label class="form-label">طريقة احتساب رسوم مجلس الأمناء <span style="color:#DC2626;">*</span></label>
<select name="fee_method" id="board_fee_method" class="form-select" required>
<option value="">-- اختر --</option>
<option value="percentage">نسبة مئوية من قيمة العضوية (%)</option>
<option value="fixed">مبلغ ثابت (ج.م)</option>
</select>
</div>
<div class="form-group">
<label class="form-label" id="fee_value_label">القيمة <span style="color:#DC2626;">*</span></label>
<input type="number" name="fee_value" id="board_fee_value" class="form-input" min="0" step="0.01" required placeholder="0.00">
<p id="fee_calc_hint" style="font-size:11px;color:#9CA3AF;margin:4px 0 0;"></p>
</div>
<div class="form-group">
<label class="form-label">رقم قرار المجلس</label>
<input type="text" name="board_decision_reference" class="form-input" placeholder="مثال: قرار ٢٠٢٦/١٥">
</div>
<div class="form-group">
<label class="form-label">تاريخ قرار المجلس</label>
<input type="date" name="board_decision_date" class="form-input" max="<?= date('Y-m-d') ?>">
</div>
<div class="form-group" style="grid-column:1/-1;">
<label class="form-label">ملاحظات المجلس</label>
<textarea name="board_notes" class="form-textarea" rows="2"></textarea>
</div>
</div>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" onclick="return confirm('تأكيد اعتماد القضية وإرسال طلب الدفع للخزينة؟')">اعتماد وإرسال للخزينة</button>
</div>
</form>
<hr style="border:none;border-top:1px solid #E5E7EB;margin:20px 0;">
<form method="POST" action="/death/<?= (int) $case['id'] ?>/board-reject">
<?= csrf_field() ?>
<div class="form-group" style="margin-bottom:10px;">
<label class="form-label">سبب الرفض</label>
<textarea name="board_notes" class="form-textarea" rows="2" placeholder="اذكر سبب الرفض..."></textarea>
</div>
<button type="submit" class="btn" style="background:#DC2626;color:#fff;" onclick="return confirm('تأكيد رفض القضية؟')">رفض القضية</button>
</form>
</div>
</div>
<?php endif; ?>
<!-- Board Decision Display (status >= board_approved or rejected) -->
<?php if ($case['board_approved_at'] && $case['status'] !== 'board_review'): ?>
<div class="card" style="margin-bottom:20px;border:1px solid <?= $case['status'] === 'rejected' ? '#DC2626' : '#2563EB' ?>;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;background:<?= $case['status'] === 'rejected' ? '#FEF2F2' : '#EFF6FF' ?>;">
<h3 style="margin:0;color:<?= $case['status'] === 'rejected' ? '#DC2626' : '#2563EB' ?>;font-size:15px;">
<?= $case['status'] === 'rejected' ? 'قرار الرفض' : 'قرار مجلس الإدارة' ?>
</h3>
</div>
<div style="padding:20px;font-size:14px;">
<table style="width:100%;max-width:500px;">
<?php if ($case['board_decision_reference']): ?>
<tr><td style="padding:5px 0;color:#6B7280;width:40%;">رقم القرار</td><td style="padding:5px 0;font-weight:600;"><?= e($case['board_decision_reference']) ?></td></tr>
<?php endif; ?>
<?php if ($case['board_decision_date']): ?>
<tr><td style="padding:5px 0;color:#6B7280;">تاريخ القرار</td><td style="padding:5px 0;"><?= e($case['board_decision_date']) ?></td></tr>
<?php endif; ?>
<?php if ($boardApprover): ?>
<tr><td style="padding:5px 0;color:#6B7280;">بواسطة</td><td style="padding:5px 0;"><?= e($boardApprover['full_name_ar']) ?></td></tr>
<?php endif; ?>
<tr><td style="padding:5px 0;color:#6B7280;">وقت القرار</td><td style="padding:5px 0;"><?= e($case['board_approved_at']) ?></td></tr>
<?php if ($case['board_notes']): ?>
<tr><td style="padding:5px 0;color:#6B7280;vertical-align:top;">ملاحظات</td><td style="padding:5px 0;"><?= e($case['board_notes']) ?></td></tr>
<?php endif; ?>
</table>
<?php if ($case['board_fee_amount'] !== null && $case['status'] !== 'rejected'): ?>
<hr style="border:none;border-top:1px solid #E5E7EB;margin:15px 0;">
<div style="font-weight:600;margin-bottom:8px;color:#1F2937;">تفصيل الرسوم المعتمدة</div>
<?php
$secondaryCount = 0;
if (!empty($case['secondary_spouses_json'])) {
$secondaryCount = count(json_decode($case['secondary_spouses_json'], true));
}
$multiplier = 1 + $secondaryCount;
$baseFeePerSpouse = bcadd($fees['formFee'], $fees['annualSub'], 2);
$baseFeeTotal = bcmul($baseFeePerSpouse, (string) $multiplier, 2);
?>
<table style="font-size:13px;max-width:400px;">
<tr><td style="padding:4px 20px 4px 0;color:#6B7280;">رسوم استمارة (<?= $multiplier ?> × <?= money($fees['formFee']) ?>)</td><td style="font-weight:600;"><?= money(bcmul($fees['formFee'], (string) $multiplier, 2)) ?></td></tr>
<tr><td style="padding:4px 20px 4px 0;color:#6B7280;">اشتراك سنوي (<?= $multiplier ?> × <?= money($fees['annualSub']) ?>)</td><td style="font-weight:600;"><?= money(bcmul($fees['annualSub'], (string) $multiplier, 2)) ?></td></tr>
<tr>
<td style="padding:4px 20px 4px 0;color:#6B7280;">
رسوم مجلس الأمناء
(<?= $case['board_fee_method'] === 'percentage' ? $case['board_fee_value'] . '% من قيمة العضوية' : 'مبلغ ثابت' ?>)
</td>
<td style="font-weight:600;"><?= money($case['board_fee_amount']) ?></td>
</tr>
<tr style="border-top:2px solid #0D7377;">
<td style="padding:12px 0;font-weight:700;font-size:16px;">الإجمالي المطلوب</td>
<td style="padding:12px 0;font-weight:800;font-size:22px;color:#DC2626;direction:ltr;text-align:left;"><?= money($case['fee_amount']) ?></td>
<td style="padding:10px 20px 4px 0;font-weight:700;font-size:15px;">الإجمالي</td>
<td style="padding:10px 0 4px;font-weight:800;font-size:20px;color:#DC2626;"><?= money($case['fee_amount']) ?></td>
</tr>
</table>
<?php endif; ?>
<?php if ($paymentRequest): ?>
<div style="margin-top:15px;padding:10px 15px;background:#F0FDF4;border:1px solid #86EFAC;border-radius:6px;font-size:13px;">
طلب الدفع #<?= (int) $paymentRequest['id'] ?>
الحالة: <strong><?= e($paymentRequest['status']) ?></strong>
<?php if (!empty($paymentRequest['request_number'])): ?> — رقم: <?= e($paymentRequest['request_number']) ?><?php endif; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<!-- Action Buttons -->
<?php if ($case['status'] === 'recorded' && bccomp($case['fee_amount'] ?? '0', '0', 2) > 0): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#FFF7ED;border:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">دفع رسوم نقل العضوية</h4>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/pay">
<?= csrf_field() ?>
<div style="display:flex;gap:10px;align-items:end;">
<div class="form-group"><label class="form-label">المبلغ</label><input type="text" value="<?= money($case['fee_amount']) ?>" class="form-input" style="background:#F3F4F6;font-weight:700;font-size:18px;" readonly></div>
<div class="form-group"><label class="form-label">طريقة الدفع</label><select name="payment_method" class="form-select"><option value="cash">نقدي</option><option value="visa">فيزا</option><option value="bank_transfer">تحويل</option></select></div>
<button type="submit" class="btn btn-primary" style="padding:10px 25px;" onclick="return confirm('تأكيد الدفع؟')">ادفع</button>
</div>
</form>
<!-- Fee Breakdown (legacy: for cases before board flow, status fee_paid/pending_form_fill) -->
<?php if (in_array($case['status'], ['fee_paid', 'pending_form_fill', 'completed'], true) && !$case['board_approved_at'] && bccomp($case['fee_amount'] ?? '0', '0', 2) > 0): ?>
<div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#D97706;font-size:15px;">تفصيل الرسوم</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;max-width:500px;font-size:14px;">
<tr><td style="padding:8px 0;color:#6B7280;">رسوم الاستمارة</td><td style="padding:8px 0;font-weight:600;"><?= money($fees['formFee']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">اشتراك سنوي</td><td style="padding:8px 0;font-weight:600;"><?= money($fees['annualSubBase']) ?></td></tr>
<tr><td style="padding:8px 0;color:#6B7280;">رسوم تنمية</td><td style="padding:8px 0;font-weight:600;"><?= money($fees['devFee']) ?></td></tr>
<tr style="border-top:2px solid #0D7377;">
<td style="padding:12px 0;font-weight:700;font-size:16px;">الإجمالي المدفوع</td>
<td style="padding:12px 0;font-weight:800;font-size:22px;color:#059669;"><?= money($case['fee_amount']) ?></td>
</tr>
</table>
</div>
</div>
<?php endif; ?>
<!-- Form Fill Section -->
<?php if ($case['status'] === 'pending_form_fill'): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#F5F3FF;border:2px solid #7C3AED;">
<h4 style="margin:0 0 10px;color:#7C3AED;">ملء استمارة العضوية الجديدة</h4>
......@@ -111,6 +244,7 @@ $deceasedLabel = match($case['deceased_type']) {
</div>
<?php endif; ?>
<!-- Action Buttons -->
<?php if ($case['status'] === 'pending_form_fill' && $case['primary_spouse_form_filled']): ?>
<form method="POST" action="/death/<?= (int) $case['id'] ?>/complete">
<?= csrf_field() ?>
......@@ -129,3 +263,41 @@ $deceasedLabel = match($case['deceased_type']) {
<?php endif; ?>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?>
<script>
document.addEventListener('DOMContentLoaded', function() {
var feeMethod = document.getElementById('board_fee_method');
var feeValue = document.getElementById('board_fee_value');
var feeLabel = document.getElementById('fee_value_label');
var feeHint = document.getElementById('fee_calc_hint');
var membershipValue = <?= (float) ($case['member_membership_value'] ?? 0) ?>;
if (feeMethod) {
feeMethod.addEventListener('change', function() {
if (this.value === 'percentage') {
feeLabel.innerHTML = 'النسبة المئوية (%) <span style="color:#DC2626;">*</span>';
feeValue.placeholder = 'مثال: 8';
feeValue.step = '0.01';
feeHint.textContent = membershipValue > 0
? 'مثال: 8% من ' + membershipValue.toLocaleString('ar-EG') + ' = ' + (membershipValue * 0.08).toFixed(2)
: 'أدخل النسبة المئوية';
} else if (this.value === 'fixed') {
feeLabel.innerHTML = 'المبلغ الثابت (ج.م) <span style="color:#DC2626;">*</span>';
feeValue.placeholder = '0.00';
feeValue.step = '0.01';
feeHint.textContent = '';
}
});
feeValue.addEventListener('input', function() {
if (feeMethod.value === 'percentage' && membershipValue > 0) {
var pct = parseFloat(this.value) || 0;
var calc = (membershipValue * pct / 100).toFixed(2);
feeHint.textContent = 'المبلغ المحسوب: ' + parseFloat(calc).toLocaleString('ar-EG') + ' ج.م';
}
});
}
});
</script>
<?php $__template->endSection(); ?>
......@@ -111,6 +111,9 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<?php if (!empty($divorceTransfer)): ?>
<span style="background:#FEF3C7;padding:2px 8px;border-radius:10px;font-size:11px;">محوّل من عضوية: <a href="/members/<?= (int) $divorceTransfer['original_member_id'] ?>" style="color:#D97706;font-weight:700;"><?= e($divorceTransfer['original_membership_number']) ?></a> (<?= e($divorceTransfer['original_member_name'] ?? '') ?>)</span>
<?php endif; ?>
<?php if (!empty($member->transferred_from_death_id)): ?>
<span style="background:#DBEAFE;padding:2px 8px;border-radius:10px;font-size:11px;">أُنشئت من <a href="/death/<?= (int) $member->transferred_from_death_id ?>" style="color:#2563EB;font-weight:700;">حالة وفاة #<?= (int) $member->transferred_from_death_id ?></a></span>
<?php endif; ?>
<span>📋 استمارة: <strong style="color:#D97706;"><?= e($member->form_number ?? '—') ?></strong></span>
<span>🏢 <?= e($branchName) ?></span>
<span><?= $member->gender === 'male' ? '👨' : '👩' ?> <?= $member->getGenderLabel() ?></span>
......
<?php
declare(strict_types=1);
return [
'up' => "
ALTER TABLE death_cases
ADD COLUMN inheritance_notice_path VARCHAR(500) NULL AFTER death_certificate_path,
ADD COLUMN board_fee_method ENUM('percentage','fixed') NULL AFTER fee_amount,
ADD COLUMN board_fee_value DECIMAL(10,4) NULL AFTER board_fee_method,
ADD COLUMN board_fee_amount DECIMAL(15,2) NULL AFTER board_fee_value,
ADD COLUMN board_decision_reference VARCHAR(100) NULL AFTER board_fee_amount,
ADD COLUMN board_decision_date DATE NULL AFTER board_decision_reference,
ADD COLUMN board_notes TEXT NULL AFTER board_decision_date,
ADD COLUMN board_approved_by BIGINT UNSIGNED NULL AFTER board_notes,
ADD COLUMN board_approved_at TIMESTAMP NULL AFTER board_approved_by,
ADD COLUMN payment_request_id BIGINT UNSIGNED NULL AFTER board_approved_at;
ALTER TABLE members
ADD COLUMN transferred_from_death_id BIGINT UNSIGNED NULL AFTER transferred_from_divorce_id;
",
'down' => "
ALTER TABLE death_cases
DROP COLUMN inheritance_notice_path,
DROP COLUMN board_fee_method,
DROP COLUMN board_fee_value,
DROP COLUMN board_fee_amount,
DROP COLUMN board_decision_reference,
DROP COLUMN board_decision_date,
DROP COLUMN board_notes,
DROP COLUMN board_approved_by,
DROP COLUMN board_approved_at,
DROP COLUMN payment_request_id;
ALTER TABLE members
DROP COLUMN transferred_from_death_id;
",
];
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