Commit 6291dcf2 authored by Mahmoud Aglan's avatar Mahmoud Aglan

test

parent 0b418d73
......@@ -12,6 +12,7 @@ use App\Modules\Accounting\Models\AccountPayable;
use App\Modules\Accounting\Models\AccountReceivable;
use App\Modules\Accounting\Services\LedgerService;
use App\Modules\Accounting\Services\FinancialReportService;
use App\Modules\Payments\Services\PaymentService;
class ReportController extends Controller
{
......@@ -40,12 +41,106 @@ class ReportController extends Controller
"SELECT COUNT(*) as cnt FROM journal_entries WHERE status = 'draft' AND is_archived = 0"
);
// ── Payment stats from the payments table ──
$paymentStats = [];
// Today totals
$todayRow = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as cnt
FROM payments WHERE payment_date = ? AND is_voided = 0",
[$today]
);
$paymentStats['today_total'] = $todayRow['total'] ?? '0.00';
$paymentStats['today_count'] = (int) ($todayRow['cnt'] ?? 0);
// Month totals
$monthRow = $db->selectOne(
"SELECT COALESCE(SUM(amount), 0) as total, COUNT(*) as cnt
FROM payments WHERE payment_date >= ? AND payment_date <= ? AND is_voided = 0",
[$monthStart, $today]
);
$paymentStats['month_total'] = $monthRow['total'] ?? '0.00';
$paymentStats['month_count'] = (int) ($monthRow['cnt'] ?? 0);
// By payment method (this month)
$methodLabels = [
'cash' => 'نقدي',
'check' => 'شيك',
'visa' => 'فيزا',
'bank_transfer' => 'تحويل بنكي',
];
$byMethod = $db->select(
"SELECT payment_method, COALESCE(SUM(amount), 0) as total, COUNT(*) as cnt
FROM payments WHERE payment_date >= ? AND payment_date <= ? AND is_voided = 0
GROUP BY payment_method ORDER BY total DESC",
[$monthStart, $today]
);
$paymentStats['by_method'] = [];
foreach ($byMethod as $m) {
$paymentStats['by_method'][] = [
'method' => $m['payment_method'],
'label' => $methodLabels[$m['payment_method']] ?? $m['payment_method'],
'total' => $m['total'],
'count' => $m['cnt'],
];
}
// By payment type (this month)
$byType = $db->select(
"SELECT payment_type, COALESCE(SUM(amount), 0) as total, COUNT(*) as cnt
FROM payments WHERE payment_date >= ? AND payment_date <= ? AND is_voided = 0
GROUP BY payment_type ORDER BY total DESC",
[$monthStart, $today]
);
$paymentStats['by_type'] = [];
foreach ($byType as $t) {
$paymentStats['by_type'][] = [
'type' => $t['payment_type'],
'label' => \App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($t['payment_type']),
'total' => $t['total'],
'count' => $t['cnt'],
];
}
// Recent payments (last 15)
$recentPayments = $db->select(
"SELECT p.*, r.receipt_number, m.full_name_ar as member_name
FROM payments p
LEFT JOIN receipts r ON r.id = p.receipt_id
LEFT JOIN members m ON m.id = p.member_id
WHERE p.is_voided = 0
ORDER BY p.id DESC LIMIT 15"
);
$formattedPayments = [];
foreach ($recentPayments as $rp) {
$guestLabel = 'زائر';
if (!$rp['member_id'] && $rp['notes']) {
// Extract guest name from notes (format: "description — guestName")
$parts = explode(' — ', $rp['notes']);
if (count($parts) > 1) {
$guestLabel = end($parts);
}
}
$formattedPayments[] = [
'receipt_number' => $rp['receipt_number'],
'payment_date' => $rp['payment_date'],
'member_id' => $rp['member_id'] ? (int) $rp['member_id'] : null,
'member_name' => $rp['member_name'],
'guest_label' => $guestLabel,
'type_label' => \App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($rp['payment_type']),
'method_label' => $methodLabels[$rp['payment_method']] ?? $rp['payment_method'],
'amount' => $rp['amount'],
];
}
return $this->view('Accounting/Views/dashboard/index', [
'fiscal_year' => $currentFY ? $currentFY->toArray() : null,
'monthly_totals' => $monthlyTotals,
'ar_summary' => $arSummary,
'ap_summary' => $apSummary,
'draft_count' => (int) ($draftCount['cnt'] ?? 0),
'payment_stats' => $paymentStats,
'recent_payments' => $formattedPayments,
]);
}
......@@ -234,4 +329,253 @@ class ReportController extends Controller
'date_to' => $dateTo,
]);
}
/**
* Treasury & Payments report — all payments across all modules.
*/
public function treasury(): Response
{
$this->authorize('accounting.reports.treasury');
$db = App::getInstance()->db();
$dateFrom = $_GET['date_from'] ?? date('Y-m-01');
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
$paymentType = $_GET['payment_type'] ?? '';
$paymentMethod = $_GET['payment_method'] ?? '';
$search = trim($_GET['search'] ?? '');
$voidedFilter = $_GET['voided'] ?? '0'; // 0 = active, 1 = voided, all = both
$where = 'p.payment_date >= ? AND p.payment_date <= ?';
$params = [$dateFrom, $dateTo];
if ($voidedFilter === '0') {
$where .= ' AND p.is_voided = 0';
} elseif ($voidedFilter === '1') {
$where .= ' AND p.is_voided = 1';
}
if ($paymentType !== '') {
$where .= ' AND p.payment_type = ?';
$params[] = $paymentType;
}
if ($paymentMethod !== '') {
$where .= ' AND p.payment_method = ?';
$params[] = $paymentMethod;
}
if ($search !== '') {
$where .= ' AND (m.full_name_ar LIKE ? OR m.form_number LIKE ? OR r.receipt_number LIKE ? OR p.notes LIKE ?)';
$searchTerm = '%' . $search . '%';
$params = array_merge($params, [$searchTerm, $searchTerm, $searchTerm, $searchTerm]);
}
// Totals
$totalsRow = $db->selectOne(
"SELECT COALESCE(SUM(p.amount), 0) as total_amount, COUNT(*) as total_count
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}",
$params
);
// Paginated results
$page = max(1, (int) ($_GET['page'] ?? 1));
$perPage = 50;
$offset = ($page - 1) * $perPage;
$totalCount = (int) ($totalsRow['total_count'] ?? 0);
$totalPages = max(1, (int) ceil($totalCount / $perPage));
$payments = $db->select(
"SELECT p.*, r.receipt_number, m.full_name_ar as member_name, m.form_number,
e.full_name_ar as received_by_name
FROM payments p
LEFT JOIN receipts r ON r.id = p.receipt_id
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN employees e ON e.id = p.received_by_employee_id
WHERE {$where}
ORDER BY p.id DESC
LIMIT {$perPage} OFFSET {$offset}",
$params
);
// Summary by method (for the filtered period)
$byMethod = $db->select(
"SELECT p.payment_method, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}
GROUP BY p.payment_method ORDER BY total DESC",
$params
);
// Summary by type (for the filtered period)
$byType = $db->select(
"SELECT p.payment_type, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN receipts r ON r.id = p.receipt_id
WHERE {$where}
GROUP BY p.payment_type ORDER BY total DESC",
$params
);
$methodLabels = [
'cash' => 'نقدي', 'check' => 'شيك', 'visa' => 'فيزا', 'bank_transfer' => 'تحويل بنكي',
];
return $this->view('Accounting/Views/reports/treasury', [
'payments' => $payments,
'total_amount' => $totalsRow['total_amount'] ?? '0.00',
'total_count' => $totalCount,
'by_method' => $byMethod,
'by_type' => $byType,
'method_labels' => $methodLabels,
'page' => $page,
'total_pages' => $totalPages,
'filters' => [
'date_from' => $dateFrom,
'date_to' => $dateTo,
'payment_type' => $paymentType,
'payment_method' => $paymentMethod,
'search' => $search,
'voided' => $voidedFilter,
],
]);
}
/**
* Revenue Analysis — breakdown by type, method, period, branch.
*/
public function revenueAnalysis(): Response
{
$this->authorize('accounting.reports.revenue_analysis');
$db = App::getInstance()->db();
$currentFY = FiscalYear::current();
$dateFrom = $_GET['date_from'] ?? ($currentFY ? $currentFY->start_date : date('Y-01-01'));
$dateTo = $_GET['date_to'] ?? date('Y-m-d');
$branchId = !empty($_GET['branch_id']) ? (int) $_GET['branch_id'] : null;
$branchWhere = '';
$params = [$dateFrom, $dateTo];
if ($branchId !== null) {
$branchWhere = ' AND m.branch_id = ?';
$params[] = $branchId;
}
// Revenue by payment type
$byType = $db->select(
"SELECT p.payment_type, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$branchWhere}
GROUP BY p.payment_type ORDER BY total DESC",
$params
);
// Revenue by payment method
$byMethod = $db->select(
"SELECT p.payment_method, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$branchWhere}
GROUP BY p.payment_method ORDER BY total DESC",
$params
);
// Monthly breakdown
$monthly = $db->select(
"SELECT DATE_FORMAT(p.payment_date, '%Y-%m') as month,
COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$branchWhere}
GROUP BY month ORDER BY month ASC",
$params
);
// Daily breakdown (current month only)
$monthStart = date('Y-m-01');
$dailyParams = [$monthStart, date('Y-m-d')];
$dailyBranchWhere = '';
if ($branchId !== null) {
$dailyBranchWhere = ' AND m.branch_id = ?';
$dailyParams[] = $branchId;
}
$daily = $db->select(
"SELECT p.payment_date as day, COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0 {$dailyBranchWhere}
GROUP BY p.payment_date ORDER BY p.payment_date ASC",
$dailyParams
);
// Grand total
$grandTotal = '0.00';
$grandCount = 0;
foreach ($byType as $t) {
$grandTotal = bcadd($grandTotal, (string) $t['total'], 2);
$grandCount += (int) $t['cnt'];
}
// Revenue by branch (if no specific branch filter)
$byBranch = [];
if ($branchId === null) {
$byBranch = $db->select(
"SELECT COALESCE(b.name_ar, 'بدون فرع') as branch_name, b.id as branch_id,
COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
LEFT JOIN branches b ON b.id = m.branch_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 0
GROUP BY b.id, b.name_ar ORDER BY total DESC",
[$dateFrom, $dateTo]
);
}
// Voided payments summary
$voidedParams = [$dateFrom, $dateTo];
if ($branchId !== null) {
$voidedParams[] = $branchId;
}
$voidedRow = $db->selectOne(
"SELECT COALESCE(SUM(p.amount), 0) as total, COUNT(*) as cnt
FROM payments p
LEFT JOIN members m ON m.id = p.member_id
WHERE p.payment_date >= ? AND p.payment_date <= ? AND p.is_voided = 1" .
($branchId !== null ? ' AND m.branch_id = ?' : ''),
$voidedParams
);
$methodLabels = [
'cash' => 'نقدي', 'check' => 'شيك', 'visa' => 'فيزا', 'bank_transfer' => 'تحويل بنكي',
];
$branches = $db->select("SELECT id, name_ar FROM branches WHERE is_active = 1 ORDER BY name_ar");
return $this->view('Accounting/Views/reports/revenue_analysis', [
'by_type' => $byType,
'by_method' => $byMethod,
'by_branch' => $byBranch,
'monthly' => $monthly,
'daily' => $daily,
'grand_total' => $grandTotal,
'grand_count' => $grandCount,
'voided_total' => $voidedRow['total'] ?? '0.00',
'voided_count' => (int) ($voidedRow['cnt'] ?? 0),
'method_labels' => $methodLabels,
'branches' => $branches,
'filters' => [
'date_from' => $dateFrom,
'date_to' => $dateTo,
'branch_id' => $branchId,
],
]);
}
}
......@@ -65,4 +65,6 @@ return [
['GET', '/accounting/reports/accounts-receivable', 'Accounting\Controllers\ReportController@accountsReceivable', ['auth'], 'accounting.reports.ar'],
['GET', '/accounting/reports/accounts-payable', 'Accounting\Controllers\ReportController@accountsPayable', ['auth'], 'accounting.reports.ap'],
['GET', '/accounting/reports/member-statement', 'Accounting\Controllers\ReportController@memberStatement', ['auth'], 'accounting.reports.member_statement'],
['GET', '/accounting/reports/treasury', 'Accounting\Controllers\ReportController@treasury', ['auth'], 'accounting.reports.treasury'],
['GET', '/accounting/reports/revenue-analysis', 'Accounting\Controllers\ReportController@revenueAnalysis', ['auth'], 'accounting.reports.revenue_analysis'],
];
......@@ -12,19 +12,19 @@
<?php endif; ?>
</div>
<!-- Quick Stats -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:25px;">
<!-- Quick Stats Row 1 -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:15px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0D7377;"><?= money($monthly_totals['total_debit'] ?? '0.00') ?></div>
<div style="color:#6B7280;margin-top:5px;">إجمالي حركة الشهر</div>
<div style="color:#6B7280;margin-top:5px;">إجمالي حركة الشهر (قيود)</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= (int)($monthly_totals['count'] ?? 0) ?></div>
<div style="color:#6B7280;margin-top:5px;">قيود الشهر الحالي</div>
<div style="font-size:28px;font-weight:700;color:#059669;"><?= money($payment_stats['month_total'] ?? '0.00') ?></div>
<div style="color:#6B7280;margin-top:5px;">إيرادات الشهر (مدفوعات)</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#DC2626;"><?= money($ar_summary['total'] ?? '0.00') ?></div>
<div style="color:#6B7280;margin-top:5px;">إجمالي المدينين</div>
<div style="font-size:28px;font-weight:700;color:#7C3AED;"><?= (int)($payment_stats['month_count'] ?? 0) ?></div>
<div style="color:#6B7280;margin-top:5px;">عدد المدفوعات هذا الشهر</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#F59E0B;"><?= $draft_count ?></div>
......@@ -32,6 +32,26 @@
</div>
</div>
<!-- Quick Stats Row 2 -->
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:25px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= money($payment_stats['today_total'] ?? '0.00') ?></div>
<div style="color:#6B7280;margin-top:5px;">إيرادات اليوم</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0284C7;"><?= (int)($payment_stats['today_count'] ?? 0) ?></div>
<div style="color:#6B7280;margin-top:5px;">عدد مدفوعات اليوم</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#DC2626;"><?= money($ar_summary['total'] ?? '0.00') ?></div>
<div style="color:#6B7280;margin-top:5px;">إجمالي المدينين (AR)</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#EA580C;"><?= money($ap_summary['total'] ?? '0.00') ?></div>
<div style="color:#6B7280;margin-top:5px;">إجمالي الدائنين (AP)</div>
</div>
</div>
<!-- Quick Actions -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:25px;">
<div class="card" style="padding:20px;">
......@@ -41,17 +61,125 @@
<a href="/accounting/reports/trial-balance" class="btn btn-outline">ميزان المراجعة</a>
<a href="/accounting/reports/income-statement" class="btn btn-outline">قائمة الدخل</a>
<a href="/accounting/reports/balance-sheet" class="btn btn-outline">الميزانية العمومية</a>
<a href="/accounting/reports/treasury" class="btn btn-outline">الخزينة والمدفوعات</a>
<a href="/accounting/reports/revenue-analysis" class="btn btn-outline">تحليل الإيرادات</a>
</div>
</div>
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 15px;">تقادم المدينين (AR Aging)</h3>
<h3 style="margin:0 0 15px;">ملخص الإيرادات — الشهر الحالي</h3>
<?php if (!empty($payment_stats['by_method'])): ?>
<table style="width:100%;font-size:14px;">
<tr><td style="color:#6B7280;">جاري</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['current_amount'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;">30 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['days_30'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;">60 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['days_60'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;">90 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['days_90'] ?? '0.00') ?></td></tr>
<tr><td style="color:#DC2626;font-weight:600;">أكثر من 90 يوم</td><td style="text-align:left;direction:ltr;color:#DC2626;font-weight:600;"><?= money($ar_summary['over_90'] ?? '0.00') ?></td></tr>
<?php foreach ($payment_stats['by_method'] as $m): ?>
<tr>
<td style="color:#6B7280;padding:3px 0;"><?= e($m['label']) ?></td>
<td style="text-align:left;direction:ltr;padding:3px 0;"><?= money($m['total']) ?></td>
<td style="text-align:left;direction:ltr;color:#6B7280;padding:3px 0;">(<?= (int) $m['count'] ?>)</td>
</tr>
<?php endforeach; ?>
</table>
<?php else: ?>
<p style="color:#9CA3AF;">لا توجد مدفوعات هذا الشهر</p>
<?php endif; ?>
</div>
</div>
<!-- Revenue by Type + AR/AP Aging -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:25px;">
<!-- Revenue by Type -->
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 15px;">الإيرادات حسب النوع — الشهر الحالي</h3>
<?php if (!empty($payment_stats['by_type'])): ?>
<div class="table-responsive">
<table style="width:100%;font-size:14px;">
<thead>
<tr style="border-bottom:1px solid #E5E7EB;">
<th style="text-align:right;padding:6px 0;color:#6B7280;">النوع</th>
<th style="text-align:left;padding:6px 0;color:#6B7280;">المبلغ</th>
<th style="text-align:left;padding:6px 0;color:#6B7280;">العدد</th>
</tr>
</thead>
<tbody>
<?php foreach ($payment_stats['by_type'] as $t): ?>
<tr style="border-bottom:1px solid #F3F4F6;">
<td style="padding:6px 0;"><?= e($t['label']) ?></td>
<td style="text-align:left;direction:ltr;padding:6px 0;font-weight:600;"><?= money($t['total']) ?></td>
<td style="text-align:left;direction:ltr;padding:6px 0;color:#6B7280;"><?= (int) $t['count'] ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#9CA3AF;">لا توجد مدفوعات هذا الشهر</p>
<?php endif; ?>
</div>
<!-- AR/AP Aging Side by Side -->
<div style="display:flex;flex-direction:column;gap:15px;">
<div class="card" style="padding:20px;flex:1;">
<h3 style="margin:0 0 10px;">تقادم المدينين (AR Aging)</h3>
<table style="width:100%;font-size:14px;">
<tr><td style="color:#6B7280;padding:3px 0;">جاري</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['current_amount'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;padding:3px 0;">30 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['days_30'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;padding:3px 0;">60 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['days_60'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;padding:3px 0;">90 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ar_summary['days_90'] ?? '0.00') ?></td></tr>
<tr><td style="color:#DC2626;font-weight:600;padding:3px 0;">أكثر من 90 يوم</td><td style="text-align:left;direction:ltr;color:#DC2626;font-weight:600;"><?= money($ar_summary['over_90'] ?? '0.00') ?></td></tr>
</table>
</div>
<div class="card" style="padding:20px;flex:1;">
<h3 style="margin:0 0 10px;">تقادم الدائنين (AP Aging)</h3>
<table style="width:100%;font-size:14px;">
<tr><td style="color:#6B7280;padding:3px 0;">جاري</td><td style="text-align:left;direction:ltr;"><?= money($ap_summary['current_amount'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;padding:3px 0;">30 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ap_summary['days_30'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;padding:3px 0;">60 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ap_summary['days_60'] ?? '0.00') ?></td></tr>
<tr><td style="color:#6B7280;padding:3px 0;">90 يوم</td><td style="text-align:left;direction:ltr;"><?= money($ap_summary['days_90'] ?? '0.00') ?></td></tr>
<tr><td style="color:#DC2626;font-weight:600;padding:3px 0;">أكثر من 90 يوم</td><td style="text-align:left;direction:ltr;color:#DC2626;font-weight:600;"><?= money($ap_summary['over_90'] ?? '0.00') ?></td></tr>
</table>
</div>
</div>
</div>
<!-- Recent Payments -->
<div class="card" style="padding:20px;margin-bottom:25px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 style="margin:0;">آخر المدفوعات</h3>
<a href="/accounting/reports/treasury" class="btn btn-outline" style="font-size:13px;">عرض الكل</a>
</div>
<?php if (!empty($recent_payments)): ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>الإيصال</th>
<th>التاريخ</th>
<th>العضو / الاسم</th>
<th>النوع</th>
<th>الطريقة</th>
<th>المبلغ</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent_payments as $p): ?>
<tr>
<td style="font-family:monospace;font-size:12px;"><?= e($p['receipt_number'] ?? '—') ?></td>
<td><?= e($p['payment_date']) ?></td>
<td>
<?php if ($p['member_id']): ?>
<a href="/members/<?= (int) $p['member_id'] ?>"><?= e($p['member_name'] ?? 'عضو #' . $p['member_id']) ?></a>
<?php else: ?>
<span style="color:#6B7280;"><?= e($p['guest_label'] ?? 'زائر') ?></span>
<?php endif; ?>
</td>
<td><span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;"><?= e($p['type_label']) ?></span></td>
<td><?= e($p['method_label']) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($p['amount']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#9CA3AF;text-align:center;padding:20px 0;">لا توجد مدفوعات حديثة</p>
<?php endif; ?>
</div>
<?php $__template->endSection(); ?>
<?php $__template->layout('Layout.main'); ?>
<?php $__template->section('title'); ?>تحليل الإيرادات<?php $__template->endSection(); ?>
<?php $__template->section('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="margin:0;">تحليل الإيرادات</h2>
<a href="/accounting" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;"></i> العودة للمحاسبة</a>
</div>
<!-- Filters -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<form method="GET" action="/accounting/reports/revenue-analysis" style="display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;">
<div style="flex:1;min-width:140px;">
<label class="form-label">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from']) ?>">
</div>
<div style="flex:1;min-width:140px;">
<label class="form-label">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to']) ?>">
</div>
<div style="flex:1;min-width:160px;">
<label class="form-label">الفرع</label>
<select name="branch_id" class="form-select">
<option value="">جميع الفروع</option>
<?php foreach ($branches as $b): ?>
<option value="<?= (int) $b['id'] ?>" <?= $filters['branch_id'] == $b['id'] ? 'selected' : '' ?>><?= e($b['name_ar']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<button type="submit" class="btn btn-primary">عرض</button>
</div>
</form>
</div>
<!-- Grand Totals -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-bottom:25px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#059669;"><?= money($grand_total) ?></div>
<div style="color:#6B7280;margin-top:5px;">إجمالي الإيرادات</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#0D7377;"><?= number_format($grand_count) ?></div>
<div style="color:#6B7280;margin-top:5px;">عدد المعاملات</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:28px;font-weight:700;color:#DC2626;"><?= money($voided_total) ?> <span style="font-size:14px;font-weight:400;">(<?= $voided_count ?>)</span></div>
<div style="color:#6B7280;margin-top:5px;">المدفوعات الملغاة</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:25px;">
<!-- Revenue by Type -->
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 15px;">الإيرادات حسب النوع</h3>
<?php if (!empty($by_type)): ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr><th>النوع</th><th>العدد</th><th>المبلغ</th><th>النسبة</th></tr>
</thead>
<tbody>
<?php foreach ($by_type as $t):
$pct = bccomp($grand_total, '0', 2) > 0 ? bcdiv(bcmul((string) $t['total'], '100', 2), $grand_total, 1) : '0.0';
?>
<tr>
<td><?= e(\App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($t['payment_type'])) ?></td>
<td><?= (int) $t['cnt'] ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($t['total']) ?></td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;">
<div style="width:<?= min(100, (float)$pct) ?>%;background:#059669;border-radius:4px;height:8px;"></div>
</div>
<span style="font-size:12px;color:#6B7280;min-width:40px;text-align:left;"><?= $pct ?>%</span>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#9CA3AF;">لا توجد بيانات</p>
<?php endif; ?>
</div>
<!-- Revenue by Method -->
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 15px;">الإيرادات حسب طريقة الدفع</h3>
<?php if (!empty($by_method)): ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr><th>الطريقة</th><th>العدد</th><th>المبلغ</th><th>النسبة</th></tr>
</thead>
<tbody>
<?php foreach ($by_method as $m):
$pct = bccomp($grand_total, '0', 2) > 0 ? bcdiv(bcmul((string) $m['total'], '100', 2), $grand_total, 1) : '0.0';
?>
<tr>
<td><?= e($method_labels[$m['payment_method']] ?? $m['payment_method']) ?></td>
<td><?= (int) $m['cnt'] ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($m['total']) ?></td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;">
<div style="width:<?= min(100, (float)$pct) ?>%;background:#7C3AED;border-radius:4px;height:8px;"></div>
</div>
<span style="font-size:12px;color:#6B7280;min-width:40px;text-align:left;"><?= $pct ?>%</span>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php else: ?>
<p style="color:#9CA3AF;">لا توجد بيانات</p>
<?php endif; ?>
</div>
</div>
<!-- Revenue by Branch -->
<?php if (!empty($by_branch)): ?>
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;">الإيرادات حسب الفرع</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr><th>الفرع</th><th>العدد</th><th>المبلغ</th><th>النسبة</th></tr>
</thead>
<tbody>
<?php foreach ($by_branch as $b):
$pct = bccomp($grand_total, '0', 2) > 0 ? bcdiv(bcmul((string) $b['total'], '100', 2), $grand_total, 1) : '0.0';
?>
<tr>
<td>
<?php if ($b['branch_id']): ?>
<a href="?<?= http_build_query(array_merge($filters, ['branch_id' => $b['branch_id']])) ?>"><?= e($b['branch_name']) ?></a>
<?php else: ?>
<span style="color:#6B7280;"><?= e($b['branch_name']) ?></span>
<?php endif; ?>
</td>
<td><?= (int) $b['cnt'] ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($b['total']) ?></td>
<td>
<div style="display:flex;align-items:center;gap:6px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;">
<div style="width:<?= min(100, (float)$pct) ?>%;background:#0284C7;border-radius:4px;height:8px;"></div>
</div>
<span style="font-size:12px;color:#6B7280;min-width:40px;text-align:left;"><?= $pct ?>%</span>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Monthly Breakdown -->
<?php if (!empty($monthly)): ?>
<div class="card" style="padding:20px;margin-bottom:25px;">
<h3 style="margin:0 0 15px;">الإيرادات الشهرية</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr><th>الشهر</th><th>العدد</th><th>المبلغ</th><th></th></tr>
</thead>
<tbody>
<?php
$maxMonthly = '0.00';
foreach ($monthly as $m) {
if (bccomp((string) $m['total'], $maxMonthly, 2) > 0) $maxMonthly = (string) $m['total'];
}
foreach ($monthly as $m):
$barPct = bccomp($maxMonthly, '0', 2) > 0 ? bcdiv(bcmul((string) $m['total'], '100', 2), $maxMonthly, 1) : '0.0';
?>
<tr>
<td style="font-family:monospace;"><?= e($m['month']) ?></td>
<td><?= (int) $m['cnt'] ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($m['total']) ?></td>
<td style="width:40%;">
<div style="background:#E5E7EB;border-radius:4px;height:20px;">
<div style="width:<?= min(100, (float)$barPct) ?>%;background:#0D7377;border-radius:4px;height:20px;display:flex;align-items:center;padding:0 8px;">
<?php if ((float)$barPct > 15): ?>
<span style="font-size:11px;color:#fff;"><?= money($m['total']) ?></span>
<?php endif; ?>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Daily Breakdown (Current Month) -->
<?php if (!empty($daily)): ?>
<div class="card" style="padding:20px;">
<h3 style="margin:0 0 15px;">الإيرادات اليومية — الشهر الحالي</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr><th>اليوم</th><th>العدد</th><th>المبلغ</th><th></th></tr>
</thead>
<tbody>
<?php
$maxDaily = '0.00';
foreach ($daily as $d) {
if (bccomp((string) $d['total'], $maxDaily, 2) > 0) $maxDaily = (string) $d['total'];
}
foreach ($daily as $d):
$barPct = bccomp($maxDaily, '0', 2) > 0 ? bcdiv(bcmul((string) $d['total'], '100', 2), $maxDaily, 1) : '0.0';
$dayName = date('D', strtotime($d['day']));
?>
<tr>
<td style="font-family:monospace;"><?= e($d['day']) ?> <span style="color:#6B7280;font-size:12px;">(<?= $dayName ?>)</span></td>
<td><?= (int) $d['cnt'] ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($d['total']) ?></td>
<td style="width:40%;">
<div style="background:#E5E7EB;border-radius:4px;height:16px;">
<div style="width:<?= min(100, (float)$barPct) ?>%;background:#059669;border-radius:4px;height:16px;"></div>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<script>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('content'); ?>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2 style="margin:0;">الخزينة والمدفوعات</h2>
<a href="/accounting" class="btn btn-outline"><i data-lucide="arrow-right" style="width:16px;height:16px;"></i> العودة للمحاسبة</a>
</div>
<!-- Filters -->
<div class="card" style="padding:20px;margin-bottom:20px;">
<form method="GET" action="/accounting/reports/treasury" style="display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;">
<div style="flex:1;min-width:140px;">
<label class="form-label">من تاريخ</label>
<input type="date" name="date_from" class="form-input" value="<?= e($filters['date_from']) ?>">
</div>
<div style="flex:1;min-width:140px;">
<label class="form-label">إلى تاريخ</label>
<input type="date" name="date_to" class="form-input" value="<?= e($filters['date_to']) ?>">
</div>
<div style="flex:1;min-width:160px;">
<label class="form-label">نوع الدفعة</label>
<select name="payment_type" class="form-select">
<option value="">الكل</option>
<?php
$typeOptions = [
'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' => 'تحويل رياضي', 'inventory_sale' => 'مبيعات مخزون',
'activity_subscription' => 'اشتراك نشاط', 'other' => 'أخرى',
];
foreach ($typeOptions as $val => $label):
?>
<option value="<?= $val ?>" <?= $filters['payment_type'] === $val ? 'selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
</div>
<div style="flex:1;min-width:120px;">
<label class="form-label">طريقة الدفع</label>
<select name="payment_method" class="form-select">
<option value="">الكل</option>
<option value="cash" <?= $filters['payment_method'] === 'cash' ? 'selected' : '' ?>>نقدي</option>
<option value="check" <?= $filters['payment_method'] === 'check' ? 'selected' : '' ?>>شيك</option>
<option value="visa" <?= $filters['payment_method'] === 'visa' ? 'selected' : '' ?>>فيزا</option>
<option value="bank_transfer" <?= $filters['payment_method'] === 'bank_transfer' ? 'selected' : '' ?>>تحويل بنكي</option>
</select>
</div>
<div style="flex:1;min-width:120px;">
<label class="form-label">الحالة</label>
<select name="voided" class="form-select">
<option value="0" <?= $filters['voided'] === '0' ? 'selected' : '' ?>>فعالة فقط</option>
<option value="1" <?= $filters['voided'] === '1' ? 'selected' : '' ?>>ملغاة فقط</option>
<option value="all" <?= $filters['voided'] === 'all' ? 'selected' : '' ?>>الكل</option>
</select>
</div>
<div style="flex:2;min-width:180px;">
<label class="form-label">بحث</label>
<input type="text" name="search" class="form-input" placeholder="اسم العضو / رقم الإيصال / ملاحظات..." value="<?= e($filters['search']) ?>">
</div>
<div>
<button type="submit" class="btn btn-primary">بحث</button>
</div>
</form>
</div>
<!-- Summary Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:15px;margin-bottom:20px;">
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#059669;"><?= money($total_amount) ?></div>
<div style="color:#6B7280;margin-top:5px;">إجمالي المبلغ</div>
</div>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:24px;font-weight:700;color:#0D7377;"><?= number_format($total_count) ?></div>
<div style="color:#6B7280;margin-top:5px;">عدد المدفوعات</div>
</div>
<?php foreach ($by_method as $m): ?>
<div class="card" style="padding:20px;text-align:center;">
<div style="font-size:20px;font-weight:700;color:#7C3AED;"><?= money($m['total']) ?></div>
<div style="color:#6B7280;margin-top:5px;"><?= e($method_labels[$m['payment_method']] ?? $m['payment_method']) ?> (<?= (int) $m['cnt'] ?>)</div>
</div>
<?php endforeach; ?>
</div>
<!-- By Type Summary (collapsible) -->
<?php if (!empty($by_type)): ?>
<div class="card" style="padding:20px;margin-bottom:20px;">
<h3 style="margin:0 0 15px;">ملخص حسب النوع</h3>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr><th>النوع</th><th>العدد</th><th>المبلغ</th><th>النسبة</th></tr>
</thead>
<tbody>
<?php foreach ($by_type as $t):
$pct = $total_amount > 0 ? bcdiv(bcmul((string) $t['total'], '100', 2), (string) $total_amount, 1) : '0.0';
?>
<tr>
<td><?= e(\App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($t['payment_type'])) ?></td>
<td><?= (int) $t['cnt'] ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($t['total']) ?></td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div style="flex:1;background:#E5E7EB;border-radius:4px;height:8px;">
<div style="width:<?= min(100, (float)$pct) ?>%;background:#0D7377;border-radius:4px;height:8px;"></div>
</div>
<span style="font-size:12px;color:#6B7280;min-width:40px;text-align:left;"><?= $pct ?>%</span>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
<!-- Payments Table -->
<div class="card" style="padding:20px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 style="margin:0;">المدفوعات (<?= number_format($total_count) ?>)</h3>
<span style="color:#6B7280;font-size:13px;">صفحة <?= $page ?> من <?= $total_pages ?></span>
</div>
<?php if (!empty($payments)): ?>
<div class="table-responsive">
<table class="data-table" style="width:100%;">
<thead>
<tr>
<th>#</th>
<th>الإيصال</th>
<th>التاريخ</th>
<th>العضو / الاسم</th>
<th>رقم الاستمارة</th>
<th>النوع</th>
<th>الطريقة</th>
<th>المبلغ</th>
<th>المحصّل</th>
<th>الحالة</th>
</tr>
</thead>
<tbody>
<?php foreach ($payments as $p): ?>
<tr style="<?= $p['is_voided'] ? 'opacity:0.5;text-decoration:line-through;' : '' ?>">
<td><?= (int) $p['id'] ?></td>
<td style="font-family:monospace;font-size:12px;"><?= e($p['receipt_number'] ?? '—') ?></td>
<td><?= e($p['payment_date']) ?></td>
<td>
<?php if ($p['member_id']): ?>
<a href="/members/<?= (int) $p['member_id'] ?>"><?= e($p['member_name'] ?? 'عضو #' . $p['member_id']) ?></a>
<?php else: ?>
<span style="color:#6B7280;">زائر</span>
<?php endif; ?>
</td>
<td style="font-family:monospace;"><?= e($p['form_number'] ?? '—') ?></td>
<td><span style="background:#F3F4F6;padding:2px 8px;border-radius:4px;font-size:12px;"><?= e(\App\Modules\Payments\Services\PaymentService::getPaymentTypeLabel($p['payment_type'])) ?></span></td>
<td><?= e($method_labels[$p['payment_method']] ?? $p['payment_method']) ?></td>
<td style="font-weight:600;direction:ltr;text-align:left;"><?= money($p['amount']) ?></td>
<td style="font-size:12px;color:#6B7280;"><?= e($p['received_by_name'] ?? '—') ?></td>
<td>
<?php if ($p['is_voided']): ?>
<span style="background:#FEE2E2;color:#DC2626;padding:2px 8px;border-radius:4px;font-size:12px;">ملغاة</span>
<?php else: ?>
<span style="background:#D1FAE5;color:#059669;padding:2px 8px;border-radius:4px;font-size:12px;">فعالة</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<div style="display:flex;justify-content:center;gap:5px;margin-top:20px;">
<?php
$baseUrl = '/accounting/reports/treasury?' . http_build_query(array_filter($filters));
for ($i = max(1, $page - 3); $i <= min($total_pages, $page + 3); $i++):
?>
<a href="<?= $baseUrl ?>&page=<?= $i ?>"
class="btn <?= $i === $page ? 'btn-primary' : 'btn-outline' ?>"
style="min-width:40px;text-align:center;"><?= $i ?></a>
<?php endfor; ?>
</div>
<?php endif; ?>
<?php else: ?>
<p style="color:#9CA3AF;text-align:center;padding:40px 0;">لا توجد مدفوعات تطابق معايير البحث</p>
<?php endif; ?>
</div>
<script>if(typeof lucide!=='undefined')lucide.createIcons();</script>
<?php $__template->endSection(); ?>
......@@ -21,6 +21,8 @@ PermissionRegistry::register('accounting', [
'accounting.reports.ar' => ['ar' => 'تقرير المدينين', 'en' => 'Accounts Receivable Report'],
'accounting.reports.ap' => ['ar' => 'تقرير الدائنين', 'en' => 'Accounts Payable Report'],
'accounting.reports.member_statement' => ['ar' => 'كشف حساب عضو', 'en' => 'Member Statement'],
'accounting.reports.treasury' => ['ar' => 'تقرير الخزينة والمدفوعات', 'en' => 'Treasury & Payments Report'],
'accounting.reports.revenue_analysis' => ['ar' => 'تحليل الإيرادات', 'en' => 'Revenue Analysis'],
// Fiscal Year
'accounting.fiscal_year.view' => ['ar' => 'عرض السنوات المالية', 'en' => 'View Fiscal Years'],
......@@ -84,6 +86,8 @@ MenuRegistry::register('accounting', [
['label_ar' => 'المدينون (AR)', 'label_en' => 'Accounts Receivable', 'route' => '/accounting/reports/accounts-receivable', 'permission' => 'accounting.reports.ar', 'order' => 14],
['label_ar' => 'الدائنون (AP)', 'label_en' => 'Accounts Payable', 'route' => '/accounting/reports/accounts-payable', 'permission' => 'accounting.reports.ap', 'order' => 15],
['label_ar' => 'كشف حساب عضو', 'label_en' => 'Member Statement', 'route' => '/accounting/reports/member-statement', 'permission' => 'accounting.reports.member_statement','order' => 16],
['label_ar' => 'الخزينة والمدفوعات', 'label_en' => 'Treasury & Payments', 'route' => '/accounting/reports/treasury', 'permission' => 'accounting.reports.treasury', 'order' => 17],
['label_ar' => 'تحليل الإيرادات', 'label_en' => 'Revenue Analysis', 'route' => '/accounting/reports/revenue-analysis', 'permission' => 'accounting.reports.revenue_analysis','order' => 18],
],
]);
......
......@@ -129,6 +129,7 @@ class ActivitySubscriptionController extends Controller
} else {
$result = InventoryPaymentService::processGuestPayment([
'amount' => $amount,
'payment_type' => 'activity_subscription',
'payment_method' => $paymentMethod,
'guest_name' => $sub['player_name'] ?? 'لاعب',
'related_entity_type' => 'activity_subscriptions',
......
......@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\EventBus;
use App\Modules\Foreign\Models\ForeignMemberDetail;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService;
class ForeignController extends Controller
{
......@@ -108,6 +109,27 @@ class ForeignController extends Controller
$db->update('members', ['membership_type' => 'foreign', 'updated_at' => date('Y-m-d H:i:s')], '`id` = ?', [(int) $memberId]);
// Process payment through central PaymentService (use EGP amount if available, otherwise USD)
$chargeAmount = $feeEgp ?? $feeUsd;
$chargeCurrency = $feeEgp ? 'EGP' : 'USD';
if (bccomp((string) $chargeAmount, '0.00', 2) > 0) {
$payResult = PaymentService::processPayment([
'member_id' => (int) $memberId,
'amount' => (string) $chargeAmount,
'payment_type' => 'membership_fee',
'payment_method' => $data['payment_method'] ?? 'cash',
'currency' => $chargeCurrency,
'related_entity_type' => 'foreign_member_details',
'related_entity_id' => (int) $foreign->id,
'description' => 'رسوم عضوية أجنبية — ' . number_format((float) $feeUsd, 2) . ' USD',
]);
if (!$payResult['success']) {
return $this->redirect("/members/{$memberId}")
->withError('تم تسجيل العضوية الأجنبية لكن فشل تسجيل الدفع: ' . ($payResult['error'] ?? ''));
}
}
EventBus::dispatch('foreign.registered', ['member_id' => (int) $memberId, 'foreign_id' => (int) $foreign->id]);
return $this->redirect("/members/{$memberId}")->withSuccess('تم تسجيل العضوية الأجنبية — الرسوم: ' . number_format((float) $feeUsd, 2) . ' USD');
......
......@@ -13,6 +13,7 @@ use App\Modules\HR\Models\HrPayrollRun;
use App\Modules\HR\Models\HrEmployeeProfile;
use App\Modules\HR\Services\PayrollCalculationService;
use App\Modules\HR\Services\SalarySlipService;
use App\Core\EventBus;
class PayrollController extends Controller
{
......@@ -197,6 +198,19 @@ class PayrollController extends Controller
$db->commit();
// Dispatch payroll paid event for each run — triggers Accounting auto-post
$runs = $db->select(
"SELECT id FROM hr_payroll_runs WHERE period_id = ? AND is_archived = 0",
[(int) $id]
);
foreach ($runs as $run) {
EventBus::dispatch('hr.payroll.paid', [
'payroll_run_id' => (int) $run['id'],
'period_id' => (int) $id,
'payment_date' => $paymentDate,
]);
}
Logger::info("Payroll period marked paid", ['period_id' => (int) $id, 'payment_date' => $paymentDate]);
return $this->redirect('/hr/payroll/periods/' . $id)->withSuccess('تم صرف الرواتب بنجاح');
......
......@@ -41,15 +41,13 @@ final class PaymentService
$employee = App::getInstance()->currentEmployee();
// Validate required fields
$memberId = (int) ($data['member_id'] ?? 0);
$memberId = isset($data['member_id']) && $data['member_id'] !== null ? (int) $data['member_id'] : null;
$amount = $data['amount'] ?? '0.00';
$paymentType = $data['payment_type'] ?? '';
$paymentMethod = $data['payment_method'] ?? 'cash';
$description = $data['description'] ?? '';
$guestName = $data['guest_name'] ?? null;
if ($memberId <= 0) {
return ['success' => false, 'error' => 'العضو غير محدد'];
}
if (bccomp((string) $amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر'];
}
......@@ -57,11 +55,14 @@ final class PaymentService
return ['success' => false, 'error' => 'نوع الدفعة مطلوب'];
}
// Verify member exists
// Verify member exists (skip for guest payments)
$member = null;
if ($memberId !== null && $memberId > 0) {
$member = $db->selectOne("SELECT id, full_name_ar, form_number FROM members WHERE id = ? AND is_archived = 0", [$memberId]);
if (!$member) {
return ['success' => false, 'error' => 'العضو غير موجود'];
}
}
$db->beginTransaction();
try {
......@@ -70,7 +71,7 @@ final class PaymentService
// Create payment record
$paymentId = $db->insert('payments', [
'member_id' => $memberId,
'member_id' => ($memberId !== null && $memberId > 0) ? $memberId : null,
'payment_type' => $paymentType,
'amount' => $amount,
'currency' => $data['currency'] ?? 'EGP',
......@@ -84,7 +85,7 @@ final class PaymentService
'transfer_bank' => $data['transfer_bank'] ?? null,
'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => $data['related_entity_id'] ?? null,
'notes' => $data['notes'] ?? $description,
'notes' => $data['notes'] ?? ($guestName ? $description . ' — ' . $guestName : $description),
'payment_date' => $data['payment_date'] ?? date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null,
'is_voided' => 0,
......@@ -96,7 +97,7 @@ final class PaymentService
// Auto-generate description if empty
if ($description === '') {
$description = self::getPaymentTypeLabel($paymentType);
if ($member['form_number']) {
if ($member && $member['form_number']) {
$description .= ' — استمارة ' . $member['form_number'];
}
}
......@@ -104,7 +105,7 @@ final class PaymentService
// Create receipt
$receiptId = $db->insert('receipts', [
'receipt_number' => $receiptNumber,
'member_id' => $memberId,
'member_id' => ($memberId !== null && $memberId > 0) ? $memberId : null,
'payment_id' => $paymentId,
'receipt_type' => 'payment',
'amount' => $amount,
......@@ -127,7 +128,7 @@ final class PaymentService
'payment_id' => $paymentId,
'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber,
'member_id' => $memberId,
'member_id' => ($memberId !== null && $memberId > 0) ? $memberId : 0,
'type' => $paymentType,
'amount' => $amount,
'method' => $paymentMethod,
......@@ -135,7 +136,7 @@ final class PaymentService
Logger::info("Payment processed", [
'payment_id' => $paymentId,
'member_id' => $memberId,
'member_id' => $memberId ?? 0,
'type' => $paymentType,
'amount' => $amount,
]);
......@@ -151,7 +152,7 @@ final class PaymentService
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Payment failed: " . $e->getMessage(), [
'member_id' => $memberId,
'member_id' => $memberId ?? 0,
'type' => $paymentType,
'amount' => $amount,
]);
......@@ -312,6 +313,7 @@ final class PaymentService
'seasonal_fee' => 'رسوم عضوية موسمية',
'sports_conversion' => 'رسوم تحويل رياضي',
'inventory_sale' => 'مبيعات مخزون',
'activity_subscription' => 'اشتراك نشاط',
'other' => 'أخرى',
default => $type,
};
......
......@@ -3,108 +3,29 @@ declare(strict_types=1);
namespace App\Modules\Sales\Services;
use App\Core\App;
use App\Core\EventBus;
use App\Core\Logger;
use App\Modules\Payments\Services\PaymentService;
/**
* Guest payment wrapper — bypasses the member_id validation in PaymentService.
* Creates payment + receipt directly for guest sales.
* Guest payment wrapper — delegates to PaymentService with member_id = null.
* Kept as a thin adapter so callers don't need to change.
*/
final class InventoryPaymentService
{
/**
* Process a guest payment (no member_id required).
* Delegates entirely to the central PaymentService.
*/
public static function processGuestPayment(array $data): array
{
$db = App::getInstance()->db();
$employee = App::getInstance()->currentEmployee();
$amount = $data['amount'] ?? '0.00';
$paymentMethod = $data['payment_method'] ?? 'cash';
$guestName = $data['guest_name'] ?? 'زائر';
$description = $data['description'] ?? 'مبيعات مخزون (زائر)';
if (bccomp((string) $amount, '0.01', 2) < 0) {
return ['success' => false, 'error' => 'المبلغ يجب أن يكون أكبر من صفر'];
}
$db->beginTransaction();
try {
// Generate receipt number (same pattern as PaymentService)
$year = date('Y');
$prefix = 'REC-' . $year . '-';
$last = $db->selectOne(
"SELECT receipt_number FROM receipts WHERE receipt_number LIKE ? ORDER BY id DESC LIMIT 1",
[$prefix . '%']
);
$seq = $last ? ((int) substr($last['receipt_number'], -6)) + 1 : 1;
$receiptNumber = $prefix . str_pad((string) $seq, 6, '0', STR_PAD_LEFT);
// Create payment record (member_id = NULL for guest)
$paymentId = $db->insert('payments', [
return PaymentService::processPayment([
'member_id' => null,
'payment_type' => 'inventory_sale',
'amount' => $amount,
'currency' => 'EGP',
'payment_method' => $paymentMethod,
'amount' => $data['amount'] ?? '0.00',
'payment_type' => $data['payment_type'] ?? 'inventory_sale',
'payment_method' => $data['payment_method'] ?? 'cash',
'guest_name' => $data['guest_name'] ?? 'زائر',
'related_entity_type' => $data['related_entity_type'] ?? null,
'related_entity_id' => $data['related_entity_id'] ?? null,
'notes' => $description . ' — ' . $guestName,
'payment_date' => date('Y-m-d'),
'received_by_employee_id' => $employee ? (int) $employee->id : null,
'is_voided' => 0,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $employee ? (int) $employee->id : null,
]);
// Create receipt (member_id = NULL — requires Phase_25_021 migration)
$receiptId = $db->insert('receipts', [
'receipt_number' => $receiptNumber,
'member_id' => null,
'payment_id' => $paymentId,
'receipt_type' => 'payment',
'amount' => $amount,
'amount_in_words_ar' => function_exists('number_to_arabic_words') ? number_to_arabic_words((float) $amount) : '',
'description_ar' => $description,
'issued_by_employee_id' => $employee ? (int) $employee->id : null,
'issued_at' => date('Y-m-d H:i:s'),
'is_voided' => 0,
'print_count' => 0,
'created_at' => date('Y-m-d H:i:s'),
'description' => $data['description'] ?? 'مبيعات مخزون (زائر)',
]);
$db->update('payments', ['receipt_id' => $receiptId], '`id` = ?', [$paymentId]);
$db->commit();
// Fire payment.completed so Accounting module auto-posts GL entries
EventBus::dispatch('payment.completed', [
'payment_id' => $paymentId,
'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber,
'member_id' => 0,
'type' => 'inventory_sale',
'amount' => $amount,
'method' => $paymentMethod,
]);
Logger::info("Guest payment #{$paymentId} processed — receipt: {$receiptNumber}");
return [
'success' => true,
'payment_id' => $paymentId,
'receipt_id' => $receiptId,
'receipt_number' => $receiptNumber,
'amount' => $amount,
];
} catch (\Throwable $e) {
$db->rollBack();
Logger::error("Guest payment failed: " . $e->getMessage());
return ['success' => false, 'error' => 'فشل تسجيل الدفع: ' . $e->getMessage()];
}
}
}
......@@ -10,6 +10,7 @@ use App\Core\App;
use App\Core\EventBus;
use App\Modules\Seasonal\Models\SeasonalMembership;
use App\Modules\Rules\Services\RuleEngine;
use App\Modules\Payments\Services\PaymentService;
class SeasonalController extends Controller
{
......@@ -109,6 +110,24 @@ class SeasonalController extends Controller
'notes' => $data['notes'] ?? null,
]);
// Process payment through central PaymentService
if (bccomp($fee, '0.00', 2) > 0) {
$payResult = PaymentService::processPayment([
'member_id' => (int) $memberId,
'amount' => $fee,
'payment_type' => 'seasonal_fee',
'payment_method' => $data['payment_method'] ?? 'cash',
'related_entity_type' => 'seasonal_memberships',
'related_entity_id' => (int) $seasonal->id,
'description' => 'رسوم عضوية موسمية — من ' . $startDate . ' إلى ' . $endDate,
]);
if (!$payResult['success']) {
return $this->redirect("/members/{$memberId}/seasonal/create")
->withError('تم إنشاء العضوية الموسمية لكن فشل تسجيل الدفع: ' . ($payResult['error'] ?? ''));
}
}
EventBus::dispatch('seasonal.created', [
'member_id' => (int) $memberId,
'seasonal_id' => (int) $seasonal->id,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment