501 lines
20 KiB
PHP
501 lines
20 KiB
PHP
<?php
|
|
require_once 'auth.php';
|
|
require_once '../db/config.php';
|
|
require_login();
|
|
|
|
$user = get_user();
|
|
$pdo = db();
|
|
|
|
// Fetch Categories for filter
|
|
$stmt = $pdo->query("SELECT id, name_en FROM categories ORDER BY name_en ASC");
|
|
$categories = $stmt->fetchAll();
|
|
|
|
// Filters
|
|
$start_date = $_GET['start_date'] ?? '';
|
|
$end_date = $_GET['end_date'] ?? '';
|
|
$category_id = $_GET['category_id'] ?? '';
|
|
$status_filter = $_GET['status'] ?? '';
|
|
|
|
// Build base where clauses for donations table
|
|
$where_clauses = ["1=1"];
|
|
$params = [];
|
|
|
|
if (!empty($start_date)) {
|
|
$where_clauses[] = "d.created_at >= :start_date";
|
|
$params[':start_date'] = $start_date . " 00:00:00";
|
|
}
|
|
if (!empty($end_date)) {
|
|
$where_clauses[] = "d.created_at <= :end_date";
|
|
$params[':end_date'] = $end_date . " 23:59:59";
|
|
}
|
|
if (!empty($status_filter)) {
|
|
$where_clauses[] = "d.status = :status";
|
|
$params[':status'] = $status_filter;
|
|
}
|
|
|
|
// Category filter requires join with cases
|
|
$category_join = "";
|
|
if (!empty($category_id)) {
|
|
$category_join = " JOIN cases cs_filter ON d.case_id = cs_filter.id ";
|
|
$where_clauses[] = "cs_filter.category_id = :category_id";
|
|
$params[':category_id'] = $category_id;
|
|
}
|
|
|
|
$where_sql = implode(" AND ", $where_clauses);
|
|
|
|
// Basic Stats
|
|
$stats_stmt = $pdo->prepare("
|
|
SELECT
|
|
COUNT(*) as total_count,
|
|
SUM(CASE WHEN d.status = 'completed' THEN d.amount ELSE 0 END) as total_revenue,
|
|
AVG(CASE WHEN d.status = 'completed' THEN d.amount ELSE NULL END) as avg_donation,
|
|
SUM(CASE WHEN d.status = 'pending' THEN 1 ELSE 0 END) as pending_count,
|
|
SUM(CASE WHEN d.status = 'completed' THEN 1 ELSE 0 END) as completed_count
|
|
FROM donations d
|
|
$category_join
|
|
WHERE $where_sql
|
|
");
|
|
$stats_stmt->execute($params);
|
|
$stats = $stats_stmt->fetch();
|
|
|
|
// Revenue by Category
|
|
$cat_where = $where_sql;
|
|
$cat_params = $params;
|
|
if (empty($status_filter)) {
|
|
$cat_where .= " AND d.status = 'completed'";
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT c.name_en, SUM(d.amount) as total
|
|
FROM categories c
|
|
JOIN cases cs ON cs.category_id = c.id
|
|
JOIN donations d ON d.case_id = cs.id
|
|
WHERE $cat_where
|
|
GROUP BY c.id
|
|
ORDER BY total DESC
|
|
");
|
|
$stmt->execute($cat_params);
|
|
$category_revenue = $stmt->fetchAll();
|
|
|
|
$cat_labels = [];
|
|
$cat_totals = [];
|
|
foreach ($category_revenue as $row) {
|
|
$cat_labels[] = $row['name_en'];
|
|
$cat_totals[] = (float)$row['total'];
|
|
}
|
|
|
|
// Monthly Revenue Trend
|
|
$trend_where = $where_sql;
|
|
$trend_params = $params;
|
|
if (empty($status_filter)) {
|
|
$trend_where .= " AND d.status = 'completed'";
|
|
}
|
|
|
|
if (empty($start_date) && empty($end_date)) {
|
|
$trend_where .= " AND d.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)";
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
DATE_FORMAT(d.created_at, '%Y-%m') as month,
|
|
SUM(d.amount) as total
|
|
FROM donations d
|
|
$category_join
|
|
WHERE $trend_where
|
|
GROUP BY month
|
|
ORDER BY month ASC
|
|
");
|
|
$stmt->execute($trend_params);
|
|
$monthly_trend = $stmt->fetchAll();
|
|
|
|
$trend_labels = [];
|
|
$trend_totals = [];
|
|
foreach ($monthly_trend as $row) {
|
|
$trend_labels[] = date('M Y', strtotime($row['month'] . '-01'));
|
|
$trend_totals[] = (float)$row['total'];
|
|
}
|
|
|
|
// Top Cases by Revenue
|
|
$top_where = $where_sql;
|
|
$top_params = $params;
|
|
if (empty($status_filter)) {
|
|
$top_where .= " AND d.status = 'completed'";
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT cs.title_en, SUM(d.amount) as total, cs.goal
|
|
FROM cases cs
|
|
JOIN donations d ON d.case_id = cs.id
|
|
WHERE $top_where
|
|
GROUP BY cs.id
|
|
ORDER BY total DESC
|
|
LIMIT 5
|
|
");
|
|
$stmt->execute($top_params);
|
|
$top_cases = $stmt->fetchAll();
|
|
|
|
// Gift vs Regular
|
|
$gift_where = $where_sql;
|
|
$gift_params = $params;
|
|
if (empty($status_filter)) {
|
|
$gift_where .= " AND d.status = 'completed'";
|
|
}
|
|
|
|
$stmt = $pdo->prepare("
|
|
SELECT
|
|
d.is_gift,
|
|
COUNT(*) as count,
|
|
SUM(d.amount) as total
|
|
FROM donations d
|
|
$category_join
|
|
WHERE $gift_where
|
|
GROUP BY d.is_gift
|
|
");
|
|
$stmt->execute($gift_params);
|
|
$gift_stats = $stmt->fetchAll();
|
|
|
|
$gift_labels = ['Regular', 'Gift'];
|
|
$gift_totals = [0, 0];
|
|
foreach ($gift_stats as $row) {
|
|
if ($row['is_gift']) {
|
|
$gift_totals[1] = (float)$row['total'];
|
|
} else {
|
|
$gift_totals[0] = (float)$row['total'];
|
|
}
|
|
}
|
|
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Financial Summary - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
:root { --sidebar-width: 260px; --primary-color: #059669; }
|
|
body { background-color: #f3f4f6; }
|
|
.sidebar { width: var(--sidebar-width); height: 100vh; position: fixed; left: 0; top: 0; background: #111827; color: #fff; padding: 1.5rem; }
|
|
.main-content { margin-left: var(--sidebar-width); padding: 2rem; }
|
|
.card { border: none; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
.stat-card { padding: 1.5rem; border-left: 4px solid var(--primary-color); }
|
|
.chart-container { position: relative; height: 250px; }
|
|
.filter-section { background: #fff; padding: 1.25rem; border-radius: 12px; margin-bottom: 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
|
|
|
@media print {
|
|
.sidebar, .filter-section, .no-print {
|
|
display: none !important;
|
|
}
|
|
.main-content {
|
|
margin-left: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
.card {
|
|
box-shadow: none !important;
|
|
border: 1px solid #eee !important;
|
|
break-inside: avoid;
|
|
}
|
|
body {
|
|
background-color: #fff !important;
|
|
}
|
|
.stat-card {
|
|
border-left: 4px solid var(--primary-color) !important;
|
|
-webkit-print-color-adjust: exact;
|
|
print-color-adjust: exact;
|
|
}
|
|
.progress-bar {
|
|
-webkit-print-color-adjust: exact;
|
|
print-color-adjust: exact;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<?php include "sidebar.php"; ?>
|
|
|
|
<div class="main-content">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h2 class="mb-0">Financial Summary Report</h2>
|
|
<p class="text-muted mb-0">Detailed analysis of donations and revenue streams.</p>
|
|
</div>
|
|
<div class="text-end">
|
|
<div class="fw-bold"><?= date('l, F j, Y') ?></div>
|
|
<div class="text-muted small no-print"><?= htmlspecialchars(get_org_name()) ?> Admin Panel</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="filter-section">
|
|
<form method="GET" class="row g-3 align-items-end">
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold text-muted">Start Date</label>
|
|
<input type="date" name="start_date" class="form-control" value="<?= htmlspecialchars($start_date) ?>">
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold text-muted">End Date</label>
|
|
<input type="date" name="end_date" class="form-control" value="<?= htmlspecialchars($end_date) ?>">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label small fw-bold text-muted">Category</label>
|
|
<select name="category_id" class="form-select">
|
|
<option value="">All Categories</option>
|
|
<?php foreach ($categories as $cat): ?>
|
|
<option value="<?= $cat['id'] ?>" <?= $category_id == $cat['id'] ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($cat['name_en']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label small fw-bold text-muted">Status</label>
|
|
<select name="status" class="form-select">
|
|
<option value="">All (Default: Completed)</option>
|
|
<option value="completed" <?= $status_filter == 'completed' ? 'selected' : '' ?>>Completed</option>
|
|
<option value="pending" <?= $status_filter == 'pending' ? 'selected' : '' ?>>Pending</option>
|
|
<option value="failed" <?= $status_filter == 'failed' ? 'selected' : '' ?>>Failed</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<button type="submit" class="btn btn-primary px-3">
|
|
<i class="bi bi-filter"></i> Apply
|
|
</button>
|
|
<a href="financial_summary.php" class="btn btn-outline-secondary ms-1">Reset</a>
|
|
<button type="button" onclick="window.print()" class="btn btn-outline-danger ms-1">
|
|
<i class="bi bi-file-earmark-pdf"></i> Export PDF
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<?php if (!empty($start_date) || !empty($end_date) || !empty($category_id) || !empty($status_filter)): ?>
|
|
<div class="mt-3">
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<?php if (!empty($start_date) || !empty($end_date)): ?>
|
|
<span class="badge bg-light text-dark border p-2">
|
|
<i class="bi bi-calendar-check me-1"></i>
|
|
<?= $start_date ?: 'Beginning' ?> - <?= $end_date ?: 'Today' ?>
|
|
</span>
|
|
<?php endif; ?>
|
|
<?php if (!empty($category_id)):
|
|
$cat_name = "";
|
|
foreach($categories as $c) if($c['id'] == $category_id) $cat_name = $c['name_en'];
|
|
?>
|
|
<span class="badge bg-light text-dark border p-2">
|
|
<i class="bi bi-tag me-1"></i>
|
|
Category: <?= htmlspecialchars($cat_name) ?>
|
|
</span>
|
|
<?php endif; ?>
|
|
<?php if (!empty($status_filter)): ?>
|
|
<span class="badge bg-light text-dark border p-2">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
Status: <?= ucfirst(htmlspecialchars($status_filter)) ?>
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Print-only filter summary -->
|
|
<div class="d-none d-print-block mb-4 p-3 bg-light border rounded">
|
|
<h6 class="mb-2 fw-bold text-uppercase small text-muted">Report Filters</h6>
|
|
<div class="row">
|
|
<div class="col-3 small"><strong>Date Range:</strong> <?= ($start_date ?: 'All') . ' to ' . ($end_date ?: 'Today') ?></div>
|
|
<div class="col-3 small"><strong>Category:</strong> <?php
|
|
if($category_id) {
|
|
foreach($categories as $c) if($c['id'] == $category_id) echo htmlspecialchars($c['name_en']);
|
|
} else {
|
|
echo "All Categories";
|
|
}
|
|
?></div>
|
|
<div class="col-3 small"><strong>Status:</strong> <?= $status_filter ? ucfirst(htmlspecialchars($status_filter)) : 'Completed (Default)' ?></div>
|
|
<div class="col-3 small text-end text-muted">Generated by: <?= htmlspecialchars($user['name']) ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card stat-card">
|
|
<div class="text-muted small">Total Revenue</div>
|
|
<div class="h3 mb-0">OMR <?= number_format($stats['total_revenue'] ?? 0, 3) ?></div>
|
|
<div class="text-success small"><i class="bi bi-check-circle"></i> From <?= $stats['completed_count'] ?> donations</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stat-card" style="border-left-color: #3b82f6;">
|
|
<div class="text-muted small">Avg. Donation</div>
|
|
<div class="h3 mb-0">OMR <?= number_format($stats['avg_donation'] ?? 0, 3) ?></div>
|
|
<div class="text-primary small">Per completed donation</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stat-card" style="border-left-color: #f59e0b;">
|
|
<div class="text-muted small">Pending Count</div>
|
|
<div class="h3 mb-0"><?= $stats['pending_count'] ?></div>
|
|
<div class="text-warning small">Awaiting payment</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card stat-card" style="border-left-color: #ec4899;">
|
|
<div class="text-muted small">Total Donations</div>
|
|
<div class="h3 mb-0"><?= $stats['total_count'] ?></div>
|
|
<div class="text-danger small">Based on active filters</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<!-- Monthly Trend -->
|
|
<div class="col-lg-8">
|
|
<div class="card p-4 h-100">
|
|
<h5 class="mb-4">Revenue Trend <?= empty($start_date) && empty($end_date) ? '(Last 12 Months)' : '' ?></h5>
|
|
<div style="height: 300px;">
|
|
<canvas id="trendChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Category Distribution -->
|
|
<div class="col-lg-4">
|
|
<div class="card p-4 h-100">
|
|
<h5 class="mb-4">Revenue by Category</h5>
|
|
<div class="chart-container">
|
|
<canvas id="categoryChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<!-- Top Cases -->
|
|
<div class="col-lg-6">
|
|
<div class="card p-4 h-100">
|
|
<h5 class="mb-4">Top Performing Cases</h5>
|
|
<div class="list-group list-group-flush">
|
|
<?php if (empty($top_cases)): ?>
|
|
<p class="text-muted text-center py-5">No donations found for this selection.</p>
|
|
<?php else: ?>
|
|
<?php foreach ($top_cases as $case): ?>
|
|
<div class="list-group-item px-0 border-0 mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="fw-bold"><?= htmlspecialchars($case['title_en']) ?></span>
|
|
<span>OMR <?= number_format($case['total'], 3) ?></span>
|
|
</div>
|
|
<?php
|
|
$percent = $case['goal'] > 0 ? ($case['total'] / $case['goal']) * 100 : 0;
|
|
?>
|
|
<div class="progress" style="height: 8px;">
|
|
<div class="progress-bar bg-success" role="progressbar" style="width: <?= min(100, $percent) ?>%"></div>
|
|
</div>
|
|
<div class="text-muted small mt-1"><?= number_format($percent, 1) ?>% of OMR <?= number_format($case['goal'], 0) ?> goal</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Gift vs Regular -->
|
|
<div class="col-lg-6">
|
|
<div class="card p-4 h-100">
|
|
<h5 class="mb-4">Regular vs Gift Donations (Revenue)</h5>
|
|
<div class="d-flex align-items-center justify-content-center" style="height: 250px;">
|
|
<canvas id="giftChart"></canvas>
|
|
</div>
|
|
<div class="mt-4 row text-center">
|
|
<div class="col-6">
|
|
<div class="text-muted small">Regular</div>
|
|
<div class="h5">OMR <?= number_format($gift_totals[0], 3) ?></div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-muted small">Gift</div>
|
|
<div class="h5">OMR <?= number_format($gift_totals[1], 3) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Disable animations for cleaner printing
|
|
const isPrinting = window.matchMedia('print');
|
|
const animationConfig = {
|
|
duration: isPrinting.matches ? 0 : 1000
|
|
};
|
|
|
|
// Trend Chart
|
|
new Chart(document.getElementById('trendChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: <?= json_encode($trend_labels) ?>,
|
|
datasets: [{
|
|
label: 'Revenue (OMR)',
|
|
data: <?= json_encode($trend_totals) ?>,
|
|
backgroundColor: '#059669',
|
|
borderRadius: 6
|
|
}]
|
|
},
|
|
options: {
|
|
animation: animationConfig,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: { beginAtZero: true, grid: { color: '#f3f4f6' } },
|
|
x: { grid: { display: false } }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Category Chart
|
|
new Chart(document.getElementById('categoryChart'), {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: <?= json_encode($cat_labels) ?>,
|
|
datasets: [{
|
|
data: <?= json_encode($cat_totals) ?>,
|
|
backgroundColor: ['#059669', '#3b82f6', '#f59e0b', '#ec4899', '#8b5cf6', '#06b6d4']
|
|
}]
|
|
},
|
|
options: {
|
|
animation: animationConfig,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { position: 'bottom', labels: { boxWidth: 12 } }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Gift Chart
|
|
new Chart(document.getElementById('giftChart'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: <?= json_encode($gift_labels) ?>,
|
|
datasets: [{
|
|
data: <?= json_encode($gift_totals) ?>,
|
|
backgroundColor: ['#10b981', '#f472b6']
|
|
}]
|
|
},
|
|
options: {
|
|
animation: animationConfig,
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Listen for print event to disable animations if not already handled
|
|
window.onbeforeprint = () => {
|
|
Chart.instances.forEach(chart => {
|
|
chart.options.animation = false;
|
|
chart.update('none');
|
|
});
|
|
};
|
|
</script>
|
|
</body>
|
|
</html>
|