adding favicon

This commit is contained in:
Flatlogic Bot 2026-02-13 10:15:56 +00:00
parent 0649f8fffd
commit 6d57961985
21 changed files with 393 additions and 237 deletions

View File

@ -21,6 +21,9 @@ $logs = $pdo->query("
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Audit Logs - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

View File

@ -1,60 +1,64 @@
<?php
// admin/auth.php
// Detect HTTPS even behind a proxy
$is_https = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https');
// Ensure session is started with basic secure defaults
if (session_status() === PHP_SESSION_NONE) {
session_name('CHARITYHUB_SESS');
// Explicitly set session cookie parameters
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => $is_https,
'httponly' => true,
'samesite' => 'Lax'
]);
ini_set('session.use_only_cookies', 1);
ini_set('session.use_strict_mode', 1); // Re-enabling strict mode for security now that login works
session_name('ORG_ADMIN_SESS');
session_start();
}
require_once __DIR__ . '/../db/config.php';
/**
* Check if the user is logged in
*/
function is_logged_in() {
return isset($_SESSION['user_id']);
}
/**
* Require login for a page
*/
function require_login() {
if (!is_logged_in()) {
header('Location: login.php?auth_error=1');
header('Location: login.php');
exit;
}
}
/**
* Get the current logged in user
*/
function get_user() {
return $_SESSION['user'] ?? null;
}
function is_super_admin() {
$user = get_user();
return $user && $user['role'] === 'super_admin';
if (!isset($_SESSION['user_id'])) return null;
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch();
} catch (Exception $e) {
return null;
}
}
/**
* Log an administrative action to the audit_logs table
* Check if user is super admin
*/
function log_action($action, $details = null) {
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$userId = $_SESSION['user_id'] ?? null;
$stmt = $pdo->prepare("INSERT INTO audit_logs (user_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$userId, $action, $details]);
function is_super_admin() {
$user = get_user();
return $user && isset($user['role']) && $user['role'] === 'super_admin';
}
/**
* Log an action to audit_logs table
*/
function log_action($action, $details = '') {
try {
$pdo = db();
$user_id = $_SESSION['user_id'] ?? null;
$stmt = $pdo->prepare("INSERT INTO audit_logs (user_id, action, details) VALUES (?, ?, ?)");
$stmt->execute([$user_id, $action, $details]);
} catch (Exception $e) {
error_log("Failed to log action: " . $e->getMessage());
}
}
/**
@ -65,14 +69,57 @@ function get_org_name() {
return $_SESSION['org_name'];
}
require_once __DIR__ . '/../db/config.php';
try {
$pdo = db();
$profile = $pdo->query("SELECT name_en FROM org_profile LIMIT 1")->fetch();
$name = $profile['name_en'] ?? 'CharityHub';
$_SESSION['org_name'] = $name;
return $name;
if ($profile && !empty($profile['name_en'])) {
$_SESSION['org_name'] = $profile['name_en'];
return $profile['name_en'];
}
} catch (Exception $e) {
return 'CharityHub';
}
return 'Organization';
}
/**
* Get organization favicon URL
*/
function get_favicon_url() {
if (isset($_SESSION['favicon_url']) && !empty($_SESSION['favicon_url'])) {
return $_SESSION['favicon_url'];
}
try {
$pdo = db();
$profile = $pdo->query("SELECT favicon_url FROM org_profile LIMIT 1")->fetch();
if ($profile && !empty($profile['favicon_url'])) {
$_SESSION['favicon_url'] = $profile['favicon_url'];
return $profile['favicon_url'];
}
} catch (Exception $e) {
}
return '';
}
/**
* Get organization logo URL
*/
function get_logo_url() {
if (isset($_SESSION['logo_url']) && !empty($_SESSION['logo_url'])) {
return $_SESSION['logo_url'];
}
try {
$pdo = db();
$profile = $pdo->query("SELECT logo_url FROM org_profile LIMIT 1")->fetch();
if ($profile && !empty($profile['logo_url'])) {
$_SESSION['logo_url'] = $profile['logo_url'];
return $profile['logo_url'];
}
} catch (Exception $e) {
}
return '';
}

View File

@ -112,6 +112,9 @@ if (isset($_GET['edit'])) {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Cases - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

View File

@ -51,6 +51,9 @@ if (isset($_GET['edit'])) {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Categories - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

View File

@ -19,6 +19,9 @@ $donations = $pdo->query("SELECT d.*, c.title_en as case_title, cat.name_en as c
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Donations - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

View File

@ -27,6 +27,9 @@ $donors = $pdo->query("
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Donors CRM - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

View File

@ -171,6 +171,9 @@ foreach ($gift_stats as $row) {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Financial Summary - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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>
@ -311,7 +314,7 @@ foreach ($gift_stats as $row) {
}
?></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 class="col-3 small text-end text-muted">Generated by: <?= htmlspecialchars($user['email']) ?></div>
</div>
</div>

View File

@ -42,6 +42,9 @@ $recent_donations = $pdo->query("
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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>
@ -68,7 +71,7 @@ $recent_donations = $pdo->query("
<div class="main-content">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="mb-0">Welcome, <?= htmlspecialchars($user['name'] ?? 'Admin') ?></h2>
<h2 class="mb-0">Welcome, <?= htmlspecialchars($user['email'] ?? 'Admin') ?></h2>
<p class="text-muted mb-0">Manage your charity activities and donations.</p>
</div>
<div class="text-muted"><?= date('l, F j, Y') ?></div>

View File

@ -51,6 +51,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - <?= htmlspecialchars(get_org_name()) ?></title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

View File

@ -20,12 +20,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$phone = $_POST['phone'];
$address = $_POST['address'];
$logo_url = $profile['logo_url'] ?? '';
$favicon_url = $profile['favicon_url'] ?? '';
$upload_dir = '../assets/images/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
// Handle Logo Upload
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
$upload_dir = '../assets/images/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true);
$file_ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
$file_name = 'logo_' . time() . '.' . $file_ext;
$target_file = $upload_dir . $file_name;
@ -35,15 +36,29 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
}
// Handle Favicon Upload
if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
$file_ext = pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION);
$file_name = 'favicon_' . time() . '.' . $file_ext;
$target_file = $upload_dir . $file_name;
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $target_file)) {
$favicon_url = 'assets/images/' . $file_name;
}
}
if ($profile) {
$stmt = $pdo->prepare("UPDATE org_profile SET name_en=?, name_ar=?, description_en=?, description_ar=?, email=?, phone=?, address=?, logo_url=? WHERE id=?");
$stmt->execute([$name_en, $name_ar, $description_en, $description_ar, $email, $phone, $address, $logo_url, $profile['id']]);
$stmt = $pdo->prepare("UPDATE org_profile SET name_en=?, name_ar=?, description_en=?, description_ar=?, email=?, phone=?, address=?, logo_url=?, favicon_url=? WHERE id=?");
$stmt->execute([$name_en, $name_ar, $description_en, $description_ar, $email, $phone, $address, $logo_url, $favicon_url, $profile['id']]);
} else {
$stmt = $pdo->prepare("INSERT INTO org_profile (name_en, name_ar, description_en, description_ar, email, phone, address, logo_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$name_en, $name_ar, $description_en, $description_ar, $email, $phone, $address, $logo_url]);
$stmt = $pdo->prepare("INSERT INTO org_profile (name_en, name_ar, description_en, description_ar, email, phone, address, logo_url, favicon_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$name_en, $name_ar, $description_en, $description_ar, $email, $phone, $address, $logo_url, $favicon_url]);
}
$_SESSION['org_name'] = $name_en;
$_SESSION['logo_url'] = $logo_url;
$_SESSION['favicon_url'] = $favicon_url;
header('Location: profile.php?success=1');
exit;
}
@ -54,6 +69,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Organization Profile - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>
@ -120,7 +138,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<div class="mb-4">
<label class="form-label">Organization Logo</label>
<div class="mb-2">
<?php if (!empty($profile['logo_url'])): ?>
@ -133,6 +151,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
</div>
<input type="file" name="logo" class="form-control" accept="image/*">
</div>
<div class="mb-3">
<label class="form-label">Favicon (Small icon for browser tabs)</label>
<div class="mb-2">
<?php if (!empty($profile['favicon_url'])): ?>
<img src="../<?= htmlspecialchars($profile['favicon_url']) ?>" class="img-thumbnail" style="max-height: 64px;">
<?php else: ?>
<div class="bg-light d-flex align-items-center justify-content-center border rounded" style="height: 64px; width: 64px;">
<i class="bi bi-app text-muted fs-3"></i>
</div>
<?php endif; ?>
</div>
<input type="file" name="favicon" class="form-control" accept="image/*">
</div>
</div>
</div>
<hr>
@ -145,4 +176,4 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
</html>

View File

@ -31,6 +31,9 @@ foreach ($settings_raw as $s) {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gateway Settings - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
<?php if ($favicon = get_favicon_url()): ?>
<link rel="icon" type="image/x-icon" href="../<?= htmlspecialchars($favicon) ?>">
<?php endif; ?>
<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">
<style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -17,6 +17,7 @@ $don = $stmt->fetch();
if (!$don) exit('Donation not found or not completed.');
$org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
$orgName = $org['name_en'] ?? 'Organization';
// This is a simple HTML certificate that can be printed or saved as PDF by the user
?>
@ -25,7 +26,7 @@ $org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Donation Certificate - CharityHub</title>
<title>Donation Certificate - <?= htmlspecialchars($orgName) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Inter:wght@400;600&display=swap" rel="stylesheet">
@ -83,7 +84,7 @@ $org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
</div>
<div class="stamp">
OFFICIAL<br>SEAL<br>CHARITYHUB
OFFICIAL<br>SEAL<br><?= strtoupper(htmlspecialchars($orgName)) ?>
</div>
<div class="cert-footer">
@ -92,7 +93,7 @@ $org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
</div>
<div class="signature">
Authorized Signature<br>
<?= htmlspecialchars($org['name_en'] ?? 'CharityHub') ?>
<?= htmlspecialchars($orgName) ?>
</div>
</div>
</div>

View File

@ -6,8 +6,8 @@ INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
('mail_username', ''),
('mail_password', ''),
('mail_from_address', ''),
('mail_from_name', 'CharityHub'),
('mail_from_name', ''),
('pop3_host', ''),
('pop3_port', '995'),
('pop3_username', ''),
('pop3_password', '');
('pop3_password', '');

View File

@ -0,0 +1,2 @@
-- Add favicon_url column to org_profile table
ALTER TABLE org_profile ADD COLUMN favicon_url VARCHAR(255) DEFAULT NULL AFTER logo_url;

View File

@ -151,6 +151,9 @@ function safe_truncate($text, $limit = 120) {
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= ($lang === 'en' ? ($profile['name_en'] ?? 'Organization') : ($profile['name_ar'] ?? 'المؤسسة')) . ' - ' . $t['title'] ?></title>
<?php if (!empty($profile["favicon_url"])): ?>
<link rel="icon" href="<?= htmlspecialchars($profile["favicon_url"]) ?>">
<?php endif; ?>
<!-- Meta Tags -->
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">

View File

@ -13,31 +13,32 @@ class MailService
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
{
$cfg = self::loadConfig();
$orgName = $cfg['org_name'] ?? 'Organization';
$autoload = __DIR__ . '/../vendor/autoload.php';
if (file_exists($autoload)) {
require_once $autoload;
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'libphp-phpmailer/autoload.php';
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'libphp-phpmailer/src/Exception.php';
@require_once 'libphp-phpmailer/src/SMTP.php';
@require_once 'libphp-phpmailer/src/PHPMailer.php';
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'PHPMailer/src/Exception.php';
@require_once 'PHPMailer/src/SMTP.php';
@require_once 'PHPMailer/src/PHPMailer.php';
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'PHPMailer/Exception.php';
@require_once 'PHPMailer/SMTP.php';
@require_once 'PHPMailer/PHPMailer.php';
}
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
}
@ -55,7 +56,7 @@ class MailService
$mail->Password = $cfg['smtp_pass'] ?? '';
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App');
$fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? $orgName);
$mail->setFrom($fromEmail, $fromName);
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
$mail->addReplyTo($opts['reply_to']);
@ -91,11 +92,22 @@ class MailService
$mail->isHTML(true);
$mail->Subject = $subject;
// Append footer if not explicitly disabled
if (!isset($opts['no_footer']) || !$opts['no_footer']) {
$footerHtml = "<br><br>Best regards,<br>The <strong>{$orgName}</strong> Team";
$footerText = "\n\nBest regards,\nThe {$orgName} Team";
$htmlBody .= $footerHtml;
if ($textBody) {
$textBody .= $footerText;
}
}
$mail->Body = $htmlBody;
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
$ok = $mail->send();
return [ 'success' => $ok ];
} catch (\Throwable $e) {
} catch (\throwable $e) {
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
}
}
@ -124,21 +136,21 @@ class MailService
require_once $autoload;
}
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
// Debian/Ubuntu package layout (libphp-phpmailer)
@require_once 'libphp-phpmailer/autoload.php';
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'libphp-phpmailer/src/Exception.php';
@require_once 'libphp-phpmailer/src/SMTP.php';
@require_once 'libphp-phpmailer/src/PHPMailer.php';
}
// Alternative layout (older PHPMailer package names)
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'PHPMailer/src/Exception.php';
@require_once 'PHPMailer/src/SMTP.php';
@require_once 'PHPMailer/src/PHPMailer.php';
}
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
@require_once 'PHPMailer/Exception.php';
@require_once 'PHPMailer/SMTP.php';
@require_once 'PHPMailer/PHPMailer.php';
@ -146,7 +158,7 @@ class MailService
}
$transport = $cfg['transport'] ?? 'smtp';
if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
if ($transport === 'smtp' && class_exists('PHPMailer\PHPMailer\PHPMailer')) {
return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject);
}
@ -156,6 +168,7 @@ class MailService
private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject)
{
$orgName = $cfg['org_name'] ?? 'Organization';
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
$mail->isSMTP();
@ -170,7 +183,7 @@ class MailService
$mail->Password = $cfg['smtp_pass'] ?? '';
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
$fromName = $cfg['from_name'] ?? 'App';
$fromName = $cfg['from_name'] ?? $orgName;
$mail->setFrom($fromEmail, $fromName);
// Use Reply-To for the user's email to avoid spoofing From
@ -216,12 +229,15 @@ class MailService
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}";
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}";
$footer = "<hr><p><small>This message was sent via the <strong>{$orgName}</strong> contact form.</small></p>";
$mail->Body = "<p><strong>Name:</strong> {$safeName}</p><p><strong>Email:</strong> {$safeEmail}</p><hr>{$safeBody}{$footer}";
$mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}\n\n---\nThis message was sent via the {$orgName} contact form.";
$ok = $mail->send();
return [ 'success' => $ok ];
} catch (\Throwable $e) {
} catch (\throwable $e) {
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
}
}
@ -232,4 +248,4 @@ class MailService
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
return self::sendMail($to, $subject, $html, $body, $opts);
}
}
}

View File

@ -62,9 +62,16 @@ class WablasService {
return ['success' => false, 'error' => $result];
}
public static function getOrgName() {
$pdo = db();
$org = $pdo->query("SELECT name_en FROM org_profile LIMIT 1")->fetch();
return $org['name_en'] ?? 'Organization';
}
public static function sendThankYou($donation) {
$name = !empty($donation['donor_name']) ? $donation['donor_name'] : 'Donor';
$amount = number_format($donation['amount'], 3);
$orgName = self::getOrgName();
// Fetch case title
$pdo = db();
@ -73,7 +80,7 @@ class WablasService {
$case = $stmt->fetch();
$caseTitle = $case ? $case['title_en'] : 'a cause';
$message = "Dear $name,\n\nThank you for your generous donation of OMR $amount to \"$caseTitle\".\n\nYour support makes a real difference! ❤️\n\nCharityHub Team";
$message = "Dear $name,\n\nThank you for your generous donation of OMR $amount to \"$caseTitle\".\n\nYour support makes a real difference! ❤️\n\n$orgName Team";
if (!empty($donation['donor_phone'])) {
return self::sendMessage($donation['donor_phone'], $message);
@ -90,6 +97,7 @@ class WablasService {
$donorName = !empty($donation['donor_name']) ? $donation['donor_name'] : 'A generous donor';
$recipientName = !empty($donation['gift_recipient_name']) ? $donation['gift_recipient_name'] : 'Friend';
$giftMessage = !empty($donation['gift_message']) ? "\n\nMessage: \"" . $donation['gift_message'] . "\"" : "";
$orgName = self::getOrgName();
// Fetch case title
$pdo = db();
@ -98,7 +106,7 @@ class WablasService {
$case = $stmt->fetch();
$caseTitle = $case ? $case['title_en'] : 'a charitable cause';
$message = "Hello $recipientName! ✨\n\n$donorName has made a donation to \"$caseTitle\" in your name as a special gift.$giftMessage\n\nMay this kindness bring joy to your day! ❤️\n\nCharityHub Team";
$message = "Hello $recipientName! ✨\n\n$donorName has made a donation to \"$caseTitle\" in your name as a special gift.$giftMessage\n\nMay this kindness bring joy to your day! ❤️\n\n$orgName Team";
return self::sendMessage($donation['gift_recipient_phone'], $message);
}

View File

@ -35,14 +35,21 @@ load_dotenv_if_needed([
// Fetch from DB
$db_settings = [];
$org_name = 'Organization';
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'];
}
// Also fetch org name for default
$org = $pdo->query("SELECT name_en FROM org_profile LIMIT 1")->fetch();
if ($org && !empty($org['name_en'])) {
$org_name = $org['name_en'];
}
} catch (Exception $e) {
// Fallback to empty if DB fails
// Fallback if DB fails
}
$transport = env_val('MAIL_TRANSPORT', $db_settings['mail_transport'] ?? 'smtp');
@ -53,7 +60,14 @@ $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');
// Ensure from_name falls back to org_name if DB setting is empty
$db_from_name = $db_settings['mail_from_name'] ?? '';
if (empty($db_from_name)) {
$db_from_name = $org_name;
}
$from_name = env_val('MAIL_FROM_NAME', $db_from_name);
$reply_to = env_val('MAIL_REPLY_TO');
return [
@ -66,6 +80,7 @@ return [
'from_email' => $from_email,
'from_name' => $from_name,
'reply_to' => $reply_to,
'org_name' => $org_name,
'pop3' => [
'host' => $db_settings['pop3_host'] ?? '',
'port' => $db_settings['pop3_port'] ?? '995',

View File

@ -1,157 +1,160 @@
<?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";
}
<?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");
}
// Get org profile
$org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
$orgName = $org['name_en'] ?? 'Organization';
// 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 " . htmlspecialchars($orgName) . " Admin Panel.</p>
<p>&copy; " . date('Y') . " " . htmlspecialchars($orgName) . ". 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";
}

View File

@ -77,13 +77,16 @@ if ($donation) {
$final_donation_id = $existing['id'];
}
}
$org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
$orgName = $org['name_en'] ?? 'Organization';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Donation Successful - CharityHub</title>
<title>Donation Successful - <?= htmlspecialchars($orgName) ?></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">
<style>