Commit 4b76c4b3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Enhance billing display with full calculation breakdowns for all membership types

- Foreign bill: branch-specific fee lookup, exchange rate conversion, EGP display
- Seasonal bill: duration, nationality, dates, base amount, discounts, VAT, family members
- Sports bill: improved breakdown with separator and total line
- Form fee: add in_queue status to bill item
- Controller: type-specific payment types, validation whitelist, descriptions
- View: conditional installment option, working-only family links, type-specific labels
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 39382feb
...@@ -217,7 +217,10 @@ class MemberController extends Controller ...@@ -217,7 +217,10 @@ class MemberController extends Controller
\App\Modules\Members\Services\MembershipPaymentGuard::reconcile((int) $id); \App\Modules\Members\Services\MembershipPaymentGuard::reconcile((int) $id);
$bill = BillingService::getMemberBill((int) $id); $bill = BillingService::getMemberBill((int) $id);
$formFilled = ($member->qualification_id !== null && $member->qualification_id > 0); $membershipType = $member->membership_type ?? 'working';
$formFilled = ($membershipType !== 'working')
? true
: ($member->qualification_id !== null && $member->qualification_id > 0);
$specialDiscount = null; $specialDiscount = null;
if ($member->special_discount_id) { if ($member->special_discount_id) {
...@@ -357,6 +360,11 @@ class MemberController extends Controller ...@@ -357,6 +360,11 @@ class MemberController extends Controller
$paymentType = trim((string) $request->post('payment_type', '')); $paymentType = trim((string) $request->post('payment_type', ''));
$amount = trim((string) $request->post('amount', '0')); $amount = trim((string) $request->post('amount', '0'));
$validPaymentTypes = ['membership_fee', 'down_payment', 'foreign_membership_fee', 'sports_membership_fee', 'seasonal_fee'];
if (!in_array($paymentType, $validPaymentTypes, true)) {
return $this->redirect('/members/' . $id)->withError('نوع الدفع غير صالح');
}
if (bccomp($amount, '0.01', 2) < 0) return $this->redirect('/members/' . $id)->withError('المبلغ غير صالح'); if (bccomp($amount, '0.01', 2) < 0) return $this->redirect('/members/' . $id)->withError('المبلغ غير صالح');
$months = ($paymentType === 'down_payment') ? min(30, max(1, (int) $request->post('installment_months', 30))) : null; $months = ($paymentType === 'down_payment') ? min(30, max(1, (int) $request->post('installment_months', 30))) : null;
...@@ -387,13 +395,22 @@ class MemberController extends Controller ...@@ -387,13 +395,22 @@ class MemberController extends Controller
$notesData = ['fee_breakdown' => $breakdown]; $notesData = ['fee_breakdown' => $breakdown];
if ($months) $notesData['installment_months'] = $months; if ($months) $notesData['installment_months'] = $months;
$typeDescriptions = [
'down_payment' => 'مقدم تقسيط',
'membership_fee' => 'قيمة العضوية',
'foreign_membership_fee' => 'رسوم عضوية أجنبية',
'sports_membership_fee' => 'رسوم عضوية رياضية',
'seasonal_fee' => 'رسوم عضوية موسمية',
];
$descriptionAr = ($typeDescriptions[$paymentType] ?? 'قيمة العضوية') . ' — استمارة ' . ($member->form_number ?? '');
$result = PaymentRequestService::createRequest([ $result = PaymentRequestService::createRequest([
'member_id' => (int) $id, 'member_id' => (int) $id,
'amount' => $amount, 'amount' => $amount,
'payment_type' => $paymentType, 'payment_type' => $paymentType,
'related_entity_type' => 'members', 'related_entity_type' => 'members',
'related_entity_id' => (int) $id, 'related_entity_id' => (int) $id,
'description_ar' => ($paymentType === 'down_payment' ? 'مقدم تقسيط' : 'قيمة العضوية') . ' — استمارة ' . ($member->form_number ?? ''), 'description_ar' => $descriptionAr,
'notes' => json_encode($notesData, JSON_UNESCAPED_UNICODE), 'notes' => json_encode($notesData, JSON_UNESCAPED_UNICODE),
]); ]);
if (!$result['success']) return $this->redirect('/members/' . $id)->withError($result['error']); if (!$result['success']) return $this->redirect('/members/' . $id)->withError($result['error']);
...@@ -416,7 +433,7 @@ class MemberController extends Controller ...@@ -416,7 +433,7 @@ class MemberController extends Controller
// Block if a combined membership payment is already pending // Block if a combined membership payment is already pending
$hasCombined = $db->selectOne( $hasCombined = $db->selectOne(
"SELECT id FROM payment_requests WHERE member_id = ? AND payment_type IN ('membership_fee','down_payment') AND status IN ('pending','processing') AND is_voided = 0 LIMIT 1", "SELECT id FROM payment_requests WHERE member_id = ? AND payment_type IN ('membership_fee','down_payment','foreign_membership_fee','sports_membership_fee','seasonal_fee') AND status IN ('pending','processing') AND is_voided = 0 LIMIT 1",
[(int) $id] [(int) $id]
); );
if ($hasCombined) { if ($hasCombined) {
......
...@@ -83,18 +83,20 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -83,18 +83,20 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<div class="card" style="margin-bottom:20px;padding:25px;background:#F0FDF4;border:2px solid #059669;"> <div class="card" style="margin-bottom:20px;padding:25px;background:#F0FDF4;border:2px solid #059669;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div> <div>
<h3 style="color:#059669;margin:0 0 8px;">✅ تم دفع رسوم الاستمارة — الآن قم بملء البيانات وإضافة الأسرة</h3> <h3 style="color:#059669;margin:0 0 8px;">✅ تم دفع رسوم الاستمارة — الآن قم بملء البيانات<?= ($member->membership_type ?? 'working') === 'working' ? ' وإضافة الأسرة' : '' ?></h3>
<p style="color:#6B7280;margin:0;font-size:14px;">استمارة رقم <strong><?= e($member->form_number) ?></strong> — يمكنك إضافة أفراد الأسرة الآن بدون رسوم إضافية</p> <p style="color:#6B7280;margin:0;font-size:14px;">استمارة رقم <strong><?= e($member->form_number) ?></strong><?= ($member->membership_type ?? 'working') === 'working' ? ' — يمكنك إضافة أفراد الأسرة الآن بدون رسوم إضافية' : '' ?></p>
</div> </div>
<?php if (can('member.fill_form')): ?> <?php if (can('member.fill_form')): ?>
<a href="/members/<?= (int) $member->id ?>/fill-form" class="btn btn-primary" style="padding:15px 30px;font-size:18px;">📝 ملء الاستمارة</a> <a href="/members/<?= (int) $member->id ?>/fill-form" class="btn btn-primary" style="padding:15px 30px;font-size:18px;">📝 ملء الاستمارة</a>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if (($member->membership_type ?? 'working') === 'working'): ?>
<div style="display:flex;gap:8px;flex-wrap:wrap;padding-top:12px;border-top:1px solid #A7F3D0;"> <div style="display:flex;gap:8px;flex-wrap:wrap;padding-top:12px;border-top:1px solid #A7F3D0;">
<a href="/members/<?= (int) $member->id ?>/spouses/create" class="btn btn-sm btn-outline">💍 إضافة زوج/ة</a> <a href="/members/<?= (int) $member->id ?>/spouses/create" class="btn btn-sm btn-outline">💍 إضافة زوج/ة</a>
<a href="/members/<?= (int) $member->id ?>/children/create" class="btn btn-sm btn-outline">👶 إضافة ابن/ة</a> <a href="/members/<?= (int) $member->id ?>/children/create" class="btn btn-sm btn-outline">👶 إضافة ابن/ة</a>
<a href="/members/<?= (int) $member->id ?>/temporary/create" class="btn btn-sm btn-outline">👤 إضافة عضو مؤقت</a> <a href="/members/<?= (int) $member->id ?>/temporary/create" class="btn btn-sm btn-outline">👤 إضافة عضو مؤقت</a>
</div> </div>
<?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
...@@ -228,13 +230,15 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -228,13 +230,15 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
</table> </table>
</div> </div>
<!-- Add Family Before Paying --> <!-- Add Family Before Paying (working type only) -->
<?php if (($member->membership_type ?? 'working') === 'working'): ?>
<div style="padding:0 20px 15px;display:flex;gap:10px;flex-wrap:wrap;border-top:1px solid #E5E7EB;padding-top:15px;"> <div style="padding:0 20px 15px;display:flex;gap:10px;flex-wrap:wrap;border-top:1px solid #E5E7EB;padding-top:15px;">
<span style="color:#6B7280;font-size:13px;padding:8px 0;">أضف أفراد الأسرة (بدون رسوم استمارة إضافية):</span> <span style="color:#6B7280;font-size:13px;padding:8px 0;">أضف أفراد الأسرة (بدون رسوم استمارة إضافية):</span>
<a href="/members/<?= (int) $member->id ?>/spouses/create" class="btn btn-sm btn-outline">💍 إضافة زوج/ة</a> <a href="/members/<?= (int) $member->id ?>/spouses/create" class="btn btn-sm btn-outline">💍 إضافة زوج/ة</a>
<a href="/members/<?= (int) $member->id ?>/children/create" class="btn btn-sm btn-outline">👶 إضافة ابن/ابنة</a> <a href="/members/<?= (int) $member->id ?>/children/create" class="btn btn-sm btn-outline">👶 إضافة ابن/ابنة</a>
<a href="/members/<?= (int) $member->id ?>/temporary/create" class="btn btn-sm btn-outline">👤 عضو مؤقت</a> <a href="/members/<?= (int) $member->id ?>/temporary/create" class="btn btn-sm btn-outline">👤 عضو مؤقت</a>
</div> </div>
<?php endif; ?>
<!-- Special Discount Section --> <!-- Special Discount Section -->
<?php if (!empty($availableDiscounts) && in_array($member->status, ['accepted', 'payment_pending']) && empty($pendingMembership)): ?> <?php if (!empty($availableDiscounts) && in_array($member->status, ['accepted', 'payment_pending']) && empty($pendingMembership)): ?>
...@@ -302,9 +306,18 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -302,9 +306,18 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php else: ?> <?php else: ?>
<?php
$paymentTypeForType = match ($member->membership_type ?? 'working') {
'foreign' => 'foreign_membership_fee',
'sports' => 'sports_membership_fee',
'seasonal' => 'seasonal_fee',
default => 'membership_fee',
};
$allowInstallment = in_array($member->membership_type ?? 'working', ['working', 'sports'], true);
?>
<div style="padding:20px;background:#FFF7ED;border-top:2px solid #F59E0B;"> <div style="padding:20px;background:#FFF7ED;border-top:2px solid #F59E0B;">
<h4 style="margin:0 0 15px;color:#D97706;">&#x1f4b0; اختر طريقة السداد وأرسل للخزينة</h4> <h4 style="margin:0 0 15px;color:#D97706;">&#x1f4b0; اختر طريقة السداد وأرسل للخزينة</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;"> <div style="display:grid;grid-template-columns:<?= $allowInstallment ? '1fr 1fr' : '1fr' ?>;gap:20px;">
<!-- Cash Full --> <!-- Cash Full -->
<div style="background:#fff;border:2px solid #059669;border-radius:12px;padding:20px;"> <div style="background:#fff;border:2px solid #059669;border-radius:12px;padding:20px;">
<h5 style="margin:0 0 10px;color:#059669;">&#x1f4b5; كاش كامل</h5> <h5 style="margin:0 0 10px;color:#059669;">&#x1f4b5; كاش كامل</h5>
...@@ -312,11 +325,12 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -312,11 +325,12 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<div style="font-size:24px;font-weight:700;color:#059669;margin-bottom:15px;"><?= money($bill['total_pending']) ?></div> <div style="font-size:24px;font-weight:700;color:#059669;margin-bottom:15px;"><?= money($bill['total_pending']) ?></div>
<form method="POST" action="/members/<?= (int) $member->id ?>/pay-membership"> <form method="POST" action="/members/<?= (int) $member->id ?>/pay-membership">
<?= csrf_field() ?> <?= csrf_field() ?>
<input type="hidden" name="payment_type" value="membership_fee"> <input type="hidden" name="payment_type" value="<?= e($paymentTypeForType) ?>">
<input type="hidden" name="amount" value="<?= e($bill['total_pending']) ?>"> <input type="hidden" name="amount" value="<?= e($bill['total_pending']) ?>">
<button type="submit" class="btn btn-primary" style="width:100%;background:#D97706;border-color:#D97706;" onclick="return confirm('إرسال طلب دفع <?= money($bill['total_pending']) ?> للخزينة؟')">&#x1f4e4; إرسال للخزينة</button> <button type="submit" class="btn btn-primary" style="width:100%;background:#D97706;border-color:#D97706;" onclick="return confirm('إرسال طلب دفع <?= money($bill['total_pending']) ?> للخزينة؟')">&#x1f4e4; إرسال للخزينة</button>
</form> </form>
</div> </div>
<?php if ($allowInstallment): ?>
<!-- Installment --> <!-- Installment -->
<div style="background:#fff;border:2px solid #0284C7;border-radius:12px;padding:20px;"> <div style="background:#fff;border:2px solid #0284C7;border-radius:12px;padding:20px;">
<h5 style="margin:0 0 10px;color:#0284C7;">&#x1f4c5; تقسيط</h5> <h5 style="margin:0 0 10px;color:#0284C7;">&#x1f4c5; تقسيط</h5>
...@@ -348,6 +362,7 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -348,6 +362,7 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<button type="submit" class="btn btn-primary" style="width:100%;background:#D97706;border-color:#D97706;" onclick="return confirm('إرسال طلب تقسيط للخزينة؟')">&#x1f4e4; إرسال للخزينة</button> <button type="submit" class="btn btn-primary" style="width:100%;background:#D97706;border-color:#D97706;" onclick="return confirm('إرسال طلب تقسيط للخزينة؟')">&#x1f4e4; إرسال للخزينة</button>
</form> </form>
</div> </div>
<?php endif; ?>
</div> </div>
<div style="margin-top:15px;padding:10px;background:#FEF2F2;border-radius:8px;font-size:12px;color:#DC2626;"> <div style="margin-top:15px;padding:10px;background:#FEF2F2;border-radius:8px;font-size:12px;color:#DC2626;">
&#x26a0;&#xfe0f; مهلة السداد: 15 يوم من تاريخ القبول — بعدها تنتهي الاستمارة &#x26a0;&#xfe0f; مهلة السداد: 15 يوم من تاريخ القبول — بعدها تنتهي الاستمارة
...@@ -357,7 +372,9 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false)); ...@@ -357,7 +372,9 @@ $canEdit = can('member.edit') && (!$isLocked || ($isSuperAdmin ?? false));
<?php elseif ($formFilled && in_array($member->status, ['under_review', 'interview_scheduled'])): ?> <?php elseif ($formFilled && in_array($member->status, ['under_review', 'interview_scheduled'])): ?>
<div style="padding:20px;background:#EFF6FF;border-top:2px solid #0284C7;"> <div style="padding:20px;background:#EFF6FF;border-top:2px solid #0284C7;">
<p style="margin:0;color:#0284C7;font-size:14px;">📋 الفاتورة جاهزة — في انتظار قرار مجلس الأمناء قبل السداد</p> <p style="margin:0;color:#0284C7;font-size:14px;">📋 الفاتورة جاهزة — في انتظار قرار مجلس الأمناء قبل السداد</p>
<?php if (($member->membership_type ?? 'working') === 'working'): ?>
<p style="margin:5px 0 0;color:#6B7280;font-size:13px;">يمكنك إضافة أفراد الأسرة الآن وستُضاف رسومهم تلقائياً</p> <p style="margin:5px 0 0;color:#6B7280;font-size:13px;">يمكنك إضافة أفراد الأسرة الآن وستُضاف رسومهم تلقائياً</p>
<?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
...@@ -492,16 +509,30 @@ if ($bill['form_fee_paid'] && !$formFilled) { ...@@ -492,16 +509,30 @@ if ($bill['form_fee_paid'] && !$formFilled) {
$missingSteps[] = ['icon' => '&#x2705;', 'text' => 'الاستمارة مكتملة', 'color' => '#059669', 'done' => true]; $missingSteps[] = ['icon' => '&#x2705;', 'text' => 'الاستمارة مكتملة', 'color' => '#059669', 'done' => true];
} }
if ($formFilled && !$bill['membership_paid'] && !in_array($member->status, ['active'], true)) { $membershipFeeLabel = match ($member->membership_type ?? 'working') {
'foreign' => 'رسوم العضوية الأجنبية',
'sports' => 'رسوم العضوية الرياضية',
'seasonal' => 'رسوم العضوية الموسمية',
'honorary' => 'تفعيل العضوية الشرفية',
default => 'رسوم العضوية',
};
if (($member->membership_type ?? 'working') === 'honorary') {
if ($member->status !== 'active') {
$missingSteps[] = ['icon' => '&#x1f4cb;', 'text' => 'في انتظار تسجيل بيانات العضوية الشرفية والتفعيل', 'color' => '#3B82F6', 'done' => false];
} else {
$missingSteps[] = ['icon' => '&#x2705;', 'text' => $membershipFeeLabel, 'color' => '#059669', 'done' => true];
}
} elseif ($formFilled && !$bill['membership_paid'] && !in_array($member->status, ['active'], true)) {
if (!empty($bill['membership_pending']) || !empty($pendingMembership)) { if (!empty($bill['membership_pending']) || !empty($pendingMembership)) {
$missingSteps[] = ['icon' => '&#x1f4b3;', 'text' => 'رسوم العضوية في انتظار الخزينة', 'color' => '#D97706', 'done' => false]; $missingSteps[] = ['icon' => '&#x1f4b3;', 'text' => $membershipFeeLabel . ' في انتظار الخزينة', 'color' => '#D97706', 'done' => false];
} elseif (in_array($member->status, ['accepted', 'payment_pending'], true)) { } elseif (in_array($member->status, ['accepted', 'payment_pending'], true)) {
$missingSteps[] = ['icon' => '&#x23f3;', 'text' => 'لم يتم سداد رسوم العضوية', 'color' => '#DC2626', 'done' => false]; $missingSteps[] = ['icon' => '&#x23f3;', 'text' => 'لم يتم سداد ' . $membershipFeeLabel, 'color' => '#DC2626', 'done' => false];
} elseif (in_array($member->status, ['under_review', 'interview_scheduled'], true)) { } elseif (in_array($member->status, ['under_review', 'interview_scheduled'], true)) {
$missingSteps[] = ['icon' => '&#x1f4cb;', 'text' => 'في انتظار قرار مجلس الأمناء', 'color' => '#3B82F6', 'done' => false]; $missingSteps[] = ['icon' => '&#x1f4cb;', 'text' => 'في انتظار قرار مجلس الأمناء', 'color' => '#3B82F6', 'done' => false];
} }
} elseif ($bill['membership_paid'] || $member->status === 'active') { } elseif ($bill['membership_paid'] || $member->status === 'active') {
$missingSteps[] = ['icon' => '&#x2705;', 'text' => 'رسوم العضوية', 'color' => '#059669', 'done' => true]; $missingSteps[] = ['icon' => '&#x2705;', 'text' => $membershipFeeLabel, 'color' => '#059669', 'done' => true];
} }
foreach ($bill['items'] as $item) { foreach ($bill['items'] as $item) {
......
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