diff --git a/api/add_candidate.php b/api/add_candidate.php
new file mode 100644
index 0000000..b7e67d2
--- /dev/null
+++ b/api/add_candidate.php
@@ -0,0 +1,39 @@
+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());
+ }
+}
diff --git a/api/add_position.php b/api/add_position.php
new file mode 100644
index 0000000..6d9660b
--- /dev/null
+++ b/api/add_position.php
@@ -0,0 +1,29 @@
+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());
+ }
+}
diff --git a/api/create_election.php b/api/create_election.php
new file mode 100644
index 0000000..61e59bc
--- /dev/null
+++ b/api/create_election.php
@@ -0,0 +1,35 @@
+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;
+}
diff --git a/api/submit_vote.php b/api/submit_vote.php
new file mode 100644
index 0000000..a4f18ad
--- /dev/null
+++ b/api/submit_vote.php
@@ -0,0 +1,58 @@
+ 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());
+ }
+}
diff --git a/api/toggle_candidate_approval.php b/api/toggle_candidate_approval.php
new file mode 100644
index 0000000..f9138ec
--- /dev/null
+++ b/api/toggle_candidate_approval.php
@@ -0,0 +1,30 @@
+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());
+ }
+}
diff --git a/api/update_election_status.php b/api/update_election_status.php
new file mode 100644
index 0000000..9697604
--- /dev/null
+++ b/api/update_election_status.php
@@ -0,0 +1,27 @@
+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());
+ }
+}
diff --git a/assets/css/landing.css b/assets/css/landing.css
new file mode 100644
index 0000000..a917007
--- /dev/null
+++ b/assets/css/landing.css
@@ -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;
+}
diff --git a/assets/css/style.css b/assets/css/style.css
new file mode 100644
index 0000000..a3e865c
--- /dev/null
+++ b/assets/css/style.css
@@ -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; }
diff --git a/assets/images/background.jpg b/assets/images/background.jpg
new file mode 100644
index 0000000..6976256
Binary files /dev/null and b/assets/images/background.jpg differ
diff --git a/assets/images/logo.png b/assets/images/logo.png
new file mode 100644
index 0000000..2f98938
Binary files /dev/null and b/assets/images/logo.png differ
diff --git a/assets/pasted-20260215-184130-92b105b7.png b/assets/pasted-20260215-184130-92b105b7.png
new file mode 100644
index 0000000..c5b2b5f
Binary files /dev/null and b/assets/pasted-20260215-184130-92b105b7.png differ
diff --git a/assets/pasted-20260215-184400-75694580.png b/assets/pasted-20260215-184400-75694580.png
new file mode 100644
index 0000000..9cc2ad8
Binary files /dev/null and b/assets/pasted-20260215-184400-75694580.png differ
diff --git a/assets/pasted-20260215-184844-2497180e.jpg b/assets/pasted-20260215-184844-2497180e.jpg
new file mode 100644
index 0000000..6976256
Binary files /dev/null and b/assets/pasted-20260215-184844-2497180e.jpg differ
diff --git a/assets/pasted-20260215-185354-bdf656b8.jpg b/assets/pasted-20260215-185354-bdf656b8.jpg
new file mode 100644
index 0000000..6976256
Binary files /dev/null and b/assets/pasted-20260215-185354-bdf656b8.jpg differ
diff --git a/assets/pasted-20260215-185608-1a733a27.jpg b/assets/pasted-20260215-185608-1a733a27.jpg
new file mode 100644
index 0000000..6976256
Binary files /dev/null and b/assets/pasted-20260215-185608-1a733a27.jpg differ
diff --git a/assets/pasted-20260215-185725-11a8f403.png b/assets/pasted-20260215-185725-11a8f403.png
new file mode 100644
index 0000000..2f98938
Binary files /dev/null and b/assets/pasted-20260215-185725-11a8f403.png differ
diff --git a/auth_helper.php b/auth_helper.php
new file mode 100644
index 0000000..4523b7e
--- /dev/null
+++ b/auth_helper.php
@@ -0,0 +1,48 @@
+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
+ ]);
+}
diff --git a/ballot.php b/ballot.php
new file mode 100644
index 0000000..297637f
--- /dev/null
+++ b/ballot.php
@@ -0,0 +1,124 @@
+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();
+?>
+
+
+
+
+ Vote: = htmlspecialchars($election['title']) ?>
+
+
+
+
+
+
+ E-Vote Pro
+
+ Logged in as = htmlspecialchars($user['name']) ?>
+
+
+
+
+
+
= htmlspecialchars($election['title']) ?>
+
Please select your candidates carefully. Your vote is immutable once cast.
+
+
+
+
+
+
diff --git a/create_election.php b/create_election.php
new file mode 100644
index 0000000..70cc12d
--- /dev/null
+++ b/create_election.php
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ Create Election | = htmlspecialchars($projectDescription) ?>
+
+
+
+
+
+
+
+ E-Vote Pro
+
+
+
+
+
+
New Election
+
Fill in the details to schedule a new election.
+
+
+
+
+
+
diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql
new file mode 100644
index 0000000..13ff25a
--- /dev/null
+++ b/db/migrations/001_initial_schema.sql
@@ -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);
diff --git a/includes/pexels.php b/includes/pexels.php
new file mode 100644
index 0000000..26d3ffe
--- /dev/null
+++ b/includes/pexels.php
@@ -0,0 +1,25 @@
+ 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;
+}
diff --git a/index.php b/index.php
index 7205f3d..77ca2cd 100644
--- a/index.php
+++ b/index.php
@@ -1,150 +1,106 @@
query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
+
+$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
?>
- New Style
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ Election Dashboard | = htmlspecialchars($projectDescription) ?>
+
-
-
+
+
-
-
-
Analyzing your requirements and generating your website…
-
- Loading…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
+
+ E-Vote Pro
+
+
= htmlspecialchars($user['name']) ?> (= $user['role'] ?>)
+
Logout
-
-
- Page updated: = htmlspecialchars($now) ?> (UTC)
+
+
+
+
+
+ Action completed successfully.
+
+
+
+
+
+
+
+
No elections found. Create your first election to get started.
+
+
Setup Election
+
+
+
+
+
+
+ Title
+ Status
+ Period
+ Actions
+
+
+
+
+
+
+ = htmlspecialchars($election['title']) ?>
+ = htmlspecialchars($election['description']) ?>
+
+
+
+ = htmlspecialchars($election['status']) ?>
+
+
+
+
+ = date('M d, H:i', strtotime($election['start_date_and_time'])) ?> -
+ = date('M d, H:i', strtotime($election['end_date_and_time'])) ?>
+
+
+
+ View
+
+ Vote
+
+
+
+
+
+
+
+
+
+
+
+ © = date('Y') ?> E-Vote Pro | High School Online Election System
diff --git a/landing.php b/landing.php
new file mode 100644
index 0000000..a005844
--- /dev/null
+++ b/landing.php
@@ -0,0 +1,170 @@
+
+
+
+
+
+
Iloilo National High School | Election System
+
+
+
+
+
+
+
+
+
+
+
+
+
Click to Vote
+
+ This portal is currently a PROTOTYPE MODEL 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.
+
+
+
+
+
+ LOGIN
+
+
+
+
+
+
+
+
+ Built with Flatlogic
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($_GET['error']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LOGIN
+
+
+ Forgot Password?
+
+
+
+
+
+
+
+
diff --git a/login.php b/login.php
new file mode 100644
index 0000000..96d2515
--- /dev/null
+++ b/login.php
@@ -0,0 +1,63 @@
+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;
+ }
+ }
+}
+?>
+
+
+
+
+
Login - Online Election System
+
+
+
+
+
+
Election Login
+
+
+
+
+
+ Student ID (XX-XXXX)
+
+
+
+ Password
+
+
+ Login
+
+
+
Don't have an account? Register
+
+
+
+
diff --git a/logout.php b/logout.php
new file mode 100644
index 0000000..85facf7
--- /dev/null
+++ b/logout.php
@@ -0,0 +1,5 @@
+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();
+?>
+
+
+
+
+
Manage Candidates | = htmlspecialchars($position['name']) ?>
+
+
+
+
+
+ E-Vote Pro
+
+
+
+
+
+
+
+
+
Add Candidate
+
+
+
+
+ Select User (Student)
+
+ -- Choose Student --
+
+ = htmlspecialchars($u['name']) ?> (= $u['student_id'] ?>)
+
+
+
+
+ Party Name
+
+
+
+ Manifesto
+
+
+ Add Candidate
+
+
+
+
+
Current Candidates
+
+
No candidates added yet.
+
+
+
+
+ Student
+ Party
+ Approved
+ Actions
+
+
+
+
+
+
+ = htmlspecialchars($c['name']) ?>
+ = $c['student_id'] ?>
+
+ = htmlspecialchars($c['party_name'] ?: 'None') ?>
+
+
+ = $c['approved'] ? 'Yes' : 'Pending' ?>
+
+
+
+
+
+
+ = $c['approved'] ? 'Revoke' : 'Approve' ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/migrate.php b/migrate.php
new file mode 100644
index 0000000..e882fd3
--- /dev/null
+++ b/migrate.php
@@ -0,0 +1,20 @@
+exec($statement);
+ }
+ }
+ echo "Migration successful!\n";
+} catch (Exception $e) {
+ echo "Migration failed: " . $e->getMessage() . "\n";
+}
diff --git a/signup.php b/signup.php
new file mode 100644
index 0000000..e692a8f
--- /dev/null
+++ b/signup.php
@@ -0,0 +1,87 @@
+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();
+ }
+ }
+ }
+}
+?>
+
+
+
+
+
Signup - Online Election System
+
+
+
+
+
+
Voter Registration
+
+
+
+
+
+ Full Name
+
+
+
+ Student ID (XX-XXXX)
+
+
+
+ Email Address
+
+
+
+ Password
+
+
+
+ Role
+
+ Voter
+ Officer
+ Adviser
+ Admin
+
+
+ Register
+
+
+
Already have an account? Login
+
+
+
+
diff --git a/view_election.php b/view_election.php
new file mode 100644
index 0000000..3c4a137
--- /dev/null
+++ b/view_election.php
@@ -0,0 +1,169 @@
+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';
+?>
+
+
+
+
+
+
= htmlspecialchars($election['title']) ?> | Manage
+
+
+
+
+
+
+
+ E-Vote Pro
+
+
+
+
+
+
+
+
= htmlspecialchars($election['title']) ?>
+
= htmlspecialchars($election['description']) ?>
+
+ Status: = $election['status'] ?> |
+ Period: = date('M d, H:i', strtotime($election['start_date_and_time'])) ?> to = date('M d, H:i', strtotime($election['end_date_and_time'])) ?>
+
+
+
+
+ Edit Info
+
+
+
+
+ Launch Election
+
+
+
+
+
+ End Election
+
+
+
+
+
+
+
+
+
+
+
+
Positions
+
+ + Add Position
+
+
+
+
+
+
+
+
No positions defined yet.
+
+
+
+
+
+ Position Name
+ Max Votes
+ Candidates
+ Actions
+
+
+
+ prepare("SELECT COUNT(*) FROM candidates WHERE position_id = ?");
+ $cStmt->execute([$pos['id']]);
+ $cCount = $cStmt->fetchColumn();
+ ?>
+
+ = htmlspecialchars($pos['name']) ?>
+ = $pos['max_votes'] ?>
+ = $cCount ?>
+
+ Candidates
+
+
+
+
+
+
+
+
+
+
+
+
Quick Stats
+ prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
+ $vStmt->execute([$id]);
+ $votesCount = $vStmt->fetchColumn();
+ ?>
+
+
Total Votes Cast
+
= $votesCount ?>
+
+
+ prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
+ $canStmt->execute([$id]);
+ $candidatesTotal = $canStmt->fetchColumn();
+ ?>
+
Total Candidates
+
= $candidatesTotal ?>
+
+
+
+
+
+
+
diff --git a/view_results.php b/view_results.php
new file mode 100644
index 0000000..14a44a2
--- /dev/null
+++ b/view_results.php
@@ -0,0 +1,108 @@
+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();
+
+?>
+
+
+
+
+
Results: = htmlspecialchars($election['title']) ?>
+
+
+
+
+
+
+ E-Vote Pro
+
+
+
+
+
+
= htmlspecialchars($election['title']) ?> - Results
+
= $election['status'] ?> Election
+
= $totalVoters ?>
+
Total Ballots Cast
+
+
+
+
+
Results will be available once the election starts.
+
+
+
+
+
+
+
+ = htmlspecialchars($pos['name']) ?>
+
+
+ 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'));
+ ?>
+
+ 0 ? round(($res['vote_count'] / $posTotal) * 100, 1) : 0;
+ ?>
+
+
+ = htmlspecialchars($res['name']) ?> (= htmlspecialchars($res['party_name'] ?: 'Ind.') ?>)
+ = $res['vote_count'] ?> votes (= $percent ?>%)
+
+
+
+
+
+
+
+
+
+
+
+