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']) ?> + + + + + + + +
+
+

+

Please select your candidates carefully. Your vote is immutable once cast.

+
+ +
+ + + +
+
+

+ Select candidate(s) +
+ + 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(); + ?> + + +

No candidates for this position.

+ +
+ + + +
+ +
+ + +
+

By clicking "Cast My Vote", I acknowledge that my selection is final.

+ +
+
+
+ + 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) ?> + + + + + + + + +
+
+

New Election

+

Fill in the details to schedule a new election.

+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + Cancel +
+
+
+
+ + 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… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

+
-