Autosave: 20260215-190109
39
api/add_candidate.php
Normal 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
@ -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
@ -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
@ -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());
|
||||
}
|
||||
}
|
||||
30
api/toggle_candidate_approval.php
Normal 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());
|
||||
}
|
||||
}
|
||||
27
api/update_election_status.php
Normal 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
@ -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
@ -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; }
|
||||
BIN
assets/images/background.jpg
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/pasted-20260215-184130-92b105b7.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
assets/pasted-20260215-184400-75694580.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
assets/pasted-20260215-184844-2497180e.jpg
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
assets/pasted-20260215-185354-bdf656b8.jpg
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
assets/pasted-20260215-185608-1a733a27.jpg
Normal file
|
After Width: | Height: | Size: 408 KiB |
BIN
assets/pasted-20260215-185725-11a8f403.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
48
auth_helper.php
Normal 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
@ -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
@ -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>
|
||||
106
db/migrations/001_initial_schema.sql
Normal 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
@ -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
@ -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;">
|
||||
© <?= date('Y') ?> E-Vote Pro | High School Online Election System
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
170
landing.php
Normal 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">
|
||||
© <?= 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
@ -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
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
119
manage_candidates.php
Normal 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
@ -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
@ -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
@ -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
@ -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>
|
||||