476 lines
21 KiB
PHP
476 lines
21 KiB
PHP
<?php
|
|
require_once 'auth_helper.php';
|
|
require_login();
|
|
$user = get_user();
|
|
|
|
$pdo = db();
|
|
|
|
// Global Election Context
|
|
$electionId = get_active_election_id();
|
|
$election = get_active_election();
|
|
|
|
if (!$electionId) {
|
|
die("No active election selected. Please create an election first.");
|
|
}
|
|
|
|
// Statistics (Filtered by Election)
|
|
$totalCandidates = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
|
|
$totalCandidates->execute([$electionId]);
|
|
$totalCandidates = $totalCandidates->fetchColumn();
|
|
|
|
$uniquePositions = $pdo->prepare("SELECT COUNT(*) FROM positions WHERE election_id = ?");
|
|
$uniquePositions->execute([$electionId]);
|
|
$uniquePositions = $uniquePositions->fetchColumn();
|
|
|
|
$activeParties = $pdo->prepare("SELECT COUNT(*) FROM parties WHERE election_id = ?");
|
|
$activeParties->execute([$electionId]);
|
|
$activeParties = $activeParties->fetchColumn();
|
|
|
|
// Candidates by Position
|
|
$posStats = $pdo->prepare("SELECT p.name, COUNT(c.id) as count
|
|
FROM positions p LEFT JOIN candidates c ON p.id = c.position_id
|
|
WHERE p.election_id = ? GROUP BY p.id ORDER BY p.sort_order");
|
|
$posStats->execute([$electionId]);
|
|
$posStats = $posStats->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Candidates by Party
|
|
$partyStats = $pdo->prepare("SELECT p.name as party_name, COUNT(c.id) as count
|
|
FROM parties p LEFT JOIN candidates c ON p.name = c.party_name AND c.election_id = p.election_id
|
|
WHERE p.election_id = ? GROUP BY p.id ORDER BY count DESC");
|
|
$partyStats->execute([$electionId]);
|
|
$partyStats = $partyStats->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Filters
|
|
$search = $_GET['search'] ?? '';
|
|
$filterPosition = $_GET['position'] ?? 'All Positions';
|
|
$filterParty = $_GET['party'] ?? 'All Parties';
|
|
|
|
// Main Query
|
|
$query = "SELECT c.*, u.name as user_name, u.email as user_email, u.student_id, u.grade_level, u.track, p.name as position_name
|
|
FROM candidates c
|
|
JOIN users u ON c.user_id = u.id
|
|
JOIN positions p ON c.position_id = p.id
|
|
WHERE c.election_id = ?";
|
|
|
|
$params = [$electionId];
|
|
|
|
if ($search) {
|
|
$query .= " AND (u.name LIKE ? OR u.email LIKE ? OR c.party_name LIKE ?)";
|
|
$params[] = "%$search%";
|
|
$params[] = "%$search%";
|
|
$params[] = "%$search%";
|
|
}
|
|
|
|
if ($filterPosition !== 'All Positions') {
|
|
$query .= " AND p.name = ?";
|
|
$params[] = $filterPosition;
|
|
}
|
|
|
|
if ($filterParty !== 'All Parties') {
|
|
$query .= " AND c.party_name = ?";
|
|
$params[] = $filterParty;
|
|
}
|
|
|
|
$query .= " ORDER BY p.sort_order, u.name";
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
$candidates = $stmt->fetchAll();
|
|
|
|
// Options for Modals/Filters
|
|
$allPositions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order");
|
|
$allPositions->execute([$electionId]);
|
|
$allPositions = $allPositions->fetchAll();
|
|
|
|
$allParties = $pdo->prepare("SELECT * FROM parties WHERE election_id = ? ORDER BY name");
|
|
$allParties->execute([$electionId]);
|
|
$allParties = $allParties->fetchAll();
|
|
|
|
$allVoters = $pdo->query("SELECT id, name, student_id FROM users WHERE role = 'Voter' ORDER BY name")->fetchAll();
|
|
|
|
$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>Candidate Management | <?= 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() ?>">
|
|
<link rel="stylesheet" href="assets/css/candidate_management.css?v=<?= time() ?>">
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
.management-actions {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
.btn-manage {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 16px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
border: 1px solid #e2e8f0;
|
|
background: white;
|
|
color: #1e293b;
|
|
}
|
|
.btn-manage:hover {
|
|
background: #f8fafc;
|
|
border-color: #cbd5e1;
|
|
}
|
|
.btn-manage.primary {
|
|
background: #4f46e5;
|
|
color: white;
|
|
border-color: #4f46e5;
|
|
}
|
|
.btn-manage.primary:hover {
|
|
background: #4338ca;
|
|
}
|
|
.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: 32px;
|
|
border-radius: 16px;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
|
|
}
|
|
.modal-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
}
|
|
.modal-header h2 { margin: 0; font-size: 1.25rem; color: #1e293b; }
|
|
.form-group { margin-bottom: 16px; }
|
|
.form-group label { display: block; font-size: 12px; font-weight: 600; color: #64748b; margin-bottom: 6px; }
|
|
.form-group input, .form-group select, .form-group textarea {
|
|
width: 100%;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid #e2e8f0;
|
|
font-size: 14px;
|
|
}
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
margin-top: 24px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="dashboard-body">
|
|
|
|
<?php require_once 'includes/sidebar.php'; ?>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-wrapper">
|
|
<header class="top-header">
|
|
<div class="search-bar">
|
|
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
|
|
<input type="text" placeholder="Quick search...">
|
|
</div>
|
|
|
|
<div class="user-profile">
|
|
<div class="user-info">
|
|
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
|
|
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
|
|
</div>
|
|
<div class="user-avatar">
|
|
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="dashboard-content animate-fade-in">
|
|
<div class="dashboard-header" style="display: flex; justify-content: space-between; align-items: flex-start;">
|
|
<div style="display: flex; align-items: center; gap: 16px;">
|
|
<div class="header-icon-container">
|
|
<i data-lucide="user-square-2" style="width: 24px; color: #4f46e5;"></i>
|
|
</div>
|
|
<div>
|
|
<h1 style="margin: 0; font-size: 1.5rem; color: #1e293b;">Candidate Management</h1>
|
|
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 0.875rem;">Managing <?= htmlspecialchars($election['title']) ?></p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span class="status-badge status-<?= strtolower($election['status'] ?? 'preparing') ?>">
|
|
<?= strtoupper($election['status'] ?? 'PREPARING') ?>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="management-actions animate-stagger">
|
|
<button class="btn-manage primary" onclick="openModal('addCandidateModal')">
|
|
<i data-lucide="plus"></i> ADD CANDIDATE
|
|
</button>
|
|
<button class="btn-manage" onclick="openModal('addPositionModal')">
|
|
<i data-lucide="layout-list"></i> DEFINE POSITION
|
|
</button>
|
|
<button class="btn-manage" onclick="openModal('addPartyModal')">
|
|
<i data-lucide="flag"></i> DEFINE PARTY
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="candidate-stats-grid animate-stagger">
|
|
<div class="candidate-stat-card">
|
|
<div class="candidate-stat-label">TOTAL CANDIDATES</div>
|
|
<div class="candidate-stat-value"><?= number_format($totalCandidates) ?></div>
|
|
</div>
|
|
<div class="candidate-stat-card">
|
|
<div class="candidate-stat-label">UNIQUE POSITIONS</div>
|
|
<div class="candidate-stat-value"><?= number_format($uniquePositions) ?></div>
|
|
</div>
|
|
<div class="candidate-stat-card">
|
|
<div class="candidate-stat-label" style="color: #10b981;">ACTIVE PARTIES</div>
|
|
<div class="candidate-stat-value" style="color: #10b981;"><?= number_format($activeParties) ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Distribution Row -->
|
|
<div class="distribution-row animate-stagger" style="margin-bottom: 32px;">
|
|
<div class="distribution-card">
|
|
<div class="distribution-header">Candidates by Position</div>
|
|
<div class="distribution-list">
|
|
<?php foreach ($posStats as $stat): ?>
|
|
<div class="distribution-item">
|
|
<span><?= htmlspecialchars($stat['name']) ?></span>
|
|
<span class="distribution-count"><?= $stat['count'] ?></span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($posStats)): ?>
|
|
<div style="padding: 12px; color: #94a3b8; font-size: 0.875rem; text-align: center;">No positions defined.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="distribution-card">
|
|
<div class="distribution-header">Candidates by Party</div>
|
|
<div class="distribution-list">
|
|
<?php foreach ($partyStats as $stat): ?>
|
|
<div class="distribution-item">
|
|
<span><?= htmlspecialchars($stat['party_name'] ?: 'Independent') ?></span>
|
|
<span class="distribution-count"><?= $stat['count'] ?></span>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<?php if (empty($partyStats)): ?>
|
|
<div style="padding: 12px; color: #94a3b8; font-size: 0.875rem; text-align: center;">No parties defined.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters & Table Section -->
|
|
<div class="content-section animate-fade-in" style="background: white; border-radius: 12px; border: 1px solid #f3f4f6; overflow: hidden;">
|
|
<form method="GET" class="filter-bar">
|
|
<div class="filter-group" style="flex: 2;">
|
|
<label>SEARCH</label>
|
|
<div class="search-input-wrapper">
|
|
<i data-lucide="search" style="width: 14px; color: #94a3b8;"></i>
|
|
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Search candidates...">
|
|
</div>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>POSITION</label>
|
|
<select name="position" onchange="this.form.submit()">
|
|
<option>All Positions</option>
|
|
<?php foreach ($allPositions as $p): ?>
|
|
<option value="<?= htmlspecialchars($p['name']) ?>" <?= $filterPosition === $p['name'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>PARTY</label>
|
|
<select name="party" onchange="this.form.submit()">
|
|
<option>All Parties</option>
|
|
<?php foreach ($allParties as $pt): ?>
|
|
<option value="<?= htmlspecialchars($pt['name']) ?>" <?= $filterParty === $pt['name'] ? 'selected' : '' ?>><?= htmlspecialchars($pt['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
</form>
|
|
|
|
<table class="candidates-table">
|
|
<thead>
|
|
<tr>
|
|
<th>CANDIDATE</th>
|
|
<th>POSITION</th>
|
|
<th>PARTY</th>
|
|
<th>GRADE/TRACK</th>
|
|
<th>ACTIONS</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($candidates)): ?>
|
|
<tr>
|
|
<td colspan="5" style="text-align: center; color: #94a3b8; padding: 32px;">No candidates found in this election.</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($candidates as $cand): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="candidate-info">
|
|
<div class="candidate-avatar">
|
|
<?= strtoupper(substr($cand['user_name'], 0, 1)) ?>
|
|
</div>
|
|
<div class="candidate-details">
|
|
<span class="candidate-name"><?= htmlspecialchars($cand['user_name']) ?></span>
|
|
<span class="candidate-sub"><?= htmlspecialchars($cand['student_id']) ?> | <?= htmlspecialchars($cand['user_email']) ?></span>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="position-badge"><?= htmlspecialchars($cand['position_name']) ?></span>
|
|
</td>
|
|
<td><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></td>
|
|
<td>
|
|
<div class="candidate-details">
|
|
<span class="candidate-name">Grade <?= htmlspecialchars($cand['grade_level'] ?: '12') ?></span>
|
|
<span class="candidate-sub"><?= htmlspecialchars($cand['track'] ?: 'N/A') ?></span>
|
|
</div>
|
|
</td>
|
|
<td class="actions-cell">
|
|
<button title="Edit"><i data-lucide="edit-2"></i></button>
|
|
<button title="Delete" style="color: #ef4444;"><i data-lucide="trash-2"></i></button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Modals -->
|
|
<div id="addCandidateModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Add New Candidate</h2>
|
|
<button onclick="closeModal('addCandidateModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
</div>
|
|
<form action="api/add_candidate.php" method="POST">
|
|
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
<div class="form-group">
|
|
<label>Select Student</label>
|
|
<select name="user_id" required>
|
|
<option value="">-- Choose Voter --</option>
|
|
<?php foreach ($allVoters as $v): ?>
|
|
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?> (<?= $v['student_id'] ?>)</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Position</label>
|
|
<select name="position_id" required>
|
|
<option value="">-- Choose Position --</option>
|
|
<?php foreach ($allPositions as $p): ?>
|
|
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Party</label>
|
|
<select name="party_name">
|
|
<option value="">Independent</option>
|
|
<?php foreach ($allParties as $pt): ?>
|
|
<option value="<?= htmlspecialchars($pt['name']) ?>"><?= htmlspecialchars($pt['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Manifesto / Vision</label>
|
|
<textarea name="manifesto" rows="3" placeholder="Enter candidate vision..."></textarea>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" onclick="closeModal('addCandidateModal')" class="btn-manage">Cancel</button>
|
|
<button type="submit" class="btn-manage primary">Save Candidate</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="addPositionModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Define New Position</h2>
|
|
<button onclick="closeModal('addPositionModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
</div>
|
|
<form action="api/add_position.php" method="POST">
|
|
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
<div class="form-group">
|
|
<label>Position Name</label>
|
|
<input type="text" name="name" placeholder="e.g. President, Secretary" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Max Votes (Winners)</label>
|
|
<input type="number" name="max_votes" value="1" min="1" required>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" onclick="closeModal('addPositionModal')" class="btn-manage">Cancel</button>
|
|
<button type="submit" class="btn-manage primary">Create Position</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="addPartyModal" class="modal">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h2>Define New Party</h2>
|
|
<button onclick="closeModal('addPartyModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
</div>
|
|
<form action="api/add_party.php" method="POST">
|
|
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
<div class="form-group">
|
|
<label>Party Name</label>
|
|
<input type="text" name="name" placeholder="e.g. Unity Party" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description</label>
|
|
<textarea name="description" rows="2" placeholder="Party slogan or vision..."></textarea>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" onclick="closeModal('addPartyModal')" class="btn-manage">Cancel</button>
|
|
<button type="submit" class="btn-manage primary">Create Party</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
lucide.createIcons();
|
|
|
|
function openModal(id) {
|
|
document.getElementById(id).style.display = 'flex';
|
|
}
|
|
|
|
function closeModal(id) {
|
|
document.getElementById(id).style.display = 'none';
|
|
}
|
|
|
|
window.onclick = function(event) {
|
|
if (event.target.className === 'modal') {
|
|
event.target.style.display = 'none';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|