adding favicon
This commit is contained in:
parent
0649f8fffd
commit
6d57961985
@ -21,6 +21,9 @@ $logs = $pdo->query("
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Audit Logs - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
131
admin/auth.php
131
admin/auth.php
@ -1,60 +1,64 @@
|
|||||||
<?php
|
<?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) {
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
session_name('CHARITYHUB_SESS');
|
session_name('ORG_ADMIN_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_start();
|
session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user is logged in
|
||||||
|
*/
|
||||||
function is_logged_in() {
|
function is_logged_in() {
|
||||||
return isset($_SESSION['user_id']);
|
return isset($_SESSION['user_id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require login for a page
|
||||||
|
*/
|
||||||
function require_login() {
|
function require_login() {
|
||||||
if (!is_logged_in()) {
|
if (!is_logged_in()) {
|
||||||
header('Location: login.php?auth_error=1');
|
header('Location: login.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current logged in user
|
||||||
|
*/
|
||||||
function get_user() {
|
function get_user() {
|
||||||
return $_SESSION['user'] ?? null;
|
if (!isset($_SESSION['user_id'])) return null;
|
||||||
}
|
|
||||||
|
try {
|
||||||
function is_super_admin() {
|
$pdo = db();
|
||||||
$user = get_user();
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
|
||||||
return $user && $user['role'] === 'super_admin';
|
$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) {
|
function is_super_admin() {
|
||||||
require_once __DIR__ . '/../db/config.php';
|
$user = get_user();
|
||||||
$pdo = db();
|
return $user && isset($user['role']) && $user['role'] === 'super_admin';
|
||||||
$userId = $_SESSION['user_id'] ?? null;
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO audit_logs (user_id, action, details) VALUES (?, ?, ?)");
|
/**
|
||||||
$stmt->execute([$userId, $action, $details]);
|
* 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'];
|
return $_SESSION['org_name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
|
||||||
try {
|
try {
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$profile = $pdo->query("SELECT name_en FROM org_profile LIMIT 1")->fetch();
|
$profile = $pdo->query("SELECT name_en FROM org_profile LIMIT 1")->fetch();
|
||||||
$name = $profile['name_en'] ?? 'CharityHub';
|
if ($profile && !empty($profile['name_en'])) {
|
||||||
$_SESSION['org_name'] = $name;
|
$_SESSION['org_name'] = $profile['name_en'];
|
||||||
return $name;
|
return $profile['name_en'];
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} 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 '';
|
||||||
}
|
}
|
||||||
@ -112,6 +112,9 @@ if (isset($_GET['edit'])) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Manage Cases - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -51,6 +51,9 @@ if (isset($_GET['edit'])) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Manage Categories - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -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 charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Donations - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -27,6 +27,9 @@ $donors = $pdo->query("
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Donors CRM - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -171,6 +171,9 @@ foreach ($gift_stats as $row) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Financial Summary - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
@ -311,7 +314,7 @@ foreach ($gift_stats as $row) {
|
|||||||
}
|
}
|
||||||
?></div>
|
?></div>
|
||||||
<div class="col-3 small"><strong>Status:</strong> <?= $status_filter ? ucfirst(htmlspecialchars($status_filter)) : 'Completed (Default)' ?></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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,9 @@ $recent_donations = $pdo->query("
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Dashboard - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
@ -68,7 +71,7 @@ $recent_donations = $pdo->query("
|
|||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div>
|
<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>
|
<p class="text-muted mb-0">Manage your charity activities and donations.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted"><?= date('l, F j, Y') ?></div>
|
<div class="text-muted"><?= date('l, F j, Y') ?></div>
|
||||||
|
|||||||
@ -51,6 +51,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Admin Login - <?= htmlspecialchars(get_org_name()) ?></title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@ -20,12 +20,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$phone = $_POST['phone'];
|
$phone = $_POST['phone'];
|
||||||
$address = $_POST['address'];
|
$address = $_POST['address'];
|
||||||
$logo_url = $profile['logo_url'] ?? '';
|
$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
|
// Handle Logo Upload
|
||||||
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
|
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_ext = pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION);
|
||||||
$file_name = 'logo_' . time() . '.' . $file_ext;
|
$file_name = 'logo_' . time() . '.' . $file_ext;
|
||||||
$target_file = $upload_dir . $file_name;
|
$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) {
|
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 = $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, $profile['id']]);
|
$stmt->execute([$name_en, $name_ar, $description_en, $description_ar, $email, $phone, $address, $logo_url, $favicon_url, $profile['id']]);
|
||||||
} else {
|
} else {
|
||||||
$stmt = $pdo->prepare("INSERT INTO org_profile (name_en, name_ar, description_en, description_ar, email, phone, address, logo_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
$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]);
|
$stmt->execute([$name_en, $name_ar, $description_en, $description_ar, $email, $phone, $address, $logo_url, $favicon_url]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['org_name'] = $name_en;
|
$_SESSION['org_name'] = $name_en;
|
||||||
|
$_SESSION['logo_url'] = $logo_url;
|
||||||
|
$_SESSION['favicon_url'] = $favicon_url;
|
||||||
|
|
||||||
header('Location: profile.php?success=1');
|
header('Location: profile.php?success=1');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
@ -54,6 +69,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Organization Profile - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
@ -120,7 +138,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="mb-3">
|
<div class="mb-4">
|
||||||
<label class="form-label">Organization Logo</label>
|
<label class="form-label">Organization Logo</label>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<?php if (!empty($profile['logo_url'])): ?>
|
<?php if (!empty($profile['logo_url'])): ?>
|
||||||
@ -133,6 +151,19 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
</div>
|
</div>
|
||||||
<input type="file" name="logo" class="form-control" accept="image/*">
|
<input type="file" name="logo" class="form-control" accept="image/*">
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<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>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -31,6 +31,9 @@ foreach ($settings_raw as $s) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Gateway Settings - <?= htmlspecialchars(get_org_name()) ?> Admin</title>
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
BIN
assets/images/favicon_1770977716.jpg
Normal file
BIN
assets/images/favicon_1770977716.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
@ -17,6 +17,7 @@ $don = $stmt->fetch();
|
|||||||
if (!$don) exit('Donation not found or not completed.');
|
if (!$don) exit('Donation not found or not completed.');
|
||||||
|
|
||||||
$org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
|
$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
|
// 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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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>
|
||||||
|
|
||||||
<div class="stamp">
|
<div class="stamp">
|
||||||
OFFICIAL<br>SEAL<br>CHARITYHUB
|
OFFICIAL<br>SEAL<br><?= strtoupper(htmlspecialchars($orgName)) ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cert-footer">
|
<div class="cert-footer">
|
||||||
@ -92,7 +93,7 @@ $org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
|
|||||||
</div>
|
</div>
|
||||||
<div class="signature">
|
<div class="signature">
|
||||||
Authorized Signature<br>
|
Authorized Signature<br>
|
||||||
<?= htmlspecialchars($org['name_en'] ?? 'CharityHub') ?>
|
<?= htmlspecialchars($orgName) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -6,8 +6,8 @@ INSERT IGNORE INTO settings (setting_key, setting_value) VALUES
|
|||||||
('mail_username', ''),
|
('mail_username', ''),
|
||||||
('mail_password', ''),
|
('mail_password', ''),
|
||||||
('mail_from_address', ''),
|
('mail_from_address', ''),
|
||||||
('mail_from_name', 'CharityHub'),
|
('mail_from_name', ''),
|
||||||
('pop3_host', ''),
|
('pop3_host', ''),
|
||||||
('pop3_port', '995'),
|
('pop3_port', '995'),
|
||||||
('pop3_username', ''),
|
('pop3_username', ''),
|
||||||
('pop3_password', '');
|
('pop3_password', '');
|
||||||
2
db/migrations/20260213_add_favicon_url.sql
Normal file
2
db/migrations/20260213_add_favicon_url.sql
Normal 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;
|
||||||
@ -151,6 +151,9 @@ function safe_truncate($text, $limit = 120) {
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?= ($lang === 'en' ? ($profile['name_en'] ?? 'Organization') : ($profile['name_ar'] ?? 'المؤسسة')) . ' - ' . $t['title'] ?></title>
|
<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 Tags -->
|
||||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>">
|
||||||
|
|||||||
@ -13,31 +13,32 @@ class MailService
|
|||||||
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
|
public static function sendMail($to, string $subject, string $htmlBody, ?string $textBody = null, array $opts = [])
|
||||||
{
|
{
|
||||||
$cfg = self::loadConfig();
|
$cfg = self::loadConfig();
|
||||||
|
$orgName = $cfg['org_name'] ?? 'Organization';
|
||||||
|
|
||||||
$autoload = __DIR__ . '/../vendor/autoload.php';
|
$autoload = __DIR__ . '/../vendor/autoload.php';
|
||||||
if (file_exists($autoload)) {
|
if (file_exists($autoload)) {
|
||||||
require_once $autoload;
|
require_once $autoload;
|
||||||
}
|
}
|
||||||
if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) {
|
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
|
||||||
@require_once 'libphp-phpmailer/autoload.php';
|
@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/Exception.php';
|
||||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||||
@require_once 'libphp-phpmailer/src/PHPMailer.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/Exception.php';
|
||||||
@require_once 'PHPMailer/src/SMTP.php';
|
@require_once 'PHPMailer/src/SMTP.php';
|
||||||
@require_once 'PHPMailer/src/PHPMailer.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/Exception.php';
|
||||||
@require_once 'PHPMailer/SMTP.php';
|
@require_once 'PHPMailer/SMTP.php';
|
||||||
@require_once 'PHPMailer/PHPMailer.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' ];
|
return [ 'success' => false, 'error' => 'PHPMailer not available' ];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +56,7 @@ class MailService
|
|||||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||||
|
|
||||||
$fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost');
|
$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);
|
$mail->setFrom($fromEmail, $fromName);
|
||||||
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) {
|
||||||
$mail->addReplyTo($opts['reply_to']);
|
$mail->addReplyTo($opts['reply_to']);
|
||||||
@ -91,11 +92,22 @@ class MailService
|
|||||||
|
|
||||||
$mail->isHTML(true);
|
$mail->isHTML(true);
|
||||||
$mail->Subject = $subject;
|
$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->Body = $htmlBody;
|
||||||
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
$mail->AltBody = $textBody ?? strip_tags($htmlBody);
|
||||||
$ok = $mail->send();
|
$ok = $mail->send();
|
||||||
return [ 'success' => $ok ];
|
return [ 'success' => $ok ];
|
||||||
} catch (\Throwable $e) {
|
} catch (\throwable $e) {
|
||||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,21 +136,21 @@ class MailService
|
|||||||
require_once $autoload;
|
require_once $autoload;
|
||||||
}
|
}
|
||||||
// Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer)
|
// 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)
|
// Debian/Ubuntu package layout (libphp-phpmailer)
|
||||||
@require_once 'libphp-phpmailer/autoload.php';
|
@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/Exception.php';
|
||||||
@require_once 'libphp-phpmailer/src/SMTP.php';
|
@require_once 'libphp-phpmailer/src/SMTP.php';
|
||||||
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
@require_once 'libphp-phpmailer/src/PHPMailer.php';
|
||||||
}
|
}
|
||||||
// Alternative layout (older PHPMailer package names)
|
// 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/Exception.php';
|
||||||
@require_once 'PHPMailer/src/SMTP.php';
|
@require_once 'PHPMailer/src/SMTP.php';
|
||||||
@require_once 'PHPMailer/src/PHPMailer.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/Exception.php';
|
||||||
@require_once 'PHPMailer/SMTP.php';
|
@require_once 'PHPMailer/SMTP.php';
|
||||||
@require_once 'PHPMailer/PHPMailer.php';
|
@require_once 'PHPMailer/PHPMailer.php';
|
||||||
@ -146,7 +158,7 @@ class MailService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$transport = $cfg['transport'] ?? 'smtp';
|
$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);
|
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)
|
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);
|
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||||||
try {
|
try {
|
||||||
$mail->isSMTP();
|
$mail->isSMTP();
|
||||||
@ -170,7 +183,7 @@ class MailService
|
|||||||
$mail->Password = $cfg['smtp_pass'] ?? '';
|
$mail->Password = $cfg['smtp_pass'] ?? '';
|
||||||
|
|
||||||
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
|
$fromEmail = $cfg['from_email'] ?? 'no-reply@localhost';
|
||||||
$fromName = $cfg['from_name'] ?? 'App';
|
$fromName = $cfg['from_name'] ?? $orgName;
|
||||||
$mail->setFrom($fromEmail, $fromName);
|
$mail->setFrom($fromEmail, $fromName);
|
||||||
|
|
||||||
// Use Reply-To for the user's email to avoid spoofing From
|
// 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');
|
$safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||||
$safeEmail = htmlspecialchars($email, 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'));
|
$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();
|
$ok = $mail->send();
|
||||||
return [ 'success' => $ok ];
|
return [ 'success' => $ok ];
|
||||||
} catch (\Throwable $e) {
|
} catch (\throwable $e) {
|
||||||
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,4 +248,4 @@ class MailService
|
|||||||
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
$html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
|
||||||
return self::sendMail($to, $subject, $html, $body, $opts);
|
return self::sendMail($to, $subject, $html, $body, $opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,9 +62,16 @@ class WablasService {
|
|||||||
return ['success' => false, 'error' => $result];
|
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) {
|
public static function sendThankYou($donation) {
|
||||||
$name = !empty($donation['donor_name']) ? $donation['donor_name'] : 'Donor';
|
$name = !empty($donation['donor_name']) ? $donation['donor_name'] : 'Donor';
|
||||||
$amount = number_format($donation['amount'], 3);
|
$amount = number_format($donation['amount'], 3);
|
||||||
|
$orgName = self::getOrgName();
|
||||||
|
|
||||||
// Fetch case title
|
// Fetch case title
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
@ -73,7 +80,7 @@ class WablasService {
|
|||||||
$case = $stmt->fetch();
|
$case = $stmt->fetch();
|
||||||
$caseTitle = $case ? $case['title_en'] : 'a cause';
|
$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'])) {
|
if (!empty($donation['donor_phone'])) {
|
||||||
return self::sendMessage($donation['donor_phone'], $message);
|
return self::sendMessage($donation['donor_phone'], $message);
|
||||||
@ -90,6 +97,7 @@ class WablasService {
|
|||||||
$donorName = !empty($donation['donor_name']) ? $donation['donor_name'] : 'A generous donor';
|
$donorName = !empty($donation['donor_name']) ? $donation['donor_name'] : 'A generous donor';
|
||||||
$recipientName = !empty($donation['gift_recipient_name']) ? $donation['gift_recipient_name'] : 'Friend';
|
$recipientName = !empty($donation['gift_recipient_name']) ? $donation['gift_recipient_name'] : 'Friend';
|
||||||
$giftMessage = !empty($donation['gift_message']) ? "\n\nMessage: \"" . $donation['gift_message'] . "\"" : "";
|
$giftMessage = !empty($donation['gift_message']) ? "\n\nMessage: \"" . $donation['gift_message'] . "\"" : "";
|
||||||
|
$orgName = self::getOrgName();
|
||||||
|
|
||||||
// Fetch case title
|
// Fetch case title
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
@ -98,7 +106,7 @@ class WablasService {
|
|||||||
$case = $stmt->fetch();
|
$case = $stmt->fetch();
|
||||||
$caseTitle = $case ? $case['title_en'] : 'a charitable cause';
|
$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);
|
return self::sendMessage($donation['gift_recipient_phone'], $message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,14 +35,21 @@ load_dotenv_if_needed([
|
|||||||
|
|
||||||
// Fetch from DB
|
// Fetch from DB
|
||||||
$db_settings = [];
|
$db_settings = [];
|
||||||
|
$org_name = 'Organization';
|
||||||
try {
|
try {
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$stmt = $pdo->query("SELECT setting_key, setting_value FROM settings WHERE setting_key LIKE 'mail_%' OR setting_key LIKE 'pop3_%'");
|
$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)) {
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$db_settings[$row['setting_key']] = $row['setting_value'];
|
$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) {
|
} catch (Exception $e) {
|
||||||
// Fallback to empty if DB fails
|
// Fallback if DB fails
|
||||||
}
|
}
|
||||||
|
|
||||||
$transport = env_val('MAIL_TRANSPORT', $db_settings['mail_transport'] ?? 'smtp');
|
$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'] ?? '');
|
$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_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');
|
$reply_to = env_val('MAIL_REPLY_TO');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -66,6 +80,7 @@ return [
|
|||||||
'from_email' => $from_email,
|
'from_email' => $from_email,
|
||||||
'from_name' => $from_name,
|
'from_name' => $from_name,
|
||||||
'reply_to' => $reply_to,
|
'reply_to' => $reply_to,
|
||||||
|
'org_name' => $org_name,
|
||||||
'pop3' => [
|
'pop3' => [
|
||||||
'host' => $db_settings['pop3_host'] ?? '',
|
'host' => $db_settings['pop3_host'] ?? '',
|
||||||
'port' => $db_settings['pop3_port'] ?? '995',
|
'port' => $db_settings['pop3_port'] ?? '995',
|
||||||
|
|||||||
@ -1,157 +1,160 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Monthly Financial Report Script
|
* Monthly Financial Report Script
|
||||||
* This script calculates financial statistics for the previous month and sends an email report.
|
* This script calculates financial statistics for the previous month and sends an email report.
|
||||||
* Run via CLI: php scripts/monthly_report.php
|
* Run via CLI: php scripts/monthly_report.php
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../db/config.php';
|
require_once __DIR__ . '/../db/config.php';
|
||||||
require_once __DIR__ . '/../mail/MailService.php';
|
require_once __DIR__ . '/../mail/MailService.php';
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
|
|
||||||
// Get settings
|
// Get settings
|
||||||
$settings_raw = $pdo->query("SELECT * FROM settings")->fetchAll();
|
$settings_raw = $pdo->query("SELECT * FROM settings")->fetchAll();
|
||||||
$settings = [];
|
$settings = [];
|
||||||
foreach ($settings_raw as $s) {
|
foreach ($settings_raw as $s) {
|
||||||
$settings[$s['setting_key']] = $s['setting_value'];
|
$settings[$s['setting_key']] = $s['setting_value'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$recipient = $settings['report_recipient_email'] ?? getenv('MAIL_TO');
|
$recipient = $settings['report_recipient_email'] ?? getenv('MAIL_TO');
|
||||||
|
|
||||||
if (empty($recipient)) {
|
if (empty($recipient)) {
|
||||||
die("Error: No recipient email configured in settings or environment.\n");
|
die("Error: No recipient email configured in settings or environment.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate last month date range
|
// Get org profile
|
||||||
$start_date = date('Y-m-01', strtotime('first day of last month'));
|
$org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
|
||||||
$end_date = date('Y-m-t', strtotime('last month'));
|
$orgName = $org['name_en'] ?? 'Organization';
|
||||||
$month_name = date('F Y', strtotime('last month'));
|
|
||||||
|
// Calculate last month date range
|
||||||
// Fetch statistics for the period
|
$start_date = date('Y-m-01', strtotime('first day of last month'));
|
||||||
$stats = [];
|
$end_date = date('Y-m-t', strtotime('last month'));
|
||||||
|
$month_name = date('F Y', strtotime('last month'));
|
||||||
// 1. Total Donations
|
|
||||||
$stmt = $pdo->prepare("SELECT SUM(amount) as total, COUNT(*) as count FROM donations WHERE status = 'paid' AND created_at BETWEEN ? AND ?");
|
// Fetch statistics for the period
|
||||||
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
|
$stats = [];
|
||||||
$donation_stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
$stats['total_amount'] = $donation_stats['total'] ?? 0;
|
// 1. Total Donations
|
||||||
$stats['total_count'] = $donation_stats['count'] ?? 0;
|
$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']);
|
||||||
// 2. Donations by Category
|
$donation_stats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
$stmt = $pdo->prepare("
|
$stats['total_amount'] = $donation_stats['total'] ?? 0;
|
||||||
SELECT c.name, SUM(d.amount) as total
|
$stats['total_count'] = $donation_stats['count'] ?? 0;
|
||||||
FROM donations d
|
|
||||||
JOIN cases cs ON d.case_id = cs.id
|
// 2. Donations by Category
|
||||||
JOIN categories c ON cs.category_id = c.id
|
$stmt = $pdo->prepare("
|
||||||
WHERE d.status = 'paid' AND d.created_at BETWEEN ? AND ?
|
SELECT c.name, SUM(d.amount) as total
|
||||||
GROUP BY c.id
|
FROM donations d
|
||||||
ORDER BY total DESC
|
JOIN cases cs ON d.case_id = cs.id
|
||||||
");
|
JOIN categories c ON cs.category_id = c.id
|
||||||
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
|
WHERE d.status = 'paid' AND d.created_at BETWEEN ? AND ?
|
||||||
$stats['by_category'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
GROUP BY c.id
|
||||||
|
ORDER BY total DESC
|
||||||
// 3. Top Cases
|
");
|
||||||
$stmt = $pdo->prepare("
|
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
|
||||||
SELECT cs.title, SUM(d.amount) as total
|
$stats['by_category'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
FROM donations d
|
|
||||||
JOIN cases cs ON d.case_id = cs.id
|
// 3. Top Cases
|
||||||
WHERE d.status = 'paid' AND d.created_at BETWEEN ? AND ?
|
$stmt = $pdo->prepare("
|
||||||
GROUP BY cs.id
|
SELECT cs.title, SUM(d.amount) as total
|
||||||
ORDER BY total DESC
|
FROM donations d
|
||||||
LIMIT 5
|
JOIN cases cs ON d.case_id = cs.id
|
||||||
");
|
WHERE d.status = 'paid' AND d.created_at BETWEEN ? AND ?
|
||||||
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
|
GROUP BY cs.id
|
||||||
$stats['top_cases'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
ORDER BY total DESC
|
||||||
|
LIMIT 5
|
||||||
// Build HTML Email
|
");
|
||||||
$html = "
|
$stmt->execute([$start_date . ' 00:00:00', $end_date . ' 23:59:59']);
|
||||||
<html>
|
$stats['top_cases'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
<head>
|
|
||||||
<style>
|
// Build HTML Email
|
||||||
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; }
|
$html = "
|
||||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px; }
|
<html>
|
||||||
.header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #059669; padding-bottom: 10px; }
|
<head>
|
||||||
.header h1 { color: #059669; margin: 0; }
|
<style>
|
||||||
.summary-box { background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; text-align: center; }
|
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; }
|
||||||
.stat-value { font-size: 24px; font-weight: bold; color: #059669; }
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 10px; }
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
.header { text-align: center; margin-bottom: 30px; border-bottom: 2px solid #059669; padding-bottom: 10px; }
|
||||||
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
|
.header h1 { color: #059669; margin: 0; }
|
||||||
th { background: #f4f4f4; }
|
.summary-box { background: #f9f9f9; padding: 20px; border-radius: 8px; margin-bottom: 20px; text-align: center; }
|
||||||
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; }
|
.stat-value { font-size: 24px; font-weight: bold; color: #059669; }
|
||||||
</style>
|
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||||
</head>
|
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
|
||||||
<body>
|
th { background: #f4f4f4; }
|
||||||
<div class='container'>
|
.footer { margin-top: 30px; font-size: 12px; color: #777; text-align: center; }
|
||||||
<div class='header'>
|
</style>
|
||||||
<h1>Monthly Financial Summary</h1>
|
</head>
|
||||||
<p>Period: {$month_name}</p>
|
<body>
|
||||||
</div>
|
<div class='container'>
|
||||||
|
<div class='header'>
|
||||||
<div class='summary-box'>
|
<h1>Monthly Financial Summary</h1>
|
||||||
<div style='display: inline-block; width: 45%;'>
|
<p>Period: {$month_name}</p>
|
||||||
<p style='margin: 0; color: #666;'>Total Donations</p>
|
</div>
|
||||||
<div class='stat-value'>$" . number_format($stats['total_amount'], 2) . "</div>
|
|
||||||
</div>
|
<div class='summary-box'>
|
||||||
<div style='display: inline-block; width: 45%; border-left: 1px solid #ddd;'>
|
<div style='display: inline-block; width: 45%;'>
|
||||||
<p style='margin: 0; color: #666;'>Donation Count</p>
|
<p style='margin: 0; color: #666;'>Total Donations</p>
|
||||||
<div class='stat-value'>{$stats['total_count']}</div>
|
<div class='stat-value'>$ " . number_format($stats['total_amount'], 2) . "</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style='display: inline-block; width: 45%; border-left: 1px solid #ddd;'>
|
||||||
|
<p style='margin: 0; color: #666;'>Donation Count</p>
|
||||||
<h3>Donations by Category</h3>
|
<div class='stat-value'>{$stats['total_count']}</div>
|
||||||
<table>
|
</div>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
|
||||||
<th>Category</th>
|
<h3>Donations by Category</h3>
|
||||||
<th>Amount</th>
|
<table>
|
||||||
</tr>
|
<thead>
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>";
|
<th>Category</th>
|
||||||
|
<th>Amount</th>
|
||||||
foreach ($stats['by_category'] as $cat) {
|
</tr>
|
||||||
$html .= "<tr><td>" . htmlspecialchars($cat['name']) . "</td><td>$" . number_format($cat['total'], 2) . "</td></tr>";
|
</thead>
|
||||||
}
|
<tbody>";
|
||||||
|
|
||||||
$html .= "
|
foreach ($stats['by_category'] as $cat) {
|
||||||
</tbody>
|
$html .= "<tr><td>" . htmlspecialchars($cat['name']) . "</td><td>$" . number_format($cat['total'], 2) . "</td></tr>";
|
||||||
</table>
|
}
|
||||||
|
|
||||||
<h3>Top 5 Performing Cases</h3>
|
$html .= "
|
||||||
<table>
|
</tbody>
|
||||||
<thead>
|
</table>
|
||||||
<tr>
|
|
||||||
<th>Case Title</th>
|
<h3>Top 5 Performing Cases</h3>
|
||||||
<th>Amount</th>
|
<table>
|
||||||
</tr>
|
<thead>
|
||||||
</thead>
|
<tr>
|
||||||
<tbody>";
|
<th>Case Title</th>
|
||||||
|
<th>Amount</th>
|
||||||
foreach ($stats['top_cases'] as $case) {
|
</tr>
|
||||||
$html .= "<tr><td>" . htmlspecialchars($case['title']) . "</td><td>$" . number_format($case['total'], 2) . "</td></tr>";
|
</thead>
|
||||||
}
|
<tbody>";
|
||||||
|
|
||||||
$html .= "
|
foreach ($stats['top_cases'] as $case) {
|
||||||
</tbody>
|
$html .= "<tr><td>" . htmlspecialchars($case['title']) . "</td><td>$" . number_format($case['total'], 2) . "</td></tr>";
|
||||||
</table>
|
}
|
||||||
|
|
||||||
<div class='footer'>
|
$html .= "
|
||||||
<p>This is an automated report from your CharityHub Admin Panel.</p>
|
</tbody>
|
||||||
<p>© " . date('Y') . " CharityHub. All rights reserved.</p>
|
</table>
|
||||||
</div>
|
|
||||||
</div>
|
<div class='footer'>
|
||||||
</body>
|
<p>This is an automated report from your " . htmlspecialchars($orgName) . " Admin Panel.</p>
|
||||||
</html>";
|
<p>© " . date('Y') . " " . htmlspecialchars($orgName) . ". All rights reserved.</p>
|
||||||
|
</div>
|
||||||
// Send Email
|
</div>
|
||||||
$subject = "Financial Summary Report - {$month_name}";
|
</body>
|
||||||
$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.";
|
</html>";
|
||||||
|
|
||||||
$res = MailService::sendMail($recipient, $subject, $html, $plain_text);
|
// Send Email
|
||||||
|
$subject = "Financial Summary Report - {$month_name}";
|
||||||
if (!empty($res['success'])) {
|
$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.";
|
||||||
echo "Report sent successfully to {$recipient} for {$month_name}.\n";
|
|
||||||
} else {
|
$res = MailService::sendMail($recipient, $subject, $html, $plain_text);
|
||||||
echo "Failed to send report: " . ($res['error'] ?? 'Unknown error') . "\n";
|
|
||||||
}
|
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";
|
||||||
|
}
|
||||||
@ -77,13 +77,16 @@ if ($donation) {
|
|||||||
$final_donation_id = $existing['id'];
|
$final_donation_id = $existing['id'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$org = $pdo->query("SELECT * FROM org_profile LIMIT 1")->fetch();
|
||||||
|
$orgName = $org['name_en'] ?? 'Organization';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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@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">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user