248 lines
9.5 KiB
PHP
248 lines
9.5 KiB
PHP
<?php
|
|
require_once 'auth_helper.php';
|
|
require_login();
|
|
$user = get_user();
|
|
|
|
$pdo = db();
|
|
$electionId = get_active_election_id();
|
|
$election = get_active_election();
|
|
|
|
// Statistics (Filtered by Election)
|
|
$totalVoters = $pdo->prepare("SELECT COUNT(*) FROM election_assignments WHERE election_id = ? AND role_in_election = 'Voter'");
|
|
$totalVoters->execute([$electionId]);
|
|
$totalVoters = $totalVoters->fetchColumn();
|
|
|
|
$totalCandidates = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
|
|
$totalCandidates->execute([$electionId]);
|
|
$totalCandidates = $totalCandidates->fetchColumn();
|
|
|
|
$totalVotes = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
|
|
$totalVotes->execute([$electionId]);
|
|
$totalVotes = $totalVotes->fetchColumn();
|
|
|
|
// Chart Data: Participation per Grade Level
|
|
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
$gradeCol = ($driver === 'pgsql') ? "u.grade_level::TEXT" : "CAST(u.grade_level AS CHAR)";
|
|
$gradeStats = $pdo->prepare("SELECT COALESCE($gradeCol, 'Unknown') as label, COUNT(DISTINCT v.voter_id) as count
|
|
FROM users u JOIN votes v ON u.id = v.voter_id
|
|
WHERE v.election_id = ?
|
|
GROUP BY u.grade_level ORDER BY u.grade_level");
|
|
$gradeStats->execute([$electionId]);
|
|
$gradeStats = $gradeStats->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Chart Data: Participation per Track
|
|
$trackStats = $pdo->prepare("SELECT COALESCE(u.track, 'Unknown') as label, COUNT(DISTINCT v.voter_id) as count
|
|
FROM users u JOIN votes v ON u.id = v.voter_id
|
|
WHERE v.election_id = ?
|
|
GROUP BY u.track");
|
|
$trackStats->execute([$electionId]);
|
|
$trackStats = $trackStats->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Chart Data: Participation per Section
|
|
$sectionStats = $pdo->prepare("SELECT u.track, u.section as label, COUNT(DISTINCT v.voter_id) as count
|
|
FROM users u JOIN votes v ON u.id = v.voter_id
|
|
WHERE v.election_id = ?
|
|
GROUP BY u.track, u.section");
|
|
$sectionStats->execute([$electionId]);
|
|
$sectionStats = $sectionStats->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Tracks for dropdown
|
|
$tracks = array_unique(array_column($sectionStats, 'track'));
|
|
sort($tracks);
|
|
|
|
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
|
|
?>
|
|
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Election Dashboard | <?= htmlspecialchars($projectDescription) ?></title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
</head>
|
|
<body class="dashboard-body">
|
|
|
|
<?php require_once 'includes/sidebar.php'; ?>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-wrapper">
|
|
<?php require_once 'includes/header.php'; ?>
|
|
|
|
<main class="dashboard-content animate-fade-in">
|
|
<div class="dashboard-header">
|
|
<div>
|
|
<h1 style="margin: 0 0 4px 0; font-size: 1.5rem; color: #1e293b;">Election Dashboard</h1>
|
|
<div class="welcome-msg">
|
|
Active Election: <strong><?= htmlspecialchars($election['title'] ?? 'None') ?></strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="stats-grid animate-stagger">
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Voters</div>
|
|
<div class="stat-value"><?= number_format($totalVoters) ?></div>
|
|
<div class="stat-footer voters">
|
|
<i data-lucide="users-2" style="width: 14px;"></i>
|
|
Assigned Students
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Candidates</div>
|
|
<div class="stat-value"><?= number_format($totalCandidates) ?></div>
|
|
<div class="stat-footer candidates">
|
|
<i data-lucide="user-circle" style="width: 14px;"></i>
|
|
Validated Contestants
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Votes Cast</div>
|
|
<div class="stat-value"><?= number_format($totalVotes) ?></div>
|
|
<div class="stat-footer votes">
|
|
<i data-lucide="check-circle-2" style="width: 14px;"></i>
|
|
Verified Ballots
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analytics Charts -->
|
|
<div class="analytics-row animate-stagger">
|
|
<div class="analytics-card">
|
|
<div class="card-header">
|
|
<div class="card-title">Votes Per Grade Level</div>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="gradeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="analytics-card">
|
|
<div class="card-header">
|
|
<div class="card-title">Votes Per Track</div>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="trackChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="analytics-row animate-stagger">
|
|
<div class="analytics-card" style="grid-column: span 2;">
|
|
<div class="card-header">
|
|
<div class="card-title">Votes Per Section</div>
|
|
<select id="trackFilter" class="chart-filter">
|
|
<?php if (empty($tracks)): ?>
|
|
<option>No data</option>
|
|
<?php endif; ?>
|
|
<?php foreach ($tracks as $t): ?>
|
|
<option value="<?= htmlspecialchars($t) ?>"><?= htmlspecialchars($t) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="chart-container" style="height: 300px;">
|
|
<canvas id="sectionChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
lucide.createIcons();
|
|
|
|
// Chart Data from PHP
|
|
const gradeData = <?= json_encode($gradeStats) ?>;
|
|
const trackData = <?= json_encode($trackStats) ?>;
|
|
const sectionData = <?= json_encode($sectionStats) ?>;
|
|
|
|
// Common Chart Options
|
|
const commonOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false }
|
|
},
|
|
scales: {
|
|
y: { beginAtZero: true, grid: { display: false } },
|
|
x: { grid: { display: false } }
|
|
}
|
|
};
|
|
|
|
// Grade Level Bar Chart
|
|
if (gradeData.length) {
|
|
new Chart(document.getElementById('gradeChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: gradeData.map(d => 'Grade ' + d.label),
|
|
datasets: [{
|
|
label: 'Votes',
|
|
data: gradeData.map(d => d.count),
|
|
backgroundColor: '#4f46e5',
|
|
borderRadius: 6
|
|
}]
|
|
},
|
|
options: commonOptions
|
|
});
|
|
}
|
|
|
|
// Track Bar Chart
|
|
if (trackData.length) {
|
|
new Chart(document.getElementById('trackChart'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: trackData.map(d => d.label),
|
|
datasets: [{
|
|
label: 'Votes',
|
|
data: trackData.map(d => d.count),
|
|
backgroundColor: '#10b981',
|
|
borderRadius: 6
|
|
}]
|
|
},
|
|
options: commonOptions
|
|
});
|
|
}
|
|
|
|
// Section Chart
|
|
let sectionChart;
|
|
function updateSectionChart(track) {
|
|
const canvas = document.getElementById('sectionChart');
|
|
if (!canvas) return;
|
|
const filtered = sectionData.filter(d => d.track === track);
|
|
const data = {
|
|
labels: filtered.map(d => d.label),
|
|
datasets: [{
|
|
label: 'Votes',
|
|
data: filtered.map(d => d.count),
|
|
backgroundColor: '#4f46e5',
|
|
borderRadius: 6
|
|
}]
|
|
};
|
|
|
|
if (sectionChart) {
|
|
sectionChart.data = data;
|
|
sectionChart.update();
|
|
} else {
|
|
sectionChart = new Chart(canvas, {
|
|
type: 'bar',
|
|
data: data,
|
|
options: commonOptions
|
|
});
|
|
}
|
|
}
|
|
|
|
const trackFilter = document.getElementById('trackFilter');
|
|
if (trackFilter && trackData.length) {
|
|
trackFilter.addEventListener('change', (e) => {
|
|
updateSectionChart(e.target.value);
|
|
});
|
|
updateSectionChart(trackFilter.value);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|