38458-vm/dashboard.php
Flatlogic Bot 5ac812ef05 Final3
2026-02-16 03:29:05 +00:00

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>