v4
This commit is contained in:
parent
d987612018
commit
57a1969d5e
13
admin.php
13
admin.php
@ -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
133
bulk-upload.php
Normal 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'; ?>
|
||||
@ -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
196
notifications.php
Normal 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
6
sw.js
@ -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) => {
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user