Commit c4a7701a authored by Administrator's avatar Administrator

Update 6 files via Son of Anton

parent 6140d228
...@@ -7,12 +7,17 @@ use App\Core\Controller; ...@@ -7,12 +7,17 @@ use App\Core\Controller;
use App\Core\Request; use App\Core\Request;
use App\Core\Response; use App\Core\Response;
use App\Core\App; use App\Core\App;
use App\Core\EventBus;
use App\Modules\Payments\Models\Payment; use App\Modules\Payments\Models\Payment;
use App\Modules\Payments\Services\PaymentService; use App\Modules\Payments\Services\PaymentService;
use App\Modules\Payments\Services\BalanceCalculator; use App\Modules\Payments\Services\BalanceCalculator;
use App\Modules\Members\Services\MemberNumberGenerator;
class PaymentController extends Controller class PaymentController extends Controller
{ {
/**
* List all payments with filters.
*/
public function index(Request $request): Response public function index(Request $request): Response
{ {
$filters = [ $filters = [
...@@ -21,7 +26,6 @@ class PaymentController extends Controller ...@@ -21,7 +26,6 @@ class PaymentController extends Controller
'payment_method' => $request->get('payment_method', ''), 'payment_method' => $request->get('payment_method', ''),
'date_from' => $request->get('date_from', ''), 'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''), 'date_to' => $request->get('date_to', ''),
'is_voided' => $request->get('is_voided', ''),
]; ];
$page = max(1, (int) $request->get('page', 1)); $page = max(1, (int) $request->get('page', 1));
$result = Payment::search($filters, 25, $page); $result = Payment::search($filters, 25, $page);
...@@ -33,90 +37,272 @@ class PaymentController extends Controller ...@@ -33,90 +37,272 @@ class PaymentController extends Controller
]); ]);
} }
public function processForm(Request $request, string $memberId): Response /**
* Show the payment form for a member.
* Lists all available payment types and outstanding amounts.
*/
public function process(Request $request, string $memberId): Response
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]); $member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود'); if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$summary = BalanceCalculator::getSummary((int) $memberId);
// Build available payment options
$paymentOptions = [];
// Form fee
if (!$summary['form_fee_paid']) {
$paymentOptions[] = [
'type' => 'form_fee',
'label' => 'رسوم استمارة',
'amount' => $summary['form_fee'],
'description' => 'رسوم استمارة رقم ' . ($member['form_number'] ?? ''),
'required' => true,
];
}
// Membership value (full cash or down payment)
if ($member['status'] === 'accepted' && !$summary['membership_fee_paid']) {
$paymentOptions[] = [
'type' => 'membership_fee',
'label' => 'قيمة العضوية (كاش كامل)',
'amount' => $member['membership_value'] ?? '0.00',
'description' => 'قيمة العضوية كاملة',
'required' => false,
];
// Down payment for installment
$minDown = bcdiv(bcmul($member['membership_value'] ?? '0', '25', 2), '100', 2);
$paymentOptions[] = [
'type' => 'down_payment',
'label' => 'مقدم تقسيط (25% على الأقل)',
'amount' => $minDown,
'description' => 'مقدم تقسيط — الحد الأدنى ' . money($minDown),
'required' => false,
'is_variable' => true,
'min_amount' => $minDown,
];
}
// Installment payment
if (($summary['installments']['next_due'] ?? null) !== null) {
$paymentOptions[] = [
'type' => 'installment',
'label' => 'قسط شهري',
'amount' => $summary['installments']['next_due']['amount'] ?? '0.00',
'description' => 'قسط مستحق ' . ($summary['installments']['next_due']['due_date'] ?? ''),
'required' => false,
];
}
// Annual subscription
if (bccomp($summary['unpaid_subscriptions'], '0', 2) > 0) {
$paymentOptions[] = [
'type' => 'annual_subscription',
'label' => 'اشتراك سنوي',
'amount' => $summary['unpaid_subscriptions'],
'description' => 'اشتراكات سنوية غير مدفوعة (' . $summary['unpaid_subs_count'] . ' اشتراك)',
'required' => false,
];
}
// Fines
if (bccomp($summary['unpaid_fines'], '0', 2) > 0) {
$paymentOptions[] = [
'type' => 'fine',
'label' => 'غرامات',
'amount' => $summary['unpaid_fines'],
'description' => 'غرامات مستحقة',
'required' => false,
];
}
$outstandingItems = BalanceCalculator::getOutstandingItems((int) $memberId); // Generic custom payment
$summary = BalanceCalculator::getMemberSummary((int) $memberId); $paymentOptions[] = [
$paymentMethods = $db->select("SELECT * FROM payment_methods WHERE is_active = 1 ORDER BY sort_order"); 'type' => 'other',
'label' => 'دفعة أخرى',
'amount' => '0.00',
'description' => '',
'required' => false,
'is_variable' => true,
];
return $this->view('Payments.Views.process', [ return $this->view('Payments.Views.process', [
'member' => $member, 'member' => $member,
'outstandingItems' => $outstandingItems,
'summary' => $summary, 'summary' => $summary,
'paymentMethods' => $paymentMethods, 'paymentOptions' => $paymentOptions,
]); ]);
} }
public function processStore(Request $request, string $memberId): Response /**
* Handle payment submission.
*/
public function store(Request $request, string $memberId): Response
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]); $member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود'); if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
$data = $request->all(); }
unset($data['_csrf_token']);
$paymentType = trim($data['payment_type'] ?? ''); $paymentType = trim((string) $request->post('payment_type', ''));
$amount = trim($data['amount'] ?? '0'); $amount = trim((string) $request->post('amount', '0'));
$method = trim($data['payment_method'] ?? 'cash'); $paymentMethod = trim((string) $request->post('payment_method', 'cash'));
$notes = trim((string) $request->post('notes', ''));
$errors = []; if ($paymentType === '' || bccomp($amount, '0.01', 2) < 0) {
if ($paymentType === '') $errors[] = 'نوع الدفع مطلوب'; return $this->redirect("/payments/process/{$memberId}")->withError('بيانات الدفع غير صالحة');
if (!is_numeric($amount) || bccomp($amount, '0', 2) <= 0) $errors[] = 'المبلغ غير صالح';
if ($method === '') $errors[] = 'طريقة الدفع مطلوبة';
if ($method === 'check') {
if (empty(trim($data['check_number'] ?? ''))) $errors[] = 'رقم الشيك مطلوب';
if (empty(trim($data['check_bank'] ?? ''))) $errors[] = 'البنك مطلوب';
} }
if (!empty($errors)) { $description = PaymentService::getPaymentTypeLabel($paymentType);
$session = App::getInstance()->session(); if ($member['form_number']) {
$session->flash('_alerts', array_map(fn($e) => ['type' => 'error', 'message' => $e], $errors)); $description .= ' — استمارة ' . $member['form_number'];
return $this->redirect("/payments/process/{$memberId}");
} }
$data['member_id'] = (int) $memberId; $result = PaymentService::processPayment([
$data['amount'] = $amount; 'member_id' => (int) $memberId,
$data['payment_method'] = $method; 'amount' => $amount,
$data['description'] = Payment::getPaymentTypeLabel($paymentType) . ' — ' . $member['full_name_ar']; 'payment_type' => $paymentType,
'payment_method' => $paymentMethod,
$result = PaymentService::processPayment($data); 'related_entity_type' => 'members',
'related_entity_id' => (int) $memberId,
'description' => $description,
'notes' => $notes,
'check_number' => $request->post('check_number'),
'check_bank' => $request->post('check_bank'),
'check_date' => $request->post('check_date'),
'visa_reference' => $request->post('visa_reference'),
'transfer_reference' => $request->post('transfer_reference'),
'transfer_bank' => $request->post('transfer_bank'),
]);
if (!$result['success']) { if (!$result['success']) {
return $this->redirect("/payments/process/{$memberId}")->withError($result['error']); return $this->redirect("/payments/process/{$memberId}")->withError($result['error']);
} }
// If addition fee, update the receipt number on the related entity // ── Post-payment actions based on type ──
if ($paymentType === 'addition_fee' && !empty($data['related_entity_type']) && !empty($data['related_entity_id'])) {
$entityTable = $data['related_entity_type']; // Membership fee paid → assign membership number + activate
$entityId = (int) $data['related_entity_id']; if (in_array($paymentType, ['membership_fee', 'down_payment'])) {
$allowedTables = ['spouses', 'children', 'temporary_members']; $this->handleMembershipPayment((int) $memberId, $paymentType, $amount);
if (in_array($entityTable, $allowedTables)) { }
try {
$db->update($entityTable, [ return $this->redirect("/members/{$memberId}")
'fee_receipt_number' => $result['receipt_number'], ->withSuccess('تم تسجيل الدفع — ' . money($amount) . ' — إيصال: ' . $result['receipt_number']);
}
/**
* Handle membership value payment — assign number + activate.
*/
private function handleMembershipPayment(int $memberId, string $type, string $amount): void
{
$db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [$memberId]);
if (!$member) return;
// If full cash payment → activate immediately
if ($type === 'membership_fee') {
$number = MemberNumberGenerator::assign($memberId);
$db->update('members', [
'status' => 'active',
'updated_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$entityId]); ], '`id` = ?', [$memberId]);
} catch (\Throwable $e) {
// Log but don't block EventBus::dispatch('member.activated', [
'member_id' => $memberId,
'membership_number' => $number,
'payment_method' => 'cash_full',
]);
} }
// If down payment → create installment plan + activate
if ($type === 'down_payment') {
$membershipValue = $member['membership_value'] ?? '0.00';
$remaining = bcsub($membershipValue, $amount, 2);
if (bccomp($remaining, '0', 2) > 0) {
// Create installment plan
$interestRate = '22.00';
$months = 30; // Max default, can be overridden
$totalInterest = bcdiv(bcmul($remaining, $interestRate, 4), '100', 2);
$totalWithInterest = bcadd($remaining, $totalInterest, 2);
$monthlyPayment = bcdiv($totalWithInterest, (string) $months, 2);
$planId = $db->insert('installment_plans', [
'member_id' => $memberId,
'related_entity_type' => 'members',
'related_entity_id' => $memberId,
'total_amount' => $membershipValue,
'down_payment' => $amount,
'remaining_balance' => $remaining,
'interest_rate' => $interestRate,
'total_interest' => $totalInterest,
'total_with_interest' => $totalWithInterest,
'number_of_months' => $months,
'monthly_payment' => $monthlyPayment,
'start_date' => date('Y-m-d'),
'status' => 'active',
'notes' => 'خطة تقسيط — قيمة العضوية',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// Generate schedule
$remainingAfter = $totalWithInterest;
for ($i = 1; $i <= $months; $i++) {
$principal = bcdiv($remaining, (string) $months, 2);
$interest = bcdiv($totalInterest, (string) $months, 2);
$installmentAmount = bcadd($principal, $interest, 2);
$remainingAfter = bcsub($remainingAfter, $installmentAmount, 2);
if (bccomp($remainingAfter, '0', 2) < 0) $remainingAfter = '0.00';
$dueDate = date('Y-m-d', strtotime("+{$i} months"));
$db->insert('installment_schedule', [
'installment_plan_id' => $planId,
'installment_number' => $i,
'due_date' => $dueDate,
'amount' => $installmentAmount,
'principal' => $principal,
'interest' => $interest,
'remaining_after' => $remainingAfter,
'paid_amount' => '0.00',
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
} }
} }
return $this->redirect("/receipts/{$result['receipt_id']}/print") // Assign number and activate
->withSuccess('تم تسجيل الدفعة بنجاح — إيصال رقم: ' . $result['receipt_number']); $number = MemberNumberGenerator::assign($memberId);
$db->update('members', [
'status' => 'active',
'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$memberId]);
EventBus::dispatch('member.activated', [
'member_id' => $memberId,
'membership_number' => $number,
'payment_method' => 'installment',
]);
}
} }
/**
* Show single payment details.
*/
public function show(Request $request, string $id): Response public function show(Request $request, string $id): Response
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$payment = $db->selectOne( $payment = $db->selectOne(
"SELECT p.*, m.full_name_ar as member_name, m.membership_number, r.receipt_number, "SELECT p.*, m.full_name_ar as member_name, m.membership_number,
e.full_name_ar as received_by_name, ve.full_name_ar as voided_by_name r.receipt_number, e.full_name_ar as received_by_name,
ve.full_name_ar as voided_by_name
FROM payments p FROM payments p
JOIN members m ON m.id = p.member_id JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id LEFT JOIN receipts r ON r.id = p.receipt_id
...@@ -125,23 +311,45 @@ class PaymentController extends Controller ...@@ -125,23 +311,45 @@ class PaymentController extends Controller
WHERE p.id = ?", WHERE p.id = ?",
[(int) $id] [(int) $id]
); );
if (!$payment) return $this->redirect('/payments')->withError('الدفعة غير موجودة'); if (!$payment) {
return $this->redirect('/payments')->withError('الدفعة غير موجودة');
}
$canVoid = PaymentService::canVoid((int) $id); // Check if void is allowed
$canVoid = ['allowed' => false];
if (!$payment['is_voided']) {
$hoursSince = (time() - strtotime($payment['created_at'])) / 3600;
$canVoid['allowed'] = ($hoursSince <= 24);
// Super admin can always void
$employee = App::getInstance()->currentEmployee();
if ($employee) {
$sa = $db->selectOne(
"SELECT 1 FROM employee_roles er JOIN roles r ON r.id = er.role_id
WHERE er.employee_id = ? AND r.role_code = 'super_admin' AND er.is_active = 1 LIMIT 1",
[(int) $employee->id]
);
if ($sa) $canVoid['allowed'] = true;
}
}
return $this->view('Payments.Views.history', [ return $this->view('Payments.Views.history', [
'payment' => $payment, 'payment' => $payment,
'canVoid' => $canVoid, 'canVoid' => $canVoid,
'singleView' => true, 'singleView' => true,
'rows' => [], 'pagination' => ['last_page' => 1, 'current_page' => 1], 'filters' => [], 'rows' => [],
'pagination' => ['last_page' => 1, 'current_page' => 1],
'filters' => [],
]); ]);
} }
public function voidPayment(Request $request, string $id): Response /**
* Void a payment.
*/
public function void(Request $request, string $id): Response
{ {
$reason = trim((string) $request->post('void_reason', '')); $reason = trim((string) $request->post('void_reason', ''));
if ($reason === '') { if ($reason === '') {
return $this->redirect("/payments/{$id}")->withError('يجب إدخال سبب الإلغاء'); return $this->redirect("/payments/{$id}")->withError('سبب الإلغاء مطلوب');
} }
$result = PaymentService::voidPayment((int) $id, $reason); $result = PaymentService::voidPayment((int) $id, $reason);
...@@ -149,21 +357,26 @@ class PaymentController extends Controller ...@@ -149,21 +357,26 @@ class PaymentController extends Controller
return $this->redirect("/payments/{$id}")->withError($result['error']); return $this->redirect("/payments/{$id}")->withError($result['error']);
} }
return $this->redirect('/payments')->withSuccess('تم إلغاء الدفعة والإيصال بنجاح'); return $this->redirect("/payments/{$id}")->withSuccess('تم إلغاء الدفعة والإيصال');
} }
/**
* Member payment history.
*/
public function memberHistory(Request $request, string $memberId): Response public function memberHistory(Request $request, string $memberId): Response
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]); $member = $db->selectOne("SELECT * FROM members WHERE id = ?", [(int) $memberId]);
if (!$member) return $this->redirect('/members')->withError('العضو غير موجود'); if (!$member) {
return $this->redirect('/members')->withError('العضو غير موجود');
}
$payments = Payment::getForMember((int) $memberId, true); $payments = Payment::getForMember((int) $memberId, true);
$summary = BalanceCalculator::getMemberSummary((int) $memberId); $summary = BalanceCalculator::getSummary((int) $memberId);
return $this->view('Payments.Views.history', [ return $this->view('Payments.Views.history', [
'member' => $member,
'rows' => $payments, 'rows' => $payments,
'member' => $member,
'summary' => $summary, 'summary' => $summary,
'memberView' => true, 'memberView' => true,
'pagination' => ['last_page' => 1, 'current_page' => 1], 'pagination' => ['last_page' => 1, 'current_page' => 1],
...@@ -171,20 +384,38 @@ class PaymentController extends Controller ...@@ -171,20 +384,38 @@ class PaymentController extends Controller
]); ]);
} }
/**
* Daily cash report.
*/
public function dailyReport(Request $request): Response public function dailyReport(Request $request): Response
{ {
$date = $request->get('date', date('Y-m-d')); $date = $request->get('date', date('Y-m-d'));
$branchId = $request->get('branch_id', '') !== '' ? (int) $request->get('branch_id') : null; $branchId = $request->get('branch_id') ? (int) $request->get('branch_id') : null;
$report = PaymentService::getDailyReport($date, $branchId); $report = Payment::getDailyTotals($date, $branchId);
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar"); $branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
// Get individual payments for the day
$where = 'p.payment_date = ? AND p.is_voided = 0';
$params = [$date];
if ($branchId) {
$where .= ' AND m.branch_id = ?';
$params[] = $branchId;
}
$payments = $db->select(
"SELECT p.*, m.full_name_ar as member_name, m.membership_number, r.receipt_number
FROM payments p JOIN members m ON m.id = p.member_id LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where} ORDER BY p.id ASC",
$params
);
return $this->view('Payments.Views.daily-report', [ return $this->view('Payments.Views.daily-report', [
'report' => $report,
'date' => $date, 'date' => $date,
'branchId' => $branchId, 'branchId' => $branchId,
'report' => $report,
'payments' => $payments,
'branches' => $branches, 'branches' => $branches,
]); ]);
} }
......
...@@ -4,9 +4,9 @@ declare(strict_types=1); ...@@ -4,9 +4,9 @@ declare(strict_types=1);
return [ return [
['GET', '/payments', 'Payments\Controllers\PaymentController@index', ['auth'], 'payment.view'], ['GET', '/payments', 'Payments\Controllers\PaymentController@index', ['auth'], 'payment.view'],
['GET', '/payments/daily-report', 'Payments\Controllers\PaymentController@dailyReport', ['auth'], 'payment.view'], ['GET', '/payments/daily-report', 'Payments\Controllers\PaymentController@dailyReport', ['auth'], 'payment.view'],
['GET', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@processForm', ['auth'], 'payment.process_cash'], ['GET', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@process', ['auth'], 'payment.create'],
['POST', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@processStore', ['auth'], 'payment.process_cash'], ['POST', '/payments/process/{memberId}', 'Payments\Controllers\PaymentController@store', ['auth'], 'payment.create'],
['GET', '/payments/member/{memberId}', 'Payments\Controllers\PaymentController@memberHistory',['auth'], 'payment.view'], ['GET', '/payments/member/{memberId}', 'Payments\Controllers\PaymentController@memberHistory', ['auth'], 'payment.view'],
['GET', '/payments/{id}', 'Payments\Controllers\PaymentController@show', ['auth'], 'payment.view'], ['GET', '/payments/{id}', 'Payments\Controllers\PaymentController@show', ['auth'], 'payment.view'],
['POST', '/payments/{id}/void', 'Payments\Controllers\PaymentController@voidPayment', ['auth'], 'payment.void_receipt'], ['POST', '/payments/{id}/void', 'Payments\Controllers\PaymentController@void', ['auth'], 'payment.void'],
]; ];
\ No newline at end of file
...@@ -5,156 +5,135 @@ namespace App\Modules\Payments\Services; ...@@ -5,156 +5,135 @@ namespace App\Modules\Payments\Services;
use App\Core\App; use App\Core\App;
/**
* Calculates all financial obligations and balances for a member.
*/
final class BalanceCalculator final class BalanceCalculator
{ {
/** /**
* Get complete financial summary for a member. * Get complete financial summary for a member.
*/ */
public static function getMemberSummary(int $memberId): array public static function getSummary(int $memberId): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$member = $db->selectOne("SELECT * FROM members WHERE id = ?", [$memberId]); $member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) return ['error' => 'Member not found']; if (!$member) return [];
$membershipValue = $member['membership_value'] ?? '0.00'; $membershipValue = $member['membership_value'] ?? '0.00';
// Total paid (non-voided) // ── Total Paid ──
$totalPaid = $db->selectOne( $totalPaid = PaymentService::totalPaid($memberId);
"SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE member_id = ? AND is_voided = 0",
// ── Form Fee ──
$formFeePaid = PaymentService::hasPaid($memberId, 'form_fee');
$formFee = '505.00';
// ── Membership Fee ──
$membershipFeePaid = PaymentService::hasPaid($memberId, 'membership_fee');
$membershipFeeAmount = PaymentService::totalPaid($memberId, 'membership_fee');
$downPaymentAmount = PaymentService::totalPaid($memberId, 'down_payment');
$membershipPaid = bcadd($membershipFeeAmount, $downPaymentAmount, 2);
$membershipRemaining = bcsub($membershipValue, $membershipPaid, 2);
if (bccomp($membershipRemaining, '0', 2) < 0) $membershipRemaining = '0.00';
// ── Installments ──
$installmentData = ['total' => '0.00', 'paid' => '0.00', 'remaining' => '0.00', 'overdue_count' => 0, 'next_due' => null];
try {
$activePlan = $db->selectOne(
"SELECT * FROM installment_plans WHERE member_id = ? AND status = 'active' LIMIT 1",
[$memberId] [$memberId]
); );
if ($activePlan) {
// Total voided $installmentData['total'] = $activePlan['total_with_interest'];
$totalVoided = $db->selectOne( $paidInstallments = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE member_id = ? AND is_voided = 1", "SELECT COALESCE(SUM(paid_amount), 0) as paid FROM installment_schedule WHERE installment_plan_id = ? AND status = 'paid'",
[$memberId] [(int) $activePlan['id']]
); );
$installmentData['paid'] = $paidInstallments['paid'] ?? '0.00';
$installmentData['remaining'] = bcsub($activePlan['total_with_interest'], bcadd($activePlan['down_payment'], $installmentData['paid'], 2), 2);
if (bccomp($installmentData['remaining'], '0', 2) < 0) $installmentData['remaining'] = '0.00';
// Breakdown by type $overdueRow = $db->selectOne(
$byType = $db->select( "SELECT COUNT(*) as cnt FROM installment_schedule WHERE installment_plan_id = ? AND status = 'pending' AND due_date < CURDATE()",
"SELECT payment_type, SUM(amount) as total, COUNT(*) as count FROM payments WHERE member_id = ? AND is_voided = 0 GROUP BY payment_type", [(int) $activePlan['id']]
[$memberId]
); );
$installmentData['overdue_count'] = (int) ($overdueRow['cnt'] ?? 0);
$membershipPaid = '0.00'; $nextDue = $db->selectOne(
foreach ($byType as $t) { "SELECT due_date, amount FROM installment_schedule WHERE installment_plan_id = ? AND status = 'pending' ORDER BY due_date ASC LIMIT 1",
if (in_array($t['payment_type'], ['membership_fee', 'down_payment'])) { [(int) $activePlan['id']]
$membershipPaid = bcadd($membershipPaid, $t['total'], 2); );
} $installmentData['next_due'] = $nextDue;
} }
} catch (\Throwable $e) {}
$membershipOutstanding = bcsub($membershipValue, $membershipPaid, 2);
if (bccomp($membershipOutstanding, '0', 2) < 0) $membershipOutstanding = '0.00'; // ── Unpaid Subscriptions ──
$unpaidSubscriptions = '0.00';
// Last payment info $unpaidSubsCount = 0;
$lastPayment = $db->selectOne( try {
"SELECT p.*, r.receipt_number FROM payments p LEFT JOIN receipts r ON r.id = p.receipt_id WHERE p.member_id = ? AND p.is_voided = 0 ORDER BY p.payment_date DESC, p.id DESC LIMIT 1", $row = $db->selectOne(
"SELECT COUNT(*) as cnt, COALESCE(SUM(total_amount - paid_amount + fine_amount), 0) as total
FROM subscriptions WHERE member_id = ? AND status IN ('pending', 'overdue')",
[$memberId] [$memberId]
); );
$unpaidSubscriptions = $row['total'] ?? '0.00';
$unpaidSubsCount = (int) ($row['cnt'] ?? 0);
} catch (\Throwable $e) {}
// ── Unpaid Fines ──
$unpaidFines = '0.00';
try {
$row = $db->selectOne(
"SELECT COALESCE(SUM(amount - paid_amount), 0) as total FROM fines WHERE member_id = ? AND status IN ('imposed', 'appeal_upheld')",
[$memberId]
);
$unpaidFines = $row['total'] ?? '0.00';
} catch (\Throwable $e) {}
// ── Total Outstanding ──
$totalOutstanding = bcadd(
bcadd($membershipRemaining, $unpaidSubscriptions, 2),
bcadd($unpaidFines, $installmentData['remaining'], 2),
2
);
return [ return [
'member_id' => $memberId,
'membership_value' => $membershipValue, 'membership_value' => $membershipValue,
'total_paid' => $totalPaid['total'] ?? '0.00', 'total_paid' => $totalPaid,
'total_voided' => $totalVoided['total'] ?? '0.00', 'form_fee_paid' => $formFeePaid,
'form_fee' => $formFee,
'membership_fee_paid' => $membershipFeePaid || bccomp($membershipPaid, '0', 2) > 0,
'membership_paid' => $membershipPaid, 'membership_paid' => $membershipPaid,
'membership_outstanding' => $membershipOutstanding, 'membership_remaining' => $membershipRemaining,
'payments_by_type' => $byType, 'installments' => $installmentData,
'last_payment' => $lastPayment, 'unpaid_subscriptions' => $unpaidSubscriptions,
'payment_count' => array_sum(array_column($byType, 'count')), 'unpaid_subs_count' => $unpaidSubsCount,
'unpaid_fines' => $unpaidFines,
'total_outstanding' => $totalOutstanding,
]; ];
} }
/** /**
* Get list of outstanding items a member owes. * Check if member can print carnet.
* Blocked if: unpaid subscriptions, overdue installments, active suspension.
*/ */
public static function getOutstandingItems(int $memberId): array public static function canPrintCarnet(int $memberId): array
{ {
$db = App::getInstance()->db(); $summary = self::getSummary($memberId);
$items = []; $blocks = [];
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) return $items;
$membershipValue = $member['membership_value'] ?? '0.00';
// Check membership fee outstanding
$membershipPaid = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE member_id = ? AND is_voided = 0 AND payment_type IN ('membership_fee', 'down_payment')",
[$memberId]
);
$mPaid = $membershipPaid['total'] ?? '0.00';
$mRemaining = bcsub($membershipValue, $mPaid, 2);
if (bccomp($mRemaining, '0', 2) > 0) {
$items[] = [
'type' => 'membership_fee',
'label' => 'رسوم العضوية',
'total' => $membershipValue,
'paid' => $mPaid,
'outstanding' => $mRemaining,
'entity_type' => 'members',
'entity_id' => $memberId,
];
}
// Check unpaid spouse addition fees
if ($db->tableExists('spouses')) {
$unpaidSpouses = $db->select(
"SELECT * FROM spouses WHERE member_id = ? AND is_archived = 0 AND addition_fee > 0 AND (fee_receipt_number IS NULL OR fee_receipt_number = '')",
[$memberId]
);
foreach ($unpaidSpouses as $s) {
$items[] = [
'type' => 'addition_fee',
'label' => 'رسوم إضافة زوجة: ' . $s['full_name_ar'],
'total' => $s['addition_fee'],
'paid' => '0.00',
'outstanding' => $s['addition_fee'],
'entity_type' => 'spouses',
'entity_id' => (int) $s['id'],
];
}
}
// Check unpaid children addition fees if (bccomp($summary['unpaid_subscriptions'], '0', 2) > 0) {
if ($db->tableExists('children')) { $blocks[] = 'اشتراكات غير مدفوعة: ' . money($summary['unpaid_subscriptions']);
$unpaidChildren = $db->select(
"SELECT * FROM children WHERE member_id = ? AND is_archived = 0 AND addition_fee > 0 AND (fee_receipt_number IS NULL OR fee_receipt_number = '')",
[$memberId]
);
foreach ($unpaidChildren as $c) {
$items[] = [
'type' => 'addition_fee',
'label' => 'رسوم إضافة ابن/ابنة: ' . $c['full_name_ar'],
'total' => $c['addition_fee'],
'paid' => '0.00',
'outstanding' => $c['addition_fee'],
'entity_type' => 'children',
'entity_id' => (int) $c['id'],
];
}
} }
if (($summary['installments']['overdue_count'] ?? 0) > 0) {
// Check unpaid temporary member fees $blocks[] = 'أقساط متأخرة: ' . $summary['installments']['overdue_count'] . ' قسط';
if ($db->tableExists('temporary_members')) {
$unpaidTemp = $db->select(
"SELECT * FROM temporary_members WHERE member_id = ? AND is_archived = 0 AND addition_fee > 0 AND (fee_receipt_number IS NULL OR fee_receipt_number = '')",
[$memberId]
);
foreach ($unpaidTemp as $t) {
$items[] = [
'type' => 'addition_fee',
'label' => 'رسوم عضو مؤقت: ' . $t['full_name_ar'],
'total' => $t['addition_fee'],
'paid' => '0.00',
'outstanding' => $t['addition_fee'],
'entity_type' => 'temporary_members',
'entity_id' => (int) $t['id'],
];
} }
if (bccomp($summary['unpaid_fines'], '0', 2) > 0) {
$blocks[] = 'غرامات غير مدفوعة: ' . money($summary['unpaid_fines']);
} }
return $items; return ['allowed' => empty($blocks), 'blocks' => $blocks];
} }
} }
\ No newline at end of file
...@@ -6,55 +6,86 @@ namespace App\Modules\Payments\Services; ...@@ -6,55 +6,86 @@ namespace App\Modules\Payments\Services;
use App\Core\App; use App\Core\App;
use App\Core\EventBus; use App\Core\EventBus;
use App\Core\Logger; use App\Core\Logger;
use App\Modules\Payments\Models\Payment;
use App\Modules\Receipts\Models\Receipt;
/**
* Central payment processor — EVERY payment in the system goes through here.
* Creates payment + receipt in a single transaction.
*
* Payment Types:
* form_fee رسوم استمارة 505 ج.م
* membership_fee قيمة العضوية 150,000 / 225,000 / 300,000
* addition_fee رسوم إضافة (زوجة/ابن/مؤقت)
* annual_subscription اشتراك سنوي 492 + 35 تنمية = 527
* development_fee رسوم تنمية 35 ج.م
* down_payment مقدم تقسيط 25% minimum
* installment قسط شهري
* fine غرامة مخالفة 1,000 - 10,000
* separation_fee رسوم فصل 30%/20%/15%/10%/5%/2.5%
* divorce_fee رسوم طلاق 10% or 50%
* death_fee رسوم وفاة (استمارة + اشتراك)
* waiver_fee رسوم تنازل 30%
* carnet_replacement بدل فاقد كارنيه
* seasonal_fee رسوم عضوية موسمية
* sports_conversion رسوم تحويل رياضي 50%
* other أخرى
*/
final class PaymentService final class PaymentService
{ {
/** /**
* Process a payment: create payment record + receipt, fire events. * Process a payment and create receipt.
* Returns ['success' => bool, 'payment_id' => int, 'receipt_id' => int, 'receipt_number' => string]
*/ */
public static function processPayment(array $data): array public static function processPayment(array $data): array
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee(); $employee = App::getInstance()->currentEmployee();
// Validate required fields
$memberId = (int) ($data['member_id'] ?? 0); $memberId = (int) ($data['member_id'] ?? 0);
$paymentType = trim($data['payment_type'] ?? '');
$amount = $data['amount'] ?? '0.00'; $amount = $data['amount'] ?? '0.00';
$method = $data['payment_method'] ?? 'cash'; $paymentType = $data['payment_type'] ?? '';
$paymentMethod = $data['payment_method'] ?? 'cash';
$description = $data['description'] ?? ''; $description = $data['description'] ?? '';
$notes = $data['notes'] ?? null;
if ($memberId <= 0 || $paymentType === '' || bccomp($amount, '0.00', 2) <= 0) { if ($memberId <= 0) {
return ['success' => false, 'error' => 'بيانات غير مكتملة']; return ['success' => false, 'error' => 'العضو غير محدد'];
}
if (bccomp((string) $amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر'];
}
if ($paymentType === '') {
return ['success' => false, 'error' => 'نوع الدفعة مطلوب'];
} }
$member = $db->selectOne("SELECT * FROM members WHERE id = ? AND is_archived = 0", [$memberId]); // Verify member exists
$member = $db->selectOne("SELECT id, full_name_ar, form_number FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) { if (!$member) {
return ['success' => false, 'error' => 'العضو غير موجود']; return ['success' => false, 'error' => 'العضو غير موجود'];
} }
$db->beginTransaction(); $db->beginTransaction();
try { try {
// Generate receipt number
$receiptNumber = self::generateReceiptNumber();
// Create payment record // Create payment record
$paymentId = $db->insert('payments', [ $paymentId = $db->insert('payments', [
'member_id' => $memberId, 'member_id' => $memberId,
'payment_type' => $paymentType, 'payment_type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
'currency' => 'EGP', 'currency' => $data['currency'] ?? 'EGP',
'payment_method' => $method, 'payment_method' => $paymentMethod,
'check_number' => $data['check_number'] ?? null, 'check_number' => $data['check_number'] ?? null,
'check_bank' => $data['check_bank'] ?? null, 'check_bank' => $data['check_bank'] ?? null,
'check_date' => !empty($data['check_date']) ? $data['check_date'] : null, 'check_date' => $data['check_date'] ?? null,
'check_status' => $method === 'check' ? 'pending' : null, 'check_status' => $paymentMethod === 'check' ? 'pending' : null,
'visa_reference' => $data['visa_reference'] ?? null, 'visa_reference' => $data['visa_reference'] ?? null,
'transfer_reference' => $data['transfer_reference'] ?? null, 'transfer_reference' => $data['transfer_reference'] ?? null,
'transfer_bank' => $data['transfer_bank'] ?? null, 'transfer_bank' => $data['transfer_bank'] ?? null,
'related_entity_type' => $data['related_entity_type'] ?? null, 'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => !empty($data['related_entity_id']) ? (int) $data['related_entity_id'] : null, 'related_entity_id' => $data['related_entity_id'] ?? null,
'notes' => $notes, 'notes' => $data['notes'] ?? $description,
'payment_date' => date('Y-m-d'), 'payment_date' => $data['payment_date'] ?? date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null, 'received_by_employee_id' => $employee ? (int) $employee->id : null,
'is_voided' => 0, 'is_voided' => 0,
'created_at' => date('Y-m-d H:i:s'), 'created_at' => date('Y-m-d H:i:s'),
...@@ -62,45 +93,51 @@ final class PaymentService ...@@ -62,45 +93,51 @@ final class PaymentService
'created_by' => $employee ? (int) $employee->id : null, 'created_by' => $employee ? (int) $employee->id : null,
]); ]);
// Generate receipt // Auto-generate description if empty
$receiptNumber = Receipt::generateNumber();
$amountWords = self::amountToArabicWords($amount);
if ($description === '') { if ($description === '') {
$description = Payment::getPaymentTypeLabel($paymentType) . ' — ' . ($member['full_name_ar'] ?? ''); $description = self::getPaymentTypeLabel($paymentType);
if ($member['form_number']) {
$description .= ' — استمارة ' . $member['form_number'];
}
} }
// Create receipt
$receiptId = $db->insert('receipts', [ $receiptId = $db->insert('receipts', [
'receipt_number' => $receiptNumber, 'receipt_number' => $receiptNumber,
'member_id' => $memberId, 'member_id' => $memberId,
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'receipt_type' => 'payment', 'receipt_type' => 'payment',
'amount' => $amount, 'amount' => $amount,
'amount_in_words_ar' => $amountWords, 'amount_in_words_ar' => number_to_arabic_words((float) $amount),
'description_ar' => $description, 'description_ar' => $description,
'issued_by_employee_id' => $employee ? (int) $employee->id : null, 'issued_by_employee_id' => $employee ? (int) $employee->id : null,
'issued_at' => date('Y-m-d H:i:s'), 'issued_at' => date('Y-m-d H:i:s'),
'is_voided' => 0, 'is_voided' => 0,
'print_count' => 0,
'created_at' => date('Y-m-d H:i:s'), 'created_at' => date('Y-m-d H:i:s'),
]); ]);
// Link receipt to payment // Link receipt to payment
$db->update('payments', ['receipt_id' => $receiptId, 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [$paymentId]); $db->update('payments', ['receipt_id' => $receiptId], '`id` = ?', [$paymentId]);
$db->commit(); $db->commit();
// Fire event AFTER commit // Fire event
EventBus::dispatch('payment.completed', [ EventBus::dispatch('payment.completed', [
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'receipt_id' => $receiptId, 'receipt_id' => $receiptId,
'receipt_number'=> $receiptNumber, 'receipt_number' => $receiptNumber,
'member_id' => $memberId, 'member_id' => $memberId,
'payment_type' => $paymentType, 'type' => $paymentType,
'amount' => $amount, 'amount' => $amount,
'method' => $method, 'method' => $paymentMethod,
]); ]);
Logger::info("Payment processed: #{$paymentId}, Receipt: {$receiptNumber}", [ Logger::info("Payment processed", [
'member_id' => $memberId, 'type' => $paymentType, 'amount' => $amount, 'payment_id' => $paymentId,
'member_id' => $memberId,
'type' => $paymentType,
'amount' => $amount,
]); ]);
return [ return [
...@@ -108,12 +145,17 @@ final class PaymentService ...@@ -108,12 +145,17 @@ final class PaymentService
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'receipt_id' => $receiptId, 'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber, 'receipt_number' => $receiptNumber,
'amount' => $amount,
]; ];
} catch (\Throwable $e) { } catch (\Throwable $e) {
$db->rollBack(); $db->rollBack();
Logger::error("Payment processing failed: " . $e->getMessage()); Logger::error("Payment failed: " . $e->getMessage(), [
return ['success' => false, 'error' => 'فشل في معالجة الدفع: ' . $e->getMessage()]; 'member_id' => $memberId,
'type' => $paymentType,
'amount' => $amount,
]);
return ['success' => false, 'error' => 'فشل تسجيل الدفع: ' . $e->getMessage()];
} }
} }
...@@ -125,41 +167,47 @@ final class PaymentService ...@@ -125,41 +167,47 @@ final class PaymentService
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee(); $employee = App::getInstance()->currentEmployee();
$payment = $db->selectOne("SELECT * FROM payments WHERE id = ?", [$paymentId]); $payment = $db->selectOne("SELECT * FROM payments WHERE id = ? AND is_voided = 0", [$paymentId]);
if (!$payment) { if (!$payment) {
return ['success' => false, 'error' => 'الدفعة غير موجودة']; return ['success' => false, 'error' => 'الدفعة غير موجودة أو ملغاة بالفعل'];
}
if ($payment['is_voided']) {
return ['success' => false, 'error' => 'الدفعة ملغاة بالفعل'];
} }
// Check 24h rule // Check 24-hour void window (unless super admin)
$canVoid = self::canVoid($paymentId); $hoursSince = (time() - strtotime($payment['created_at'])) / 3600;
if (!$canVoid['allowed']) { $isSuperAdmin = false;
return ['success' => false, 'error' => $canVoid['reason']]; if ($employee) {
try {
$sa = $db->selectOne(
"SELECT 1 FROM employee_roles er JOIN roles r ON r.id = er.role_id
WHERE er.employee_id = ? AND r.role_code = 'super_admin' AND er.is_active = 1 LIMIT 1",
[(int) $employee->id]
);
$isSuperAdmin = ($sa !== null);
} catch (\Throwable $e) {}
} }
$now = date('Y-m-d H:i:s'); if ($hoursSince > 24 && !$isSuperAdmin) {
$empId = $employee ? (int) $employee->id : null; return ['success' => false, 'error' => 'لا يمكن إلغاء الدفعة بعد 24 ساعة — يتطلب صلاحية مدير النظام'];
}
$db->beginTransaction(); $db->beginTransaction();
try { try {
// Void payment // Void the payment
$db->update('payments', [ $db->update('payments', [
'is_voided' => 1, 'is_voided' => 1,
'voided_at' => $now, 'voided_at' => date('Y-m-d H:i:s'),
'voided_by' => $empId, 'voided_by' => $employee ? (int) $employee->id : null,
'void_reason'=> $reason, 'void_reason' => $reason,
'updated_at' => $now, 'updated_at' => date('Y-m-d H:i:s'),
], '`id` = ?', [$paymentId]); ], '`id` = ?', [$paymentId]);
// Void linked receipt // Void the receipt
if ($payment['receipt_id']) { if ($payment['receipt_id']) {
$db->update('receipts', [ $db->update('receipts', [
'is_voided' => 1, 'is_voided' => 1,
'voided_at' => $now, 'voided_at' => date('Y-m-d H:i:s'),
'voided_by' => $empId, 'voided_by' => $employee ? (int) $employee->id : null,
'void_reason'=> $reason, 'void_reason' => $reason,
], '`id` = ?', [(int) $payment['receipt_id']]); ], '`id` = ?', [(int) $payment['receipt_id']]);
} }
...@@ -168,158 +216,103 @@ final class PaymentService ...@@ -168,158 +216,103 @@ final class PaymentService
EventBus::dispatch('payment.voided', [ EventBus::dispatch('payment.voided', [
'payment_id' => $paymentId, 'payment_id' => $paymentId,
'member_id' => (int) $payment['member_id'], 'member_id' => (int) $payment['member_id'],
'type' => $payment['payment_type'],
'amount' => $payment['amount'], 'amount' => $payment['amount'],
'reason' => $reason, 'reason' => $reason,
]); ]);
Logger::info("Payment #{$paymentId} voided", ['reason' => $reason]);
return ['success' => true]; return ['success' => true];
} catch (\Throwable $e) { } catch (\Throwable $e) {
$db->rollBack(); $db->rollBack();
return ['success' => false, 'error' => 'فشل في إلغاء الدفعة']; return ['success' => false, 'error' => 'فشل الإلغاء: ' . $e->getMessage()];
} }
} }
/** /**
* Check if a payment can be voided (24h rule). * Check if a specific payment type has been paid for a member.
*/ */
public static function canVoid(int $paymentId): array public static function hasPaid(int $memberId, string $paymentType, ?string $relatedEntityType = null, ?int $relatedEntityId = null): bool
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee(); $where = 'member_id = ? AND payment_type = ? AND is_voided = 0';
$params = [$memberId, $paymentType];
$payment = $db->selectOne("SELECT * FROM payments WHERE id = ?", [$paymentId]);
if (!$payment) return ['allowed' => false, 'reason' => 'الدفعة غير موجودة'];
if ($payment['is_voided']) return ['allowed' => false, 'reason' => 'ملغاة بالفعل'];
// Super admin can always void if ($relatedEntityType !== null) {
if ($employee) { $where .= ' AND related_entity_type = ?';
$isSuperAdmin = $db->selectOne( $params[] = $relatedEntityType;
"SELECT 1 FROM employee_roles er JOIN roles r ON r.id = er.role_id WHERE er.employee_id = ? AND r.role_code = 'super_admin' AND er.is_active = 1",
[(int) $employee->id]
);
if ($isSuperAdmin) return ['allowed' => true, 'reason' => ''];
} }
if ($relatedEntityId !== null) {
// 24h rule $where .= ' AND related_entity_id = ?';
$createdAt = strtotime($payment['created_at']); $params[] = $relatedEntityId;
$hoursElapsed = (time() - $createdAt) / 3600;
if ($hoursElapsed > 24) {
return ['allowed' => false, 'reason' => 'مضى أكثر من 24 ساعة. يلزم صلاحية المدير العام للإلغاء'];
} }
return ['allowed' => true, 'reason' => '']; $row = $db->selectOne("SELECT COUNT(*) as cnt FROM payments WHERE {$where}", $params);
return (int) ($row['cnt'] ?? 0) > 0;
} }
/** /**
* Get daily report data. * Get total paid for a member of a specific type.
*/ */
public static function getDailyReport(string $date, ?int $branchId = null): array public static function totalPaid(int $memberId, ?string $paymentType = null): string
{ {
$db = App::getInstance()->db(); $db = App::getInstance()->db();
$where = 'p.payment_date = ? AND p.is_voided = 0'; $where = 'member_id = ? AND is_voided = 0';
$params = [$date]; $params = [$memberId];
if ($branchId) {
$where .= ' AND m.branch_id = ?';
$params[] = $branchId;
}
$payments = $db->select(
"SELECT p.*, m.full_name_ar as member_name, m.membership_number, r.receipt_number
FROM payments p
JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}
ORDER BY p.id ASC",
$params
);
$totals = Payment::getDailyTotals($date, $branchId);
$voidedCount = $db->selectOne( if ($paymentType !== null) {
"SELECT COUNT(*) as cnt, COALESCE(SUM(p.amount),0) as total $where .= ' AND payment_type = ?';
FROM payments p JOIN members m ON m.id = p.member_id $params[] = $paymentType;
WHERE p.payment_date = ? AND p.is_voided = 1" . ($branchId ? ' AND m.branch_id = ?' : ''), }
$branchId ? [$date, $branchId] : [$date]
);
return [ $row = $db->selectOne("SELECT COALESCE(SUM(amount), 0) as total FROM payments WHERE {$where}", $params);
'date' => $date, return $row['total'] ?? '0.00';
'branch_id' => $branchId,
'payments' => $payments,
'totals' => $totals,
'voided' => $voidedCount,
];
} }
/** /**
* Convert numeric amount to Arabic words for receipt. * Generate unique receipt number.
*/ */
public static function amountToArabicWords(string $amount): string public static function generateReceiptNumber(): string
{ {
$amount = (float) $amount; $db = App::getInstance()->db();
$intPart = (int) floor($amount); $year = date('Y');
$decPart = (int) round(($amount - $intPart) * 100); $prefix = 'REC-' . $year . '-';
$last = $db->selectOne(
$ones = ['', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية', 'تسعة', "SELECT receipt_number FROM receipts WHERE receipt_number LIKE ? ORDER BY id DESC LIMIT 1",
'عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر', [$prefix . '%']
'ستة عشر', 'سبعة عشر', 'ثمانية عشر', 'تسعة عشر']; );
$tens = ['', '', 'عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون']; if ($last) {
$hundreds = ['', 'مائة', 'مائتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة', 'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة']; $parts = explode('-', $last['receipt_number']);
$seq = (int) end($parts) + 1;
$convertGroup = function (int $n) use ($ones, $tens, $hundreds): string {
if ($n === 0) return '';
$parts = [];
$h = (int) floor($n / 100);
$remainder = $n % 100;
if ($h > 0) $parts[] = $hundreds[$h];
if ($remainder > 0 && $remainder < 20) {
$parts[] = $ones[$remainder];
} elseif ($remainder >= 20) {
$t = (int) floor($remainder / 10);
$o = $remainder % 10;
if ($o > 0) {
$parts[] = $ones[$o] . ' و' . $tens[$t];
} else {
$parts[] = $tens[$t];
}
}
return implode(' و', $parts);
};
$result = '';
if ($intPart === 0) {
$result = 'صفر';
} else { } else {
$groups = []; $seq = 1;
$millions = (int) floor($intPart / 1000000);
$thousands = (int) floor(($intPart % 1000000) / 1000);
$remainder = $intPart % 1000;
if ($millions > 0) {
if ($millions === 1) $groups[] = 'مليون';
elseif ($millions === 2) $groups[] = 'مليونان';
elseif ($millions <= 10) $groups[] = $convertGroup($millions) . ' ملايين';
else $groups[] = $convertGroup($millions) . ' مليون';
}
if ($thousands > 0) {
if ($thousands === 1) $groups[] = 'ألف';
elseif ($thousands === 2) $groups[] = 'ألفان';
elseif ($thousands <= 10) $groups[] = $convertGroup($thousands) . ' آلاف';
else $groups[] = $convertGroup($thousands) . ' ألف';
} }
if ($remainder > 0) { return $prefix . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
$groups[] = $convertGroup($remainder);
}
$result = implode(' و', $groups);
} }
$result .= ' جنيه مصري'; /**
if ($decPart > 0) { * Get Arabic label for payment type.
$result .= ' و' . $convertGroup($decPart) . ' قرش'; */
} public static function getPaymentTypeLabel(string $type): string
return $result . ' فقط لا غير'; {
return match ($type) {
'form_fee' => 'رسوم استمارة',
'membership_fee' => 'قيمة العضوية',
'addition_fee' => 'رسوم إضافة',
'annual_subscription' => 'اشتراك سنوي',
'development_fee' => 'رسوم تنمية',
'down_payment' => 'مقدم تقسيط',
'installment' => 'قسط شهري',
'fine' => 'غرامة',
'separation_fee' => 'رسوم فصل',
'divorce_fee' => 'رسوم طلاق',
'death_fee' => 'رسوم نقل وفاة',
'waiver_fee' => 'رسوم تنازل',
'carnet_replacement' => 'بدل فاقد كارنيه',
'seasonal_fee' => 'رسوم عضوية موسمية',
'sports_conversion' => 'رسوم تحويل رياضي',
'other' => 'أخرى',
default => $type,
};
} }
} }
\ No newline at end of file
...@@ -2,77 +2,86 @@ ...@@ -2,77 +2,86 @@
<?php $__template->section('title'); ?>التقرير اليومي — <?= e($date) ?><?php $__template->endSection(); ?> <?php $__template->section('title'); ?>التقرير اليومي — <?= e($date) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<!-- Date Filter -->
<div class="card" style="margin-bottom:20px;padding:15px;"> <div class="card" style="margin-bottom:20px;padding:15px;">
<form method="GET" action="/payments/daily-report" style="display:flex;gap:10px;align-items:end;"> <form method="GET" action="/payments/daily-report" style="display:flex;gap:10px;align-items:end;">
<div><label class="form-label" style="font-size:12px;">التاريخ</label><input type="date" name="date" value="<?= e($date) ?>" class="form-input" required></div> <div class="form-group">
<div><label class="form-label" style="font-size:12px;">الفرع</label> <label class="form-label" style="font-size:12px;">التاريخ</label>
<select name="branch_id" class="form-select"><option value="">جميع الفروع</option> <input type="date" name="date" value="<?= e($date) ?>" class="form-input">
<?php foreach ($branches as $b): ?><option value="<?= (int) $b['id'] ?>" <?= $branchId == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option><?php endforeach; ?> </div>
<div class="form-group">
<label class="form-label" style="font-size:12px;">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">كل الفروع</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= $branchId == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
<button type="submit" class="btn btn-outline">عرض</button> <button type="submit" class="btn btn-primary">عرض التقرير</button>
</form> </form>
</div> </div>
<?php $totals = $report['totals'] ?? []; ?> <!-- Summary Cards -->
<?php $grand = $totals['grand_total'] ?? ['count' => 0, 'total' => '0.00']; ?> <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;margin-bottom:20px;">
<?php $voided = $report['voided'] ?? ['cnt' => 0, 'total' => '0.00']; ?> <div class="card" style="padding:20px;text-align:center;background:#F0FDF4;border:1px solid #BBF7D0;">
<div style="font-size:32px;font-weight:700;color:#059669;"><?= money($report['grand_total']['total'] ?? '0') ?></div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-bottom:20px;"> <div style="color:#6B7280;font-size:14px;">إجمالي التحصيل</div>
<div style="background:#F0FDF4;padding:20px;border-radius:8px;text-align:center;"> <div style="color:#9CA3AF;font-size:12px;margin-top:5px;"><?= (int) ($report['grand_total']['count'] ?? 0) ?> دفعة</div>
<div style="font-size:28px;font-weight:700;color:#059669;"><?= money($grand['total'] ?? '0') ?></div>
<div style="color:#6B7280;">إجمالي التحصيل (<?= (int) ($grand['count'] ?? 0) ?> عملية)</div>
</div>
<div style="background:#FEF2F2;padding:20px;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#DC2626;"><?= money($voided['total'] ?? '0') ?></div>
<div style="color:#6B7280;">إيصالات ملغاة (<?= (int) ($voided['cnt'] ?? 0) ?>)</div>
</div> </div>
<div style="background:#EFF6FF;padding:20px;border-radius:8px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0284C7;"><?= money(bcsub($grand['total'] ?? '0', $voided['total'] ?? '0', 2)) ?></div>
<div style="color:#6B7280;">الصافي</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:20px;">
<div class="card" style="padding:20px;"> <div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">حسب النوع</h4> <h4 style="color:#0D7377;margin:0 0 10px;font-size:14px;">حسب النوع</h4>
<table style="width:100%;font-size:14px;"> <?php foreach ($report['by_type'] as $t): ?>
<?php foreach ($totals['by_type'] ?? [] as $t): ?> <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;">
<tr><td style="padding:6px 0;"><?= e(\App\Modules\Payments\Models\Payment::getPaymentTypeLabel($t['payment_type'])) ?></td><td style="padding:6px 0;text-align:left;"><?= (int) $t['count'] ?> عملية</td><td style="padding:6px 0;font-weight:700;direction:ltr;text-align:left;"><?= money($t['total']) ?></td></tr> <span><?= e(\App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($t['payment_type'])) ?></span>
<strong><?= money($t['total']) ?> <small style="color:#9CA3AF;">(<?= (int) $t['count'] ?>)</small></strong>
</div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($totals['by_type'])): ?><tr><td colspan="3" style="text-align:center;color:#6B7280;padding:15px;">لا توجد بيانات</td></tr><?php endif; ?>
</table>
</div> </div>
<div class="card" style="padding:20px;"> <div class="card" style="padding:20px;">
<h4 style="color:#0D7377;margin-bottom:15px;">حسب طريقة الدفع</h4> <h4 style="color:#0D7377;margin:0 0 10px;font-size:14px;">حسب طريقة الدفع</h4>
<table style="width:100%;font-size:14px;"> <?php foreach ($report['by_method'] as $m): ?>
<?php foreach ($totals['by_method'] ?? [] as $m): ?> <div style="display:flex;justify-content:space-between;padding:4px 0;font-size:13px;">
<tr><td style="padding:6px 0;"><?= e(\App\Modules\Payments\Models\Payment::getPaymentMethodLabel($m['payment_method'])) ?></td><td style="padding:6px 0;text-align:left;"><?= (int) $m['count'] ?> عملية</td><td style="padding:6px 0;font-weight:700;direction:ltr;text-align:left;"><?= money($m['total']) ?></td></tr> <span><?= e(\App\Modules\Payments\Models\Payment::getPaymentMethodLabel($m['payment_method'])) ?></span>
<strong><?= money($m['total']) ?> <small style="color:#9CA3AF;">(<?= (int) $m['count'] ?>)</small></strong>
</div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($totals['by_method'])): ?><tr><td colspan="3" style="text-align:center;color:#6B7280;padding:15px;">لا توجد بيانات</td></tr><?php endif; ?>
</table>
</div> </div>
</div> </div>
<!-- Individual Payments Table -->
<div class="card"> <div class="card">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">تفاصيل العمليات</h3></div> <div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;">
<h3 style="margin:0;color:#0D7377;">تفاصيل الدفعات</h3>
<button onclick="window.print()" class="btn btn-sm btn-outline">🖨️ طباعة</button>
</div>
<div class="table-responsive"> <div class="table-responsive">
<table class="data-table"> <table class="data-table">
<thead><tr><th>#</th><th>الإيصال</th><th>العضو</th><th>النوع</th><th>الطريقة</th><th>المبلغ</th></tr></thead> <thead><tr><th>#</th><th>الإيصال</th><th>العضو</th><th>النوع</th><th>الطريقة</th><th>المبلغ</th></tr></thead>
<tbody> <tbody>
<?php foreach ($report['payments'] ?? [] as $i => $p): ?> <?php $grandTotal = '0.00'; ?>
<?php foreach ($payments as $i => $p): ?>
<?php $grandTotal = bcadd($grandTotal, $p['amount'], 2); ?>
<tr> <tr>
<td><?= $i + 1 ?></td> <td><?= $i + 1 ?></td>
<td style="font-size:12px;direction:ltr;text-align:right;"><?= e($p['receipt_number'] ?? '—') ?></td> <td style="direction:ltr;text-align:right;font-weight:600;"><?= e($p['receipt_number'] ?? '—') ?></td>
<td><a href="/members/<?= (int) $p['member_id'] ?>" style="color:#0D7377;"><?= e($p['member_name']) ?></a></td> <td><a href="/members/<?= (int) $p['member_id'] ?>" style="color:#0D7377;"><?= e($p['member_name']) ?></a></td>
<td style="font-size:13px;"><?= e(\App\Modules\Payments\Models\Payment::getPaymentTypeLabel($p['payment_type'])) ?></td> <td style="font-size:13px;"><?= e(\App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($p['payment_type'])) ?></td>
<td style="font-size:13px;"><?= e(\App\Modules\Payments\Models\Payment::getPaymentMethodLabel($p['payment_method'])) ?></td> <td style="font-size:13px;"><?= e(\App\Modules\Payments\Models\Payment::getPaymentMethodLabel($p['payment_method'])) ?></td>
<td style="font-weight:700;direction:ltr;text-align:right;"><?= money($p['amount']) ?></td> <td style="font-weight:700;direction:ltr;text-align:right;"><?= money($p['amount']) ?></td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($report['payments'])): ?><tr><td colspan="6" style="text-align:center;padding:40px;color:#6B7280;">لا توجد عمليات في هذا اليوم</td></tr><?php endif; ?> <?php if (empty($payments)): ?>
<tr><td colspan="6" style="text-align:center;padding:40px;color:#6B7280;">لا توجد دفعات في هذا التاريخ</td></tr>
<?php else: ?>
<tr style="background:#F0FDF4;font-weight:700;">
<td colspan="5" style="padding:12px 15px;">الإجمالي</td>
<td style="padding:12px 15px;direction:ltr;text-align:right;font-size:18px;color:#059669;"><?= money($grandTotal) ?></td>
</tr>
<?php endif; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
\ No newline at end of file
<?php $__template->layout('Layout.main'); ?> <?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تحصيل دفعة — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?> <?php $__template->section('title'); ?>تسجيل دفعة — <?= e($member['full_name_ar']) ?><?php $__template->endSection(); ?>
<?php $__template->section('content'); ?> <?php $__template->section('content'); ?>
<div class="card" style="margin-bottom:15px;padding:15px;display:flex;justify-content:space-between;align-items:center;"> <!-- Member Info Banner -->
<div class="card" style="margin-bottom:20px;padding:20px;display:flex;justify-content:space-between;align-items:center;">
<div> <div>
<strong>العضو:</strong> <?= e($member['full_name_ar']) ?> <strong style="font-size:18px;"><?= e($member['full_name_ar']) ?></strong>
&nbsp;|&nbsp; <strong>رقم العضوية:</strong> <?= e($member['membership_number'] ?? 'لم يُحدد') ?> <div style="font-size:13px;color:#6B7280;margin-top:5px;">
&nbsp;|&nbsp; <strong>قيمة العضوية:</strong> <?= money($member['membership_value'] ?? '0') ?> 📋 استمارة: <?= e($member['form_number'] ?? '—') ?>
&nbsp;|&nbsp; <strong>الحالة:</strong> <?= e($member['status']) ?> <?php if ($member['membership_number']): ?>
&nbsp;|&nbsp; 🪪 عضوية: <?= e($member['membership_number']) ?>
<?php endif; ?>
&nbsp;|&nbsp; الحالة: <strong style="color:<?= match($member['status']) { 'active' => '#059669', 'accepted' => '#059669', default => '#D97706' } ?>;"><?= e($member['status']) ?></strong>
</div>
</div> </div>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a> <a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العودة للعضو</a>
</div> </div>
<?php if (!empty($summary)): ?> <!-- Financial Summary -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:20px;"> <div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));gap:15px;margin-bottom:20px;">
<div style="background:#F0FDF4;padding:15px;border-radius:8px;text-align:center;"> <div class="card" style="padding:15px;text-align:center;border-right:4px solid #0D7377;">
<div style="font-size:20px;font-weight:700;color:#0D7377;"><?= money($summary['membership_value'] ?? '0') ?></div>
<div style="font-size:11px;color:#6B7280;">قيمة العضوية</div>
</div>
<div class="card" style="padding:15px;text-align:center;border-right:4px solid #059669;">
<div style="font-size:20px;font-weight:700;color:#059669;"><?= money($summary['total_paid'] ?? '0') ?></div> <div style="font-size:20px;font-weight:700;color:#059669;"><?= money($summary['total_paid'] ?? '0') ?></div>
<div style="color:#6B7280;font-size:12px;">إجمالي المدفوع</div> <div style="font-size:11px;color:#6B7280;">إجمالي المدفوع</div>
</div> </div>
<div style="background:#FEF2F2;padding:15px;border-radius:8px;text-align:center;"> <div class="card" style="padding:15px;text-align:center;border-right:4px solid #DC2626;">
<div style="font-size:20px;font-weight:700;color:#DC2626;"><?= money($summary['membership_outstanding'] ?? '0') ?></div> <div style="font-size:20px;font-weight:700;color:#DC2626;"><?= money($summary['total_outstanding'] ?? '0') ?></div>
<div style="color:#6B7280;font-size:12px;">المتبقي من العضوية</div> <div style="font-size:11px;color:#6B7280;">إجمالي المتبقي</div>
</div> </div>
<div style="background:#EFF6FF;padding:15px;border-radius:8px;text-align:center;"> <div class="card" style="padding:15px;text-align:center;border-right:4px solid <?= $summary['form_fee_paid'] ? '#059669' : '#DC2626' ?>;">
<div style="font-size:20px;font-weight:700;color:#0284C7;"><?= (int) ($summary['payment_count'] ?? 0) ?></div> <div style="font-size:20px;font-weight:700;color:<?= $summary['form_fee_paid'] ? '#059669' : '#DC2626' ?>;">
<div style="color:#6B7280;font-size:12px;">عدد المدفوعات</div> <?= $summary['form_fee_paid'] ? '✅' : '❌' ?>
</div> </div>
<div style="background:#FFF7ED;padding:15px;border-radius:8px;text-align:center;"> <div style="font-size:11px;color:#6B7280;">رسوم الاستمارة</div>
<div style="font-size:20px;font-weight:700;color:#D97706;"><?= count($outstandingItems) ?></div>
<div style="color:#6B7280;font-size:12px;">بنود مستحقة</div>
</div> </div>
</div> </div>
<?php endif; ?>
<form method="POST" action="/payments/process/<?= (int) $member['id'] ?>" id="payment-form">
<?= csrf_field() ?>
<div class="card" style="margin-bottom:20px;"> <!-- Payment Options -->
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;"><h3 style="margin:0;color:#0D7377;">تفاصيل الدفعة</h3></div> <div class="card" style="margin-bottom:20px;">
<div style="padding:15px 20px;border-bottom:1px solid #E5E7EB;">
<h3 style="margin:0;color:#0D7377;">💰 اختر نوع الدفعة</h3>
</div>
<div style="padding:20px;"> <div style="padding:20px;">
<?php foreach ($paymentOptions as $i => $opt): ?>
<div style="border:2px solid #E5E7EB;border-radius:10px;padding:20px;margin-bottom:15px;<?= $opt['required'] ?? false ? 'border-color:#DC2626;background:#FEF2F2;' : '' ?>">
<form method="POST" action="/payments/process/<?= (int) $member['id'] ?>">
<?= csrf_field() ?>
<input type="hidden" name="payment_type" value="<?= e($opt['type']) ?>">
<?php if (!empty($outstandingItems)): ?> <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<div style="margin-bottom:20px;"> <div>
<label class="form-label" style="font-weight:700;">البنود المستحقة (اختر للدفع)</label> <h4 style="margin:0;color:#1A1A2E;"><?= e($opt['label']) ?></h4>
<div style="display:grid;gap:10px;margin-top:10px;"> <p style="margin:3px 0 0;font-size:13px;color:#6B7280;"><?= e($opt['description']) ?></p>
<?php foreach ($outstandingItems as $i => $item): ?> <?php if ($opt['required'] ?? false): ?>
<label style="display:flex;align-items:center;gap:12px;padding:12px;border:2px solid #E5E7EB;border-radius:8px;cursor:pointer;" class="outstanding-item" data-amount="<?= e($item['outstanding']) ?>" data-type="<?= e($item['type']) ?>" data-entity-type="<?= e($item['entity_type']) ?>" data-entity-id="<?= (int) $item['entity_id'] ?>"> <span style="color:#DC2626;font-size:12px;font-weight:600;">⚠ مطلوب</span>
<input type="radio" name="selected_item" value="<?= $i ?>" style="flex-shrink:0;"> <?php endif; ?>
<div style="flex:1;"> </div>
<strong><?= e($item['label']) ?></strong> <div style="text-align:left;">
<div style="font-size:12px;color:#6B7280;">إجمالي: <?= money($item['total']) ?> — مدفوع: <?= money($item['paid']) ?></div> <div style="font-size:24px;font-weight:700;color:#0D7377;">
</div> <?= money($opt['amount']) ?>
<div style="font-weight:700;color:#DC2626;font-size:16px;"><?= money($item['outstanding']) ?></div>
</label>
<?php endforeach; ?>
</div> </div>
</div> </div>
<?php endif; ?>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group">
<label class="form-label">نوع الدفعة <span style="color:#DC2626;">*</span></label>
<select name="payment_type" id="payment_type" class="form-select" required>
<option value="">-- اختر --</option>
<option value="membership_fee">رسوم عضوية</option>
<option value="down_payment">مقدم (تقسيط)</option>
<option value="form_fee">رسوم استمارة</option>
<option value="addition_fee">رسوم إضافة</option>
<option value="development_fee">رسوم تنمية</option>
<option value="annual_subscription">اشتراك سنوي</option>
<option value="fine">غرامة</option>
<option value="carnet_replacement">بدل فاقد كارنيه</option>
<option value="other">أخرى</option>
</select>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:10px;align-items:end;">
<!-- Amount -->
<div class="form-group"> <div class="form-group">
<label class="form-label">المبلغ (ج.م) <span style="color:#DC2626;">*</span></label> <label class="form-label">المبلغ</label>
<input type="number" name="amount" id="payment_amount" class="form-input" step="0.01" min="0.01" required style="direction:ltr;text-align:left;font-size:18px;font-weight:700;"> <?php if (!empty($opt['is_variable'])): ?>
<small style="color:#6B7280;">المبلغ من الكتالوج — لا يُدخل يدوياً</small> <input type="number" name="amount" value="<?= e($opt['amount']) ?>" class="form-input" step="0.01" min="<?= e($opt['min_amount'] ?? '0.01') ?>" required style="direction:ltr;text-align:left;font-weight:700;">
<?php else: ?>
<input type="number" name="amount" value="<?= e($opt['amount']) ?>" class="form-input" step="0.01" readonly style="direction:ltr;text-align:left;font-weight:700;background:#F3F4F6;">
<?php endif; ?>
</div> </div>
<!-- Payment Method -->
<div class="form-group"> <div class="form-group">
<label class="form-label">طريقة الدفع <span style="color:#DC2626;">*</span></label> <label class="form-label">طريقة الدفع</label>
<select name="payment_method" id="payment_method" class="form-select" required> <select name="payment_method" class="form-select">
<?php foreach ($paymentMethods as $pm): ?> <option value="cash">نقدي 💵</option>
<option value="<?= e($pm['code']) ?>"><?= e($pm['name_ar']) ?></option> <option value="visa">فيزا 💳</option>
<?php endforeach; ?> <option value="bank_transfer">تحويل بنكي 🏦</option>
<option value="check">شيك 📄</option>
</select> </select>
</div> </div>
<div class="form-group" id="check-fields" style="display:none;grid-column:1/-1;">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:15px;padding:15px;background:#F9FAFB;border-radius:8px;">
<div class="form-group"><label class="form-label">رقم الشيك</label><input type="text" name="check_number" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">البنك</label><input type="text" name="check_bank" class="form-input"></div>
<div class="form-group"><label class="form-label">تاريخ الشيك</label><input type="date" name="check_date" class="form-input"></div>
</div>
</div>
<div class="form-group" id="visa-fields" style="display:none;">
<label class="form-label">مرجع العملية (فيزا)</label>
<input type="text" name="visa_reference" class="form-input" style="direction:ltr;text-align:left;">
</div>
<div class="form-group" id="transfer-fields" style="display:none;grid-column:1/-1;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;">
<div class="form-group"><label class="form-label">مرجع التحويل</label><input type="text" name="transfer_reference" class="form-input" style="direction:ltr;text-align:left;"></div>
<div class="form-group"><label class="form-label">البنك</label><input type="text" name="transfer_bank" class="form-input"></div>
</div>
</div>
</div>
<input type="hidden" name="related_entity_type" id="related_entity_type"> <!-- Notes -->
<input type="hidden" name="related_entity_id" id="related_entity_id"> <div class="form-group">
<div class="form-group" style="margin-top:15px;">
<label class="form-label">ملاحظات</label> <label class="form-label">ملاحظات</label>
<textarea name="notes" class="form-textarea" rows="2"></textarea> <input type="text" name="notes" class="form-input" placeholder="اختياري">
</div> </div>
<!-- Submit -->
<button type="submit" class="btn btn-primary" style="padding:9px 20px;white-space:nowrap;" onclick="return confirm('تأكيد دفع <?= e(money($opt['amount'])) ?>؟')">
💰 سجّل الدفع
</button>
</div> </div>
</form>
</div> </div>
<?php endforeach; ?>
<div style="display:flex;gap:10px;">
<button type="submit" class="btn btn-primary" style="font-size:16px;padding:12px 30px;">تأكيد الدفع وإصدار الإيصال</button>
<a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">إلغاء</a>
</div> </div>
</form> </div>
<?php $__template->endSection(); ?>
<?php $__template->section('scripts'); ?> <!-- Quick links -->
<script> <div style="display:flex;gap:10px;">
document.addEventListener('DOMContentLoaded', function() { <a href="/payments/member/<?= (int) $member['id'] ?>" class="btn btn-outline">📜 سجل المدفوعات</a>
var methodSelect = document.getElementById('payment_method'); <a href="/members/<?= (int) $member['id'] ?>" class="btn btn-outline">← العضو</a>
function toggleMethodFields() { </div>
document.getElementById('check-fields').style.display = methodSelect.value === 'check' ? 'block' : 'none';
document.getElementById('visa-fields').style.display = methodSelect.value === 'visa' ? 'block' : 'none';
document.getElementById('transfer-fields').style.display = methodSelect.value === 'bank_transfer' ? 'block' : 'none';
}
methodSelect.addEventListener('change', toggleMethodFields);
toggleMethodFields();
// When outstanding item selected, fill amount and type
document.querySelectorAll('.outstanding-item input[type="radio"]').forEach(function(radio) {
radio.addEventListener('change', function() {
var item = this.closest('.outstanding-item');
document.getElementById('payment_amount').value = item.dataset.amount;
document.getElementById('payment_type').value = item.dataset.type;
document.getElementById('related_entity_type').value = item.dataset.entityType;
document.getElementById('related_entity_id').value = item.dataset.entityId;
item.style.borderColor = '#0D7377';
document.querySelectorAll('.outstanding-item').forEach(function(el) {
if (el !== item) el.style.borderColor = '#E5E7EB';
});
});
});
});
</script>
<?php $__template->endSection(); ?> <?php $__template->endSection(); ?>
\ No newline at end of file
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