adding monthly email

This commit is contained in:
Flatlogic Bot 2026-02-13 09:51:35 +00:00
parent 699f776eee
commit ad3177ebeb
6 changed files with 670 additions and 93 deletions

26
admin/ajax_test_email.php Normal file
View File

@ -0,0 +1,26 @@
<?php
require_once 'auth.php';
require_once '../mail/MailService.php';
header('Content-Type: application/json');
if (!is_logged_in() || !is_super_admin()) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$recipient = $_POST['recipient'] ?? '';
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'error' => 'Invalid recipient email address']);
exit;
}
$subject = "SMTP Test Email - CharityHub";
$htmlBody = "<h1>Test Successful!</h1><p>If you are reading this, your SMTP configuration is working correctly.</p><p>Sent at: " . date('Y-m-d H:i:s') . "</p>";
$textBody = "SMTP Test Email - CharityHub\n\nTest Successful!\nIf you are reading this, your SMTP configuration is working correctly.\nSent at: " . date('Y-m-d H:i:s');
$result = MailService::sendMail($recipient, $subject, $htmlBody, $textBody);
echo json_encode($result);

View File

@ -6,27 +6,76 @@ 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 = $pdo->query("
$stats_stmt = $pdo->prepare("
SELECT
COUNT(*) as total_count,
SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) as total_revenue,
AVG(CASE WHEN status = 'completed' THEN amount ELSE NULL END) as avg_donation,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_count,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_count
FROM donations
")->fetch();
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
$category_revenue = $pdo->query("
$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 d.status = 'completed'
WHERE $cat_where
GROUP BY c.id
ORDER BY total DESC
")->fetchAll();
");
$stmt->execute($cat_params);
$category_revenue = $stmt->fetchAll();
$cat_labels = [];
$cat_totals = [];
@ -35,16 +84,29 @@ foreach ($category_revenue as $row) {
$cat_totals[] = (float)$row['total'];
}
// Monthly Revenue Trend (Last 12 Months)
$monthly_trend = $pdo->query("
// 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(created_at, '%Y-%m') as month,
SUM(amount) as total
FROM donations
WHERE status = 'completed' AND created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
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
")->fetchAll();
");
$stmt->execute($trend_params);
$monthly_trend = $stmt->fetchAll();
$trend_labels = [];
$trend_totals = [];
@ -54,26 +116,43 @@ foreach ($monthly_trend as $row) {
}
// Top Cases by Revenue
$top_cases = $pdo->query("
$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 d.status = 'completed'
WHERE $top_where
GROUP BY cs.id
ORDER BY total DESC
LIMIT 5
")->fetchAll();
");
$stmt->execute($top_params);
$top_cases = $stmt->fetchAll();
// Gift vs Regular
$gift_stats = $pdo->query("
$gift_where = $where_sql;
$gift_params = $params;
if (empty($status_filter)) {
$gift_where .= " AND d.status = 'completed'";
}
$stmt = $pdo->prepare("
SELECT
is_gift,
d.is_gift,
COUNT(*) as count,
SUM(amount) as total
FROM donations
WHERE status = 'completed'
GROUP BY is_gift
")->fetchAll();
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];
@ -103,6 +182,34 @@ foreach ($gift_stats as $row) {
.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>
@ -111,10 +218,101 @@ foreach ($gift_stats as $row) {
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0">Financial Summary</h2>
<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-muted"><?= date('l, F j, Y') ?></div>
<div class="text-end">
<div class="fw-bold"><?= date('l, F j, Y') ?></div>
<div class="text-muted small no-print">CharityHub 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 -->
@ -135,7 +333,7 @@ foreach ($gift_stats as $row) {
</div>
<div class="col-md-3">
<div class="card stat-card" style="border-left-color: #f59e0b;">
<div class="text-muted small">Pending Revenue</div>
<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>
@ -144,7 +342,7 @@ foreach ($gift_stats as $row) {
<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">All statuses included</div>
<div class="text-danger small">Based on active filters</div>
</div>
</div>
</div>
@ -153,7 +351,7 @@ foreach ($gift_stats as $row) {
<!-- Monthly Trend -->
<div class="col-lg-8">
<div class="card p-4 h-100">
<h5 class="mb-4">Monthly Revenue Trend (Last 12 Months)</h5>
<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>
@ -176,21 +374,25 @@ foreach ($gift_stats as $row) {
<div class="card p-4 h-100">
<h5 class="mb-4">Top Performing Cases</h5>
<div class="list-group list-group-flush">
<?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>
<?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
$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 endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
@ -217,6 +419,12 @@ foreach ($gift_stats as $row) {
</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',
@ -230,6 +438,7 @@ foreach ($gift_stats as $row) {
}]
},
options: {
animation: animationConfig,
responsive: true,
maintainAspectRatio: false,
scales: {
@ -250,6 +459,7 @@ foreach ($gift_stats as $row) {
}]
},
options: {
animation: animationConfig,
responsive: true,
maintainAspectRatio: false,
plugins: {
@ -269,6 +479,7 @@ foreach ($gift_stats as $row) {
}]
},
options: {
animation: animationConfig,
responsive: true,
maintainAspectRatio: false,
plugins: {
@ -276,6 +487,14 @@ foreach ($gift_stats as $row) {
}
}
});
// 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>

View File

@ -12,8 +12,8 @@ $pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($_POST['settings'] as $key => $value) {
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?");
$stmt->execute([$value, $key]);
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
$stmt->execute([$key, $value]);
}
header('Location: settings.php?success=1');
exit;
@ -41,7 +41,9 @@ foreach ($settings_raw as $s) {
.nav-link { color: #9ca3af; margin-bottom: 0.5rem; border-radius: 8px; }
.nav-link:hover, .nav-link.active { color: #fff; background: #1f2937; }
.nav-link.active { background: var(--primary-color); }
.card { border: none; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.card { border: none; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1.5rem; }
.card-header { font-weight: 600; }
.btn-test { font-size: 0.85rem; padding: 0.25rem 0.75rem; }
</style>
</head>
<body>
@ -59,11 +61,101 @@ foreach ($settings_raw as $s) {
</div>
<?php endif; ?>
<div id="test-alert-container"></div>
<form method="POST">
<div class="row g-4">
<!-- Thawani Settings -->
<div class="row">
<!-- SMTP Settings -->
<div class="col-md-6">
<div class="card h-100">
<div class="card">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-envelope me-2"></i> SMTP Configuration</h5>
<button type="button" class="btn btn-outline-secondary btn-test" id="btn-test-smtp">
<i class="bi bi-send me-1"></i> Test Email
</button>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">SMTP Host</label>
<input type="text" name="settings[mail_host]" class="form-control" value="<?= htmlspecialchars($settings['mail_host'] ?? '') ?>" placeholder="e.g. smtp.gmail.com">
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">SMTP Port</label>
<input type="text" name="settings[mail_port]" class="form-control" value="<?= htmlspecialchars($settings['mail_port'] ?? '587') ?>">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Encryption</label>
<select name="settings[mail_encryption]" class="form-select">
<option value="tls" <?= ($settings['mail_encryption'] ?? 'tls') === 'tls' ? 'selected' : '' ?>>TLS</option>
<option value="ssl" <?= ($settings['mail_encryption'] ?? '') === 'ssl' ? 'selected' : '' ?>>SSL</option>
<option value="none" <?= ($settings['mail_encryption'] ?? '') === 'none' ? 'selected' : '' ?>>None</option>
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">SMTP Username</label>
<input type="text" name="settings[mail_username]" class="form-control" value="<?= htmlspecialchars($settings['mail_username'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label">SMTP Password</label>
<input type="password" name="settings[mail_password]" class="form-control" value="<?= htmlspecialchars($settings['mail_password'] ?? '') ?>">
</div>
<hr>
<div class="mb-3">
<label class="form-label">From Address</label>
<input type="email" name="settings[mail_from_address]" class="form-control" value="<?= htmlspecialchars($settings['mail_from_address'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label">From Name</label>
<input type="text" name="settings[mail_from_name]" class="form-control" value="<?= htmlspecialchars($settings['mail_from_name'] ?? 'CharityHub') ?>">
</div>
</div>
</div>
<!-- Report Settings -->
<div class="card">
<div class="card-header bg-white py-3">
<h5 class="mb-0"><i class="bi bi-file-earmark-bar-graph me-2"></i> Report Settings</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Monthly Report Recipient Email</label>
<input type="email" name="settings[report_recipient_email]" class="form-control" value="<?= htmlspecialchars($settings['report_recipient_email'] ?? '') ?>" placeholder="admin@example.com">
<div class="form-text">The email address that will receive the automated monthly financial summary.</div>
</div>
</div>
</div>
</div>
<!-- POP3 Settings -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-white py-3">
<h5 class="mb-0"><i class="bi bi-mailbox me-2"></i> POP3 Configuration</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">POP3 Host</label>
<input type="text" name="settings[pop3_host]" class="form-control" value="<?= htmlspecialchars($settings['pop3_host'] ?? '') ?>" placeholder="e.g. pop.gmail.com">
</div>
<div class="mb-3">
<label class="form-label">POP3 Port</label>
<input type="text" name="settings[pop3_port]" class="form-control" value="<?= htmlspecialchars($settings['pop3_port'] ?? '995') ?>">
</div>
<div class="mb-3">
<label class="form-label">POP3 Username</label>
<input type="text" name="settings[pop3_username]" class="form-control" value="<?= htmlspecialchars($settings['pop3_username'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label">POP3 Password</label>
<input type="password" name="settings[pop3_password]" class="form-control" value="<?= htmlspecialchars($settings['pop3_password'] ?? '') ?>">
</div>
</div>
</div>
<!-- Thawani Settings -->
<div class="card">
<div class="card-header bg-white py-3">
<h5 class="mb-0"><i class="bi bi-credit-card me-2"></i> Thawani Payment Gateway</h5>
</div>
@ -71,12 +163,10 @@ foreach ($settings_raw as $s) {
<div class="mb-3">
<label class="form-label">Secret Key</label>
<input type="password" name="settings[thawani_secret_key]" class="form-control" value="<?= htmlspecialchars($settings['thawani_secret_key'] ?? '') ?>">
<div class="form-text">Your Thawani Secret Key from merchant dashboard.</div>
</div>
<div class="mb-3">
<label class="form-label">Publishable Key</label>
<input type="text" name="settings[thawani_publishable_key]" class="form-control" value="<?= htmlspecialchars($settings['thawani_publishable_key'] ?? '') ?>">
<div class="form-text">Your Thawani Publishable Key.</div>
</div>
<div class="mb-3">
<label class="form-label">Environment</label>
@ -87,11 +177,9 @@ foreach ($settings_raw as $s) {
</div>
</div>
</div>
</div>
<!-- Wablas Settings -->
<div class="col-md-6">
<div class="card h-100">
<!-- Wablas Settings -->
<div class="card">
<div class="card-header bg-white py-3">
<h5 class="mb-0"><i class="bi bi-whatsapp me-2"></i> Wablas WhatsApp Gateway</h5>
</div>
@ -99,17 +187,14 @@ foreach ($settings_raw as $s) {
<div class="mb-3">
<label class="form-label">API Token</label>
<input type="password" name="settings[wablas_api_token]" class="form-control" value="<?= htmlspecialchars($settings['wablas_api_token'] ?? '') ?>">
<div class="form-text">Your Wablas API Token.</div>
</div>
<div class="mb-3">
<label class="form-label">Security Key</label>
<input type="password" name="settings[wablas_security_key]" class="form-control" value="<?= htmlspecialchars($settings['wablas_security_key'] ?? '') ?>">
<div class="form-text">Optional security key if required by your Wablas account.</div>
</div>
<div class="mb-3">
<label class="form-label">Server URL</label>
<input type="url" name="settings[wablas_server_url]" class="form-control" value="<?= htmlspecialchars($settings['wablas_server_url'] ?? 'https://console.wablas.com') ?>">
<div class="form-text">The server URL for Wablas API (e.g., https://console.wablas.com).</div>
</div>
</div>
</div>
@ -122,6 +207,84 @@ foreach ($settings_raw as $s) {
</form>
</div>
<!-- Test Email Modal -->
<div class="modal fade" id="testEmailModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Test SMTP Connection</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Please save your settings before testing if you have made any changes.</p>
<div class="mb-3">
<label class="form-label">Recipient Email</label>
<input type="email" id="test_recipient" class="form-control" placeholder="your-email@example.com">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="btn-send-test">Send Test Email</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const testEmailModal = new bootstrap.Modal(document.getElementById('testEmailModal'));
const btnTestSmtp = document.getElementById('btn-test-smtp');
const btnSendTest = document.getElementById('btn-send-test');
const recipientInput = document.getElementById('test_recipient');
const alertContainer = document.getElementById('test-alert-container');
btnTestSmtp.addEventListener('click', function() {
testEmailModal.show();
});
btnSendTest.addEventListener('click', function() {
const recipient = recipientInput.value.trim();
if (!recipient) {
alert('Please enter a recipient email.');
return;
}
btnSendTest.disabled = true;
btnSendTest.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> Sending...';
const formData = new FormData();
formData.append('recipient', recipient);
fetch('ajax_test_email.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
btnSendTest.disabled = false;
btnSendTest.innerHTML = 'Send Test Email';
testEmailModal.hide();
const alertType = data.success ? 'success' : 'danger';
const alertMsg = data.success ? 'Test email sent successfully! Please check your inbox.' : 'Failed to send test email: ' + (data.error || 'Unknown error');
const alertHtml = `
<div class="alert alert-${alertType} alert-dismissible fade show">
${alertMsg}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
alertContainer.innerHTML = alertHtml;
})
.catch(error => {
btnSendTest.disabled = false;
btnSendTest.innerHTML = 'Send Test Email';
console.error('Error:', error);
alert('An error occurred while sending the test email.');
});
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('mail_transport', 'smtp'),
('mail_host', ''),
('mail_port', '587'),
('mail_encryption', 'tls'),
('mail_username', ''),
('mail_password', ''),
('mail_from_address', ''),
('mail_from_name', 'CharityHub'),
('pop3_host', ''),
('pop3_port', '995'),
('pop3_username', ''),
('pop3_password', '');

View File

@ -1,15 +1,12 @@
<?php
// Mail configuration sourced from environment variables.
// No secrets are stored here; the file just maps env -> config array for MailService.
// Mail configuration sourced from environment variables and database settings.
require_once __DIR__ . '/../db/config.php';
function env_val(string $key, $default = null) {
$v = getenv($key);
return ($v === false || $v === null || $v === '') ? $default : $v;
}
// Fallback: if critical vars are missing from process env, try to parse executor/.env
// This helps in web/Apache contexts where .env is not exported.
// Supports simple KEY=VALUE lines; ignores quotes and comments.
function load_dotenv_if_needed(array $keys): void {
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
if (empty($missing)) return;
@ -22,9 +19,7 @@ function load_dotenv_if_needed(array $keys): void {
if ($line[0] === '#' || trim($line) === '') continue;
if (!str_contains($line, '=')) continue;
[$k, $v] = array_map('trim', explode('=', $line, 2));
// Strip potential surrounding quotes
$v = trim($v, "\"' ");
// Do not override existing env
$v = trim($v, "' ");
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
putenv("{$k}={$v}");
}
@ -35,42 +30,46 @@ function load_dotenv_if_needed(array $keys): void {
load_dotenv_if_needed([
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO',
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO'
]);
$transport = env_val('MAIL_TRANSPORT', 'smtp');
$smtp_host = env_val('SMTP_HOST');
$smtp_port = (int) env_val('SMTP_PORT', 587);
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
$smtp_user = env_val('SMTP_USER');
$smtp_pass = env_val('SMTP_PASS');
// Fetch from DB
$db_settings = [];
try {
$pdo = db();
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'mail_%' OR setting_key LIKE 'pop3_%'");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$db_settings[$row['setting_key']] = $row['setting_value'];
}
} catch (Exception $e) {
// Fallback to empty if DB fails
}
$from_email = env_val('MAIL_FROM', 'no-reply@localhost');
$from_name = env_val('MAIL_FROM_NAME', 'App');
$transport = env_val('MAIL_TRANSPORT', $db_settings['mail_transport'] ?? 'smtp');
$smtp_host = env_val('SMTP_HOST', $db_settings['mail_host'] ?? '');
$smtp_port = (int) env_val('SMTP_PORT', $db_settings['mail_port'] ?? 587);
$smtp_secure = env_val('SMTP_SECURE', $db_settings['mail_encryption'] ?? 'tls');
$smtp_user = env_val('SMTP_USER', $db_settings['mail_username'] ?? '');
$smtp_pass = env_val('SMTP_PASS', $db_settings['mail_password'] ?? '');
$from_email = env_val('MAIL_FROM', $db_settings['mail_from_address'] ?? 'no-reply@localhost');
$from_name = env_val('MAIL_FROM_NAME', $db_settings['mail_from_name'] ?? 'App');
$reply_to = env_val('MAIL_REPLY_TO');
$dkim_domain = env_val('DKIM_DOMAIN');
$dkim_selector = env_val('DKIM_SELECTOR');
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
return [
'transport' => $transport,
// SMTP
'smtp_host' => $smtp_host,
'smtp_port' => $smtp_port,
'smtp_secure' => $smtp_secure,
'smtp_user' => $smtp_user,
'smtp_pass' => $smtp_pass,
// From / Reply-To
'from_email' => $from_email,
'from_name' => $from_name,
'reply_to' => $reply_to,
// DKIM (optional)
'dkim_domain' => $dkim_domain,
'dkim_selector' => $dkim_selector,
'dkim_private_key_path' => $dkim_private_key_path,
];
'pop3' => [
'host' => $db_settings['pop3_host'] ?? '',
'port' => $db_settings['pop3_port'] ?? '995',
'user' => $db_settings['pop3_username'] ?? '',
'pass' => $db_settings['pop3_password'] ?? '',
]
];

157
scripts/monthly_report.php Normal file
View File

@ -0,0 +1,157 @@
<?php
/**
* Monthly Financial Report Script
* This script calculates financial statistics for the previous month and sends an email report.
* Run via CLI: php scripts/monthly_report.php
*/
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../mail/MailService.php';
$pdo = db();
// Get settings
$settings_raw = $pdo->query("SELECT * FROM settings")->fetchAll();
$settings = [];
foreach ($settings_raw as $s) {
$settings[$s['setting_key']] = $s['setting_value'];
}
$recipient = $settings['report_recipient_email'] ?? getenv('MAIL_TO');
if (empty($recipient)) {
die("Error: No recipient email configured in settings or environment.\n");
}
// Calculate last month date range
$start_date = date('Y-m-01', strtotime('first day of last month'));
$end_date = date('Y-m-t', strtotime('last month'));
$month_name = date('F Y', strtotime('last month'));
// Fetch statistics for the period
$stats = [];
// 1. Total Donations
$stmt = $pdo->prepare("SELECT SUM(amount) as total, COUNT(*) as count FROM donations WHERE status = 'paid' AND created_at BETWEEN ? AND ?");
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
$donation_stats = $stmt->fetch(PDO::FETCH_ASSOC);
$stats['total_amount'] = $donation_stats['total'] ?? 0;
$stats['total_count'] = $donation_stats['count'] ?? 0;
// 2. Donations by Category
$stmt = $pdo->prepare("
SELECT c.name, SUM(d.amount) as total
FROM donations d
JOIN cases cs ON d.case_id = cs.id
JOIN categories c ON cs.category_id = c.id
WHERE d.status = 'paid' AND d.created_at BETWEEN ? AND ?
GROUP BY c.id
ORDER BY total DESC
");
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
$stats['by_category'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 3. Top Cases
$stmt = $pdo->prepare("
SELECT cs.title, SUM(d.amount) as total
FROM donations d
JOIN cases cs ON d.case_id = cs.id
WHERE d.status = 'paid' AND d.created_at BETWEEN ? AND ?
GROUP BY cs.id
ORDER BY total DESC
LIMIT 5
");
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
$stats['top_cases'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Build HTML Email
$html = "
<html>
<head>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px; }
.header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #059669; padding-bottom: 10px; }
.header h1 { color: #059669; margin: 0; }
.summary-box { background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; text-align: center; }
.stat-value { font-size: 24px; font-weight: bold; color: #059669; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
th { background: #f4f4f4; }
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h1>Monthly Financial Summary</h1>
<p>Period: {$month_name}</p>
</div>
<div class='summary-box'>
<div style='display: inline-block; width: 45%;'>
<p style='margin: 0; color: #666;'>Total Donations</p>
<div class='stat-value'>$" . number_format($stats['total_amount'], 2) . "</div>
</div>
<div style='display: inline-block; width: 45%; border-left: 1px solid #ddd;'>
<p style='margin: 0; color: #666;'>Donation Count</p>
<div class='stat-value'>{$stats['total_count']}</div>
</div>
</div>
<h3>Donations by Category</h3>
<table>
<thead>
<tr>
<th>Category</th>
<th>Amount</th>
</tr>
</thead>
<tbody>";
foreach ($stats['by_category'] as $cat) {
$html .= "<tr><td>" . htmlspecialchars($cat['name']) . "</td><td>$" . number_format($cat['total'], 2) . "</td></tr>";
}
$html .= "
</tbody>
</table>
<h3>Top 5 Performing Cases</h3>
<table>
<thead>
<tr>
<th>Case Title</th>
<th>Amount</th>
</tr>
</thead>
<tbody>";
foreach ($stats['top_cases'] as $case) {
$html .= "<tr><td>" . htmlspecialchars($case['title']) . "</td><td>$" . number_format($case['total'], 2) . "</td></tr>";
}
$html .= "
</tbody>
</table>
<div class='footer'>
<p>This is an automated report from your CharityHub Admin Panel.</p>
<p>&copy; " . date('Y') . " CharityHub. All rights reserved.</p>
</div>
</div>
</body>
</html>";
// Send Email
$subject = "Financial Summary Report - {$month_name}";
$plain_text = "Monthly Financial Summary for {$month_name}\n\nTotal Amount: $" . number_format($stats['total_amount'], 2) . "\nTotal Count: {$stats['total_count']}\n\nPlease view the HTML version for detailed breakdown.";
$res = MailService::sendMail($recipient, $subject, $html, $plain_text);
if (!empty($res['success'])) {
echo "Report sent successfully to {$recipient} for {$month_name}.\n";
} else {
echo "Failed to send report: " . ($res['error'] ?? 'Unknown error') . "\n";
}