Autosave: 20260215-190109

This commit is contained in:
Flatlogic Bot 2026-02-15 19:01:09 +00:00
parent fc1fe0083d
commit 82e2096ac9
30 changed files with 1937 additions and 134 deletions

39
api/add_candidate.php Normal file
View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$election_id = $_POST['election_id'] ?? '';
$position_id = $_POST['position_id'] ?? '';
$user_id = $_POST['user_id'] ?? '';
$party_name = $_POST['party_name'] ?? '';
$manifesto = $_POST['manifesto'] ?? '';
if (!$election_id || !$position_id || !$user_id) {
die("Missing fields");
}
try {
$pdo = db();
$id = uuid();
// Check if user is already a candidate in this election
$check = $pdo->prepare("SELECT id FROM candidates WHERE election_id = ? AND user_id = ?");
$check->execute([$election_id, $user_id]);
if ($check->fetch()) {
die("User is already a candidate in this election.");
}
$stmt = $pdo->prepare("INSERT INTO candidates (id, election_id, position_id, user_id, party_name, manifesto, approved) VALUES (?, ?, ?, ?, ?, ?, TRUE)");
$stmt->execute([$id, $election_id, $position_id, $user_id, $party_name, $manifesto]);
audit_log('Added candidate', 'candidates', $id);
header("Location: ../manage_candidates.php?position_id=$position_id&success=1");
exit;
} catch (Exception $e) {
die($e->getMessage());
}
}

29
api/add_position.php Normal file
View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$election_id = $_POST['election_id'] ?? '';
$name = $_POST['name'] ?? '';
$max_votes = (int)($_POST['max_votes'] ?? 1);
if (!$election_id || !$name) {
die("Missing fields");
}
try {
$pdo = db();
$id = uuid();
$stmt = $pdo->prepare("INSERT INTO positions (id, election_id, name, max_votes) VALUES (?, ?, ?, ?)");
$stmt->execute([$id, $election_id, $name, $max_votes]);
audit_log('Added position', 'positions', $id);
header("Location: ../view_election.php?id=$election_id&success=1");
exit;
} catch (Exception $e) {
die($e->getMessage());
}
}

35
api/create_election.php Normal file
View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = $_POST['title'] ?? '';
$description = $_POST['description'] ?? '';
$start_date = $_POST['start_date'] ?? '';
$end_date = $_POST['end_date'] ?? '';
$user = get_user();
if (!$title || !$start_date || !$end_date) {
die("Missing required fields.");
}
try {
$pdo = db();
$id = uuid();
$stmt = $pdo->prepare("INSERT INTO elections (id, title, description, status, start_date_and_time, end_date_and_time, created_by) VALUES (?, ?, ?, 'Preparing', ?, ?, ?)");
$stmt->execute([$id, $title, $description, $start_date, $end_date, $user['id']]);
audit_log('Created election', 'elections', $id);
header("Location: ../view_election.php?id=$id&success=1");
exit;
} catch (Exception $e) {
die("Error: " . $e->getMessage());
}
} else {
header("Location: ../index.php");
exit;
}

58
api/submit_vote.php Normal file
View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$election_id = $_POST['election_id'] ?? '';
$votes = $_POST['votes'] ?? []; // Array of position_id => candidate_id
$user = get_user();
if (!$election_id || empty($votes)) {
die("Invalid submission.");
}
try {
$pdo = db();
$pdo->beginTransaction();
// 1. Verify election is ongoing
$eStmt = $pdo->prepare("SELECT status FROM elections WHERE id = ?");
$eStmt->execute([$election_id]);
if ($eStmt->fetchColumn() !== 'Ongoing') {
throw new Exception("Election is not ongoing.");
}
// 2. Verify user hasn't voted yet
$vCheck = $pdo->prepare("SELECT COUNT(*) FROM votes WHERE election_id = ? AND voter_id = ?");
$vCheck->execute([$election_id, $user['id']]);
if ($vCheck->fetchColumn() > 0) {
throw new Exception("You have already cast your vote for this election.");
}
// 3. Insert votes
$stmt = $pdo->prepare("INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?)");
foreach ($votes as $position_id => $candidate_id) {
$vote_id = uuid();
$stmt->execute([
$vote_id,
$election_id,
$position_id,
$candidate_id,
$user['id'],
$_SERVER['REMOTE_ADDR'] ?? 'unknown',
$_SERVER['HTTP_USER_AGENT'] ?? 'unknown'
]);
}
audit_log('Cast ballot', 'elections', $election_id);
$pdo->commit();
header("Location: ../view_results.php?id=$election_id&success=vote_cast");
exit;
} catch (Exception $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
die("Error casting vote: " . $e->getMessage());
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? '';
if (!$id) {
die("Missing ID");
}
try {
$pdo = db();
$stmt = $pdo->prepare("UPDATE candidates SET approved = NOT approved WHERE id = ?");
$stmt->execute([$id]);
$stmt = $pdo->prepare("SELECT position_id FROM candidates WHERE id = ?");
$stmt->execute([$id]);
$pos_id = $stmt->fetchColumn();
audit_log('Toggled candidate approval', 'candidates', $id);
header("Location: ../manage_candidates.php?position_id=$pos_id&success=1");
exit;
} catch (Exception $e) {
die($e->getMessage());
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? '';
$status = $_POST['status'] ?? '';
if (!$id || !$status) {
die("Missing fields");
}
try {
$pdo = db();
$stmt = $pdo->prepare("UPDATE elections SET status = ? WHERE id = ?");
$stmt->execute([$status, $id]);
audit_log("Updated election status to $status", 'elections', $id);
header("Location: ../view_election.php?id=$id&success=1");
exit;
} catch (Exception $e) {
die($e->getMessage());
}
}

364
assets/css/landing.css Normal file
View File

@ -0,0 +1,364 @@
:root {
--landing-primary: #5c7cfa;
--landing-dark: #212529;
--landing-gray: #f8f9fa;
--landing-text-muted: #6c757d;
}
body.landing-page {
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
height: 100vh;
width: 100vw;
overflow: hidden;
position: relative;
background-repeat: no-repeat;
background-position: center center;
background-attachment: fixed;
background-size: cover;
}
body.landing-page::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.6));
backdrop-filter: brightness(0.8);
z-index: 1;
}
.landing-container {
position: relative;
z-index: 2;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.school-header {
position: absolute;
top: 2rem;
left: 2rem;
display: flex;
align-items: center;
background: white;
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
border-radius: 50px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.school-logo {
width: 40px;
height: 40px;
margin-right: 10px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.school-info h1 {
margin: 0;
font-size: 0.9rem;
font-weight: 800;
color: #1a3a8a;
text-transform: uppercase;
}
.school-info p {
margin: 0;
font-size: 0.7rem;
color: var(--landing-text-muted);
}
.info-card {
background: white;
width: 90%;
max-width: 500px;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.info-card-header {
background: var(--landing-primary);
color: white;
padding: 0.75rem 1.5rem;
display: flex;
align-items: center;
font-weight: 600;
font-size: 0.9rem;
letter-spacing: 0.05em;
}
.info-card-header i {
margin-right: 10px;
background: white;
color: var(--landing-primary);
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-style: normal;
font-weight: bold;
}
.info-card-body {
padding: 2.5rem;
text-align: center;
}
.info-card-body h2 {
margin: 0 0 1.5rem 0;
font-size: 2rem;
font-weight: 900;
color: var(--landing-dark);
text-transform: uppercase;
letter-spacing: -0.02em;
}
.info-card-body p {
color: var(--landing-text-muted);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 2rem;
}
.btn-login {
background: var(--landing-primary);
color: white;
border: none;
padding: 0.8rem 2.5rem;
border-radius: 50px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
display: inline-flex;
align-items: center;
text-decoration: none;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 4px 6px rgba(92, 124, 250, 0.3);
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 6px 10px rgba(92, 124, 250, 0.4);
}
.btn-login i {
margin-right: 10px;
}
.landing-footer {
position: absolute;
bottom: 1.5rem;
width: 100%;
text-align: center;
color: #495057;
font-size: 0.75rem;
z-index: 2;
}
.flatlogic-badge {
position: absolute;
bottom: 1.5rem;
right: 1.5rem;
background: white;
padding: 0.4rem 1rem;
border-radius: 5px;
display: flex;
align-items: center;
font-size: 0.75rem;
font-weight: 600;
color: #1a3a8a;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
z-index: 2;
}
.flatlogic-badge img {
margin-right: 8px;
height: 14px;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(4px);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.login-modal {
background: white;
width: 90%;
max-width: 450px;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
animation: modalIn 0.3s ease-out;
}
@keyframes modalIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.modal-header {
background: #5c7cfa;
color: white;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.modal-header-content {
display: flex;
align-items: center;
gap: 12px;
}
.modal-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
.header-icon {
background: rgba(255, 255, 255, 0.2);
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.3);
}
.btn-close {
background: rgba(0, 0, 0, 0.2);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: background 0.2s;
}
.btn-close:hover {
background: rgba(0, 0, 0, 0.4);
}
.modal-body {
padding: 2rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-size: 0.75rem;
font-weight: 800;
color: var(--landing-dark);
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.input-container {
position: relative;
display: flex;
align-items: center;
}
.input-container i {
position: absolute;
left: 1rem;
color: var(--landing-text-muted);
}
.input-container input,
.input-container select {
width: 100%;
padding: 0.8rem 1rem 0.8rem 2.8rem;
border: 1px solid #e2e8f0;
border-radius: 10px;
font-family: inherit;
font-size: 0.95rem;
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
}
.input-container input:focus,
.input-container select:focus {
border-color: var(--landing-primary);
box-shadow: 0 0 0 3px rgba(92, 124, 250, 0.1);
}
.password-toggle {
position: absolute;
right: 1rem;
color: var(--landing-text-muted);
cursor: pointer;
}
.modal-btn-login {
width: 100%;
background: var(--landing-primary);
color: white;
border: none;
padding: 1rem;
border-radius: 10px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
margin-top: 1rem;
transition: background 0.2s;
}
.modal-btn-login:hover {
background: #4c6ef5;
}
.forgot-password {
display: block;
text-align: center;
margin-top: 1.5rem;
color: var(--landing-primary);
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
}
.forgot-password:hover {
text-decoration: underline;
}

158
assets/css/style.css Normal file
View File

@ -0,0 +1,158 @@
:root {
--primary-color: #1e293b;
--accent-color: #2563eb;
--bg-color: #f8fafc;
--surface-color: #ffffff;
--border-color: #e2e8f0;
--text-main: #1e293b;
--text-muted: #64748b;
--radius: 6px;
}
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: 'Inter', -apple-system, sans-serif;
font-size: 14px;
line-height: 1.5;
margin: 0;
}
.navbar {
background: var(--surface-color);
border-bottom: 1px solid var(--border-color);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
font-weight: 700;
font-size: 1.25rem;
color: var(--primary-color);
text-decoration: none;
}
.container {
max-width: 1000px;
margin: 2rem auto;
padding: 0 1rem;
}
.card {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: var(--radius);
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: var(--radius);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
text-decoration: none;
font-size: 0.875rem;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover {
background: #1d4ed8;
}
.btn-outline {
border-color: var(--border-color);
background: transparent;
color: var(--text-main);
}
.btn-outline:hover {
background: #f1f5f9;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
text-align: left;
padding: 0.75rem;
border-bottom: 2px solid var(--border-color);
color: var(--text-muted);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
}
.table td {
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.badge {
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-preparing { background: #fef3c7; color: #92400e; }
.badge-ongoing { background: #dcfce7; color: #166534; }
.badge-finished { background: #f1f5f9; color: #475569; }
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-control {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-family: inherit;
font-size: 0.875rem;
}
.form-control:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.header-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.text-muted { color: var(--text-muted); }
.text-center { text-align: center; }
.mb-4 { margin-bottom: 1rem; }
.mb-5 { margin-bottom: 2rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.w-100 { width: 100%; }
.row { display: flex; flex-wrap: wrap; margin-right: -0.75rem; margin-left: -0.75rem; }
.col-12 { flex: 0 0 100%; max-width: 100%; padding: 0.75rem; }
.col-md-6 { flex: 0 0 50%; max-width: 50%; padding: 0.75rem; }
.col-lg-4 { flex: 0 0 33.333333%; max-width: 33.333333%; padding: 0.75rem; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

BIN
assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

48
auth_helper.php Normal file
View File

@ -0,0 +1,48 @@
<?php
session_start();
require_once __DIR__ . '/db/config.php';
function get_user() {
if (!isset($_SESSION['user_id'])) return null;
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch();
}
function require_login() {
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
}
function require_role($roles) {
$user = get_user();
if (!$user || !in_array($user['role'], (array)$roles)) {
header('Location: index.php?error=Unauthorized');
exit;
}
}
function uuid() {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
function audit_log($action, $table = null, $record_id = null, $old = null, $new = null) {
$stmt = db()->prepare("INSERT INTO audit_logs (id, user_id, action, table_name, record_id, old_values, new_values) VALUES (?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([
uuid(),
$_SESSION['user_id'] ?? null,
$action,
$table,
$record_id,
$old ? json_encode($old) : null,
$new ? json_encode($new) : null
]);
}

124
ballot.php Normal file
View File

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/auth_helper.php';
require_login();
$user = get_user();
$id = $_GET['id'] ?? '';
if (!$id) {
header("Location: index.php");
exit;
}
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM elections WHERE id = ?");
$stmt->execute([$id]);
$election = $stmt->fetch();
if (!$election || $election['status'] !== 'Ongoing') {
die("Election is not currently ongoing.");
}
// Check if already voted
$check = $pdo->prepare("SELECT COUNT(*) FROM votes WHERE election_id = ? AND voter_id = ?");
$check->execute([$id, $user['id']]);
if ($check->fetchColumn() > 0) {
header("Location: view_results.php?id=$id&error=AlreadyVoted");
exit;
}
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
$positions->execute([$id]);
$positions = $positions->fetchAll();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Vote: <?= htmlspecialchars($election['title']) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css?v=<?= time() ?>">
<style>
.candidate-card {
border: 2px solid #e2e8f0;
border-radius: 8px;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.candidate-card:hover { border-color: #2563eb; background: #f0f9ff; }
input[type="radio"]:checked + .candidate-card {
border-color: #2563eb;
background: #eff6ff;
box-shadow: 0 0 0 1px #2563eb;
}
input[type="radio"] { display: none; }
.ballot-section { margin-bottom: 2rem; }
.ballot-header { border-bottom: 2px solid #1e293b; padding-bottom: 0.5rem; margin-bottom: 1.5rem; }
</style>
</head>
<body>
<nav class="navbar">
<a href="index.php" class="brand">E-Vote Pro</a>
<div>
<span>Logged in as <?= htmlspecialchars($user['name']) ?></span>
</div>
</nav>
<div class="container" style="max-width: 800px;">
<div class="text-center mb-5">
<h1 style="font-size: 2rem; font-weight: 800;"><?= htmlspecialchars($election['title']) ?></h1>
<p class="text-muted">Please select your candidates carefully. Your vote is immutable once cast.</p>
</div>
<form action="api/submit_vote.php" method="POST">
<input type="hidden" name="election_id" value="<?= $id ?>">
<?php foreach ($positions as $pos): ?>
<div class="ballot-section">
<div class="ballot-header">
<h2 style="margin: 0; font-size: 1.25rem;"><?= htmlspecialchars($pos['name']) ?></h2>
<small class="text-muted">Select <?= $pos['max_votes'] ?> candidate(s)</small>
</div>
<?php
$cStmt = $pdo->prepare("SELECT c.*, u.name FROM candidates c JOIN users u ON c.user_id = u.id WHERE c.position_id = ? AND c.approved = TRUE");
$cStmt->execute([$pos['id']]);
$candidates = $cStmt->fetchAll();
?>
<?php if (empty($candidates)): ?>
<p class="text-muted">No candidates for this position.</p>
<?php else: ?>
<div class="candidates-grid">
<?php foreach ($candidates as $cand): ?>
<label>
<input type="radio" name="votes[<?= $pos['id'] ?>]" value="<?= $cand['id'] ?>" required>
<div class="candidate-card">
<div style="width: 40px; height: 40px; background: #e2e8f0; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; color: #64748b;">
<?= substr($cand['name'], 0, 1) ?>
</div>
<div>
<div style="font-weight: 700;"><?= htmlspecialchars($cand['name']) ?></div>
<div style="font-size: 0.75rem; color: #64748b;"><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></div>
</div>
</div>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<div style="margin-top: 3rem; text-align: center; border-top: 1px solid #e2e8f0; padding-top: 2rem;">
<p style="font-size: 0.875rem; color: #64748b; margin-bottom: 1.5rem;">By clicking "Cast My Vote", I acknowledge that my selection is final.</p>
<button type="submit" class="btn btn-primary" style="padding: 1rem 3rem; font-size: 1.1rem; background: #1e293b;">Cast My Vote</button>
</div>
</form>
</div>
</body>
</html>

63
create_election.php Normal file
View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Create Election | <?= 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/style.css?v=<?= time() ?>">
</head>
<body>
<nav class="navbar">
<a href="index.php" class="brand">E-Vote Pro</a>
<div>
<a href="index.php" class="btn btn-outline">Back to Dashboard</a>
</div>
</nav>
<div class="container" style="max-width: 600px;">
<div class="card">
<h1 style="margin-top: 0; font-size: 1.25rem;">New Election</h1>
<p style="color: var(--text-muted); margin-bottom: 2rem;">Fill in the details to schedule a new election.</p>
<form action="api/create_election.php" method="POST">
<div class="form-group">
<label class="form-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 class="form-label">Description</label>
<textarea name="description" class="form-control" rows="3" placeholder="Briefly describe the purpose of this election..."></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label class="form-label">Start Date & Time</label>
<input type="datetime-local" name="start_date" class="form-control" required>
</div>
<div class="form-group">
<label class="form-label">End Date & Time</label>
<input type="datetime-local" name="end_date" class="form-control" required>
</div>
</div>
<div style="margin-top: 2rem; display: flex; gap: 1rem;">
<button type="submit" class="btn btn-primary" style="flex: 1;">Create Election</button>
<a href="index.php" class="btn btn-outline" style="flex: 1;">Cancel</a>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,106 @@
-- Clean slate for development
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS audit_logs;
DROP TABLE IF EXISTS votes;
DROP TABLE IF EXISTS candidates;
DROP TABLE IF EXISTS positions;
DROP TABLE IF EXISTS election_assignments;
DROP TABLE IF EXISTS elections;
DROP TABLE IF EXISTS users;
SET FOREIGN_KEY_CHECKS = 1;
-- Production-Ready Schema for Online Election System
CREATE TABLE users (
id CHAR(36) PRIMARY KEY,
student_id VARCHAR(10) UNIQUE NOT NULL, -- Format: XX-XXXX
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
grade_level INT NULL,
track VARCHAR(100) NULL,
section VARCHAR(100) NULL,
role ENUM('Admin', 'Adviser', 'Officer', 'Voter') DEFAULT 'Voter',
access_level INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP NULL
);
CREATE TABLE elections (
id CHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
status ENUM('Preparing', 'Ongoing', 'Finished') DEFAULT 'Preparing',
start_date_and_time TIMESTAMP NOT NULL,
end_date_and_time TIMESTAMP NOT NULL,
created_by CHAR(36) NOT NULL,
archived BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE election_assignments (
id CHAR(36) PRIMARY KEY,
election_id CHAR(36) NOT NULL,
user_id CHAR(36) NOT NULL,
role_in_election ENUM('Adviser', 'Officer', 'Candidate', 'Voter') DEFAULT 'Voter',
assigned_by CHAR(36) NOT NULL,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (assigned_by) REFERENCES users(id)
);
CREATE TABLE positions (
id CHAR(36) PRIMARY KEY,
election_id CHAR(36) NOT NULL,
name VARCHAR(255) NOT NULL,
max_votes INT DEFAULT 1,
sort_order INT DEFAULT 0,
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE
);
CREATE TABLE candidates (
id CHAR(36) PRIMARY KEY,
election_id CHAR(36) NOT NULL,
position_id CHAR(36) NOT NULL,
user_id CHAR(36) NOT NULL,
party_name VARCHAR(255) NULL,
manifesto TEXT NULL,
approved BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE,
FOREIGN KEY (position_id) REFERENCES positions(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE votes (
id CHAR(36) PRIMARY KEY,
election_id CHAR(36) NOT NULL,
position_id CHAR(36) NOT NULL,
candidate_id CHAR(36) NOT NULL,
voter_id CHAR(36) NOT NULL,
casted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(45),
user_agent TEXT,
UNIQUE KEY unique_vote (election_id, position_id, voter_id),
FOREIGN KEY (election_id) REFERENCES elections(id),
FOREIGN KEY (position_id) REFERENCES positions(id),
FOREIGN KEY (candidate_id) REFERENCES candidates(id),
FOREIGN KEY (voter_id) REFERENCES users(id)
);
CREATE TABLE audit_logs (
id CHAR(36) PRIMARY KEY,
user_id CHAR(36) NULL,
action VARCHAR(255) NOT NULL,
table_name VARCHAR(100) NULL,
record_id CHAR(36) NULL,
old_values TEXT NULL,
new_values TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Insert a default admin (password is 'admin123')
INSERT INTO users (id, student_id, name, email, password_hash, role, access_level)
VALUES ('admin-uuid-1', '00-0000', 'Admin User', 'admin@school.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Admin', 4);

25
includes/pexels.php Normal file
View File

@ -0,0 +1,25 @@
<?php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
$data = @file_get_contents($srcUrl);
if ($data === false) return false;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
return file_put_contents($destPath, $data) !== false;
}

224
index.php
View File

@ -1,150 +1,106 @@
<?php
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
require_once 'auth_helper.php';
$user = get_user();
$phpVersion = PHP_VERSION;
$now = date('Y-m-d H:i:s');
if (!$user) {
include 'landing.php';
exit;
}
$pdo = db();
$elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->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>New Style</title>
<?php
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<title>Election Dashboard | <?= htmlspecialchars($projectDescription) ?></title>
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
<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;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css?v=<?= time() ?>">
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
<nav class="navbar">
<a href="index.php" class="brand">E-Vote Pro</a>
<div>
<span style="margin-right: 1rem; color: var(--text-muted);"><?= htmlspecialchars($user['name']) ?> (<?= $user['role'] ?>)</span>
<a href="logout.php" class="btn btn-outline">Logout</a>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</nav>
<div class="container">
<?php if (isset($_GET['success'])): ?>
<div style="background: #dcfce7; color: #166534; padding: 1rem; border-radius: var(--radius); border: 1px solid #bbf7d0; margin-bottom: 1.5rem; font-size: 0.875rem;">
Action completed successfully.
</div>
<?php endif; ?>
<div class="header-actions">
<div>
<h1 style="margin: 0; font-size: 1.5rem;">Elections</h1>
<p style="margin: 0; color: var(--text-muted);">Manage your school elections and voting sessions.</p>
</div>
<?php if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])): ?>
<a href="create_election.php" class="btn btn-primary">+ New Election</a>
<?php endif; ?>
</div>
<div class="card">
<?php if (empty($elections)): ?>
<div style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted);">No elections found. Create your first election to get started.</p>
<?php if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])): ?>
<a href="create_election.php" class="btn btn-outline" style="margin-top: 1rem;">Setup Election</a>
<?php endif; ?>
</div>
<?php else: ?>
<table class="table">
<thead>
<tr>
<th>Title</th>
<th>Status</th>
<th>Period</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($elections as $election): ?>
<tr>
<td>
<div style="font-weight: 600;"><?= htmlspecialchars($election['title']) ?></div>
<div style="font-size: 0.75rem; color: var(--text-muted);"><?= htmlspecialchars($election['description']) ?></div>
</td>
<td>
<span class="badge badge-<?= strtolower($election['status']) ?>">
<?= htmlspecialchars($election['status']) ?>
</span>
</td>
<td>
<div style="font-size: 0.875rem;">
<?= date('M d, H:i', strtotime($election['start_date_and_time'])) ?> -
<?= date('M d, H:i', strtotime($election['end_date_and_time'])) ?>
</div>
</td>
<td>
<a href="view_election.php?id=<?= $election['id'] ?>" class="btn btn-outline" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">View</a>
<?php if ($election['status'] === 'Ongoing'): ?>
<a href="ballot.php?id=<?= $election['id'] ?>" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; background: #166534;">Vote</a>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<footer style="text-align: center; color: var(--text-muted); padding: 2rem;">
&copy; <?= date('Y') ?> E-Vote Pro | High School Online Election System
</footer>
</body>
</html>

170
landing.php Normal file
View File

@ -0,0 +1,170 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Iloilo National High School | Election System</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;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/landing.css?v=<?= time() ?>">
</head>
<body class="landing-page" style="background-image: url('assets/images/background.jpg?v=<?= filemtime('assets/images/background.jpg') ?>');">
<div class="landing-container">
<div class="school-header">
<div class="school-logo">
<img src="assets/images/logo.png?v=<?= filemtime('assets/images/logo.png') ?>" alt="Logo" style="width: 100%; height: 100%; object-fit: contain;">
</div>
<div class="school-info">
<h1>Iloilo National High School</h1>
<p>Luna St., La Paz, Iloilo City</p>
</div>
</div>
<div class="info-card">
<div class="info-card-header">
<i>i</i> SYSTEM INFORMATION
</div>
<div class="info-card-body">
<h2>Click to Vote</h2>
<p>
This portal is currently a <strong>PROTOTYPE MODEL</strong> under active development to
demonstrate the digital election process. Please be aware that all features
are in a testing phase and interactions are for demonstration purposes only.
We are continuously refining the system to ensure a seamless experience
for the final implementation.
</p>
<button class="btn-login" onclick="toggleModal()">
<svg style="width: 18px; height: 18px; margin-right: 8px;" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
LOGIN
</button>
</div>
</div>
<footer class="landing-footer">
&copy; <?= date('Y') ?> Click to Vote System [PROTOTYPE]. All Rights Reserved.
</footer>
<div class="flatlogic-badge">
<img src="https://flatlogic.com/favicon.ico" alt="Flatlogic">
Built with Flatlogic
</div>
</div>
<!-- Login Modal -->
<div id="loginModal" class="modal-overlay">
<div class="login-modal">
<div class="modal-header">
<div class="modal-header-content">
<div class="header-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</div>
<h2>Login</h2>
</div>
<button class="btn-close" onclick="toggleModal()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
<div class="modal-body">
<?php if (isset($_GET['error'])): ?>
<div style="background: #fee2e2; color: #b91c1c; padding: 0.75rem; border-radius: 10px; margin-bottom: 1.5rem; font-size: 0.85rem; border: 1px solid #fecaca; text-align: center;">
<?= htmlspecialchars($_GET['error']) ?>
</div>
<?php endif; ?>
<form action="login.php" method="POST">
<div class="form-group">
<label>User Type</label>
<div class="input-container">
<i>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></svg>
</i>
<select name="role">
<option value="Voter">Voter</option>
<option value="Officer">Officer</option>
<option value="Adviser">Adviser</option>
<option value="Admin">Admin</option>
</select>
</div>
</div>
<div class="form-group">
<label>UID</label>
<div class="input-container">
<i>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"></rect><line x1="7" y1="8" x2="17" y2="8"></line><line x1="7" y1="12" x2="17" y2="12"></line><line x1="7" y1="16" x2="12" y2="16"></line></svg>
</i>
<input type="text" name="student_id" placeholder="00-0000" required pattern="\d{2}-\d{4}">
</div>
</div>
<div class="form-group">
<label>Email Account</label>
<div class="input-container">
<i>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
</i>
<input type="email" name="email" placeholder="firstname.lastname@iloilonhs.edu.ph" required>
</div>
</div>
<div class="form-group">
<label>Password</label>
<div class="input-container">
<i>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg>
</i>
<input type="password" id="passwordInput" name="password" placeholder="Enter your password" required>
<i class="password-toggle" onclick="togglePassword()">
<svg id="eyeIcon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</i>
</div>
</div>
<button type="submit" class="modal-btn-login">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>
LOGIN
</button>
<a href="#" class="forgot-password">Forgot Password?</a>
</form>
</div>
</div>
</div>
<script>
function toggleModal() {
const modal = document.getElementById('loginModal');
modal.classList.toggle('active');
}
function togglePassword() {
const input = document.getElementById('passwordInput');
const icon = document.getElementById('eyeIcon');
if (input.type === 'password') {
input.type = 'text';
icon.innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>';
} else {
input.type = 'password';
icon.innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>';
}
}
// Close modal on click outside
window.onclick = function(event) {
const modal = document.getElementById('loginModal');
if (event.target == modal) {
toggleModal();
}
}
// Auto-open modal if error exists
<?php if (isset($_GET['error'])): ?>
window.onload = function() {
toggleModal();
}
<?php endif; ?>
</script>
</body>
</html>

63
login.php Normal file
View File

@ -0,0 +1,63 @@
<?php
require_once 'auth_helper.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$student_id = $_POST['student_id'] ?? '';
$email = $_POST['email'] ?? '';
$role = $_POST['role'] ?? '';
$password = $_POST['password'] ?? '';
// We search by student_id and verify email and role match if provided
$stmt = db()->prepare("SELECT * FROM users WHERE student_id = ? AND email = ? AND role = ?");
$stmt->execute([$student_id, $email, $role]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_role'] = $user['role'];
header('Location: index.php');
exit;
} else {
$error = 'Invalid Credentials. Please check your UID, Email, and Role.';
if (isset($_POST['role'])) { // Detect if coming from landing modal
header('Location: index.php?error=' . urlencode($error));
exit;
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login - Online Election System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8fafc; font-family: 'Inter', sans-serif; }
.login-card { max-width: 400px; margin: 100px auto; border-radius: 8px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
</style>
</head>
<body>
<div class="card login-card p-4">
<h2 class="text-center mb-4" style="color: #1e293b;">Election Login</h2>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Student ID (XX-XXXX)</label>
<input type="text" name="student_id" class="form-control" placeholder="00-0000" required pattern="\d{2}-\d{4}">
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary w-100" style="background-color: #2563eb;">Login</button>
</form>
<div class="mt-3 text-center">
<small>Don't have an account? <a href="signup.php">Register</a></small>
</div>
</div>
</body>
</html>

5
logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: index.php');
exit;

119
manage_candidates.php Normal file
View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/auth_helper.php';
require_login();
require_role(['Admin', 'Adviser', 'Officer']);
$position_id = $_GET['position_id'] ?? '';
if (!$position_id) die("Position ID required");
$pdo = db();
$pStmt = $pdo->prepare("SELECT p.*, e.title as election_title, e.id as election_id FROM positions p JOIN elections e ON p.election_id = e.id WHERE p.id = ?");
$pStmt->execute([$position_id]);
$position = $pStmt->fetch();
if (!$position) die("Position not found");
$candidates = $pdo->prepare("SELECT c.*, u.name, u.student_id FROM candidates c JOIN users u ON c.user_id = u.id WHERE c.position_id = ?");
$candidates->execute([$position_id]);
$candidates = $candidates->fetchAll();
// Get all users who could be candidates (could be improved with search)
$users = $pdo->query("SELECT id, name, student_id FROM users WHERE role = 'Voter' LIMIT 100")->fetchAll();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Manage Candidates | <?= htmlspecialchars($position['name']) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css?v=<?= time() ?>">
</head>
<body>
<nav class="navbar">
<a href="index.php" class="brand">E-Vote Pro</a>
<div>
<a href="view_election.php?id=<?= $position['election_id'] ?>" class="btn btn-outline">Back to Election</a>
</div>
</nav>
<div class="container">
<div class="header-actions">
<div>
<h1>Candidates for <?= htmlspecialchars($position['name']) ?></h1>
<p><?= htmlspecialchars($position['election_title']) ?></p>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem;">
<div class="card">
<h3>Add Candidate</h3>
<form action="api/add_candidate.php" method="POST">
<input type="hidden" name="position_id" value="<?= $position_id ?>">
<input type="hidden" name="election_id" value="<?= $position['election_id'] ?>">
<div class="form-group">
<label class="form-label">Select User (Student)</label>
<select name="user_id" class="form-control" required>
<option value="">-- Choose Student --</option>
<?php foreach ($users as $u): ?>
<option value="<?= $u['id'] ?>"><?= htmlspecialchars($u['name']) ?> (<?= $u['student_id'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label class="form-label">Party Name</label>
<input type="text" name="party_name" class="form-control" placeholder="e.g. Independent">
</div>
<div class="form-group">
<label class="form-label">Manifesto</label>
<textarea name="manifesto" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Add Candidate</button>
</form>
</div>
<div class="card">
<h3>Current Candidates</h3>
<?php if (empty($candidates)): ?>
<p class="text-muted">No candidates added yet.</p>
<?php else: ?>
<table class="table">
<thead>
<tr>
<th>Student</th>
<th>Party</th>
<th>Approved</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($candidates as $c): ?>
<tr>
<td>
<strong><?= htmlspecialchars($c['name']) ?></strong><br>
<small><?= $c['student_id'] ?></small>
</td>
<td><?= htmlspecialchars($c['party_name'] ?: 'None') ?></td>
<td>
<span class="badge" style="background: <?= $c['approved'] ? '#22c55e' : '#94a3b8' ?>">
<?= $c['approved'] ? 'Yes' : 'Pending' ?>
</span>
</td>
<td>
<form action="api/toggle_candidate_approval.php" method="POST" style="display:inline;">
<input type="hidden" name="id" value="<?= $c['id'] ?>">
<button type="submit" class="btn btn-outline" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">
<?= $c['approved'] ? 'Revoke' : 'Approve' ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
</body>
</html>

20
migrate.php Normal file
View File

@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/db/config.php';
try {
$pdo = db();
$sql = file_get_contents(__DIR__ . '/db/migrations/001_initial_schema.sql');
// Split SQL by semicolon and execute each statement
// Note: This is a simple parser, might fail on complex SQL but should work for this schema
$statements = explode(';', $sql);
foreach ($statements as $statement) {
$statement = trim($statement);
if ($statement) {
$pdo->exec($statement);
}
}
echo "Migration successful!\n";
} catch (Exception $e) {
echo "Migration failed: " . $e->getMessage() . "\n";
}

87
signup.php Normal file
View File

@ -0,0 +1,87 @@
<?php
require_once 'auth_helper.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$student_id = $_POST['student_id'] ?? '';
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$role = $_POST['role'] ?? 'Voter';
// Simple validation
if (!preg_match('/^\d{2}-\d{4}$/', $student_id)) {
$error = 'Invalid Student ID format. Use XX-XXXX.';
} else {
try {
$id = uuid();
$hash = password_hash($password, PASSWORD_DEFAULT);
$stmt = db()->prepare("INSERT INTO users (id, student_id, name, email, password_hash, role) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$id, $student_id, $name, $email, $hash, $role]);
$_SESSION['user_id'] = $id;
$_SESSION['user_role'] = $role;
audit_log('User registered', 'users', $id);
header('Location: index.php');
exit;
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = 'Student ID or Email already exists.';
} else {
$error = 'An error occurred: ' . $e->getMessage();
}
}
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Signup - Online Election System</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8fafc; font-family: 'Inter', sans-serif; }
.signup-card { max-width: 500px; margin: 50px auto; border-radius: 8px; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
</style>
</head>
<body>
<div class="card signup-card p-4">
<h2 class="text-center mb-4" style="color: #1e293b;">Voter Registration</h2>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Full Name</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Student ID (XX-XXXX)</label>
<input type="text" name="student_id" class="form-control" placeholder="00-0000" required pattern="\d{2}-\d{4}">
</div>
<div class="mb-3">
<label class="form-label">Email Address</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Role</label>
<select name="role" class="form-select">
<option value="Voter">Voter</option>
<option value="Officer">Officer</option>
<option value="Adviser">Adviser</option>
<option value="Admin">Admin</option>
</select>
</div>
<button type="submit" class="btn btn-primary w-100" style="background-color: #2563eb;">Register</button>
</form>
<div class="mt-3 text-center">
<small>Already have an account? <a href="login.php">Login</a></small>
</div>
</div>
</body>
</html>

169
view_election.php Normal file
View File

@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/auth_helper.php';
require_login();
$user = get_user();
$id = $_GET['id'] ?? '';
if (!$id) {
header("Location: index.php");
exit;
}
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM elections WHERE id = ?");
$stmt->execute([$id]);
$election = $stmt->fetch();
if (!$election) {
die("Election not found.");
}
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
$positions->execute([$id]);
$positions = $positions->fetchAll();
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System';
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= htmlspecialchars($election['title']) ?> | Manage</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/style.css?v=<?= time() ?>">
</head>
<body>
<nav class="navbar">
<a href="index.php" class="brand">E-Vote Pro</a>
<div>
<span style="margin-right: 1rem; color: var(--text-muted);"><?= htmlspecialchars($user['name']) ?></span>
<a href="index.php" class="btn btn-outline">Back to Dashboard</a>
</div>
</nav>
<div class="container">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<h1 style="margin: 0; font-size: 1.5rem;"><?= htmlspecialchars($election['title']) ?></h1>
<p style="margin: 0.5rem 0; color: var(--text-muted);"><?= htmlspecialchars($election['description']) ?></p>
<div style="margin-top: 1rem; font-size: 0.875rem;">
<strong>Status:</strong> <span class="badge badge-<?= strtolower($election['status']) ?>"><?= $election['status'] ?></span> |
<strong>Period:</strong> <?= date('M d, H:i', strtotime($election['start_date_and_time'])) ?> to <?= date('M d, H:i', strtotime($election['end_date_and_time'])) ?>
</div>
</div>
<div style="display: flex; gap: 0.5rem;">
<?php if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])): ?>
<button class="btn btn-outline">Edit Info</button>
<?php if ($election['status'] === 'Preparing'): ?>
<form action="api/update_election_status.php" method="POST">
<input type="hidden" name="id" value="<?= $election['id'] ?>">
<input type="hidden" name="status" value="Ongoing">
<button type="submit" class="btn btn-primary" style="background: #166534;">Launch Election</button>
</form>
<?php elseif ($election['status'] === 'Ongoing'): ?>
<form action="api/update_election_status.php" method="POST">
<input type="hidden" name="id" value="<?= $election['id'] ?>">
<input type="hidden" name="status" value="Finished">
<button type="submit" class="btn btn-primary" style="background: #ef4444;">End Election</button>
</form>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
<div>
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0; font-size: 1.1rem;">Positions</h2>
<?php if ($election['status'] === 'Preparing'): ?>
<button onclick="document.getElementById('addPositionForm').style.display='block'" class="btn btn-outline" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">+ Add Position</button>
<?php endif; ?>
</div>
<div id="addPositionForm" style="display:none; margin-bottom: 1.5rem; padding: 1rem; background: #f8fafc; border-radius: 8px;">
<form action="api/add_position.php" method="POST">
<input type="hidden" name="election_id" value="<?= $election['id'] ?>">
<div style="display: flex; gap: 0.5rem; align-items: flex-end;">
<div style="flex: 2;">
<label style="display:block; font-size: 0.75rem;">Position Name</label>
<input type="text" name="name" class="form-control" placeholder="e.g. President" required>
</div>
<div style="flex: 1;">
<label style="display:block; font-size: 0.75rem;">Max Votes</label>
<input type="number" name="max_votes" class="form-control" value="1" min="1">
</div>
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" onclick="document.getElementById('addPositionForm').style.display='none'" class="btn btn-outline">Cancel</button>
</div>
</form>
</div>
<?php if (empty($positions)): ?>
<div style="text-align: center; padding: 1.5rem; border: 1px dashed var(--border-color); border-radius: var(--radius);">
<p style="color: var(--text-muted); margin: 0;">No positions defined yet.</p>
</div>
<?php else: ?>
<table class="table">
<thead>
<tr>
<th>Position Name</th>
<th>Max Votes</th>
<th>Candidates</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($positions as $pos):
$cStmt = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE position_id = ?");
$cStmt->execute([$pos['id']]);
$cCount = $cStmt->fetchColumn();
?>
<tr>
<td><?= htmlspecialchars($pos['name']) ?></td>
<td><?= $pos['max_votes'] ?></td>
<td><?= $cCount ?></td>
<td>
<a href="manage_candidates.php?position_id=<?= $pos['id'] ?>" class="btn btn-outline" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">Candidates</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
<div>
<div class="card">
<h2 style="margin-top: 0; font-size: 1.1rem;">Quick Stats</h2>
<?php
$vStmt = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
$vStmt->execute([$id]);
$votesCount = $vStmt->fetchColumn();
?>
<div style="margin-bottom: 1rem;">
<div style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; font-weight: 600;">Total Votes Cast</div>
<div style="font-size: 1.5rem; font-weight: 700;"><?= $votesCount ?></div>
</div>
<div style="margin-bottom: 1rem;">
<?php
$canStmt = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
$canStmt->execute([$id]);
$candidatesTotal = $canStmt->fetchColumn();
?>
<div style="font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; font-weight: 600;">Total Candidates</div>
<div style="font-size: 1.5rem; font-weight: 700;"><?= $candidatesTotal ?></div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

108
view_results.php Normal file
View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/auth_helper.php';
require_login();
$user = get_user();
$id = $_GET['id'] ?? '';
if (!$id) {
header("Location: index.php");
exit;
}
$pdo = db();
$stmt = $pdo->prepare("SELECT * FROM elections WHERE id = ?");
$stmt->execute([$id]);
$election = $stmt->fetch();
if (!$election) die("Election not found");
// Summary stats
$vStmt = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
$vStmt->execute([$id]);
$totalVoters = $vStmt->fetchColumn();
// Positions and results
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
$positions->execute([$id]);
$positions = $positions->fetchAll();
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Results: <?= htmlspecialchars($election['title']) ?></title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css?v=<?= time() ?>">
<style>
.result-bar-container { background: #e2e8f0; height: 12px; border-radius: 6px; overflow: hidden; margin-top: 0.5rem; }
.result-bar { background: #2563eb; height: 100%; transition: width 0.5s; }
.candidate-result { margin-bottom: 1.5rem; padding: 1rem; border: 1px solid #e2e8f0; border-radius: 8px; }
</style>
</head>
<body>
<nav class="navbar">
<a href="index.php" class="brand">E-Vote Pro</a>
<div>
<a href="index.php" class="btn btn-outline">Dashboard</a>
</div>
</nav>
<div class="container">
<div class="card text-center mb-4">
<h1><?= htmlspecialchars($election['title']) ?> - Results</h1>
<p class="text-muted"><?= $election['status'] ?> Election</p>
<div style="font-size: 2rem; font-weight: 800;"><?= $totalVoters ?></div>
<div style="font-size: 0.75rem; color: #64748b; text-transform: uppercase;">Total Ballots Cast</div>
</div>
<?php if ($election['status'] === 'Preparing'): ?>
<div class="card text-center">
<p>Results will be available once the election starts.</p>
</div>
<?php else: ?>
<div class="row">
<?php foreach ($positions as $pos): ?>
<div class="col-12 mb-4">
<div class="card">
<h2 style="font-size: 1.25rem; border-bottom: 1px solid #e2e8f0; padding-bottom: 0.5rem; margin-bottom: 1rem;">
<?= htmlspecialchars($pos['name']) ?>
</h2>
<?php
$rStmt = $pdo->prepare("
SELECT c.*, u.name,
(SELECT COUNT(*) FROM votes v WHERE v.candidate_id = c.id) as vote_count
FROM candidates c
JOIN users u ON c.user_id = u.id
WHERE c.position_id = ?
ORDER BY vote_count DESC
");
$rStmt->execute([$pos['id']]);
$results = $rStmt->fetchAll();
$posTotal = array_sum(array_column($results, 'vote_count'));
?>
<?php foreach ($results as $res):
$percent = $posTotal > 0 ? round(($res['vote_count'] / $posTotal) * 100, 1) : 0;
?>
<div class="candidate-result">
<div style="display: flex; justify-content: space-between; font-weight: 600;">
<span><?= htmlspecialchars($res['name']) ?> (<?= htmlspecialchars($res['party_name'] ?: 'Ind.') ?>)</span>
<span><?= $res['vote_count'] ?> votes (<?= $percent ?>%)</span>
</div>
<div class="result-bar-container">
<div class="result-bar" style="width: <?= $percent ?>%"></div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</body>
</html>