Commit 784c7ee8 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix(membership): display completeness and logic fixes across Death/Waiver/Transfers/Subscriptions

- Death show: display deceased person's actual name (not just membership owner),
  show original membership creation/activation dates, new member acquisition date,
  and subscription status after completion. Fix index status labels.
- Death subscriptions: dedup-safe createNewSubscriptions prevents double-demand
  when subscription rows already exist (marks existing pending rows as paid).
- Waiver index: add target member column, fix missing status labels (fee_paid/rejected),
  add date column. Show view: add payment request tracking display.
- Transfers: allow males under 25 to be selected for child separation if employed+graduated
  (previously blocked by both frontend JS and backend age gate). Add marriage date
  validation to reject dates before membership creation date.
- Subscriptions: call refreshForMember on member-subscriptions page load to auto-fill
  missing subscription rows for active dependents in current FY.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent de0b8968
......@@ -14,7 +14,18 @@
<td><?= $r['deceased_type'] === 'primary_member' ? 'العضو الرئيسي' : ($r['deceased_type'] === 'spouse' ? 'الزوج/ة' : $r['deceased_type']) ?></td>
<td style="font-size:13px;"><?= e($r['death_date']) ?></td>
<td style="font-weight:600;"><?= money($r['fee_amount'] ?? '0') ?></td>
<td><span style="color:<?= $r['status'] === 'completed' ? '#059669' : '#D97706' ?>;font-weight:600;"><?= $r['status'] === 'completed' ? 'مكتمل' : 'مسجّل' ?></span></td>
<?php
$idxStatusLabel = match($r['status']) {
'completed' => 'مكتمل', 'recorded' => 'مسجّل', 'board_review' => 'مراجعة المجلس',
'board_approved' => 'معتمد — انتظار الدفع', 'rejected' => 'مرفوض', 'fee_paid' => 'تم الدفع',
'pending_form_fill' => 'انتظار الاستمارة', default => $r['status'],
};
$idxStatusColor = match($r['status']) {
'completed' => '#059669', 'rejected' => '#DC2626', 'board_approved' => '#2563EB',
'fee_paid' => '#0D7377', 'pending_form_fill' => '#7C3AED', default => '#D97706',
};
?>
<td><span style="color:<?= $idxStatusColor ?>;font-weight:600;"><?= $idxStatusLabel ?></span></td>
<td><a href="/death/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
......
......@@ -35,19 +35,27 @@ $canApprove = can('transfer.approve');
<!-- Case Info -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<div style="display:flex;justify-content:space-between;align-items:start;">
<table style="width:100%;max-width:600px;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:35%;">العضو</td><td style="padding:6px 0;font-weight:600;"><?= e($case['member_name']) ?> (<?= e($case['membership_number'] ?? '—') ?>)</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المتوفى</td><td style="padding:6px 0;font-weight:600;"><?= $deceasedLabel ?></td></tr>
<table style="width:100%;max-width:700px;font-size:14px;">
<tr><td style="padding:6px 0;color:#6B7280;width:35%;">صاحب العضوية</td><td style="padding:6px 0;font-weight:600;"><a href="/members/<?= (int) $case['member_id'] ?>" style="color:#1D4ED8;"><?= e($case['member_name']) ?></a> (<?= e($case['membership_number'] ?? '—') ?>)</td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">المتوفى</td><td style="padding:6px 0;font-weight:600;"><?= $deceasedLabel ?><?php if (!empty($deceasedName) && $deceasedName !== $case['member_name']): ?><span style="color:#DC2626;"><?= e($deceasedName) ?></span><?php endif; ?></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>
<?php endif; ?>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ تسجيل الحالة</td><td style="padding:6px 0;"><?= e(substr($case['created_at'] ?? '', 0, 10)) ?></td></tr>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ إنشاء العضوية الأصلية</td><td style="padding:6px 0;"><?= e(substr($case['membership_created_at'] ?? '', 0, 10)) ?></td></tr>
<?php if (!empty($case['membership_activated_at'])): ?>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ تفعيل العضوية الأصلية</td><td style="padding:6px 0;"><?= e(substr($case['membership_activated_at'], 0, 10)) ?></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): ?>
<tr><td style="padding:6px 0;color:#6B7280;">الزوجة الأساسية</td><td style="padding:6px 0;font-weight:600;"><?= e($primarySpouse['full_name_ar']) ?></td></tr>
<?php endif; ?>
<?php if ($newMember): ?>
<tr><td style="padding:6px 0;color:#6B7280;">نُقلت العضوية إلى</td><td style="padding:6px 0;"><a href="/members/<?= (int) $newMember['id'] ?>" style="color:#0D7377;font-weight:700;font-size:16px;"><?= e($newMember['full_name_ar']) ?> — رقم <?= e($newMember['membership_number'] ?? '—') ?></a></td></tr>
<?php if (!empty($newMember['activated_at'])): ?>
<tr><td style="padding:6px 0;color:#6B7280;">تاريخ اكتساب العضوية الجديدة</td><td style="padding:6px 0;font-weight:600;color:#059669;"><?= e(substr($newMember['activated_at'], 0, 10)) ?></td></tr>
<?php endif; ?>
<?php endif; ?>
</table>
<a href="/death" class="btn btn-outline">← القائمة</a>
......@@ -269,6 +277,40 @@ $canApprove = can('transfer.approve');
</form>
<?php endif; ?>
<!-- Subscription Status after completion -->
<?php if (!empty($newMemberSubs)): ?>
<div class="card" style="margin-bottom:20px;margin-top:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;background:#F0FDF4;">
<h3 style="margin:0;color:#065F46;font-size:15px;">الاشتراكات السنوية (العضوية الجديدة)</h3>
</div>
<div style="padding:20px;">
<?php
$subsByYear = [];
foreach ($newMemberSubs as $ns) { $subsByYear[$ns['financial_year']][] = $ns; }
?>
<?php foreach ($subsByYear as $fy => $fyRows): ?>
<div style="margin-bottom:12px;">
<div style="font-size:13px;font-weight:600;color:#1F2937;margin-bottom:6px;"><?= e($fy) ?></div>
<table style="width:100%;font-size:13px;border-collapse:collapse;">
<?php foreach ($fyRows as $sr):
$personLabel = match($sr['person_type']) { 'member' => 'عضو', 'spouse' => 'زوجة', 'child' => 'ابن', 'temporary' => 'مؤقت', default => $sr['person_type'] };
$stColor = $sr['status'] === 'paid' ? '#059669' : '#D97706';
$stLabel = $sr['status'] === 'paid' ? 'مدفوع' : ($sr['status'] === 'pending' ? 'معلق' : $sr['status']);
?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:4px 8px;color:#6B7280;"><?= $personLabel ?></td>
<td style="padding:4px 8px;"><?= e($sr['person_name'] ?? '—') ?></td>
<td style="padding:4px 8px;"><?= money($sr['total_amount']) ?></td>
<td style="padding:4px 8px;color:<?= $stColor ?>;font-weight:600;"><?= $stLabel ?></td>
</tr>
<?php endforeach; ?>
</table>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- Audit Trail (displayed when case is completed) -->
<?php
$notesData = !empty($case['notes']) ? json_decode($case['notes'], true) : [];
......
......@@ -43,6 +43,11 @@ class SubscriptionController extends Controller
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود');
// Auto-fill missing subscription rows for active dependents in current FY
if ($member['status'] === 'active') {
\App\Modules\Subscriptions\Services\SubscriptionSyncService::refreshForMember((int) $memberId);
}
$lateFine = OverdueFineApplicator::applyForMember((int) $memberId);
$subscriptions = Subscription::getForMember((int) $memberId);
......
......@@ -74,7 +74,7 @@ class TransferController extends Controller
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب اختيار الابن/الابنة');
}
// Age validation: male dependents under separation age cannot self-separate
// Age validation: male dependents under separation age can only separate if employed+graduated
// 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]);
......@@ -84,12 +84,16 @@ class TransferController extends Controller
$age = (int) $now->diff($dob)->y;
$separationAge = (int) (RuleEngine::getValue('CHILD_MANDATORY_SEPARATION_AGE', 'value') ?? 25);
if ($age < $separationAge) {
$isEmployedPost = $request->post('is_employed', '');
$graduationDate = trim($request->post('graduation_date', '')) ?: null;
if ($isEmployedPost === '' || !(bool)(int)$isEmployedPost || !$graduationDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError(
'لا يمكن فصل الملحق تحت سن ' . $separationAge . ' سنة. العمر الحالي: ' . $age . ' سنة. يتم الفصل الوجوبي عند بلوغ ' . $separationAge . ' سنة.'
'لا يمكن فصل الملحق تحت سن ' . $separationAge . ' سنة إلا إذا كان متخرجاً وموظفاً بأجر. العمر الحالي: ' . $age . ' سنة.'
);
}
}
}
}
// Fetch child record if needed (may have been loaded in age-validation block above)
$child = isset($child) ? $child : ($childId ? $db->selectOne("SELECT * FROM children WHERE id = ?", [$childId]) : null);
......@@ -111,6 +115,13 @@ class TransferController extends Controller
if (!$marriageDate) {
return $this->redirect("/transfers/create/{$memberId}")->withError('يجب إدخال تاريخ الزواج لفصل الإناث');
}
$memberRecord = $db->selectOne("SELECT created_at, activated_at FROM members WHERE id = ?", [(int) $memberId]);
$membershipStart = $memberRecord['activated_at'] ?? $memberRecord['created_at'] ?? null;
if ($membershipStart && $marriageDate < substr($membershipStart, 0, 10)) {
return $this->redirect("/transfers/create/{$memberId}")->withError(
'تاريخ الزواج (' . $marriageDate . ') لا يمكن أن يسبق تاريخ إنشاء العضوية (' . substr($membershipStart, 0, 10) . '). يرجى مراجعة البيانات.'
);
}
$effectiveTransferDate = $marriageDate;
} else {
// Male path: work + graduation + date_turned_25
......
......@@ -423,26 +423,15 @@
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') {
// 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;
// All children are eligible: males >= 25 (age), males < 25 (work/graduation), females (marriage)
opt.style.display = '';
opt.disabled = false;
} else {
opt.style.display = '';
opt.disabled = false;
}
});
if (type === 'child_separation') {
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';
}
}
fetchFeePreview();
......
......@@ -171,6 +171,15 @@ class WaiverController extends Controller
// Already-saved individual fees
$individualFees = WaiverProcessor::getIndividualFees((int) $id);
// Payment request tracking
$paymentRequest = null;
if (!empty($waiver['target_member_id'])) {
$paymentRequest = $db->selectOne(
"SELECT id, request_number, status, amount, created_at FROM payment_requests WHERE related_entity_type = 'waiver_requests' AND related_entity_id = ? AND is_voided = 0 ORDER BY id DESC LIMIT 1",
[(int) $id]
);
}
return $this->view('Waiver.Views.show', [
'waiver' => $waiver,
'source_deps' => $sourceDeps,
......@@ -183,6 +192,7 @@ class WaiverController extends Controller
'target_debt_check' => $targetDebtCheck,
'all_target_dependents' => $allTargetDependents,
'individual_fees' => $individualFees,
'paymentRequest' => $paymentRequest,
]);
}
......
......@@ -6,16 +6,19 @@
</div>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="card"><div class="table-responsive"><table class="data-table"><thead><tr><th>#</th><th>العضو المتنازل</th><th>رقم العضوية</th><th>قيمة العضوية</th><th>نسبة التنازل</th><th>رسوم التنازل</th><th>الحالة</th><th>الإجراءات</th></tr></thead><tbody>
<?php foreach ($rows as $r): ?>
<div class="card"><div class="table-responsive"><table class="data-table"><thead><tr><th>#</th><th>المتنازل</th><th>المتنازل إليه</th><th>رقم العضوية</th><th>رسوم التنازل</th><th>التاريخ</th><th>الحالة</th><th>الإجراءات</th></tr></thead><tbody>
<?php foreach ($rows as $r):
$wStatusLabel = match($r['status'] ?? '') { 'requested' => 'مقدم', 'approved' => 'معتمد', 'fee_paid' => 'تم الدفع', 'completed' => 'مكتمل', 'rejected' => 'مرفوض', default => $r['status'] ?? '' };
$wStatusColor = match($r['status'] ?? '') { 'completed' => '#059669', 'approved' => '#0284C7', 'fee_paid' => '#0D7377', 'rejected' => '#DC2626', default => '#D97706' };
?>
<tr>
<td><?= (int) $r['id'] ?></td>
<td><a href="/members/<?= (int) $r['source_member_id'] ?>" style="color:#0D7377;font-weight:600;"><?= e($r['source_name'] ?? '') ?></a></td>
<td><?php if (!empty($r['target_name'])): ?><a href="/members/<?= (int) ($r['target_member_id'] ?? 0) ?>" style="color:#1D4ED8;font-weight:600;"><?= e($r['target_name']) ?></a><?php else: ?><?php endif; ?></td>
<td style="font-weight:600;"><?= e($r['membership_number'] ?? '—') ?></td>
<td><?= money($r['membership_value_at_waiver'] ?? '0') ?></td>
<td><?= e($r['waiver_fee_percentage'] ?? '30') ?>%</td>
<td style="font-weight:700;color:#0D7377;"><?= money($r['waiver_fee_amount'] ?? '0') ?></td>
<td><span style="color:<?= match($r['status'] ?? '') { 'completed' => '#059669', 'approved' => '#0284C7', default => '#D97706' } ?>;font-weight:600;"><?= match($r['status'] ?? '') { 'requested' => 'مقدم', 'approved' => 'معتمد', 'completed' => 'مكتمل', default => $r['status'] ?? '' } ?></span></td>
<td style="font-size:12px;color:#6B7280;"><?= e(substr($r['created_at'] ?? '', 0, 10)) ?></td>
<td><span style="color:<?= $wStatusColor ?>;font-weight:600;"><?= $wStatusLabel ?></span></td>
<td><a href="/waivers/<?= (int) $r['id'] ?>" class="btn btn-sm btn-outline">عرض</a></td>
</tr>
<?php endforeach; ?>
......
......@@ -415,6 +415,21 @@ $statusIcon = match($waiver['status']) { 'requested' => '⏳', 'approved' => '
</div>
<?php endif; ?>
<!-- Payment Request Tracking -->
<?php if (!empty($paymentRequest)): ?>
<div class="card" style="padding:16px 20px;margin-bottom:20px;background:<?= $paymentRequest['status'] === 'completed' ? '#F0FDF4' : '#FFFBEB' ?>;border:1px solid <?= $paymentRequest['status'] === 'completed' ? '#86EFAC' : '#FDE68A' ?>;">
<div style="display:flex;align-items:center;gap:10px;font-size:14px;">
<span style="font-size:18px;"><?= $paymentRequest['status'] === 'completed' ? '✅' : '⏳' ?></span>
<div>
<strong>طلب الدفع <?= e($paymentRequest['request_number'] ?? '#' . $paymentRequest['id']) ?></strong>
— المبلغ: <strong><?= money($paymentRequest['amount']) ?></strong>
— الحالة: <strong style="color:<?= $paymentRequest['status'] === 'completed' ? '#059669' : '#D97706' ?>;"><?= match($paymentRequest['status']) { 'pending' => 'معلق بالخزينة', 'processing' => 'قيد التحصيل', 'completed' => 'مدفوع', default => $paymentRequest['status'] } ?></strong>
<span style="font-size:12px;color:#6B7280;margin-right:8px;">(<?= e(substr($paymentRequest['created_at'], 0, 10)) ?>)</span>
</div>
</div>
</div>
<?php endif; ?>
<!-- SECTION 7: Board Approval (per-individual fee assignment — shows ALL dependents) -->
<?php if ($waiver['status'] === 'requested' && can('waiver.approve')): ?>
<div class="card" style="padding:20px;margin-bottom:20px;background:#EFF6FF;border:2px solid #3B82F6;">
......
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