Commit e137cd61 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent 669116e2
......@@ -92,6 +92,33 @@ class CashierController extends Controller
return $this->redirect('/cashier/' . $id)->withError($result['error']);
}
$returnTo = trim((string) $request->post('return_to', ''));
if ($returnTo !== '' && str_starts_with($returnTo, '/members/')) {
return $this->redirect($returnTo)->withSuccess('تم إلغاء طلب الدفع');
}
return $this->redirect('/cashier')->withSuccess('تم إلغاء طلب الدفع');
}
public function requeue(Request $request, string $id): Response
{
$this->authorize('cashier.cancel_request');
$result = PaymentRequestService::requeueRequest((int) $id);
if (!$result['success']) {
$returnTo = trim((string) $request->post('return_to', ''));
if ($returnTo !== '' && str_starts_with($returnTo, '/members/')) {
return $this->redirect($returnTo)->withError($result['error']);
}
return $this->redirect('/cashier')->withError($result['error']);
}
$returnTo = trim((string) $request->post('return_to', ''));
if ($returnTo !== '' && str_starts_with($returnTo, '/members/')) {
return $this->redirect($returnTo)->withSuccess('تم إعادة الطلب لطابور الدفع — رقم: ' . $result['request_number']);
}
return $this->redirect('/cashier')->withSuccess('تم إعادة الطلب لطابور الدفع — رقم: ' . $result['request_number']);
}
}
......@@ -6,4 +6,5 @@ return [
['GET', '/cashier/{id}', 'Cashier\Controllers\CashierController@process', ['auth'], 'cashier.process_payment'],
['POST', '/cashier/{id}/complete', 'Cashier\Controllers\CashierController@complete', ['auth', 'csrf'], 'cashier.process_payment'],
['POST', '/cashier/{id}/cancel', 'Cashier\Controllers\CashierController@cancel', ['auth', 'csrf'], 'cashier.cancel_request'],
['POST', '/cashier/{id}/requeue', 'Cashier\Controllers\CashierController@requeue', ['auth', 'csrf'], 'cashier.cancel_request'],
];
......@@ -177,7 +177,7 @@ final class PaymentRequestService
$now = date('Y-m-d H:i:s');
// If completed, also void the linked payment
// If completed, also void the linked payment — this IS a true void
if ($request['status'] === 'completed' && !empty($request['payment_id'])) {
$paymentResult = \App\Modules\Payments\Services\PaymentService::voidPayment(
(int) $request['payment_id'],
......@@ -186,17 +186,26 @@ final class PaymentRequestService
if (!$paymentResult['success']) {
return ['success' => false, 'error' => 'فشل إلغاء الدفعة المرتبطة: ' . ($paymentResult['error'] ?? '')];
}
// Completed+voided: hide permanently
$db->update('payment_requests', [
'status' => 'cancelled',
'is_voided' => 1,
'voided_at' => $now,
'voided_by' => $employee ? (int) $employee->id : null,
'void_reason' => $reason,
'updated_at' => $now,
], '`id` = ?', [$requestId]);
} else {
// Pending/processing cancellation: keep visible for re-queue
$db->update('payment_requests', [
'status' => 'cancelled',
'voided_at' => $now,
'voided_by' => $employee ? (int) $employee->id : null,
'void_reason' => $reason,
'updated_at' => $now,
], '`id` = ?', [$requestId]);
}
$db->update('payment_requests', [
'status' => 'cancelled',
'is_voided' => 1,
'voided_at' => $now,
'voided_by' => $employee ? (int) $employee->id : null,
'void_reason' => $reason,
'updated_at' => $now,
], '`id` = ?', [$requestId]);
EventBus::dispatch('payment_request.cancelled', [
'request_id' => $requestId,
'member_id' => (int) $request['member_id'],
......@@ -208,6 +217,50 @@ final class PaymentRequestService
return ['success' => true];
}
public static function requeueRequest(int $requestId): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$request = $db->selectOne(
"SELECT * FROM payment_requests WHERE id = ? AND status = 'cancelled' AND is_voided = 0",
[$requestId]
);
if (!$request) {
return ['success' => false, 'error' => 'طلب الدفع غير موجود أو لا يمكن إعادته'];
}
// Check no duplicate pending request exists for the same entity
if ($request['related_entity_type'] && $request['related_entity_id']) {
$existing = $db->selectOne(
"SELECT id FROM payment_requests WHERE member_id = ? AND payment_type = ? AND related_entity_type = ? AND related_entity_id = ? AND status IN ('pending','processing') AND is_voided = 0 AND id != ?",
[(int) $request['member_id'], $request['payment_type'], $request['related_entity_type'], (int) $request['related_entity_id'], $requestId]
);
} else {
$existing = $db->selectOne(
"SELECT id FROM payment_requests WHERE member_id = ? AND payment_type = ? AND status IN ('pending','processing') AND is_voided = 0 AND id != ?",
[(int) $request['member_id'], $request['payment_type'], $requestId]
);
}
if ($existing) {
return ['success' => false, 'error' => 'يوجد طلب دفع معلق بالفعل لنفس البند'];
}
$now = date('Y-m-d H:i:s');
$db->update('payment_requests', [
'status' => 'pending',
'voided_at' => null,
'voided_by' => null,
'void_reason' => null,
'updated_at' => $now,
], '`id` = ?', [$requestId]);
Logger::info("Payment request re-queued", ['id' => $requestId, 'by' => $employee ? (int) $employee->id : 0]);
return ['success' => true, 'request_number' => $request['request_number']];
}
public static function getPendingQueue(?int $branchId = null, array $filters = []): array
{
$db = App::getInstance()->db();
......
......@@ -193,6 +193,7 @@ class MemberController extends Controller
$pendingFormFee = null;
$pendingMembership = null;
$pendingAdditions = [];
$cancelledRequests = [];
foreach ($pendingRequests as $pr) {
if ($pr['status'] === 'pending' || $pr['status'] === 'processing') {
if ($pr['payment_type'] === 'form_fee') $pendingFormFee = $pr;
......@@ -200,6 +201,8 @@ class MemberController extends Controller
if ($pr['payment_type'] === 'addition_fee' && $pr['related_entity_type'] && $pr['related_entity_id']) {
$pendingAdditions[$pr['related_entity_type'] . ':' . $pr['related_entity_id']] = $pr;
}
} elseif ($pr['status'] === 'cancelled' && empty($pr['is_voided'])) {
$cancelledRequests[] = $pr;
}
}
......@@ -228,6 +231,7 @@ class MemberController extends Controller
'pendingFormFee' => $pendingFormFee,
'pendingMembership' => $pendingMembership,
'pendingAdditions' => $pendingAdditions,
'cancelledRequests' => $cancelledRequests,
'isSuperAdmin' => self::isSuperAdmin(),
'divorceTransfer' => $divorceTransfer,
]);
......
......@@ -371,6 +371,51 @@ $canEdit = !$isLocked || ($isSuperAdmin ?? false);
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════ -->
<!-- CANCELLED REQUESTS — can be re-queued -->
<!-- ═══════════════════════════════════════════════ -->
<?php $cancelledRequests ??= []; ?>
<?php if (!empty($cancelledRequests)): ?>
<div class="card" style="margin-bottom:20px;border:2px solid #DC2626;">
<div style="padding:15px 20px;background:#FEF2F2;border-bottom:1px solid #FECACA;">
<h3 style="margin:0;color:#DC2626;font-size:16px;">&#x274c; طلبات ملغاة (يمكن إعادتها لطابور الدفع)</h3>
</div>
<div style="padding:20px;">
<table style="width:100%;font-size:14px;border-collapse:collapse;">
<thead>
<tr style="border-bottom:2px solid #E5E7EB;">
<th style="padding:10px;text-align:right;color:#6B7280;">البند</th>
<th style="padding:10px;text-align:left;color:#6B7280;width:120px;">المبلغ</th>
<th style="padding:10px;text-align:center;color:#6B7280;width:150px;">رقم الطلب</th>
<th style="padding:10px;text-align:right;color:#6B7280;width:180px;">سبب الإلغاء</th>
<th style="padding:10px;text-align:center;color:#6B7280;width:130px;">إجراء</th>
</tr>
</thead>
<tbody>
<?php foreach ($cancelledRequests as $cr): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:10px;">
<?= e($cr['description_ar'] ?? \App\Modules\Cashier\Services\PaymentRequestService::getPaymentTypeLabel($cr['payment_type'])) ?>
<small style="display:block;color:#DC2626;font-size:11px;">ملغى <?= e(substr($cr['voided_at'] ?? $cr['updated_at'], 0, 16)) ?></small>
</td>
<td style="padding:10px;text-align:left;font-weight:700;direction:ltr;color:#6B7280;text-decoration:line-through;"><?= money($cr['amount']) ?></td>
<td style="padding:10px;text-align:center;direction:ltr;font-size:12px;color:#9CA3AF;"><?= e($cr['request_number']) ?></td>
<td style="padding:10px;font-size:12px;color:#6B7280;"><?= e($cr['void_reason'] ?? '—') ?></td>
<td style="padding:10px;text-align:center;">
<form method="POST" action="/cashier/<?= (int) $cr['id'] ?>/requeue" style="display:inline;">
<?= csrf_field() ?>
<input type="hidden" name="return_to" value="/members/<?= (int) $member->id ?>">
<button type="submit" class="btn btn-sm btn-primary" style="background:#D97706;border-color:#D97706;font-size:12px;" onclick="return confirm('إعادة هذا الطلب لطابور الدفع؟')">&#x1f504; إعادة للطابور</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- ═══════════════════════════════════════════════ -->
<!-- MISSING STEPS — shows what still needs to happen -->
<!-- ═══════════════════════════════════════════════ -->
......
......@@ -25,7 +25,10 @@ final class PaymentLifecycleService
{
$result = MembershipPaymentGuard::activateMember($memberId, $paymentId);
if ($result['success'] && empty($result['already_active'])) {
// Always activate included dependents on successful membership payment.
// The collective payment covers all pending dependents regardless of
// whether the member was just activated or was already active.
if ($result['success']) {
MembershipPaymentGuard::activateIncludedDependents($memberId, $paymentId);
}
......
......@@ -33,7 +33,9 @@ PermissionRegistry::register('payments', [
'payment.refund' => ['ar' => 'استرداد', 'en' => 'Refund Payment'],
]);
// When payment.completed fires: activate membership, assign number, transition workflow
// When payment.completed fires: handle workflow transition only.
// Member activation + dependent activation is handled by PaymentLifecycleService
// via the payment_request.completed event in Cashier/bootstrap.php.
EventBus::listen('payment.completed', function (array $data) {
try {
$db = App::getInstance()->db();
......@@ -45,59 +47,38 @@ EventBus::listen('payment.completed', function (array $data) {
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) return;
// Membership fee payment → activate member + assign number
// Workflow transition only — activation is done by MembershipPaymentGuard
if (in_array($paymentType, ['membership_fee', 'down_payment'])) {
if (in_array($member['status'], ['potential', 'payment_pending', 'accepted'])) {
// Assign membership number if not yet assigned
if (empty($member['membership_number'])) {
try {
$number = MemberNumberGenerator::assign($memberId);
Logger::info("Membership number {$number} assigned to member #{$memberId} after payment");
} catch (\Throwable $e) {
Logger::warning("Failed to assign membership number for member #{$memberId}: " . $e->getMessage());
}
}
// Update member status to active
$db->update('members', [
'status' => 'active',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]);
if ($member['workflow_instance_id']) {
try {
$instance = $db->selectOne(
"SELECT * FROM workflow_instances WHERE id = ? AND is_completed = 0",
[(int) $member['workflow_instance_id']]
);
if ($instance && $instance['current_state'] === 'payment_pending') {
$db->update('workflow_instances', [
'current_state' => 'active',
'state_entered_at' => date('Y-m-d H:i:s'),
'is_completed' => 1,
'completed_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $instance['id']]);
// Try workflow transition
if ($member['workflow_instance_id']) {
try {
$instance = $db->selectOne(
"SELECT * FROM workflow_instances WHERE id = ? AND is_completed = 0",
[(int) $member['workflow_instance_id']]
);
if ($instance && $instance['current_state'] === 'payment_pending') {
$db->update('workflow_instances', [
'current_state' => 'active',
'state_entered_at' => date('Y-m-d H:i:s'),
'is_completed' => 1,
'completed_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [(int) $instance['id']]);
$employee = App::getInstance()->currentEmployee();
$db->insert('workflow_transition_log', [
'workflow_instance_id' => (int) $instance['id'],
'from_state' => 'payment_pending',
'to_state' => 'active',
'transition_name' => 'payment_completed',
'triggered_by_employee_id' => $employee ? (int) $employee->id : null,
'trigger_type' => 'system',
'notes' => 'Auto-transitioned by payment completion',
'created_at' => date('Y-m-d H:i:s'),
]);
}
} catch (\Throwable $e) {
Logger::warning("Workflow transition failed for member #{$memberId}: " . $e->getMessage());
$employee = App::getInstance()->currentEmployee();
$db->insert('workflow_transition_log', [
'workflow_instance_id' => (int) $instance['id'],
'from_state' => 'payment_pending',
'to_state' => 'active',
'transition_name' => 'payment_completed',
'triggered_by_employee_id' => $employee ? (int) $employee->id : null,
'trigger_type' => 'system',
'notes' => 'Auto-transitioned by payment completion',
'created_at' => date('Y-m-d H:i:s'),
]);
}
} catch (\Throwable $e) {
Logger::warning("Workflow transition failed for member #{$memberId}: " . $e->getMessage());
}
Logger::info("Member #{$memberId} activated after payment", $data);
}
}
......
......@@ -229,25 +229,27 @@ class TutorialController extends Controller
'membership-pricing' => ['title' => 'تسعير العضوية', 'subtitle' => 'حساب قيمة العضوية حسب المؤهل', 'icon' => 'calculator', 'color' => '#F59E0B', 'category' => 'registration', 'order' => 4],
'pay-membership-cash' => ['title' => 'دفع العضوية كاش', 'subtitle' => 'سداد قيمة العضوية دفعة واحدة', 'icon' => 'banknote', 'color' => '#10B981', 'category' => 'financial', 'order' => 5],
'pay-membership-installments' => ['title' => 'دفع العضوية بالتقسيط', 'subtitle' => 'مقدم 25% وتقسيط حتى 30 شهر', 'icon' => 'calendar-range', 'color' => '#06B6D4', 'category' => 'financial', 'order' => 6],
'special-discounts' => ['title' => 'الخصومات الخاصة', 'subtitle' => 'تطبيق خصم خاص أو عرض مجلس الإدارة', 'icon' => 'badge-percent', 'color' => '#EC4899', 'category' => 'financial', 'order' => 7],
'add-spouse' => ['title' => 'إضافة زوج/زوجة', 'subtitle' => 'إضافة زوجة للعضوية مع حساب الرسوم', 'icon' => 'heart', 'color' => '#F97316', 'category' => 'family', 'order' => 8],
'add-child' => ['title' => 'إضافة أبناء', 'subtitle' => 'إضافة أبناء مع التصنيف التلقائي', 'icon' => 'baby', 'color' => '#F59E0B', 'category' => 'family', 'order' => 9],
'add-temporary-member' => ['title' => 'إضافة عضو مؤقت', 'subtitle' => 'والدين أو أشقاء أو مربية', 'icon' => 'user-cog', 'color' => '#6366F1', 'category' => 'family', 'order' => 10],
'children-aging' => ['title' => 'تقادم أعمار الأبناء', 'subtitle' => 'التجميد التلقائي والفصل عند بلوغ 25', 'icon' => 'clock', 'color' => '#DC2626', 'category' => 'family', 'order' => 11],
'interview-process' => ['title' => 'المقابلات', 'subtitle' => 'جدولة وإجراء مقابلة العضوية', 'icon' => 'message-square', 'color' => '#0EA5E9', 'category' => 'procedures', 'order' => 12],
'change-status' => ['title' => 'تغيير حالة العضوية', 'subtitle' => 'دورة الحالات من محتمل حتى فعال', 'icon' => 'refresh-cw', 'color' => '#8B5CF6', 'category' => 'procedures', 'order' => 13],
'annual-subscription' => ['title' => 'الاشتراك السنوي', 'subtitle' => 'توليد وتحصيل الاشتراكات السنوية', 'icon' => 'calendar-check', 'color' => '#059669', 'category' => 'financial', 'order' => 14],
'fines-violations' => ['title' => 'المخالفات والغرامات', 'subtitle' => 'فرض غرامة وتصعيد العقوبات', 'icon' => 'alert-triangle', 'color' => '#DC2626', 'category' => 'procedures', 'order' => 15],
'carnet-issuance' => ['title' => 'إصدار الكارنيه', 'subtitle' => 'شروط وإجراءات طباعة الكارنيه', 'icon' => 'id-card', 'color' => '#3B82F6', 'category' => 'procedures', 'order' => 16],
'transfer-separation' => ['title' => 'فصل الأبناء', 'subtitle' => 'فصل ابن/ابنة لعضوية مستقلة', 'icon' => 'git-branch', 'color' => '#F97316', 'category' => 'transfers', 'order' => 17],
'divorce-transfer' => ['title' => 'تحويل عند الطلاق', 'subtitle' => 'نقل العضوية بعد الطلاق', 'icon' => 'split', 'color' => '#DC2626', 'category' => 'transfers', 'order' => 18],
'death-transfer' => ['title' => 'تحويل عند الوفاة', 'subtitle' => 'نقل العضوية للزوج/الزوجة', 'icon' => 'heart-off', 'color' => '#6B7280', 'category' => 'transfers', 'order' => 19],
'waiver-process' => ['title' => 'التنازل عن العضوية', 'subtitle' => 'تنازل لشخص آخر مع رسوم 30%', 'icon' => 'arrow-right-left', 'color' => '#8B5CF6', 'category' => 'transfers', 'order' => 20],
'membership-freeze-drop' => ['title' => 'التجميد والإسقاط', 'subtitle' => 'تجميد العضوية أو إسقاطها', 'icon' => 'snowflake', 'color' => '#06B6D4', 'category' => 'transfers', 'order' => 21],
'honorary-membership' => ['title' => 'العضوية الشرفية', 'subtitle' => 'عضوية بدون رسوم بقرار مجلس', 'icon' => 'award', 'color' => '#F59E0B', 'category' => 'types', 'order' => 22],
'foreign-membership' => ['title' => 'العضوية الأجنبية', 'subtitle' => 'عضوية لغير المصريين بـ 10,000$', 'icon' => 'globe', 'color' => '#10B981', 'category' => 'types', 'order' => 23],
'athletic-conversion' => ['title' => 'تحويل لاعب لعضو', 'subtitle' => 'تحويل عضوية رياضية لعاملة', 'icon' => 'trophy', 'color' => '#EC4899', 'category' => 'types', 'order' => 24],
'full-new-member-scenario' => ['title' => 'سيناريو كامل: عضو جديد', 'subtitle' => 'من التسجيل حتى استلام الكارنيه', 'icon' => 'play-circle', 'color' => '#6366F1', 'category' => 'scenarios', 'order' => 25],
'collective-payment' => ['title' => 'الفاتورة المجمعة وتفعيل التابعين', 'subtitle' => 'دفع العضوية يفعّل العضو وجميع تابعيه', 'icon' => 'receipt', 'color' => '#059669', 'category' => 'financial', 'order' => 7],
'payment-queue-management' => ['title' => 'إدارة طابور الدفع', 'subtitle' => 'إلغاء طلبات وإعادتها للطابور', 'icon' => 'list-ordered', 'color' => '#D97706', 'category' => 'financial', 'order' => 8],
'special-discounts' => ['title' => 'الخصومات الخاصة', 'subtitle' => 'تطبيق خصم خاص أو عرض مجلس الإدارة', 'icon' => 'badge-percent', 'color' => '#EC4899', 'category' => 'financial', 'order' => 9],
'add-spouse' => ['title' => 'إضافة زوج/زوجة', 'subtitle' => 'إضافة زوجة للعضوية مع حساب الرسوم', 'icon' => 'heart', 'color' => '#F97316', 'category' => 'family', 'order' => 10],
'add-child' => ['title' => 'إضافة أبناء', 'subtitle' => 'إضافة أبناء مع التصنيف التلقائي', 'icon' => 'baby', 'color' => '#F59E0B', 'category' => 'family', 'order' => 11],
'add-temporary-member' => ['title' => 'إضافة عضو مؤقت', 'subtitle' => 'والدين أو أشقاء أو مربية', 'icon' => 'user-cog', 'color' => '#6366F1', 'category' => 'family', 'order' => 12],
'children-aging' => ['title' => 'تقادم أعمار الأبناء', 'subtitle' => 'التجميد التلقائي والفصل عند بلوغ 25', 'icon' => 'clock', 'color' => '#DC2626', 'category' => 'family', 'order' => 13],
'interview-process' => ['title' => 'المقابلات', 'subtitle' => 'جدولة وإجراء مقابلة العضوية', 'icon' => 'message-square', 'color' => '#0EA5E9', 'category' => 'procedures', 'order' => 14],
'change-status' => ['title' => 'تغيير حالة العضوية', 'subtitle' => 'دورة الحالات من محتمل حتى فعال', 'icon' => 'refresh-cw', 'color' => '#8B5CF6', 'category' => 'procedures', 'order' => 15],
'annual-subscription' => ['title' => 'الاشتراك السنوي', 'subtitle' => 'توليد وتحصيل الاشتراكات السنوية', 'icon' => 'calendar-check', 'color' => '#059669', 'category' => 'financial', 'order' => 16],
'fines-violations' => ['title' => 'المخالفات والغرامات', 'subtitle' => 'فرض غرامة وتصعيد العقوبات', 'icon' => 'alert-triangle', 'color' => '#DC2626', 'category' => 'procedures', 'order' => 17],
'carnet-issuance' => ['title' => 'إصدار الكارنيه', 'subtitle' => 'شروط وإجراءات طباعة الكارنيه', 'icon' => 'id-card', 'color' => '#3B82F6', 'category' => 'procedures', 'order' => 18],
'transfer-separation' => ['title' => 'فصل الأبناء', 'subtitle' => 'فصل ابن/ابنة لعضوية مستقلة', 'icon' => 'git-branch', 'color' => '#F97316', 'category' => 'transfers', 'order' => 19],
'divorce-transfer' => ['title' => 'تحويل عند الطلاق', 'subtitle' => 'نقل العضوية بعد الطلاق', 'icon' => 'split', 'color' => '#DC2626', 'category' => 'transfers', 'order' => 20],
'death-transfer' => ['title' => 'تحويل عند الوفاة', 'subtitle' => 'نقل العضوية للزوج/الزوجة', 'icon' => 'heart-off', 'color' => '#6B7280', 'category' => 'transfers', 'order' => 21],
'waiver-process' => ['title' => 'التنازل عن العضوية', 'subtitle' => 'تنازل لشخص آخر مع رسوم 30%', 'icon' => 'arrow-right-left', 'color' => '#8B5CF6', 'category' => 'transfers', 'order' => 22],
'membership-freeze-drop' => ['title' => 'التجميد والإسقاط', 'subtitle' => 'تجميد العضوية أو إسقاطها', 'icon' => 'snowflake', 'color' => '#06B6D4', 'category' => 'transfers', 'order' => 23],
'honorary-membership' => ['title' => 'العضوية الشرفية', 'subtitle' => 'عضوية بدون رسوم بقرار مجلس', 'icon' => 'award', 'color' => '#F59E0B', 'category' => 'types', 'order' => 24],
'foreign-membership' => ['title' => 'العضوية الأجنبية', 'subtitle' => 'عضوية لغير المصريين بـ 10,000$', 'icon' => 'globe', 'color' => '#10B981', 'category' => 'types', 'order' => 25],
'athletic-conversion' => ['title' => 'تحويل لاعب لعضو', 'subtitle' => 'تحويل عضوية رياضية لعاملة', 'icon' => 'trophy', 'color' => '#EC4899', 'category' => 'types', 'order' => 26],
'full-new-member-scenario' => ['title' => 'سيناريو كامل: عضو جديد', 'subtitle' => 'من التسجيل حتى استلام الكارنيه', 'icon' => 'play-circle', 'color' => '#6366F1', 'category' => 'scenarios', 'order' => 27],
];
private const MEMBERSHIP_CATEGORIES = [
......
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: الفاتورة المجمعة<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#8B5CF6}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#05966908,#05966904);border-radius:16px;border:1px solid #05966920}.tut-header-icon{width:56px;height:56px;background:#059669;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#05966960}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#05966915;color:#059669;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#EDE9FE;border-color:#8B5CF6;color:#7C3AED}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/membership">شئون العضوية</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">الفاتورة المجمعة وتفعيل التابعين</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="receipt" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>الفاتورة المجمعة وتفعيل التابعين</h1><p>دفع العضوية يفعّل العضو وجميع تابعيه دفعة واحدة</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">ما هي الفاتورة المجمعة؟</h3><div class="tut-step-body">
الفاتورة المجمعة تظهر في ملف العضو بعد ملء الاستمارة وإضافة أفراد الأسرة. تشمل:
<ul>
<li><strong>قيمة العضوية</strong> حسب المؤهل (150,000 / 225,000 / 300,000)</li>
<li><strong>خصم خاص</strong> إن وُجد (نسبة مئوية)</li>
<li><strong>رسوم الزوجات</strong> (الأولى مجانية، الثانية 10%، الثالثة 20%...)</li>
<li><strong>رسوم الأبناء</strong> (3 تحت 18 مشمولين، الباقي حسب السن)</li>
<li><strong>رسوم أعضاء مؤقتين</strong> (10% من قيمة العضوية)</li>
</ul>
<span class="info">الإجمالي المعروض هو ما سيُرسل للخزينة — شامل كل البنود غير المدفوعة.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">إرسال الفاتورة للخزينة</h3><div class="tut-step-body">
من ملف العضو (بعد قبول مجلس الأمناء):
<ul>
<li>اختر <span class="field">كاش كامل</span> أو <span class="field">تقسيط</span></li>
<li>في حالة التقسيط: أدخل المقدم (25% على الأقل) وعدد الأشهر</li>
<li>اضغط <span class="field">إرسال للخزينة</span></li>
</ul>
<span class="warn">عند إرسال الفاتورة المجمعة، يتم إلغاء أي طلبات إضافة فردية معلقة تلقائياً — لأنها مشمولة في الفاتورة.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">ماذا يحدث عند التحصيل؟</h3><div class="tut-step-body">
عندما يحصّل الكاشير الفاتورة، يحدث تلقائياً:
<div class="tut-diagram">
الكاشير يضغط "تحصيل"
├── إنشاء إيصال + قيد محاسبي
├── تفعيل العضو → حالة "فعال" + تخصيص رقم عضوية
├── تفعيل الزوجة الأولى (مشمولة)
├── تفعيل 3 أبناء تحت 18 (مشمولين)
├── تفعيل أي تابع رسومه مشمولة في الفاتورة
└── إنشاء خطة تقسيط (إذا كان مقدم أقساط)
</div>
<span class="success">جميع التابعين الذين ليس لهم طلب دفع منفصل يتم تفعيلهم فوراً ضمن نفس العملية.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">التابعون ذوو الرسوم المنفصلة</h3><div class="tut-step-body">
بعض التابعين يحتاجون دفع منفصل:
<ul>
<li>الزوجة الثانية فأكثر (رسوم نسبية)</li>
<li>الابن الرابع فأكثر تحت 18 (5% من قيمة العضوية)</li>
<li>أبناء فوق 18 (10%-20% حسب السن)</li>
</ul>
<span class="info">إذا كان هناك طلب دفع <strong>addition_fee</strong> منفصل معلق لتابع معين، لن يتم تفعيله مع الفاتورة المجمعة — ينتظر حتى يُحصّل طلبه.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">التأكد من التفعيل</h3><div class="tut-step-body">
بعد التحصيل، ارجع لملف العضو وتأكد من:
<ul>
<li>حالة العضو: <span class="field" style="color:#059669;">فعال</span></li>
<li>رقم العضوية ظهر (مثال: 1234)</li>
<li>جميع التابعين المشمولين حالتهم: <span class="field" style="color:#059669;">نشط</span></li>
<li>تاريخ الالتحاق محدد لكل تابع</li>
</ul>
<span class="success">إذا كان العضو مفعّل لكن تابع ما زال "لم يتم السداد" رغم شموله في الفاتورة، النظام يقوم بالمطابقة التلقائية عند فتح ملف العضو (Reconcile).</span>
</div></div>
<div class="tut-nav">
<?php if ($prevSlug ?? null): ?>
<a href="/tutorials/membership/<?= e($prevSlug) ?>"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> <?= e($prevTitle) ?></a>
<?php else: ?><span></span><?php endif; ?>
<?php if ($nextSlug ?? null): ?>
<a href="/tutorials/membership/<?= e($nextSlug) ?>"><?= e($nextTitle) ?> <i data-lucide="arrow-left" style="width:14px;height:14px;"></i></a>
<?php else: ?><span></span><?php endif; ?>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>شرح: إدارة طابور الدفع<?php $__template->endSection(); ?>
<?php $__template->section('styles'); ?>
<style>
.tut-page{max-width:860px;margin:0 auto}.tut-breadcrumb{display:flex;align-items:center;gap:8px;margin-bottom:20px;font-size:13px}.tut-breadcrumb a{color:#6B7280;text-decoration:none}.tut-breadcrumb a:hover{color:#8B5CF6}.tut-breadcrumb span{color:#9CA3AF}.tut-header{display:flex;align-items:center;gap:18px;margin-bottom:30px;padding:24px;background:linear-gradient(135deg,#D9770608,#D9770604);border-radius:16px;border:1px solid #D9770620}.tut-header-icon{width:56px;height:56px;background:#D97706;border-radius:14px;display:flex;align-items:center;justify-content:center;flex-shrink:0}.tut-header h1{font-size:22px;font-weight:800;color:#1A1A2E;margin:0 0 4px}.tut-header p{font-size:14px;color:#6B7280;margin:0}.tut-step{position:relative;padding:20px 20px 20px 60px;margin-bottom:16px;background:#fff;border:1px solid #E5E7EB;border-radius:12px;transition:border-color .2s}.tut-step:hover{border-color:#D9770660}.tut-step-num{position:absolute;right:18px;top:20px;width:32px;height:32px;background:#D9770615;color:#D97706;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:800}.tut-step-title{font-size:15px;font-weight:700;color:#1A1A2E;margin:0 0 6px}.tut-step-body{font-size:13.5px;color:#374151;line-height:1.8}.tut-step-body ul{margin:8px 0;padding-right:18px}.tut-step-body li{margin-bottom:4px}.tut-step-body .field{display:inline-block;background:#F3F4F6;color:#1A1A2E;padding:1px 8px;border-radius:4px;font-size:12px;font-weight:600;font-family:monospace;direction:ltr}.tut-step-body .warn{display:block;background:#FEF3C7;border:1px solid #F59E0B30;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#92400E}.tut-step-body .info{display:block;background:#DBEAFE;border:1px solid #3B82F630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#1E40AF}.tut-step-body .success{display:block;background:#ECFDF5;border:1px solid #05966930;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#065F46}.tut-step-body .danger{display:block;background:#FEF2F2;border:1px solid #DC262630;border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12.5px;color:#991B1B}.tut-nav{display:flex;justify-content:space-between;align-items:center;margin-top:32px;padding-top:20px;border-top:1px solid #E5E7EB}.tut-nav a{display:inline-flex;align-items:center;gap:6px;padding:10px 16px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:10px;text-decoration:none;color:#374151;font-size:13px;font-weight:600;transition:all .15s}.tut-nav a:hover{background:#EDE9FE;border-color:#8B5CF6;color:#7C3AED}.tut-diagram{background:#F8FAFC;border:1px solid #E2E8F0;border-radius:12px;padding:16px 20px;margin:12px 0;font-family:monospace;font-size:12px;direction:ltr;text-align:left;line-height:1.6;overflow-x:auto}
</style>
<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div class="tut-page">
<div class="tut-breadcrumb">
<a href="/tutorials"><i data-lucide="graduation-cap" style="width:14px;height:14px;"></i></a>
<span>/</span>
<a href="/tutorials/membership">شئون العضوية</a>
<span>/</span>
<span style="color:#1A1A2E;font-weight:600;">إدارة طابور الدفع</span>
</div>
<div class="tut-header">
<div class="tut-header-icon"><i data-lucide="list-ordered" style="width:28px;height:28px;color:#fff;"></i></div>
<div><h1>إدارة طابور الدفع</h1><p>إلغاء طلبات الدفع وإعادتها للطابور</p></div>
</div>
<div class="tut-step"><div class="tut-step-num">1</div><h3 class="tut-step-title">كيف يعمل طابور الدفع</h3><div class="tut-step-body">
عند طلب أي دفعة (استمارة، عضوية، إضافة تابع) يتم إنشاء <strong>طلب دفع</strong> يظهر في شاشة الخزينة.
<div class="tut-diagram">
طلب الدفع → [معلق] → الخزينة تحصّل → [مكتمل] → التفعيل التلقائي
↘ إلغاء → [ملغى] → إعادة للطابور → [معلق] ↗
</div>
<span class="info">كل طلب له رقم فريد (PRQ-2026-XXXXXX) ويظل مرئياً حتى بعد الإلغاء.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">2</div><h3 class="tut-step-title">إلغاء طلب دفع (من الخزينة)</h3><div class="tut-step-body">
<ul>
<li>من شاشة <span class="field">طابور الدفع</span>، اضغط على الطلب المطلوب</li>
<li>اضغط <span class="field">إلغاء</span> وأدخل سبب الإلغاء</li>
<li>الطلب يتحول لحالة <strong style="color:#DC2626;">ملغى</strong></li>
</ul>
<span class="warn">الإلغاء لا يحذف الطلب — يبقى مرئياً في ملف العضو مع سبب الإلغاء والتاريخ.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">3</div><h3 class="tut-step-title">إلغاء طلب مكتمل (إلغاء إيصال)</h3><div class="tut-step-body">
إذا كان الطلب <strong>مكتمل</strong> (تم التحصيل بالفعل):
<ul>
<li>يتم إلغاء الإيصال المرتبط تلقائياً</li>
<li>يتم عكس التفعيل (العضو يعود لحالة <span class="field">payment_pending</span>)</li>
<li>هذا الطلب يختفي نهائياً — لا يمكن إعادته</li>
</ul>
<span class="danger">إلغاء إيصال مكتمل يُلغي تفعيل العضوية وجميع التابعين — استخدم بحذر!</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">4</div><h3 class="tut-step-title">إعادة طلب ملغى لطابور الدفع</h3><div class="tut-step-body">
الطلبات الملغاة (غير المكتملة) تظهر في ملف العضو بقسم <strong style="color:#DC2626;">طلبات ملغاة</strong>:
<ul>
<li>يظهر اسم البند، المبلغ، رقم الطلب، وسبب الإلغاء</li>
<li>اضغط زر <span class="field">إعادة للطابور</span></li>
<li>الطلب يعود لحالة <strong style="color:#D97706;">معلق</strong> ويظهر في شاشة الخزينة من جديد</li>
</ul>
<span class="success">يمكنك إعادة نفس الطلب بنفس رقمه — لا حاجة لإنشاء طلب جديد.</span>
<span class="warn">لا يمكن إعادة الطلب إذا كان هناك طلب معلق آخر لنفس البند بالفعل.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">5</div><h3 class="tut-step-title">الفاتورة المجمعة والتابعين</h3><div class="tut-step-body">
عند دفع <strong>فاتورة العضوية المجمعة</strong> (كاش أو مقدم أقساط):
<ul>
<li>يتم تفعيل العضو تلقائياً</li>
<li>يتم تفعيل <strong>جميع التابعين</strong> (الزوجات، الأبناء، المؤقتين) الذين ليس لهم طلب دفع منفصل</li>
<li>أي طلبات إضافة فردية معلقة يتم إلغاؤها تلقائياً (مشمولة في الفاتورة)</li>
</ul>
<span class="info">التابعون المشمولون في الفاتورة (الزوجة الأولى + 3 أبناء تحت 18) يتم تفعيلهم بدون رسوم إضافية.</span>
</div></div>
<div class="tut-step"><div class="tut-step-num">6</div><h3 class="tut-step-title">حالات خاصة</h3><div class="tut-step-body">
<ul>
<li><strong>تابع برسوم (بعد التفعيل):</strong> إذا تمت إضافة تابع بعد تفعيل العضوية، يحتاج طلب دفع منفصل (570 ج.م استمارة + رسوم الإضافة)</li>
<li><strong>إلغاء طلب تابع أثناء وجود فاتورة مجمعة:</strong> لا يمكن — النظام يعرض رسالة "مشمول في الفاتورة المجمعة"</li>
<li><strong>إلغاء الفاتورة المجمعة:</strong> يعيد جميع التابعين لحالة "في انتظار السداد"</li>
</ul>
</div></div>
<div class="tut-nav">
<?php if ($prevSlug ?? null): ?>
<a href="/tutorials/membership/<?= e($prevSlug) ?>"><i data-lucide="arrow-right" style="width:14px;height:14px;"></i> <?= e($prevTitle) ?></a>
<?php else: ?><span></span><?php endif; ?>
<?php if ($nextSlug ?? null): ?>
<a href="/tutorials/membership/<?= e($nextSlug) ?>"><?= e($nextTitle) ?> <i data-lucide="arrow-left" style="width:14px;height:14px;"></i></a>
<?php else: ?><span></span><?php endif; ?>
</div>
</div>
<script>document.addEventListener('DOMContentLoaded', function() { if (typeof lucide !== 'undefined') lucide.createIcons(); });</script>
<?php $__template->endSection(); ?>
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