207 lines
9.2 KiB
PHP
207 lines
9.2 KiB
PHP
<?php
|
|
session_start();
|
|
require_once 'db/config.php';
|
|
|
|
if (!isset($_SESSION['user_id'])) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
|
|
$db = db();
|
|
$school_id = $_SESSION['school_id'];
|
|
|
|
// Award badges automatically based on performance (Logic)
|
|
// Academic Star: Average >= 80%
|
|
// Consistent Performer: Average >= 60%
|
|
try {
|
|
// This is a simple logic to auto-award badges for demonstration
|
|
$stmt = $db->prepare("
|
|
SELECT l.id, AVG((m.marks_obtained / a.total_marks) * 100) as avg_score
|
|
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
|
|
");
|
|
$stmt->execute([$school_id]);
|
|
$performances = $stmt->fetchAll();
|
|
|
|
foreach ($performances as $perf) {
|
|
if ($perf['avg_score'] >= 80) {
|
|
// Award Academic Star (Badge ID 1)
|
|
$db->prepare("INSERT IGNORE INTO learner_badges (learner_id, badge_id) VALUES (?, 1)")->execute([$perf['id']]);
|
|
} elseif ($perf['avg_score'] >= 60) {
|
|
// Award Consistent Performer (Badge ID 2)
|
|
$db->prepare("INSERT IGNORE INTO learner_badges (learner_id, badge_id) VALUES (?, 2)")->execute([$perf['id']]);
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
// Silently fail if something goes wrong with auto-awarding
|
|
}
|
|
|
|
// Fetch Leaderboard (Top learners by average score)
|
|
$query = "
|
|
SELECT
|
|
l.id, l.full_name, l.grade, l.student_id,
|
|
AVG((m.marks_obtained / a.total_marks) * 100) as average_score,
|
|
COUNT(m.id) as assessments_count
|
|
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 = :school_id
|
|
GROUP BY l.id
|
|
ORDER BY average_score DESC
|
|
LIMIT 20
|
|
";
|
|
$stmt = $db->prepare($query);
|
|
$stmt->execute(['school_id' => $school_id]);
|
|
$leaderboard = $stmt->fetchAll();
|
|
|
|
// Fetch Badges for these learners
|
|
$badges_query = "
|
|
SELECT lb.learner_id, b.name, b.icon, b.description
|
|
FROM learner_badges lb
|
|
JOIN badges b ON lb.badge_id = b.id
|
|
WHERE lb.learner_id IN (SELECT id FROM learners WHERE school_id = ?)
|
|
";
|
|
$stmt = $db->prepare($badges_query);
|
|
$stmt->execute([$school_id]);
|
|
$all_learner_badges = $stmt->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_ASSOC);
|
|
|
|
$pageTitle = "Learner Leaderboard | SOMS";
|
|
include 'includes/header.php';
|
|
?>
|
|
|
|
<div class="container py-5">
|
|
<div class="text-center mb-5">
|
|
<h1 class="display-5 fw-bold mb-2"><i class="bi bi-trophy text-warning me-2"></i>Academic Leaderboard</h1>
|
|
<p class="lead text-muted">Celebrating excellence and consistent growth in our school.</p>
|
|
</div>
|
|
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-10">
|
|
<div class="card shadow border-0 overflow-hidden">
|
|
<div class="card-header bg-primary text-white py-3">
|
|
<div class="row align-items-center">
|
|
<div class="col-auto">
|
|
<span class="badge bg-white text-primary">Top 20</span>
|
|
</div>
|
|
<div class="col">
|
|
<h5 class="mb-0">Current Rankings</h5>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="ps-4" style="width: 80px;">Rank</th>
|
|
<th>Learner</th>
|
|
<th>Grade</th>
|
|
<th>Avg. Score</th>
|
|
<th>Achievements</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($leaderboard)): ?>
|
|
<tr>
|
|
<td colspan="5" class="text-center py-5 text-muted">
|
|
No assessment data available yet to generate rankings.
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
|
|
<?php foreach ($leaderboard as $index => $row): ?>
|
|
<?php
|
|
$rank = $index + 1;
|
|
$score = round($row['average_score'], 1);
|
|
$row_badges = $all_learner_badges[$row['id']] ?? [];
|
|
|
|
$rankClass = '';
|
|
if ($rank === 1) $rankClass = 'rank-1';
|
|
elseif ($rank === 2) $rankClass = 'rank-2';
|
|
elseif ($rank === 3) $rankClass = 'rank-3';
|
|
?>
|
|
<tr class="<?= $rank <= 3 ? 'table-light' : '' ?>">
|
|
<td class="ps-4">
|
|
<?php if ($rank === 1): ?>
|
|
<div class="rank-circle bg-warning text-dark"><i class="bi bi-award-fill"></i></div>
|
|
<?php elseif ($rank === 2): ?>
|
|
<div class="rank-circle bg-secondary text-white">2</div>
|
|
<?php elseif ($rank === 3): ?>
|
|
<div class="rank-circle bg-bronze text-white">3</div>
|
|
<?php else: ?>
|
|
<span class="text-muted fw-bold"><?= $rank ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-sm bg-primary text-white rounded-circle me-3 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
|
<?= strtoupper(substr($row['full_name'], 0, 1)) ?>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold"><?= htmlspecialchars($row['full_name']) ?></div>
|
|
<div class="text-muted smaller"><?= htmlspecialchars($row['student_id']) ?></div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="badge bg-light text-dark border">Grade <?= htmlspecialchars($row['grade']) ?></span></td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="progress flex-grow-1 me-3" style="height: 6px; min-width: 60px;">
|
|
<div class="progress-bar bg-primary" role="progressbar" style="width: <?= $score ?>%"></div>
|
|
</div>
|
|
<span class="fw-bold"><?= $score ?>%</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<?php if (empty($row_badges)): ?>
|
|
<span class="text-muted smaller">No badges yet</span>
|
|
<?php else: ?>
|
|
<?php foreach ($row_badges as $badge): ?>
|
|
<span class="badge bg-soft-warning text-warning border border-warning" data-bs-toggle="tooltip" title="<?= htmlspecialchars($badge['description']) ?>">
|
|
<i class="bi <?= htmlspecialchars($badge['icon']) ?> me-1"></i>
|
|
<?= htmlspecialchars($badge['name']) ?>
|
|
</span>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.rank-circle {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 0.9rem;
|
|
}
|
|
.bg-bronze { background-color: #cd7f32; }
|
|
.bg-soft-warning { background-color: rgba(255, 193, 7, 0.1); }
|
|
.smaller { font-size: 0.75rem; }
|
|
|
|
.rank-1 { background-color: rgba(255, 193, 7, 0.05); }
|
|
</style>
|
|
|
|
<script>
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl)
|
|
})
|
|
</script>
|
|
|
|
<?php include 'includes/footer.php'; ?>
|