511 lines
22 KiB
PHP
511 lines
22 KiB
PHP
<?php
|
|
require_once 'auth_helper.php';
|
|
require_login();
|
|
$user = get_user();
|
|
|
|
$pdo = db();
|
|
$electionId = get_active_election_id();
|
|
$election = get_active_election();
|
|
|
|
// For Election Management Section
|
|
$allElections = [];
|
|
if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])) {
|
|
$allElections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
|
|
}
|
|
|
|
// 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>
|
|
<style>
|
|
.modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.5);
|
|
z-index: 1000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.modal-content {
|
|
background: white;
|
|
padding: 24px;
|
|
border-radius: 12px;
|
|
width: 100%;
|
|
max-width: 400px;
|
|
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
|
}
|
|
.modal-header {
|
|
margin-bottom: 20px;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
padding-bottom: 12px;
|
|
}
|
|
.modal-title {
|
|
font-weight: 700;
|
|
font-size: 1.1rem;
|
|
color: #1e293b;
|
|
}
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
.form-group label {
|
|
display: block;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
margin-bottom: 6px;
|
|
}
|
|
.form-control {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
font-size: 0.875rem;
|
|
outline: none;
|
|
}
|
|
.modal-footer {
|
|
margin-top: 24px;
|
|
display: flex;
|
|
gap: 12px;
|
|
}
|
|
.btn-submit {
|
|
flex: 1;
|
|
background: #4f46e5;
|
|
color: white;
|
|
border: none;
|
|
padding: 10px;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
.btn-cancel {
|
|
flex: 1;
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 10px;
|
|
border-radius: 6px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
|
|
<?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">
|
|
<?php if (isset($_GET['success'])): ?>
|
|
<div style="background: #ecfdf5; color: #10b981; padding: 12px 16px; border-radius: 8px; margin-bottom: 24px; font-size: 0.875rem; border: 1px solid #10b981;">
|
|
<?= htmlspecialchars($_GET['success']) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if (isset($_GET['error'])): ?>
|
|
<div style="background: #fef2f2; color: #ef4444; padding: 12px 16px; border-radius: 8px; margin-bottom: 24px; font-size: 0.875rem; border: 1px solid #ef4444;">
|
|
<?= htmlspecialchars($_GET['error']) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<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>
|
|
|
|
<?php if (!empty($allElections)): ?>
|
|
<!-- Election Control Center -->
|
|
<div class="content-section animate-stagger" style="margin-bottom: 32px;">
|
|
<div class="section-header">
|
|
<div class="section-title">Election Control Center</div>
|
|
<button class="btn-new-election" id="btnNewElection" style="border: none; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
<i data-lucide="plus"></i> New Election
|
|
</button>
|
|
</div>
|
|
<table class="election-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Election Title</th>
|
|
<th>Status</th>
|
|
<th>Current End Time</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($allElections as $e): ?>
|
|
<tr>
|
|
<td style="font-weight: 500;"><a href="view_election.php?id=<?= $e['id'] ?>" style="color: #6366f1; text-decoration: none;"><?= htmlspecialchars($e['title']) ?></a></td>
|
|
<td>
|
|
<span class="status-badge status-<?= strtolower($e['status']) ?>">
|
|
<?= htmlspecialchars($e['status']) ?>
|
|
</span>
|
|
</td>
|
|
<td style="color: #64748b; font-size: 0.8rem;">
|
|
<?= date('M d, H:i', strtotime($e['end_date_and_time'])) ?>
|
|
</td>
|
|
<td>
|
|
<div class="quick-actions">
|
|
<?php if ($e['status'] === 'Preparing'): ?>
|
|
<form action="api/update_election_status.php" method="POST" style="display:inline;">
|
|
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
<input type="hidden" name="status" value="Ongoing">
|
|
<input type="hidden" name="redirect" value="../dashboard.php?success=Election started">
|
|
<button type="submit" class="btn-update" style="background: #10b981;">Start</button>
|
|
</form>
|
|
<?php elseif ($e['status'] === 'Ongoing'): ?>
|
|
<form action="api/update_election_status.php" method="POST" style="display:inline;">
|
|
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
<input type="hidden" name="status" value="Finished">
|
|
<input type="hidden" name="redirect" value="../dashboard.php?success=Election ended">
|
|
<button type="submit" class="btn-update" style="background: #ef4444;">End</button>
|
|
</form>
|
|
<?php endif; ?>
|
|
|
|
<?php if (in_array($user['role'], ['Admin', 'Adviser'])): ?>
|
|
<button
|
|
class="btn-update btn-manage-election"
|
|
style="background: #6366f1;"
|
|
data-id="<?= $e['id'] ?>"
|
|
data-title="<?= htmlspecialchars($e['title']) ?>"
|
|
data-status="<?= $e['status'] ?>"
|
|
data-end="<?= $e['end_date_and_time'] ?>"
|
|
>Manage</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Override/Manage Modal -->
|
|
<div id="manageElectionModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<div class="modal-title" id="modalElectionTitle">Manage Election</div>
|
|
</div>
|
|
<form action="api/manage_election_action.php" method="POST">
|
|
<input type="hidden" name="id" id="modalElectionId">
|
|
<div class="form-group">
|
|
<label>Override Status</label>
|
|
<select name="status" id="modalElectionStatus" class="form-control">
|
|
<option value="Preparing">Preparing</option>
|
|
<option value="Ongoing">Ongoing</option>
|
|
<option value="Finished">Finished</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Change End Time</label>
|
|
<input type="datetime-local" name="end_time" id="modalElectionEndTime" class="form-control">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn-cancel" onclick="closeModal('manageElectionModal')">Cancel</button>
|
|
<button type="submit" class="btn-submit">Save Changes</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New Election Modal -->
|
|
<div id="createElectionModal" class="modal">
|
|
<div class="modal-content" style="max-width: 500px;">
|
|
<div class="modal-header">
|
|
<div class="modal-title">Create New Election</div>
|
|
</div>
|
|
<form action="api/create_election.php" method="POST">
|
|
<div class="form-group">
|
|
<label>Election Title</label>
|
|
<input type="text" name="title" class="form-control" placeholder="e.g. SSG General Election 2026" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description</label>
|
|
<textarea name="description" class="form-control" rows="3" placeholder="Briefly describe the purpose..."></textarea>
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
<div class="form-group">
|
|
<label>Start Date & Time</label>
|
|
<input type="datetime-local" name="start_date" class="form-control" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>End Date & Time</label>
|
|
<input type="datetime-local" name="end_date" class="form-control" required>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn-cancel" onclick="closeModal('createElectionModal')">Cancel</button>
|
|
<button type="submit" class="btn-submit">Create Election</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.getElementById('btnNewElection').addEventListener('click', function() {
|
|
document.getElementById('createElectionModal').style.display = 'flex';
|
|
});
|
|
|
|
document.querySelectorAll('.btn-manage-election').forEach(button => {
|
|
button.addEventListener('click', function() {
|
|
const id = this.getAttribute('data-id');
|
|
const title = this.getAttribute('data-title');
|
|
const status = this.getAttribute('data-status');
|
|
const end = this.getAttribute('data-end');
|
|
|
|
document.getElementById('modalElectionId').value = id;
|
|
document.getElementById('modalElectionTitle').innerText = 'Manage: ' + title;
|
|
document.getElementById('modalElectionStatus').value = status;
|
|
|
|
if (end) {
|
|
const date = new Date(end);
|
|
const offset = date.getTimezoneOffset() * 60000;
|
|
const localISODate = new Date(date.getTime() - offset).toISOString().slice(0, 16);
|
|
document.getElementById('modalElectionEndTime').value = localISODate;
|
|
}
|
|
|
|
document.getElementById('manageElectionModal').style.display = 'flex';
|
|
});
|
|
});
|
|
|
|
function closeModal(modalId) {
|
|
if (modalId) {
|
|
document.getElementById(modalId).style.display = 'none';
|
|
} else {
|
|
document.getElementById('manageElectionModal').style.display = 'none';
|
|
document.getElementById('createElectionModal').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
window.onclick = function(event) {
|
|
if (event.target.classList.contains('modal')) {
|
|
event.target.style.display = 'none';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|