Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa1260afd6 | |||
| 35b9cba882 |
@@ -1003,6 +1003,8 @@ $routes->group('family', static function ($routes) {
|
||||
$routes->get('index', 'View\FamilyAdminController::index');
|
||||
$routes->get('search', 'View\FamilyAdminController::search');
|
||||
$routes->get('card', 'View\FamilyAdminController::card');
|
||||
$routes->get('compose-email', 'View\FamilyAdminController::composeEmail');
|
||||
$routes->post('compose-email/send', 'View\FamilyAdminController::sendComposeEmail');
|
||||
});
|
||||
// Convenience alias
|
||||
$routes->get('family', 'View\FamilyAdminController::index');
|
||||
|
||||
@@ -416,8 +416,10 @@ class AdministratorController extends BaseController
|
||||
$totalStudents = (int) (
|
||||
$this->db->table('student_class')
|
||||
->select('COUNT(DISTINCT student_class.student_id) AS cnt')
|
||||
->join('students', 'students.id = student_class.student_id', 'inner')
|
||||
->where('student_class.school_year', $this->schoolYear)
|
||||
->where('student_class.class_section_id IS NOT NULL', null, false)
|
||||
->where('students.is_active', 1)
|
||||
->get()
|
||||
->getRow('cnt')
|
||||
?? 0
|
||||
|
||||
@@ -406,4 +406,82 @@ class FamilyAdminController extends BaseController
|
||||
|
||||
return service('response')->setBody(view('family/card', ['f' => $family]));
|
||||
}
|
||||
|
||||
public function composeEmail()
|
||||
{
|
||||
$to = trim((string)$this->request->getGet('to'));
|
||||
$name = trim((string)$this->request->getGet('name'));
|
||||
$returnUrl = trim((string)$this->request->getGet('return_url'));
|
||||
if ($returnUrl === '') {
|
||||
$returnUrl = trim((string)$this->request->getServer('HTTP_REFERER'));
|
||||
}
|
||||
if ($returnUrl === '') {
|
||||
$returnUrl = site_url('family');
|
||||
}
|
||||
|
||||
return view('family/compose_email', [
|
||||
'to' => $to,
|
||||
'name' => $name,
|
||||
'return_url' => $returnUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendComposeEmail()
|
||||
{
|
||||
if (!$this->request->is('post')) {
|
||||
return redirect()->to(site_url('family'));
|
||||
}
|
||||
|
||||
$to = trim((string)$this->request->getPost('to'));
|
||||
$subject = trim((string)$this->request->getPost('subject'));
|
||||
$html = (string)($this->request->getPost('html') ?? '');
|
||||
$returnUrl = trim((string)$this->request->getPost('return_url'));
|
||||
|
||||
if ($returnUrl === '' || !$this->isLocalReturnUrl($returnUrl)) {
|
||||
$returnUrl = site_url('family');
|
||||
}
|
||||
|
||||
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please enter a valid email address.');
|
||||
}
|
||||
if ($subject === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Subject is required.');
|
||||
}
|
||||
if (trim($html) === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Email body is required.');
|
||||
}
|
||||
|
||||
$wrapped = view('emails/custom_html', [
|
||||
'subject' => $subject,
|
||||
'body_html' => $html,
|
||||
]);
|
||||
|
||||
$mailer = new \App\Controllers\View\EmailController();
|
||||
$ok = $mailer->sendEmail($to, $subject, $wrapped, 'communication');
|
||||
|
||||
if ($ok) {
|
||||
return redirect()->to($returnUrl)->with('status', 'Email sent.');
|
||||
}
|
||||
|
||||
return redirect()->to($returnUrl)->with('error', 'Unable to send email.');
|
||||
}
|
||||
|
||||
private function isLocalReturnUrl(string $url): bool
|
||||
{
|
||||
$url = trim($url);
|
||||
if ($url === '') return false;
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) return false;
|
||||
|
||||
if (!empty($parts['scheme']) || !empty($parts['host'])) {
|
||||
$base = parse_url(site_url('/'));
|
||||
if (!$base || empty($base['host'])) return false;
|
||||
$hostMatch = strcasecmp((string)($parts['host'] ?? ''), (string)$base['host']) === 0;
|
||||
return $hostMatch;
|
||||
}
|
||||
|
||||
// Relative URL (e.g., /family?x=1 or family?x=1)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,37 +517,28 @@
|
||||
<section class="mb-5">
|
||||
<h2 class="mb-4 text-center">Statistics</h2>
|
||||
<div class="stats-row">
|
||||
<!-- Students -->
|
||||
<div class="stat-circle circle-primary">
|
||||
<div class="stat-icon"><i class="bi bi-people-fill"></i></div>
|
||||
<div class="stat-icon"><i class="fas fa-user-graduate"></i></div>
|
||||
<div class="stat-value" data-stat="students">—</div>
|
||||
<div class="stat-title">Students</div>
|
||||
</div>
|
||||
|
||||
<!-- Teachers -->
|
||||
<div class="stat-circle circle-info">
|
||||
<div class="stat-icon"><i class="bi bi-person-video2"></i></div>
|
||||
<div class="stat-icon"><i class="fas fa-chalkboard-teacher"></i></div>
|
||||
<div class="stat-value" data-stat="teachers">—</div>
|
||||
<div class="stat-title">Teachers</div>
|
||||
</div>
|
||||
|
||||
<!-- Teacher Assistants -->
|
||||
<div class="stat-circle circle-success">
|
||||
<div class="stat-icon"><i class="bi bi-person-badge-fill"></i></div>
|
||||
<div class="stat-icon"><i class="fas fa-user-cog"></i></div>
|
||||
<div class="stat-value" data-stat="teacherAssistants">—</div>
|
||||
<div class="stat-title">Teachers' Assistants</div>
|
||||
<div class="stat-title">TAs</div>
|
||||
</div>
|
||||
|
||||
<!-- Admins -->
|
||||
<div class="stat-circle circle-warning">
|
||||
<div class="stat-icon"><i class="bi bi-shield-lock-fill"></i></div>
|
||||
<div class="stat-icon"><i class="fas fa-user-shield"></i></div>
|
||||
<div class="stat-value" data-stat="admins">—</div>
|
||||
<div class="stat-title">Admins</div>
|
||||
</div>
|
||||
|
||||
<!-- Parents -->
|
||||
<div class="stat-circle circle-light">
|
||||
<div class="stat-icon"><i class="bi bi-people"></i></div>
|
||||
<div class="stat-icon"><i class="fas fa-users"></i></div>
|
||||
<div class="stat-value" data-stat="parents">—</div>
|
||||
<div class="stat-title">Parents</div>
|
||||
</div>
|
||||
|
||||
@@ -71,9 +71,20 @@
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.attn-pie-wrap {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.attn-analysis-half { grid-column: span 12; }
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.attn-pie-wrap {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
@@ -291,7 +302,9 @@
|
||||
<div class="attn-analysis-grid">
|
||||
<div class="attn-analysis-card attn-analysis-half">
|
||||
<h6>Overall Distribution</h6>
|
||||
<canvas id="attnPieChart" height="50" aria-label="Overall attendance pie chart"></canvas>
|
||||
<div class="attn-pie-wrap">
|
||||
<canvas id="attnPieChart" aria-label="Overall attendance pie chart"></canvas>
|
||||
</div>
|
||||
<table class="attn-analysis-table mt-3 no-mgmt-sticky" data-no-mgmt-sticky>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -415,6 +428,8 @@
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?= $this->extend('layout/email_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
<?= $body_html ?? '' ?>
|
||||
<?= $this->endSection() ?>
|
||||
@@ -22,6 +22,10 @@ if ($title === '' || preg_match('/^\s*Family\s+of\s+User\s*\d+\s*$/i', $title))
|
||||
}
|
||||
$title = $lastName !== '' ? ('Family of ' . $lastName) : 'Family';
|
||||
}
|
||||
$returnUrl = trim((string)service('request')->getServer('HTTP_REFERER'));
|
||||
if ($returnUrl === '') {
|
||||
$returnUrl = site_url('family');
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="family-card-root" data-family-title="<?= esc($title) ?>">
|
||||
@@ -187,7 +191,14 @@ if ($title === '' || preg_match('/^\s*Family\s+of\s+User\s*\d+\s*$/i', $title))
|
||||
<td class="small" data-label="Contact">
|
||||
<div>
|
||||
<i class="bi bi-envelope me-1 text-muted"></i>
|
||||
<?= !empty($g['email']) ? ('<a href="mailto:'.esc($g['email']).'">'.esc($g['email']).'</a>') : '<span class="text-muted">—</span>' ?>
|
||||
<?php
|
||||
$gEmail = trim((string)($g['email'] ?? ''));
|
||||
$gName = trim((string)($g['firstname'] ?? '') . ' ' . (string)($g['lastname'] ?? ''));
|
||||
$composeUrl = $gEmail !== ''
|
||||
? site_url('family/compose-email?to=' . rawurlencode($gEmail) . '&name=' . rawurlencode($gName) . '&return_url=' . rawurlencode($returnUrl))
|
||||
: '';
|
||||
?>
|
||||
<?= $gEmail !== '' ? ('<a href="' . esc($composeUrl) . '" target="_blank" rel="noopener">' . esc($gEmail) . '</a>') : '<span class="text-muted">—</span>' ?>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<i class="bi bi-telephone me-1 text-muted"></i>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?= $this->extend('layout/management_layout') ?>
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 class="h4 mb-1">Compose Email</h2>
|
||||
<?php if (!empty($name)): ?>
|
||||
<div class="text-muted"><?= esc($name) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= esc((string)($return_url ?? site_url('family'))) ?>">Back</a>
|
||||
</div>
|
||||
|
||||
<form class="card shadow-sm" method="post" action="<?= site_url('family/compose-email/send') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="html" id="compose_html" value="">
|
||||
<input type="hidden" name="return_url" value="<?= esc((string)($return_url ?? '')) ?>">
|
||||
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="composeTo">To</label>
|
||||
<input type="email" class="form-control" id="composeTo" name="to" value="<?= esc((string)($to ?? '')) ?>" placeholder="parent@example.com" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="composeSubject">Subject</label>
|
||||
<input type="text" class="form-control" id="composeSubject" name="subject" placeholder="Subject" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="composeEditor">Body (Rich Text)</label>
|
||||
<textarea class="form-control" id="composeEditor" rows="14"></textarea>
|
||||
<div class="form-text">Use the toolbar to format your message before sending.</div>
|
||||
<noscript>
|
||||
<div class="alert alert-warning mt-2">
|
||||
JavaScript is disabled. The message will be sent from the hidden <code>html</code> field.
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="submit" class="btn btn-primary">Send Email</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('scripts') ?>
|
||||
<script src="<?= base_url('assets/tinymce/tinymce.min.js') ?>"></script>
|
||||
<script>
|
||||
(function () {
|
||||
const form = document.querySelector('form[action$="family/compose-email/send"]');
|
||||
const hiddenHtml = document.getElementById('compose_html');
|
||||
|
||||
tinymce.init({
|
||||
selector: '#composeEditor',
|
||||
base_url: '<?= base_url('assets/tinymce') ?>',
|
||||
suffix: '.min',
|
||||
license_key: 'gpl',
|
||||
height: 420,
|
||||
menubar: true,
|
||||
branding: false,
|
||||
promotion: false,
|
||||
plugins: 'advlist autolink lists link image charmap preview anchor ' +
|
||||
'searchreplace visualblocks code fullscreen insertdatetime media table ' +
|
||||
'help wordcount emoticons codesample',
|
||||
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough forecolor backcolor | ' +
|
||||
'alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | ' +
|
||||
'link image media table | emoticons codesample | removeformat | preview code',
|
||||
convert_urls: false,
|
||||
paste_data_images: true,
|
||||
image_caption: true,
|
||||
content_style: 'body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-size: 14px; }',
|
||||
setup(editor) {
|
||||
editor.on('keyup change undo redo SetContent', function () {
|
||||
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
|
||||
});
|
||||
if (form) {
|
||||
form.addEventListener('submit', function () {
|
||||
if (hiddenHtml) hiddenHtml.value = editor.getContent({ format: 'html' });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?= $this->endSection() ?>
|
||||
+173
-14
@@ -322,6 +322,72 @@
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.home-stats {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
|
||||
padding: 2.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.home-stats .stats-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home-stats .stat-circle {
|
||||
width: 190px;
|
||||
height: 190px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.home-stats .stat-value {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-stats .stat-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: .2rem;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.home-stats .stat-title {
|
||||
font-size: 1.1rem;
|
||||
margin-top: .25rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.home-stats .circle-primary {
|
||||
background: radial-gradient(circle at 30% 30%, #0d6efd, #0044aa);
|
||||
}
|
||||
|
||||
.home-stats .circle-success {
|
||||
background: radial-gradient(circle at 30% 30%, #198754, #0c4f2c);
|
||||
}
|
||||
|
||||
.home-stats .circle-warning {
|
||||
background: radial-gradient(circle at 30% 30%, #daa544ff, #a66f00);
|
||||
}
|
||||
|
||||
.home-stats .circle-info {
|
||||
background: radial-gradient(circle at 30% 30%, #0dcaf0, #047e9c);
|
||||
}
|
||||
|
||||
.home-stats .circle-light {
|
||||
background: radial-gradient(circle at 30% 30%, #63af4cff, #00eb7dff);
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.image-column {
|
||||
min-height: 300px;
|
||||
@@ -332,6 +398,13 @@
|
||||
border-radius: 0 0 10px 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.home-stats .stat-circle {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -448,8 +521,43 @@
|
||||
<h4 class="fw-bold my-0"><u>Join Al Rahma family today! Click here for a quick guide on how to create an account.</u></h4>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Statistics Start -->
|
||||
<div class="container-xxl py-2 content-section">
|
||||
<div class="container">
|
||||
<div class="home-stats" data-dashboard-endpoint="<?= site_url('api/administrator/dashboard') ?>">
|
||||
<h1 class="d-flex justify-content-center p-md-1">Active Participants</h1>
|
||||
<br>
|
||||
<div class="stats-row">
|
||||
<div class="stat-circle circle-primary">
|
||||
<div class="stat-icon"><i class="fa-solid fa-user-graduate"></i></div>
|
||||
<div class="stat-value" data-stat="students">—</div>
|
||||
<div class="stat-title">Students</div>
|
||||
</div>
|
||||
<div class="stat-circle circle-info">
|
||||
<div class="stat-icon"><i class="fa-solid fa-chalkboard-user"></i></div>
|
||||
<div class="stat-value" data-stat="teachers">—</div>
|
||||
<div class="stat-title">Teachers</div>
|
||||
</div>
|
||||
<div class="stat-circle circle-success">
|
||||
<div class="stat-icon"><i class="fa-solid fa-user-gear"></i></div>
|
||||
<div class="stat-value" data-stat="teacherAssistants">—</div>
|
||||
<div class="stat-title">TAs</div>
|
||||
</div>
|
||||
<div class="stat-circle circle-warning">
|
||||
<div class="stat-icon"><i class="fa-solid fa-user-shield"></i></div>
|
||||
<div class="stat-value" data-stat="admins">—</div>
|
||||
<div class="stat-title">Admins</div>
|
||||
</div>
|
||||
<div class="stat-circle circle-light">
|
||||
<div class="stat-icon"><i class="fa-solid fa-people-group"></i></div>
|
||||
<div class="stat-value" data-stat="parents">—</div>
|
||||
<div class="stat-title">Parents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Statistics End -->
|
||||
<div class="container-xxl py-2 content-section">
|
||||
<div class="container">
|
||||
<div class="row bg-light squared align-items-center">
|
||||
@@ -461,18 +569,18 @@
|
||||
<p class="mb-4">Under the umbrella of ISGL, Al Rahma Sunday School has been serving the community for over thirty years. Throughout the decades, it has provided students with a strong foundation in Islamic Studies and Quran, fostering both knowledge and character. With its dedicated teachers and well-rounded academic program, the school continues to guide generations of Muslim youth in their faith, values and practice of Islam.</p>
|
||||
<p>For more information, click below to visit ISGL official website.</p>
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#28a745" style="border-radius:6px;">
|
||||
<a href="https://isgl.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;">
|
||||
ISGL Official Website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<tr>
|
||||
<td align="center" bgcolor="#28a745" style="border-radius:6px;">
|
||||
<a href="https://isgl.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style="display:inline-block;padding:12px 18px;color:#ffffff;text-decoration:none;font-weight:600;">
|
||||
ISGL Official Website
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Column - Fixed with correct path -->
|
||||
@@ -677,6 +785,57 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const container = document.querySelector('.home-stats[data-dashboard-endpoint]');
|
||||
if (!container) return;
|
||||
const endpoint = container.dataset.dashboardEndpoint;
|
||||
const statElements = container.querySelectorAll('[data-stat]');
|
||||
const numberFormatter = new Intl.NumberFormat();
|
||||
let isLoading = false;
|
||||
|
||||
const loadStats = function() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
fetch(endpoint, {
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(payload) {
|
||||
const counts = payload && typeof payload === 'object' && payload.counts ?
|
||||
payload.counts :
|
||||
{};
|
||||
statElements.forEach(function(element) {
|
||||
const key = element.dataset.stat;
|
||||
const value = counts[key];
|
||||
element.textContent = Number.isFinite(value) ?
|
||||
numberFormatter.format(value) :
|
||||
'—';
|
||||
});
|
||||
})
|
||||
.catch(function() {})
|
||||
.finally(function() {
|
||||
isLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
loadStats();
|
||||
setInterval(loadStats, 60000);
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.visibilityState === 'visible') {
|
||||
loadStats();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user