This commit is contained in:
Flatlogic Bot 2026-01-30 14:56:15 +00:00
parent d987612018
commit 57a1969d5e
5 changed files with 348 additions and 8 deletions

View File

@ -135,11 +135,14 @@ include 'includes/header.php';
<a href="learners.php" class="btn btn-outline-primary text-start">
<i class="bi bi-person-plus me-2"></i> Manage Learners
</a>
<button class="btn btn-outline-primary text-start">
<i class="bi bi-file-earmark-bar-graph me-2"></i> Generate Reports
<a href="bulk-upload.php" class="btn btn-outline-primary text-start">
<i class="bi bi-upload me-2"></i> Bulk Upload Learners
</a>
<button class="btn btn-outline-primary text-start">
<i class="bi bi-megaphone me-2"></i> Send Parent Notice
<a href="reports.php" class="btn btn-outline-primary text-start">
<i class="bi bi-file-earmark-bar-graph me-2"></i> Academic Reports
</a>
<a href="notifications.php" class="btn btn-outline-primary text-start">
<i class="bi bi-megaphone me-2"></i> Performance Notifications
</a>
</div>
</div>
@ -148,4 +151,4 @@ include 'includes/header.php';
</div>
</div>
<?php include 'includes/footer.php'; ?>
<?php include 'includes/footer.php'; ?>

133
bulk-upload.php Normal file
View File

@ -0,0 +1,133 @@
<?php
require_once __DIR__ . '/db/config.php';
session_start();
// Auth Check
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'Admin') {
header('Location: login.php');
exit;
}
$db = db();
$school_id = $_SESSION['school_id'];
$pageTitle = 'Bulk Upload | SOMS';
$message = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
$file = $_FILES['csv_file'];
if ($file['error'] !== UPLOAD_ERR_OK) {
$error = "Error uploading file.";
} else {
$handle = fopen($file['tmp_name'], 'r');
if ($handle !== FALSE) {
// Skip header row
fgetcsv($handle);
$success_count = 0;
$fail_count = 0;
$db->beginTransaction();
try {
$stmt = $db->prepare("INSERT INTO learners (full_name, grade, student_id, parent_email, school_id) VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE full_name = VALUES(full_name), grade = VALUES(grade), parent_email = VALUES(parent_email)");
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
if (count($data) >= 3) {
$full_name = trim($data[0]);
$grade = trim($data[1]);
$student_id = trim($data[2]);
$parent_email = isset($data[3]) ? trim($data[3]) : null;
if (!empty($full_name) && !empty($grade) && !empty($student_id)) {
$stmt->execute([$full_name, $grade, $student_id, $parent_email, $school_id]);
$success_count++;
} else {
$fail_count++;
}
} else {
$fail_count++;
}
}
$db->commit();
$message = "Successfully imported $success_count learners. (Skipped/Failed: $fail_count)";
} catch (Exception $e) {
$db->rollBack();
$error = "Database error: " . $e->getMessage();
}
fclose($handle);
} else {
$error = "Could not open the uploaded file.";
}
}
}
include 'includes/header.php';
?>
<div class="container pb-5">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="admin.php">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Bulk Upload</li>
</ol>
</nav>
<h2 class="h4 mb-1">Bulk Upload Learners</h2>
<p class="text-muted small">Upload a CSV file to add multiple learners at once.</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card shadow-sm border-0">
<div class="card-body">
<?php if ($message): ?>
<div class="alert alert-success small"><?= $message ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger small"><?= $error ?></div>
<?php endif; ?>
<form action="bulk-upload.php" method="POST" enctype="multipart/form-data">
<div class="mb-4">
<label class="form-label fw-bold">Select CSV File</label>
<input type="file" name="csv_file" class="form-control" accept=".csv" required>
<div class="form-text small">
Format: <code>Full Name, Grade, Student ID, Parent Email (optional)</code>.
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="bi bi-cloud-arrow-up me-2"></i> Upload and Process
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm border-0 bg-light">
<div class="card-body">
<h5 class="fw-bold">CSV Template Guide</h5>
<p class="small text-muted">Your CSV file should look like this:</p>
<pre class="bg-white p-3 border rounded small">Full Name,Grade,Student ID,Parent Email
John Doe,10,SOW-101,parent@example.com
Jane Smith,11,SOW-102,jane_parent@gmail.com
Bob Brown,10,SOW-103,</pre>
<ul class="small text-muted">
<li>The first line is treated as a header and skipped.</li>
<li>Existing Student IDs will be updated.</li>
<li>Parent Email is used for automated performance notifications.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

View File

@ -76,6 +76,12 @@ $is_logged_in = isset($_SESSION['user_id']);
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'reports.php' ? 'active' : '' ?>" href="reports.php">Reports</a>
</li>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'bulk-upload.php' ? 'active' : '' ?>" href="bulk-upload.php">Bulk Upload</a>
</li>
<li class="nav-item">
<a class="nav-link <?= basename($_SERVER['PHP_SELF']) == 'notifications.php' ? 'active' : '' ?>" href="notifications.php">Notifications</a>
</li>
<?php endif; ?>
<?php if ($user_role === 'Parent' || $user_role === 'Guest'): ?>
@ -97,4 +103,4 @@ $is_logged_in = isset($_SESSION['user_id']);
</div>
</div>
</div>
</nav>
</nav>

196
notifications.php Normal file
View File

@ -0,0 +1,196 @@
<?php
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/mail/MailService.php';
require_once __DIR__ . '/ai/LocalAIApi.php';
session_start();
// Auth Check
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'Admin') {
header('Location: login.php');
exit;
}
$db = db();
$school_id = $_SESSION['school_id'];
$pageTitle = 'Automated Notifications | SOMS';
$message = '';
$error = '';
// Default threshold
$threshold = isset($_GET['threshold']) ? (int)$_GET['threshold'] : 50;
// Fetch learners with their averages
$query = "
SELECT
l.id,
l.full_name,
l.student_id,
l.parent_email,
l.grade,
AVG((m.marks_obtained / a.total_marks) * 100) as average_percent
FROM learners l
JOIN marks m ON l.id = m.learner_id
JOIN assessments a ON m.assessment_id = a.id
WHERE l.school_id = ?
GROUP BY l.id
HAVING average_percent < ?
";
$stmt = $db->prepare($query);
$stmt->execute([$school_id, $threshold]);
$low_performers = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (isset($_POST['send_notifications'])) {
$sent_count = 0;
$fail_count = 0;
$use_ai = isset($_POST['use_ai']);
foreach ($low_performers as $learner) {
if (empty($learner['parent_email'])) {
$fail_count++;
continue;
}
$avg = round($learner['average_percent'], 1);
$subject = "Urgent: Academic Progress Report for " . $learner['full_name'];
$htmlBody = "
<h3>Academic Progress Alert</h3>
<p>Dear Parent,</p>
<p>This is an automated notification regarding the academic performance of <strong>{$learner['full_name']}</strong> (ID: {$learner['student_id']}).</p>
<p>The current academic average is <strong>{$avg}%</strong>, which is below the school's monitoring threshold of {$threshold}%.</p>
<p>We encourage you to log into the Parent Portal using the Student ID to view detailed assessment marks and discuss this with the class teacher.</p>
<p>Best Regards,<br>School Administration</p>
";
if ($use_ai) {
$ai_prompt = "Generate a short, encouraging, and supportive email to a parent whose child, {$learner['full_name']}, is currently averaging {$avg}% in school (threshold is {$threshold}%). The tone should be professional yet empathetic. Mention that they can check the portal with ID {$learner['student_id']}. Keep it under 150 words.";
$ai_resp = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'You are an educational assistant helping schools communicate with parents.'],
['role' => 'user', 'content' => $ai_prompt],
],
]);
if (!empty($ai_resp['success'])) {
$ai_text = LocalAIApi::extractText($ai_resp);
if ($ai_text) {
$htmlBody = nl2br(htmlspecialchars($ai_text));
}
}
}
$res = MailService::sendMail($learner['parent_email'], $subject, $htmlBody);
if (!empty($res['success'])) {
$sent_count++;
} else {
$fail_count++;
}
}
$message = "Successfully sent $sent_count notifications. (Failed/Missing Email: $fail_count)";
// Refresh list
$stmt->execute([$school_id, $threshold]);
$low_performers = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
include 'includes/header.php';
?>
<div class="container pb-5">
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="admin.php">Dashboard</a></li>
<li class="breadcrumb-item active" aria-current="page">Notifications</li>
</ol>
</nav>
<h2 class="h4 mb-1">Automated Performance Notifications</h2>
<p class="text-muted small">Notify parents of learners performing below a specific threshold.</p>
</div>
</div>
<div class="row g-4">
<div class="col-md-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h5 class="fw-bold mb-3">Settings</h5>
<form action="notifications.php" method="GET" class="mb-4">
<label class="form-label small fw-bold">Performance Threshold (%)</label>
<div class="input-group">
<input type="number" name="threshold" class="form-control" value="<?= $threshold ?>" min="1" max="100">
<button class="btn btn-outline-primary" type="submit">Update List</button>
</div>
<div class="form-text small">Currently showing learners with average < <?= $threshold ?>%</div>
</form>
<form action="notifications.php?threshold=<?= $threshold ?>" method="POST">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" name="use_ai" id="useAi" checked>
<label class="form-check-label small" for="useAi">Use AI to personalize messages</label>
</div>
<div class="d-grid">
<button type="submit" name="send_notifications" class="btn btn-primary" <?= empty($low_performers) ? 'disabled' : '' ?>>
<i class="bi bi-send me-2"></i> Send to <?= count($low_performers) ?> Parents
</button>
</div>
</form>
</div>
</div>
<div class="alert alert-info small border-0 shadow-sm">
<i class="bi bi-info-circle me-2"></i> Only learners with a <strong>Parent Email</strong> defined will receive notifications.
</div>
</div>
<div class="col-md-8">
<div class="card shadow-sm border-0">
<div class="card-header bg-white py-3">
<h5 class="mb-0 fw-bold">Learners Below Threshold (<?= $threshold ?>%)</h5>
</div>
<div class="card-body p-0">
<?php if ($message): ?>
<div class="alert alert-success m-3 small"><?= $message ?></div>
<?php endif; ?>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th class="ps-3">Learner</th>
<th>Grade</th>
<th>Average</th>
<th>Parent Email</th>
</tr>
</thead>
<tbody>
<?php if (empty($low_performers)): ?>
<tr>
<td colspan="4" class="text-center py-4 text-muted small">No learners found below this threshold.</td>
</tr>
<?php endif; ?>
<?php foreach ($low_performers as $learner): ?>
<tr>
<td class="ps-3">
<div class="fw-bold"><?= htmlspecialchars($learner['full_name']) ?></div>
<div class="text-muted small">ID: <?= htmlspecialchars($learner['student_id']) ?></div>
</td>
<td>Grade <?= htmlspecialchars($learner['grade']) ?></td>
<td>
<span class="badge bg-danger"><?= round($learner['average_percent'], 1) ?>%</span>
</td>
<td class="small">
<?= $learner['parent_email'] ? htmlspecialchars($learner['parent_email']) : '<span class="text-danger italic">Missing</span>' ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<?php include 'includes/footer.php'; ?>

6
sw.js
View File

@ -1,4 +1,4 @@
const CACHE_NAME = 'township-schools-v4';
const CACHE_NAME = 'township-schools-v5';
const STATIC_ASSETS = [
'/',
'/index.php',
@ -9,6 +9,8 @@ const STATIC_ASSETS = [
'/learners.php',
'/assessments.php',
'/reports.php',
'/bulk-upload.php',
'/notifications.php',
'/parent.php',
'/assets/css/custom.css',
'/assets/js/main.js',
@ -63,4 +65,4 @@ self.addEventListener('fetch', (event) => {
});
})
);
});
});