Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -1,39 +0,0 @@
|
|||||||
<?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: ../candidate_management.php?success=1");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . "/../auth_helper.php";
|
|
||||||
require_login();
|
|
||||||
require_role(["Admin", "Adviser"]);
|
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
||||||
$election_id = $_POST["election_id"] ?? "";
|
|
||||||
$student_id = $_POST["student_id"] ?? "";
|
|
||||||
$name = $_POST["name"] ?? "";
|
|
||||||
$email = $_POST["email"] ?? "";
|
|
||||||
$role = $_POST["role"] ?? "Officer";
|
|
||||||
$password = $_POST["password"] ?? "iloilohns";
|
|
||||||
|
|
||||||
if (!$election_id || !$student_id || !$name || !$email) {
|
|
||||||
die("Missing fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
// 1. Check if user already exists
|
|
||||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE student_id = ? OR email = ?");
|
|
||||||
$stmt->execute([$student_id, $email]);
|
|
||||||
$existing = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
$user_id = $existing["id"];
|
|
||||||
// Update role if changed
|
|
||||||
$upd = $pdo->prepare("UPDATE users SET role = ? WHERE id = ?");
|
|
||||||
$upd->execute([$role, $user_id]);
|
|
||||||
} else {
|
|
||||||
// 1a. Create user in Supabase
|
|
||||||
$supabaseUser = SupabaseAuth::createUser($email, $password);
|
|
||||||
$supabase_uid = null;
|
|
||||||
if ($supabaseUser['error']) {
|
|
||||||
if (str_contains(strtolower($supabaseUser['error']), 'already registered')) {
|
|
||||||
$sbUser = SupabaseAuth::getUserByEmail($email);
|
|
||||||
$supabase_uid = $sbUser['id'] ?? null;
|
|
||||||
} else {
|
|
||||||
throw new Exception("Supabase Error: " . $supabaseUser['error']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$supabase_uid = $supabaseUser['data']['id'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new user locally
|
|
||||||
$user_id = uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (id, supabase_uid, student_id, name, email, role) VALUES (?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$user_id, $supabase_uid, $student_id, $name, $email, $role]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Assign to election
|
|
||||||
$chk = $pdo->prepare("SELECT COUNT(*) FROM election_assignments WHERE election_id = ? AND user_id = ?");
|
|
||||||
$chk->execute([$election_id, $user_id]);
|
|
||||||
if ($chk->fetchColumn() == 0) {
|
|
||||||
$role_in_election = $role; // Admin, Adviser, or Officer
|
|
||||||
$ea = $pdo->prepare("INSERT INTO election_assignments (id, election_id, user_id, role_in_election, assigned_by) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
$ea->execute([uuid(), $election_id, $user_id, $role_in_election, $_SESSION['user_id']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
audit_log('assigned_officer', 'users', $user_id, null, null, "Assigned $role $name to election $election_id");
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
header("Location: ../officers_management.php?success=officer_assigned");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
|
||||||
die("Error: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../officers_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?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'] ?? '';
|
|
||||||
$description = $_POST['description'] ?? '';
|
|
||||||
|
|
||||||
if (!$election_id || !$name) {
|
|
||||||
die("Missing fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$id = uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO parties (id, election_id, name, description) VALUES (?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$id, $election_id, $name, $description]);
|
|
||||||
|
|
||||||
audit_log('Added party', 'parties', $id);
|
|
||||||
|
|
||||||
header("Location: ../candidate_management.php?success=1");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?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'] ?? '';
|
|
||||||
$type = $_POST['type'] ?? 'Uniform';
|
|
||||||
|
|
||||||
if (!$election_id || !$name) {
|
|
||||||
die("Missing fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$id = uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO positions (id, election_id, name, type, max_votes) VALUES (?, ?, ?, ?, 1)");
|
|
||||||
$stmt->execute([$id, $election_id, $name, $type]);
|
|
||||||
|
|
||||||
audit_log('Added position', 'positions', $id);
|
|
||||||
|
|
||||||
header("Location: ../candidate_management.php?success=1");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<?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"] ?? "";
|
|
||||||
$student_id = $_POST["student_id"] ?? "";
|
|
||||||
$name = $_POST["name"] ?? "";
|
|
||||||
$email = $_POST["email"] ?? "";
|
|
||||||
$password = $_POST["password"] ?? "iloilohns";
|
|
||||||
$track = $_POST["track"] ?? "";
|
|
||||||
$grade_level = $_POST["grade_level"] ?? "";
|
|
||||||
$section = $_POST["section"] ?? "";
|
|
||||||
|
|
||||||
if (!$election_id || !$student_id || !$name || !$email) {
|
|
||||||
die("Missing fields");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
// 1. Check if user already exists
|
|
||||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE student_id = ? OR email = ?");
|
|
||||||
$stmt->execute([$student_id, $email]);
|
|
||||||
$existing = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
$user_id = $existing["id"];
|
|
||||||
// Update track/grade/section if needed
|
|
||||||
$upd = $pdo->prepare("UPDATE users SET track = ?, grade_level = ?, section = ? WHERE id = ?");
|
|
||||||
$upd->execute([$track, $grade_level, $section, $user_id]);
|
|
||||||
} else {
|
|
||||||
// 1a. Create user in Supabase
|
|
||||||
$supabaseUser = SupabaseAuth::createUser($email, $password);
|
|
||||||
$supabase_uid = null;
|
|
||||||
if ($supabaseUser['error']) {
|
|
||||||
// If user already exists in Supabase, try to get their UID
|
|
||||||
if (str_contains(strtolower($supabaseUser['error']), 'already registered')) {
|
|
||||||
$sbUser = SupabaseAuth::getUserByEmail($email);
|
|
||||||
$supabase_uid = $sbUser['id'] ?? null;
|
|
||||||
} else {
|
|
||||||
throw new Exception("Supabase Error: " . $supabaseUser['error']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$supabase_uid = $supabaseUser['data']['id'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new user locally
|
|
||||||
$user_id = uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (id, supabase_uid, student_id, name, email, track, grade_level, section, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'Voter')");
|
|
||||||
$stmt->execute([$user_id, $supabase_uid, $student_id, $name, $email, $track, $grade_level, $section]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Assign to election
|
|
||||||
$chk = $pdo->prepare("SELECT COUNT(*) FROM election_assignments WHERE election_id = ? AND user_id = ?");
|
|
||||||
$chk->execute([$election_id, $user_id]);
|
|
||||||
if ($chk->fetchColumn() == 0) {
|
|
||||||
$ea = $pdo->prepare("INSERT INTO election_assignments (id, election_id, user_id, role_in_election, assigned_by) VALUES (?, ?, ?, 'Voter', ?)");
|
|
||||||
$ea->execute([uuid(), $election_id, $user_id, $_SESSION['user_id']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
audit_log("Registered voter", "users", $user_id);
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
header("Location: ../voter_management.php?success=voter_added");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
|
||||||
die("Error: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
<?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);
|
|
||||||
|
|
||||||
$redirect = $_POST['redirect'] ?? "../view_election.php?id=$id&success=1";
|
|
||||||
header("Location: $redirect");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die("Error: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../index.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['id'])) {
|
|
||||||
$candId = $_GET['id'];
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM candidates WHERE id = ?");
|
|
||||||
$stmt->execute([$candId]);
|
|
||||||
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('candidate_deleted', 'candidates', $candId, null, null, "Deleted candidate ID $candId");
|
|
||||||
|
|
||||||
header("Location: ../candidate_management.php?success=candidate_deleted");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error deleting candidate: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../candidate_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['id']) && isset($_GET['election_id'])) {
|
|
||||||
$userId = $_GET['id'];
|
|
||||||
$electionId = $_GET['election_id'];
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Remove the assignment for this election
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM election_assignments WHERE user_id = ? AND election_id = ? AND role_in_election != 'Voter'");
|
|
||||||
$stmt->execute([$userId, $electionId]);
|
|
||||||
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('officer_removed', 'users', $userId, null, null, "Removed officer ID $userId from election $electionId");
|
|
||||||
|
|
||||||
header("Location: ../officers_management.php?success=officer_deleted");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error deleting officer: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../officers_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['id']) && isset($_GET['election_id'])) {
|
|
||||||
$userId = $_GET['id'];
|
|
||||||
$electionId = $_GET['election_id'];
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Remove the assignment for this election
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM election_assignments WHERE user_id = ? AND election_id = ? AND role_in_election = 'Voter'");
|
|
||||||
$stmt->execute([$userId, $electionId]);
|
|
||||||
|
|
||||||
// Optional: Log the action
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('voter_removed', 'users', $userId, null, null, "Removed voter ID $userId from election $electionId");
|
|
||||||
|
|
||||||
header("Location: ../voter_management.php?success=voter_deleted");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error deleting voter: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../voter_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
<?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"] ?? "";
|
|
||||||
$file = $_FILES["csv_file"] ?? null;
|
|
||||||
|
|
||||||
if (!$election_id || !$file || $file["error"] !== UPLOAD_ERR_OK) {
|
|
||||||
die("Invalid submission or file upload error.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$extension = pathinfo($file["name"], PATHINFO_EXTENSION);
|
|
||||||
if (strtolower($extension) !== "csv") {
|
|
||||||
die("Please upload a valid CSV file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
$handle = fopen($file["tmp_name"], "r");
|
|
||||||
if ($handle === false) {
|
|
||||||
throw new Exception("Could not open the uploaded file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip header if it exists
|
|
||||||
$header = fgetcsv($handle);
|
|
||||||
// Basic header validation (optional, but good)
|
|
||||||
// Expected: student_id, name, email, track, grade_level
|
|
||||||
|
|
||||||
$imported = 0;
|
|
||||||
$updated = 0;
|
|
||||||
|
|
||||||
while (($data = fgetcsv($handle)) !== false) {
|
|
||||||
if (count($data) < 5) continue; // Skip malformed rows
|
|
||||||
|
|
||||||
$student_id = trim($data[0]);
|
|
||||||
$name = trim($data[1]);
|
|
||||||
$email = trim($data[2]);
|
|
||||||
$track = trim($data[3]);
|
|
||||||
$grade_level = trim($data[4]);
|
|
||||||
$section = trim($data[5] ?? "");
|
|
||||||
|
|
||||||
if (!$student_id || !$name || !$email) continue;
|
|
||||||
|
|
||||||
// 1. Check if user already exists
|
|
||||||
$stmt = $pdo->prepare("SELECT id FROM users WHERE student_id = ? OR email = ?");
|
|
||||||
$stmt->execute([$student_id, $email]);
|
|
||||||
$existing = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
$user_id = $existing["id"];
|
|
||||||
// Update track/grade/section if needed
|
|
||||||
$upd = $pdo->prepare("UPDATE users SET track = ?, grade_level = ?, section = ? WHERE id = ?");
|
|
||||||
$upd->execute([$track, $grade_level, $section, $user_id]);
|
|
||||||
$updated++;
|
|
||||||
} else {
|
|
||||||
// 1a. Create user in Supabase
|
|
||||||
$supabaseUser = SupabaseAuth::createUser($email, "iloilohns");
|
|
||||||
$supabase_uid = null;
|
|
||||||
if ($supabaseUser['error']) {
|
|
||||||
if (str_contains(strtolower($supabaseUser['error']), 'already registered')) {
|
|
||||||
$sbUser = SupabaseAuth::getUserByEmail($email);
|
|
||||||
$supabase_uid = $sbUser['id'] ?? null;
|
|
||||||
} else {
|
|
||||||
// Log error but continue with other users? Or fail?
|
|
||||||
// Let's fail for now to be safe.
|
|
||||||
throw new Exception("Supabase Error for $email: " . $supabaseUser['error']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$supabase_uid = $supabaseUser['data']['id'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new user locally
|
|
||||||
$user_id = uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (id, supabase_uid, student_id, name, email, track, grade_level, section, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'Voter')");
|
|
||||||
$stmt->execute([$user_id, $supabase_uid, $student_id, $name, $email, $track, $grade_level, $section]);
|
|
||||||
$imported++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Assign to election
|
|
||||||
$chk = $pdo->prepare("SELECT COUNT(*) FROM election_assignments WHERE election_id = ? AND user_id = ?");
|
|
||||||
$chk->execute([$election_id, $user_id]);
|
|
||||||
if ($chk->fetchColumn() == 0) {
|
|
||||||
$ea = $pdo->prepare("INSERT INTO election_assignments (id, election_id, user_id, role_in_election, assigned_by) VALUES (?, ?, ?, 'Voter', ?)");
|
|
||||||
$ea->execute([uuid(), $election_id, $user_id, $_SESSION['user_id']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($handle);
|
|
||||||
audit_log("Imported voters via CSV", "users", "multiple");
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
header("Location: ../voter_management.php?success=import_complete&imported=$imported&updated=$updated");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
|
||||||
die("Error: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . '/../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
require_role(['Admin', 'Adviser']);
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$id = $_POST['id'] ?? '';
|
|
||||||
$status = $_POST['status'] ?? '';
|
|
||||||
$end_time = $_POST['end_time'] ?? '';
|
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
die("Missing election ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$params = [];
|
|
||||||
$sql = "UPDATE elections SET ";
|
|
||||||
|
|
||||||
if ($status) {
|
|
||||||
$sql .= "status = ?, ";
|
|
||||||
$params[] = $status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($end_time) {
|
|
||||||
$sql .= "end_date_and_time = ?, ";
|
|
||||||
$params[] = str_replace('T', ' ', $end_time);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove trailing comma and space
|
|
||||||
$sql = rtrim($sql, ', ');
|
|
||||||
$sql .= " WHERE id = ?";
|
|
||||||
$params[] = $id;
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute($params);
|
|
||||||
|
|
||||||
audit_log("Election Managed: Status=$status, EndTime=$end_time", 'elections', $id);
|
|
||||||
|
|
||||||
header("Location: ../dashboard.php?success=Election updated successfully");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
require_once __DIR__ . "/../auth_helper.php";
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER["REQUEST_METHOD"] === "POST") {
|
|
||||||
$election_id = $_POST["election_id"] ?? "";
|
|
||||||
$raw_votes = $_POST["votes"] ?? []; // Array of position_id => candidate_id (string or array)
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
if (!$election_id) {
|
|
||||||
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]);
|
|
||||||
$election = $eStmt->fetch();
|
|
||||||
if (!$election || $election["status"] !== "Ongoing") {
|
|
||||||
throw new Exception("Election is not currently 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. Prepare statement for inserting votes
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, ip_address, user_agent) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
|
|
||||||
// 4. Validate each position's votes
|
|
||||||
foreach ($raw_votes as $position_id => $candidate_id) {
|
|
||||||
if (empty($candidate_id)) continue;
|
|
||||||
|
|
||||||
// Fetch position details
|
|
||||||
$pStmt = $pdo->prepare("SELECT * FROM positions WHERE id = ? AND election_id = ?");
|
|
||||||
$pStmt->execute([$position_id, $election_id]);
|
|
||||||
$pos = $pStmt->fetch();
|
|
||||||
|
|
||||||
if (!$pos) {
|
|
||||||
throw new Exception("Invalid position detected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate candidate
|
|
||||||
$cStmt = $pdo->prepare("SELECT c.*, u.track FROM candidates c JOIN users u ON c.user_id = u.id WHERE c.id = ? AND c.position_id = ? AND c.approved = TRUE");
|
|
||||||
$cStmt->execute([$candidate_id, $position_id]);
|
|
||||||
$cand = $cStmt->fetch();
|
|
||||||
|
|
||||||
if (!$cand) {
|
|
||||||
throw new Exception("Invalid or unapproved candidate selected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Track Specific logic
|
|
||||||
if ($pos["type"] === "Track Specific" && $cand["track"] !== $user["track"]) {
|
|
||||||
throw new Exception("Candidate track mismatch for position: " . $pos["name"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert vote
|
|
||||||
$stmt->execute([
|
|
||||||
uuid(),
|
|
||||||
$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: ../index.php?success=voted");
|
|
||||||
exit;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if (isset($pdo) && $pdo->inTransaction()) $pdo->rollBack();
|
|
||||||
die("Error casting vote: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
<?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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
|
|
||||||
$candId = $_POST['id'];
|
|
||||||
$positionId = $_POST['position_id'];
|
|
||||||
$partyName = $_POST['party_name'];
|
|
||||||
$manifesto = $_POST['manifesto'];
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("UPDATE candidates SET position_id = ?, party_name = ?, manifesto = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$positionId, $partyName, $manifesto, $candId]);
|
|
||||||
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('candidate_updated', 'candidates', $candId, null, null, "Updated candidate ID $candId");
|
|
||||||
|
|
||||||
header("Location: ../candidate_management.php?success=candidate_updated");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error updating candidate: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../candidate_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
require_role(['Admin', 'Adviser', 'Officer']);
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
|
|
||||||
$id = $_POST['id'];
|
|
||||||
$title = $_POST['title'];
|
|
||||||
$description = $_POST['description'];
|
|
||||||
$startDate = $_POST['start_date'];
|
|
||||||
$endDate = $_POST['end_date'];
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("UPDATE elections SET title = ?, description = ?, start_date_and_time = ?, end_date_and_time = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$title, $description, $startDate, $endDate, $id]);
|
|
||||||
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('election_updated', 'elections', $id, null, null, "Updated election $id");
|
|
||||||
|
|
||||||
header("Location: ../view_election.php?id=$id&success=1");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error updating election: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../index.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?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);
|
|
||||||
|
|
||||||
$redirect = $_POST['redirect'] ?? "../view_election.php?id=$id&success=1";
|
|
||||||
header("Location: $redirect");
|
|
||||||
exit;
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
die($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
|
|
||||||
$userId = $_POST['id'];
|
|
||||||
$name = $_POST['name'];
|
|
||||||
$email = $_POST['email'];
|
|
||||||
$role = $_POST['role'];
|
|
||||||
$password = $_POST['password'] ?? '';
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT email, supabase_uid FROM users WHERE id = ?");
|
|
||||||
$stmt->execute([$userId]);
|
|
||||||
$userRecord = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!empty($password)) {
|
|
||||||
// Update Supabase password
|
|
||||||
if ($userRecord && $userRecord['supabase_uid']) {
|
|
||||||
SupabaseAuth::updateUserPassword($userRecord['supabase_uid'], $password);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("UPDATE users SET name = ?, email = ?, role = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$name, $email, $role, $userId]);
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("UPDATE users SET name = ?, email = ?, role = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$name, $email, $role, $userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('officer_updated', 'users', $userId, null, null, "Updated officer ID $userId info");
|
|
||||||
|
|
||||||
header("Location: ../officers_management.php?success=officer_updated");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error updating officer: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../officers_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$theme = $input['theme'] ?? 'light';
|
|
||||||
|
|
||||||
if (!in_array($theme, ['light', 'dark'])) {
|
|
||||||
echo json_encode(['success' => false, 'error' => 'Invalid theme']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = db()->prepare("UPDATE users SET theme = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$theme, $_SESSION['user_id']]);
|
|
||||||
echo json_encode(['success' => true]);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['id'])) {
|
|
||||||
$userId = $_POST['id'];
|
|
||||||
$name = $_POST['name'];
|
|
||||||
$studentId = $_POST['student_id'];
|
|
||||||
$email = $_POST['email'];
|
|
||||||
$track = $_POST['track'];
|
|
||||||
$gradeLevel = $_POST['grade_level'];
|
|
||||||
$section = $_POST['section'] ?? '';
|
|
||||||
$password = $_POST['password'] ?? '';
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT email, supabase_uid FROM users WHERE id = ?");
|
|
||||||
$stmt->execute([$userId]);
|
|
||||||
$userRecord = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!empty($password)) {
|
|
||||||
// Update Supabase password
|
|
||||||
if ($userRecord && $userRecord['supabase_uid']) {
|
|
||||||
SupabaseAuth::updateUserPassword($userRecord['supabase_uid'], $password);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare("UPDATE users SET name = ?, student_id = ?, email = ?, track = ?, grade_level = ?, section = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$name, $studentId, $email, $track, $gradeLevel, $section, $userId]);
|
|
||||||
} else {
|
|
||||||
$stmt = $pdo->prepare("UPDATE users SET name = ?, student_id = ?, email = ?, track = ?, grade_level = ?, section = ? WHERE id = ?");
|
|
||||||
$stmt->execute([$name, $studentId, $email, $track, $gradeLevel, $section, $userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the action
|
|
||||||
$currentUser = get_user();
|
|
||||||
audit_log('voter_updated', 'users', $userId, null, null, "Updated voter ID $userId info");
|
|
||||||
|
|
||||||
header("Location: ../voter_management.php?success=voter_updated");
|
|
||||||
exit;
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
die("Error updating voter: " . $e->getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
header("Location: ../voter_management.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-in {
|
|
||||||
animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Delay for children */
|
|
||||||
.animate-stagger > * {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-stagger > *:nth-child(1) { animation: fadeIn 0.5s ease-out 0.1s forwards; }
|
|
||||||
.animate-stagger > *:nth-child(2) { animation: fadeIn 0.5s ease-out 0.2s forwards; }
|
|
||||||
.animate-stagger > *:nth-child(3) { animation: fadeIn 0.5s ease-out 0.3s forwards; }
|
|
||||||
.animate-stagger > *:nth-child(4) { animation: fadeIn 0.5s ease-out 0.4s forwards; }
|
|
||||||
.animate-stagger > *:nth-child(5) { animation: fadeIn 0.5s ease-out 0.5s forwards; }
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
.header-icon-container {
|
|
||||||
background: #eef2ff;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Candidate Stats Grid */
|
|
||||||
.candidate-stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-stat-card {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-stat-label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-stat-value {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Distribution Grid */
|
|
||||||
.distribution-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-card {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-header {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2563eb;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-count {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter Bar */
|
|
||||||
.filter-bar {
|
|
||||||
padding: 24px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: flex-end;
|
|
||||||
background: var(--surface-color);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #64748b;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper i {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px 10px 36px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group select {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
background: var(--surface-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Candidates Table */
|
|
||||||
.candidates-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidates-table th {
|
|
||||||
padding: 12px 24px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
background: var(--table-header-bg);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidates-table td {
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-avatar {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-sub {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-badge {
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #94a3b8;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button:hover {
|
|
||||||
color: #4f46e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button i {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ongoing {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ongoing::before {
|
|
||||||
content: '';
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
background: #16a34a;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
@ -1,556 +0,0 @@
|
|||||||
@import 'animations.css';
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--sidebar-width: 260px;
|
|
||||||
--sidebar-bg: #ffffff;
|
|
||||||
--sidebar-active-bg: #eef2ff;
|
|
||||||
--sidebar-active-text: #4f46e5;
|
|
||||||
--sidebar-text: #4b5563;
|
|
||||||
--top-header-height: 64px;
|
|
||||||
--accent-blue: #4f46e5;
|
|
||||||
--bg-light: #f9fafb;
|
|
||||||
--border-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dashboard-body {
|
|
||||||
background-color: var(--bg-light);
|
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar Styles */
|
|
||||||
.sidebar {
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
background: var(--sidebar-bg);
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: fixed;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-brand {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-subtitle {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
color: var(--sidebar-text);
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:hover {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item.active {
|
|
||||||
background: var(--sidebar-active-bg);
|
|
||||||
color: var(--sidebar-active-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item i {
|
|
||||||
margin-right: 12px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-lucide] {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-footer {
|
|
||||||
padding: 12px;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content Area */
|
|
||||||
.main-wrapper {
|
|
||||||
margin-left: var(--sidebar-width);
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-header {
|
|
||||||
height: var(--top-header-height);
|
|
||||||
background: #ffffff;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
padding: 0 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 90;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
background: #f3f4f6;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar input {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
margin-left: 8px;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-profile {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-role {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-avatar {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
background: #e0e7ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dashboard Content */
|
|
||||||
.dashboard-content {
|
|
||||||
padding: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-msg {
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats Grid */
|
|
||||||
.stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.025em;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-footer.voters { color: #10b981; }
|
|
||||||
.stat-footer.candidates { color: #3b82f6; }
|
|
||||||
.stat-footer.votes { color: #10b981; }
|
|
||||||
|
|
||||||
.stat-footer i { margin-right: 6px; }
|
|
||||||
|
|
||||||
/* Analytics Charts */
|
|
||||||
.analytics-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.analytics-card {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.analytics-card .card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.analytics-card .card-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-filter {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #4b5563;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
position: relative;
|
|
||||||
height: 240px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table Section */
|
|
||||||
.content-section {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
padding: 20px 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-new-election {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-new-election:hover {
|
|
||||||
background: #1d4ed8;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-new-election:active {
|
|
||||||
transform: translateY(0) scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-table th {
|
|
||||||
background: #f9fafb;
|
|
||||||
padding: 12px 24px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
text-transform: uppercase;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-table td {
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-ongoing {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-preparing {
|
|
||||||
background: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-finished {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-status {
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
background: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
filter: brightness(110%);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-update:active {
|
|
||||||
transform: translateY(0) scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.flatlogic-badge {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 16px;
|
|
||||||
right: 16px;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark Theme Variables */
|
|
||||||
body.dark-theme {
|
|
||||||
--bg-light: #0f172a;
|
|
||||||
--sidebar-bg: #1e293b;
|
|
||||||
--sidebar-text: #94a3b8;
|
|
||||||
--border-color: #334155;
|
|
||||||
--sidebar-active-bg: #334155;
|
|
||||||
--sidebar-active-text: #6366f1;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .top-header {
|
|
||||||
background: #1e293b;
|
|
||||||
border-color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .stat-card,
|
|
||||||
body.dark-theme .analytics-card,
|
|
||||||
body.dark-theme .content-section,
|
|
||||||
body.dark-theme .settings-card,
|
|
||||||
body.dark-theme .modal-content {
|
|
||||||
background: #1e293b;
|
|
||||||
border-color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .sidebar-brand,
|
|
||||||
body.dark-theme .user-name,
|
|
||||||
body.dark-theme .stat-value,
|
|
||||||
body.dark-theme .card-title,
|
|
||||||
body.dark-theme .section-title,
|
|
||||||
body.dark-theme .settings-info h3,
|
|
||||||
body.dark-theme .modal-title,
|
|
||||||
body.dark-theme h1 {
|
|
||||||
color: #f1f5f9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .welcome-msg,
|
|
||||||
body.dark-theme .user-role,
|
|
||||||
body.dark-theme .stat-label,
|
|
||||||
body.dark-theme .settings-info p {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .search-bar {
|
|
||||||
background: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .search-bar input {
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .election-table th {
|
|
||||||
background: #334155;
|
|
||||||
color: #94a3b8;
|
|
||||||
border-color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .election-table td {
|
|
||||||
border-color: #334155;
|
|
||||||
color: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .nav-item:hover {
|
|
||||||
background: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .flatlogic-badge {
|
|
||||||
background: #1e293b;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global Dark Theme Overrides */
|
|
||||||
body.dark-theme .audit-table,
|
|
||||||
body.dark-theme .audit-table th,
|
|
||||||
body.dark-theme .table-container,
|
|
||||||
body.dark-theme .content-section,
|
|
||||||
body.dark-theme .settings-card {
|
|
||||||
background: #1e293b !important;
|
|
||||||
border-color: #334155 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .audit-table td,
|
|
||||||
body.dark-theme .election-table td {
|
|
||||||
color: #cbd5e1 !important;
|
|
||||||
border-color: #334155 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .audit-table th,
|
|
||||||
body.dark-theme .election-table th {
|
|
||||||
background: #334155 !important;
|
|
||||||
color: #94a3b8 !important;
|
|
||||||
border-color: #475569 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .action-badge {
|
|
||||||
background: #334155;
|
|
||||||
color: #94a3b8;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme input,
|
|
||||||
body.dark-theme select,
|
|
||||||
body.dark-theme textarea {
|
|
||||||
background-color: #334155 !important;
|
|
||||||
border-color: #475569 !important;
|
|
||||||
color: #f1f5f9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .sidebar-brand,
|
|
||||||
body.dark-theme .user-name,
|
|
||||||
body.dark-theme .stat-value,
|
|
||||||
body.dark-theme .card-title,
|
|
||||||
body.dark-theme .section-title,
|
|
||||||
body.dark-theme .settings-info h3,
|
|
||||||
body.dark-theme .modal-title,
|
|
||||||
body.dark-theme h1,
|
|
||||||
body.dark-theme h2,
|
|
||||||
body.dark-theme h3 {
|
|
||||||
color: #f1f5f9 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .nav-item {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .nav-item:hover {
|
|
||||||
background: #334155;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .nav-item.active {
|
|
||||||
background: #334155;
|
|
||||||
color: #6366f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,364 +0,0 @@
|
|||||||
: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;
|
|
||||||
}
|
|
||||||
@ -1,196 +0,0 @@
|
|||||||
/* Officer Management Specific Styles */
|
|
||||||
|
|
||||||
.officer-management-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-category-card {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-header {
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-size: 0.9375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active-count {
|
|
||||||
background: #f0fdf4;
|
|
||||||
color: #16a34a;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 99px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-list {
|
|
||||||
padding: 8px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-item:hover {
|
|
||||||
background-color: var(--surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-main-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #e0e7ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-name {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-meta {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-actions button {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: #94a3b8;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.officer-actions button:hover {
|
|
||||||
color: #4f46e5;
|
|
||||||
background: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Registration Form Section */
|
|
||||||
.registration-section {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-main);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input, .form-group select {
|
|
||||||
padding: 10px 14px;
|
|
||||||
background: var(--bg-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus, .form-group select:focus {
|
|
||||||
border-color: #4f46e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save-officer {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
height: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-save-officer:hover {
|
|
||||||
background: #1d4ed8;
|
|
||||||
}
|
|
||||||
@ -1,119 +0,0 @@
|
|||||||
/* Reports & Audit Styles */
|
|
||||||
|
|
||||||
.audit-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table th {
|
|
||||||
padding: 12px 24px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #64748b;
|
|
||||||
background: #f9fafb;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-table td {
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #1e293b;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-timestamp {
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-user-id {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-badge {
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: #dbeafe;
|
|
||||||
color: #2563eb;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-action {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f172a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-details {
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header section */
|
|
||||||
.audit-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-title h1 {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #1e293b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-subtitle {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #64748b;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Container for the table to add shadow and border radius */
|
|
||||||
.table-container {
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #f1f5f9;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-search-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-search-wrapper i {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-search-wrapper input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px 10px 40px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
background: #f8fafc;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-search-wrapper input:focus {
|
|
||||||
background: #ffffff;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
@ -1,204 +0,0 @@
|
|||||||
:root {
|
|
||||||
--primary-color: #1e293b;
|
|
||||||
--accent-color: #2563eb;
|
|
||||||
--bg-color: #f8fafc;
|
|
||||||
--surface-color: #ffffff;
|
|
||||||
--surface-hover: #f8fafc;
|
|
||||||
--border-color: #e2e8f0;
|
|
||||||
--text-main: #1e293b;
|
|
||||||
--text-muted: #64748b;
|
|
||||||
--table-header-bg: #f9fafb;
|
|
||||||
--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; }
|
|
||||||
|
|
||||||
/* Dark Mode */
|
|
||||||
body.dark-theme {
|
|
||||||
--bg-color: #0f172a;
|
|
||||||
--surface-color: #1e293b;
|
|
||||||
--surface-hover: #334155;
|
|
||||||
--border-color: #334155;
|
|
||||||
--text-main: #f1f5f9;
|
|
||||||
--text-muted: #94a3b8;
|
|
||||||
--table-header-bg: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .navbar {
|
|
||||||
background: #1e293b;
|
|
||||||
border-color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .brand {
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .btn-outline {
|
|
||||||
border-color: #475569;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .btn-outline:hover {
|
|
||||||
background: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .table th {
|
|
||||||
border-color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .table td {
|
|
||||||
border-color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-theme .form-control {
|
|
||||||
background-color: #334155;
|
|
||||||
border-color: #475569;
|
|
||||||
color: #f1f5f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,347 +0,0 @@
|
|||||||
.header-icon-container {
|
|
||||||
background: #eef2ff;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Voter Stats Grid */
|
|
||||||
.voter-stats-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-stat-card {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-stat-label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 16px;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-stat-value {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Distribution Grid */
|
|
||||||
.distribution-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-card {
|
|
||||||
background: var(--surface-color);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-header {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2563eb;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distribution-count {
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action Buttons */
|
|
||||||
.btn-action {
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add {
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-add:hover { background: #1d4ed8; }
|
|
||||||
|
|
||||||
.btn-import {
|
|
||||||
background: #4f46e5;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-import:hover { background: #4338ca; }
|
|
||||||
|
|
||||||
/* Filter Bar */
|
|
||||||
.filter-bar {
|
|
||||||
padding: 24px;
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: flex-end;
|
|
||||||
background: var(--surface-color);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #64748b;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper i {
|
|
||||||
position: absolute;
|
|
||||||
left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input-wrapper input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px 10px 36px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-group select {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
background: var(--surface-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Voters Table */
|
|
||||||
.voters-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voters-table th {
|
|
||||||
padding: 12px 24px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
background: var(--table-header-bg);
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voters-table td {
|
|
||||||
padding: 16px 24px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.voted {
|
|
||||||
background: #dcfce7;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-indicator.pending {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #475569;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #94a3b8;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button:hover {
|
|
||||||
color: #4f46e5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-cell button i {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modals */
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 2000;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(15, 23, 42, 0.5);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: var(--surface-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 600px;
|
|
||||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--text-main);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: #1e293b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #94a3b8;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input, .form-group select {
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: var(--bg-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-cancel {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #475569;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-area {
|
|
||||||
border: 2px dashed #e2e8f0;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 40px;
|
|
||||||
text-align: center;
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.import-area p {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #64748b;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 101 KiB |
@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
session_start();
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
require_once __DIR__ . '/includes/SupabaseAuth.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, $details = null) {
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$stmt = db()->prepare("INSERT INTO audit_logs (id, user_id, action, details, table_name, record_id, old_values, new_values, election_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([
|
|
||||||
uuid(),
|
|
||||||
$_SESSION['user_id'] ?? null,
|
|
||||||
$action,
|
|
||||||
$details,
|
|
||||||
$table,
|
|
||||||
$record_id,
|
|
||||||
$old ? json_encode($old) : null,
|
|
||||||
$new ? json_encode($new) : null,
|
|
||||||
$electionId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_active_election_id() {
|
|
||||||
if (isset($_GET['set_election_id'])) {
|
|
||||||
$_SESSION['active_election_id'] = $_GET['set_election_id'];
|
|
||||||
// Redirect to same page without the query param to keep URL clean
|
|
||||||
$url = strtok($_SERVER["REQUEST_URI"], '?');
|
|
||||||
header("Location: " . $url);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_SESSION['active_election_id'])) {
|
|
||||||
$election = db()->query("SELECT id FROM elections WHERE archived = FALSE ORDER BY created_at DESC LIMIT 1")->fetch();
|
|
||||||
$_SESSION['active_election_id'] = $election['id'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $_SESSION['active_election_id'];
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_active_election() {
|
|
||||||
$id = get_active_election_id();
|
|
||||||
if (!$id) return null;
|
|
||||||
$stmt = db()->prepare("SELECT * FROM elections WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
return $stmt->fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_all_elections() {
|
|
||||||
return db()->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
|
|
||||||
}
|
|
||||||
605
ballot.php
@ -1,605 +0,0 @@
|
|||||||
<?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: index.php?error=already_voted");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$positions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order ASC");
|
|
||||||
$positions->execute([$id]);
|
|
||||||
$positions = $positions->fetchAll();
|
|
||||||
|
|
||||||
$endTime = strtotime($election['end_date_and_time']) * 1000;
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Ballot: <?= htmlspecialchars($election['title']) ?></title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary: #4f46e5;
|
|
||||||
--primary-hover: #4338ca;
|
|
||||||
--primary-light: #eef2ff;
|
|
||||||
--bg: #f8fafc;
|
|
||||||
--text: #1e293b;
|
|
||||||
--text-muted: #64748b;
|
|
||||||
--border: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
background: white;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand {
|
|
||||||
font-weight: 800;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--primary);
|
|
||||||
text-decoration: none;
|
|
||||||
letter-spacing: -0.025em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ballot-container {
|
|
||||||
max-width: 900px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px 20px 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ballot-title-area {
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ballot-title-area h1 {
|
|
||||||
font-size: 1.875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin: 0 0 8px 0;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ballot-title-area p {
|
|
||||||
color: #64748b;
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-info-card {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-info-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #475569;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voter-info-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #94a3b8;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item span {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timer-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: #ef4444;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
background: #fef2f2;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #fee2e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-group {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 32px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.position-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidates-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-label {
|
|
||||||
cursor: pointer;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-card {
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-card:hover {
|
|
||||||
border-color: var(--primary);
|
|
||||||
background: #fcfdff;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"]:checked + .candidate-card {
|
|
||||||
border-color: var(--primary);
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 0 0 1px var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-circle {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
border: 2px solid #cbd5e1;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"]:checked + .candidate-card .radio-circle {
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"]:checked + .candidate-card .radio-circle::after {
|
|
||||||
content: '';
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
background: var(--primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
background: #f1f5f9;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-avatar img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-info h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.candidate-info p {
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-bar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: white;
|
|
||||||
padding: 16px 40px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 100;
|
|
||||||
box-shadow: 0 -10px 15px -3px rgba(0,0,0,0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-container {
|
|
||||||
max-width: 900px;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-text p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: var(--text-muted);
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-text h4 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 14px 40px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-submit:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal Styles */
|
|
||||||
.modal-overlay {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(15, 23, 42, 0.6);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
z-index: 2000;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-overlay.active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-modal {
|
|
||||||
background: white;
|
|
||||||
border-radius: 20px;
|
|
||||||
max-width: 450px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 32px;
|
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
||||||
animation: modalIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes modalIn {
|
|
||||||
from { transform: scale(0.9) translateY(20px); opacity: 0; }
|
|
||||||
to { transform: scale(1) translateY(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-icon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
background: #eef2ff;
|
|
||||||
color: #4f46e5;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-modal h2 {
|
|
||||||
margin: 0 0 12px 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-modal p {
|
|
||||||
margin: 0 0 32px 0;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-modal {
|
|
||||||
padding: 12px 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-modal-cancel {
|
|
||||||
background: #f1f5f9;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-modal-cancel:hover {
|
|
||||||
background: #e2e8f0;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-modal-confirm {
|
|
||||||
background: #4f46e5;
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-modal-confirm:hover {
|
|
||||||
background: #4338ca;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="radio"] { display: none; }
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.voter-info-grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="<?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
<nav class="navbar">
|
|
||||||
<a href="index.php" class="brand">Click to Vote</a>
|
|
||||||
<div>
|
|
||||||
<span style="margin-right: 1rem; color: var(--text-muted); font-size: 0.875rem; font-weight: 600;"><?= htmlspecialchars($user['name']) ?></span>
|
|
||||||
<a href="logout.php" style="color: #ef4444; font-size: 0.875rem; font-weight: 700; text-decoration: none;">Logout</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="ballot-container">
|
|
||||||
<div class="ballot-title-area">
|
|
||||||
<h1><?= htmlspecialchars($election['title']) ?></h1>
|
|
||||||
<p>Review the candidates carefully and cast your secure vote below.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="voter-info-card">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
|
|
||||||
<div class="voter-info-title">Voter Information</div>
|
|
||||||
<div class="timer-container">
|
|
||||||
<i data-lucide="clock" style="width: 16px;"></i>
|
|
||||||
<span id="countdown">00:00:00</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="voter-info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<label>Email</label>
|
|
||||||
<span><?= htmlspecialchars($user['email']) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>Grade Level</label>
|
|
||||||
<span>Grade <?= htmlspecialchars((string)($user['grade_level'] ?? 'N/A')) ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>Track/Cluster</label>
|
|
||||||
<span><?= htmlspecialchars($user['track'] ?? 'N/A') ?></span>
|
|
||||||
</div>
|
|
||||||
<div class="info-item">
|
|
||||||
<label>Section</label>
|
|
||||||
<span><?= htmlspecialchars($user['section'] ?? 'N/A') ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="ballotForm" action="api/submit_vote.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $id ?>">
|
|
||||||
|
|
||||||
<?php foreach ($positions as $index => $pos): ?>
|
|
||||||
<div class="position-group">
|
|
||||||
<div class="position-title">
|
|
||||||
<?= htmlspecialchars($pos['name']) ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
$sql = "SELECT c.*, u.name, u.track FROM candidates c JOIN users u ON c.user_id = u.id WHERE c.position_id = ? AND c.approved = TRUE";
|
|
||||||
$params = [$pos['id']];
|
|
||||||
|
|
||||||
if ($pos['type'] === 'Track Specific') {
|
|
||||||
$sql .= " AND u.track = ?";
|
|
||||||
$params[] = $user['track'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$cStmt = $pdo->prepare($sql);
|
|
||||||
$cStmt->execute($params);
|
|
||||||
$candidates = $cStmt->fetchAll();
|
|
||||||
?>
|
|
||||||
|
|
||||||
<?php if (empty($candidates)): ?>
|
|
||||||
<div style="padding: 24px; background: #f8fafc; border-radius: 12px; text-align: center; border: 1px dashed #cbd5e1;">
|
|
||||||
<p style="margin: 0; color: #64748b; font-size: 0.875rem;">No candidates available for your track.</p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
<div class="candidates-list">
|
|
||||||
<?php foreach ($candidates as $cand): ?>
|
|
||||||
<label class="candidate-label">
|
|
||||||
<input type="radio" name="votes[<?= $pos['id'] ?>]" value="<?= $cand['id'] ?>" required>
|
|
||||||
<div class="candidate-card">
|
|
||||||
<div class="radio-circle"></div>
|
|
||||||
<div class="candidate-avatar">
|
|
||||||
<?= substr($cand['name'], 0, 1) ?>
|
|
||||||
</div>
|
|
||||||
<div class="candidate-info">
|
|
||||||
<h3><?= htmlspecialchars($cand['name']) ?></h3>
|
|
||||||
<p><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<div class="submit-bar">
|
|
||||||
<div class="submit-container">
|
|
||||||
<div class="submit-text">
|
|
||||||
<p>Ready to submit?</p>
|
|
||||||
<h4>Review your selections</h4>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn-submit" onclick="openConfirmModal()">
|
|
||||||
Cast My Vote
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Confirmation Modal -->
|
|
||||||
<div id="confirmModal" class="modal-overlay">
|
|
||||||
<div class="confirm-modal">
|
|
||||||
<div class="modal-icon">
|
|
||||||
<i data-lucide="shield-check" style="width: 32px; height: 32px;"></i>
|
|
||||||
</div>
|
|
||||||
<h2>Cast your vote?</h2>
|
|
||||||
<p>You are about to submit your choices. This action is permanent and cannot be undone. Are you sure you want to proceed?</p>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button type="button" class="btn-modal btn-modal-cancel" onclick="closeConfirmModal()">Review Choices</button>
|
|
||||||
<button type="button" class="btn-modal btn-modal-confirm" onclick="submitBallot()">Yes, Cast My Vote</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer style="text-align: center; color: var(--text-muted); padding: 4rem 2rem; background: white; border-top: 1px solid var(--border);">
|
|
||||||
© <?= date('Y') ?> Click to Vote | High School Online Election System
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
function openConfirmModal() {
|
|
||||||
// Basic validation: check if all required radios are checked
|
|
||||||
const form = document.getElementById('ballotForm');
|
|
||||||
if (!form.checkValidity()) {
|
|
||||||
form.reportValidity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById('confirmModal').classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeConfirmModal() {
|
|
||||||
document.getElementById('confirmModal').classList.remove('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitBallot() {
|
|
||||||
document.getElementById('ballotForm').submit();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Countdown Timer
|
|
||||||
const endTime = <?= $endTime ?>;
|
|
||||||
|
|
||||||
function updateCountdown() {
|
|
||||||
const now = new Date().getTime();
|
|
||||||
const distance = endTime - now;
|
|
||||||
|
|
||||||
if (distance < 0) {
|
|
||||||
document.getElementById("countdown").innerHTML = "EXPIRED";
|
|
||||||
document.getElementById("ballotForm").style.opacity = "0.5";
|
|
||||||
document.getElementById("ballotForm").style.pointerEvents = "none";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
|
||||||
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
|
||||||
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
|
||||||
|
|
||||||
document.getElementById("countdown").innerHTML =
|
|
||||||
(hours < 10 ? "0" : "") + hours + ":" +
|
|
||||||
(minutes < 10 ? "0" : "") + minutes + ":" +
|
|
||||||
(seconds < 10 ? "0" : "") + seconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(updateCountdown, 1000);
|
|
||||||
updateCountdown();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,520 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// Global Election Context
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$election = get_active_election();
|
|
||||||
|
|
||||||
if (!$electionId) {
|
|
||||||
die("No active election selected. Please create an election first.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistics (Filtered by Election)
|
|
||||||
$totalCandidates = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
|
|
||||||
$totalCandidates->execute([$electionId]);
|
|
||||||
$totalCandidates = $totalCandidates->fetchColumn();
|
|
||||||
|
|
||||||
$uniquePositions = $pdo->prepare("SELECT COUNT(*) FROM positions WHERE election_id = ?");
|
|
||||||
$uniquePositions->execute([$electionId]);
|
|
||||||
$uniquePositions = $uniquePositions->fetchColumn();
|
|
||||||
|
|
||||||
$activeParties = $pdo->prepare("SELECT COUNT(*) FROM parties WHERE election_id = ?");
|
|
||||||
$activeParties->execute([$electionId]);
|
|
||||||
$activeParties = $activeParties->fetchColumn();
|
|
||||||
|
|
||||||
// Candidates by Position
|
|
||||||
$posStats = $pdo->prepare("SELECT p.name, COUNT(c.id) as count
|
|
||||||
FROM positions p LEFT JOIN candidates c ON p.id = c.position_id
|
|
||||||
WHERE p.election_id = ? GROUP BY p.id ORDER BY p.sort_order");
|
|
||||||
$posStats->execute([$electionId]);
|
|
||||||
$posStats = $posStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Candidates by Party
|
|
||||||
$partyStats = $pdo->prepare("SELECT p.name as party_name, COUNT(c.id) as count
|
|
||||||
FROM parties p LEFT JOIN candidates c ON p.name = c.party_name AND c.election_id = p.election_id
|
|
||||||
WHERE p.election_id = ? GROUP BY p.id ORDER BY count DESC");
|
|
||||||
$partyStats->execute([$electionId]);
|
|
||||||
$partyStats = $partyStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
$search = $_GET['search'] ?? '';
|
|
||||||
$filterPosition = $_GET['position'] ?? 'All Positions';
|
|
||||||
$filterParty = $_GET['party'] ?? 'All Parties';
|
|
||||||
|
|
||||||
// Main Query
|
|
||||||
$query = "SELECT c.*, u.name as user_name, u.email as user_email, u.student_id, u.grade_level, u.track, p.name as position_name
|
|
||||||
FROM candidates c
|
|
||||||
JOIN users u ON c.user_id = u.id
|
|
||||||
JOIN positions p ON c.position_id = p.id
|
|
||||||
WHERE c.election_id = ?";
|
|
||||||
|
|
||||||
$params = [$electionId];
|
|
||||||
|
|
||||||
if ($search) {
|
|
||||||
$query .= " AND (u.name LIKE ? OR u.email LIKE ? OR c.party_name LIKE ?)";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filterPosition !== 'All Positions') {
|
|
||||||
$query .= " AND p.name = ?";
|
|
||||||
$params[] = $filterPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filterParty !== 'All Parties') {
|
|
||||||
$query .= " AND c.party_name = ?";
|
|
||||||
$params[] = $filterParty;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query .= " ORDER BY p.sort_order, u.name";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($query);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$candidates = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Options for Modals/Filters
|
|
||||||
$allPositions = $pdo->prepare("SELECT * FROM positions WHERE election_id = ? ORDER BY sort_order");
|
|
||||||
$allPositions->execute([$electionId]);
|
|
||||||
$allPositions = $allPositions->fetchAll();
|
|
||||||
|
|
||||||
$allParties = $pdo->prepare("SELECT * FROM parties WHERE election_id = ? ORDER BY name");
|
|
||||||
$allParties->execute([$electionId]);
|
|
||||||
$allParties = $allParties->fetchAll();
|
|
||||||
|
|
||||||
$allVoters = $pdo->query("SELECT id, name, student_id FROM users WHERE role = 'Voter' ORDER BY name")->fetchAll();
|
|
||||||
|
|
||||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Online Election System for Senior High School';
|
|
||||||
?>
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<title>Candidate Management | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<link rel="stylesheet" href="assets/css/candidate_management.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<style>
|
|
||||||
.management-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.btn-manage {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: var(--surface-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
.btn-manage:hover {
|
|
||||||
background: var(--surface-hover);
|
|
||||||
border-color: var(--border-color);
|
|
||||||
}
|
|
||||||
.btn-manage.primary {
|
|
||||||
background: var(--accent-color);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.btn-manage.primary:hover {
|
|
||||||
background: #4338ca;
|
|
||||||
}
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; width: 100%; height: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.modal-content {
|
|
||||||
background: var(--surface-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.modal-header h2 { margin: 0; font-size: 1.25rem; color: var(--text-main); }
|
|
||||||
.form-group { margin-bottom: 16px; }
|
|
||||||
.form-group label { display: block; font-size: 12px; font-weight: 600; color: var(--text-muted); margin-bottom: 6px; }
|
|
||||||
.form-group input, .form-group select, .form-group textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background: var(--bg-color);
|
|
||||||
color: var(--text-main);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="dashboard-header" style="display: flex; justify-content: space-between; align-items: flex-start;">
|
|
||||||
<div style="display: flex; align-items: center; gap: 16px;">
|
|
||||||
<div class="header-icon-container">
|
|
||||||
<i data-lucide="user-square-2" style="width: 24px; color: #4f46e5;"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0; font-size: 1.5rem; color: var(--text-main);">Candidate Management</h1>
|
|
||||||
<p style="margin: 4px 0 0 0; color: var(--text-muted); font-size: 0.875rem;">Managing <?= htmlspecialchars($election['title']) ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="status-badge status-<?= strtolower($election['status'] ?? 'preparing') ?>">
|
|
||||||
<?= strtoupper($election['status'] ?? 'PREPARING') ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="management-actions animate-stagger">
|
|
||||||
<button class="btn-manage primary" onclick="openModal('addCandidateModal')">
|
|
||||||
<i data-lucide="plus"></i> ADD CANDIDATE
|
|
||||||
</button>
|
|
||||||
<button class="btn-manage" onclick="openModal('addPositionModal')">
|
|
||||||
<i data-lucide="layout-list"></i> DEFINE POSITION
|
|
||||||
</button>
|
|
||||||
<button class="btn-manage" onclick="openModal('addPartyModal')">
|
|
||||||
<i data-lucide="flag"></i> DEFINE PARTY
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
|
||||||
<div class="candidate-stats-grid animate-stagger">
|
|
||||||
<div class="candidate-stat-card">
|
|
||||||
<div class="candidate-stat-label">TOTAL CANDIDATES</div>
|
|
||||||
<div class="candidate-stat-value"><?= number_format($totalCandidates) ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="candidate-stat-card">
|
|
||||||
<div class="candidate-stat-label">UNIQUE POSITIONS</div>
|
|
||||||
<div class="candidate-stat-value"><?= number_format($uniquePositions) ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="candidate-stat-card">
|
|
||||||
<div class="candidate-stat-label" style="color: #10b981;">ACTIVE PARTIES</div>
|
|
||||||
<div class="candidate-stat-value" style="color: #10b981;"><?= number_format($activeParties) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Distribution Row -->
|
|
||||||
<div class="distribution-row animate-stagger" style="margin-bottom: 32px;">
|
|
||||||
<div class="distribution-card">
|
|
||||||
<div class="distribution-header">Candidates by Position</div>
|
|
||||||
<div class="distribution-list">
|
|
||||||
<?php foreach ($posStats as $stat): ?>
|
|
||||||
<div class="distribution-item">
|
|
||||||
<span style="color: var(--text-muted);"><?= htmlspecialchars($stat['name']) ?></span>
|
|
||||||
<span class="distribution-count"><?= $stat['count'] ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($posStats)): ?>
|
|
||||||
<div style="padding: 12px; color: var(--text-muted); font-size: 0.875rem; text-align: center;">No positions defined.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="distribution-card">
|
|
||||||
<div class="distribution-header">Candidates by Party</div>
|
|
||||||
<div class="distribution-list">
|
|
||||||
<?php foreach ($partyStats as $stat): ?>
|
|
||||||
<div class="distribution-item">
|
|
||||||
<span style="color: var(--text-muted);"><?= htmlspecialchars($stat['party_name'] ?: 'Independent') ?></span>
|
|
||||||
<span class="distribution-count"><?= $stat['count'] ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($partyStats)): ?>
|
|
||||||
<div style="padding: 12px; color: var(--text-muted); font-size: 0.875rem; text-align: center;">No parties defined.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters & Table Section -->
|
|
||||||
<div class="content-section animate-fade-in" style="background: var(--surface-color); border-radius: 12px; border: 1px solid var(--border-color); overflow: hidden;">
|
|
||||||
<form method="GET" class="filter-bar">
|
|
||||||
<div class="filter-group" style="flex: 2;">
|
|
||||||
<label>SEARCH</label>
|
|
||||||
<div class="search-input-wrapper">
|
|
||||||
<i data-lucide="search" style="width: 14px; color: #94a3b8;"></i>
|
|
||||||
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Search candidates...">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>POSITION</label>
|
|
||||||
<select name="position" onchange="this.form.submit()">
|
|
||||||
<option>All Positions</option>
|
|
||||||
<?php foreach ($allPositions as $p): ?>
|
|
||||||
<option value="<?= htmlspecialchars($p['name']) ?>" <?= $filterPosition === $p['name'] ? 'selected' : '' ?>><?= htmlspecialchars($p['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>PARTY</label>
|
|
||||||
<select name="party" onchange="this.form.submit()">
|
|
||||||
<option>All Parties</option>
|
|
||||||
<?php foreach ($allParties as $pt): ?>
|
|
||||||
<option value="<?= htmlspecialchars($pt['name']) ?>" <?= $filterParty === $pt['name'] ? 'selected' : '' ?>><?= htmlspecialchars($pt['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="candidates-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>CANDIDATE</th>
|
|
||||||
<th>POSITION</th>
|
|
||||||
<th>PARTY</th>
|
|
||||||
<th>GRADE/TRACK</th>
|
|
||||||
<th>ACTIONS</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($candidates)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="5" style="text-align: center; color: #94a3b8; padding: 32px;">No candidates found in this election.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($candidates as $cand): ?>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="candidate-info">
|
|
||||||
<div class="candidate-avatar">
|
|
||||||
<?= strtoupper(substr($cand['user_name'], 0, 1)) ?>
|
|
||||||
</div>
|
|
||||||
<div class="candidate-details">
|
|
||||||
<span class="candidate-name"><?= htmlspecialchars($cand['user_name']) ?></span>
|
|
||||||
<span class="candidate-sub"><?= htmlspecialchars($cand['student_id']) ?> | <?= htmlspecialchars($cand['user_email']) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="position-badge"><?= htmlspecialchars($cand['position_name']) ?></span>
|
|
||||||
</td>
|
|
||||||
<td><?= htmlspecialchars($cand['party_name'] ?: 'Independent') ?></td>
|
|
||||||
<td>
|
|
||||||
<div class="candidate-details">
|
|
||||||
<span class="candidate-name">Grade <?= htmlspecialchars($cand['grade_level'] ?: '12') ?></span>
|
|
||||||
<span class="candidate-sub"><?= htmlspecialchars($cand['track'] ?: 'N/A') ?></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="actions-cell">
|
|
||||||
<button title="Edit" onclick='editCandidate(<?= json_encode($cand) ?>)'><i data-lucide="edit-2"></i></button>
|
|
||||||
<button title="Delete" style="color: #ef4444;" onclick="deleteCandidate('<?= $cand['id'] ?>', '<?= htmlspecialchars($cand['user_name']) ?>')"><i data-lucide="trash-2"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modals -->
|
|
||||||
<div id="editCandidateModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Edit Candidate</h2>
|
|
||||||
<button onclick="closeModal('editCandidateModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/update_candidate.php" method="POST">
|
|
||||||
<input type="hidden" name="id" id="edit_cand_id">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Candidate Name</label>
|
|
||||||
<input type="text" id="edit_cand_name" disabled style="background: var(--bg-color); color: var(--text-muted);">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Position</label>
|
|
||||||
<select name="position_id" id="edit_cand_position_id" required>
|
|
||||||
<?php foreach ($allPositions as $p): ?>
|
|
||||||
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Party</label>
|
|
||||||
<select name="party_name" id="edit_cand_party_name">
|
|
||||||
<option value="">Independent</option>
|
|
||||||
<?php foreach ($allParties as $pt): ?>
|
|
||||||
<option value="<?= htmlspecialchars($pt['name']) ?>"><?= htmlspecialchars($pt['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Manifesto / Vision</label>
|
|
||||||
<textarea name="manifesto" id="edit_cand_manifesto" rows="3" placeholder="Enter candidate vision..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('editCandidateModal')" class="btn-manage">Cancel</button>
|
|
||||||
<button type="submit" class="btn-manage primary">Update Candidate</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="addCandidateModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Add New Candidate</h2>
|
|
||||||
<button onclick="closeModal('addCandidateModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/add_candidate.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Select Student</label>
|
|
||||||
<select name="user_id" required>
|
|
||||||
<option value="">-- Choose Voter --</option>
|
|
||||||
<?php foreach ($allVoters as $v): ?>
|
|
||||||
<option value="<?= $v['id'] ?>"><?= htmlspecialchars($v['name']) ?> (<?= $v['student_id'] ?>)</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Position</label>
|
|
||||||
<select name="position_id" required>
|
|
||||||
<option value="">-- Choose Position --</option>
|
|
||||||
<?php foreach ($allPositions as $p): ?>
|
|
||||||
<option value="<?= $p['id'] ?>"><?= htmlspecialchars($p['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Party</label>
|
|
||||||
<select name="party_name">
|
|
||||||
<option value="">Independent</option>
|
|
||||||
<?php foreach ($allParties as $pt): ?>
|
|
||||||
<option value="<?= htmlspecialchars($pt['name']) ?>"><?= htmlspecialchars($pt['name']) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Manifesto / Vision</label>
|
|
||||||
<textarea name="manifesto" rows="3" placeholder="Enter candidate vision..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('addCandidateModal')" class="btn-manage">Cancel</button>
|
|
||||||
<button type="submit" class="btn-manage primary">Save Candidate</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="addPositionModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Define New Position</h2>
|
|
||||||
<button onclick="closeModal('addPositionModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/add_position.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Position Name</label>
|
|
||||||
<input type="text" name="name" placeholder="e.g. President, Secretary" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Position Type</label>
|
|
||||||
<select name="type" required>
|
|
||||||
<option value="Uniform">Uniform (Global)</option>
|
|
||||||
<option value="Track Specific">Track Specific (Voter Track Only)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('addPositionModal')" class="btn-manage">Cancel</button>
|
|
||||||
<button type="submit" class="btn-manage primary">Create Position</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="addPartyModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Define New Party</h2>
|
|
||||||
<button onclick="closeModal('addPartyModal')" style="border:none; background:none; cursor:pointer;"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/add_party.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Party Name</label>
|
|
||||||
<input type="text" name="name" placeholder="e.g. Unity Party" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Description</label>
|
|
||||||
<textarea name="description" rows="2" placeholder="Party slogan or vision..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('addPartyModal')" class="btn-manage">Cancel</button>
|
|
||||||
<button type="submit" class="btn-manage primary">Create Party</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
function openModal(id) {
|
|
||||||
document.getElementById(id).style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal(id) {
|
|
||||||
document.getElementById(id).style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function editCandidate(cand) {
|
|
||||||
document.getElementById('edit_cand_id').value = cand.id;
|
|
||||||
document.getElementById('edit_cand_name').value = cand.user_name;
|
|
||||||
document.getElementById('edit_cand_position_id').value = cand.position_id;
|
|
||||||
document.getElementById('edit_cand_party_name').value = cand.party_name || '';
|
|
||||||
document.getElementById('edit_cand_manifesto').value = cand.manifesto || '';
|
|
||||||
openModal('editCandidateModal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteCandidate(id, name) {
|
|
||||||
if (confirm(`Are you sure you want to remove ${name} from being a candidate?`)) {
|
|
||||||
window.location.href = `api/delete_candidate.php?id=${id}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target.className === 'modal') {
|
|
||||||
event.target.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
$supabasePass = 'gA82h8K80T5QUAwi';
|
|
||||||
$supabaseHost = "aws-1-ap-southeast-1.pooler.supabase.com";
|
|
||||||
$supabaseUser = "postgres.siqeqnizegizxemrfgkf";
|
|
||||||
$supabaseDb = "postgres";
|
|
||||||
$supabasePort = "6543";
|
|
||||||
|
|
||||||
try {
|
|
||||||
echo "Connecting to local MariaDB...\n";
|
|
||||||
$localPdo = db();
|
|
||||||
|
|
||||||
echo "Connecting to Supabase PostgreSQL...\n";
|
|
||||||
$dsn = "pgsql:host=$supabaseHost;port=$supabasePort;dbname=$supabaseDb";
|
|
||||||
$supabasePdo = new PDO($dsn, $supabaseUser, $supabasePass, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tables = ["users", "elections", "positions", "election_assignments", "parties", "candidates", "votes", "audit_logs"];
|
|
||||||
|
|
||||||
echo "\nComparison Table:\n";
|
|
||||||
echo str_pad("Table", 25) . " | " . str_pad("Local", 10) . " | " . str_pad("Supabase", 10) . "\n";
|
|
||||||
echo str_repeat("-", 50) . "\n";
|
|
||||||
|
|
||||||
foreach ($tables as $table) {
|
|
||||||
$localCount = $localPdo->query("SELECT COUNT(*) FROM $table")->fetchColumn();
|
|
||||||
|
|
||||||
// For Supabase, check if table exists first
|
|
||||||
try {
|
|
||||||
$supabaseCount = $supabasePdo->query("SELECT COUNT(*) FROM $table")->fetchColumn();
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$supabaseCount = "N/A (Error)";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo str_pad($table, 25) . " | " . str_pad($localCount, 10) . " | " . str_pad($supabaseCount, 10) . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
<?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">Click to Vote</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>
|
|
||||||
510
dashboard.php
@ -1,510 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$election = get_active_election();
|
|
||||||
|
|
||||||
// For Election Management Section
|
|
||||||
$allElections = [];
|
|
||||||
if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])) {
|
|
||||||
$allElections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY created_at DESC")->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistics (Filtered by Election)
|
|
||||||
|
|
||||||
$totalVoters = $pdo->prepare("SELECT COUNT(*) FROM election_assignments WHERE election_id = ? AND role_in_election = 'Voter'");
|
|
||||||
$totalVoters->execute([$electionId]);
|
|
||||||
$totalVoters = $totalVoters->fetchColumn();
|
|
||||||
|
|
||||||
$totalCandidates = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
|
|
||||||
$totalCandidates->execute([$electionId]);
|
|
||||||
$totalCandidates = $totalCandidates->fetchColumn();
|
|
||||||
|
|
||||||
$totalVotes = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
|
|
||||||
$totalVotes->execute([$electionId]);
|
|
||||||
$totalVotes = $totalVotes->fetchColumn();
|
|
||||||
|
|
||||||
// Chart Data: Participation per Grade Level
|
|
||||||
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
||||||
$gradeCol = ($driver === 'pgsql') ? "u.grade_level::TEXT" : "CAST(u.grade_level AS CHAR)";
|
|
||||||
$gradeStats = $pdo->prepare("SELECT COALESCE($gradeCol, 'Unknown') as label, COUNT(DISTINCT v.voter_id) as count
|
|
||||||
FROM users u JOIN votes v ON u.id = v.voter_id
|
|
||||||
WHERE v.election_id = ?
|
|
||||||
GROUP BY u.grade_level ORDER BY u.grade_level");
|
|
||||||
$gradeStats->execute([$electionId]);
|
|
||||||
$gradeStats = $gradeStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Chart Data: Participation per Track
|
|
||||||
$trackStats = $pdo->prepare("SELECT COALESCE(u.track, 'Unknown') as label, COUNT(DISTINCT v.voter_id) as count
|
|
||||||
FROM users u JOIN votes v ON u.id = v.voter_id
|
|
||||||
WHERE v.election_id = ?
|
|
||||||
GROUP BY u.track");
|
|
||||||
$trackStats->execute([$electionId]);
|
|
||||||
$trackStats = $trackStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Chart Data: Participation per Section
|
|
||||||
$sectionStats = $pdo->prepare("SELECT u.track, u.section as label, COUNT(DISTINCT v.voter_id) as count
|
|
||||||
FROM users u JOIN votes v ON u.id = v.voter_id
|
|
||||||
WHERE v.election_id = ?
|
|
||||||
GROUP BY u.track, u.section");
|
|
||||||
$sectionStats->execute([$electionId]);
|
|
||||||
$sectionStats = $sectionStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Tracks for dropdown
|
|
||||||
$tracks = array_unique(array_column($sectionStats, 'track'));
|
|
||||||
sort($tracks);
|
|
||||||
|
|
||||||
$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>Election Dashboard | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<style>
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.5);
|
|
||||||
z-index: 1000;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.modal-content {
|
|
||||||
background: white;
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.modal-header {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
.modal-title {
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
.modal-footer {
|
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
.btn-submit {
|
|
||||||
flex: 1;
|
|
||||||
background: #4f46e5;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn-cancel {
|
|
||||||
flex: 1;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #e2e8f0;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<?php if (isset($_GET['success'])): ?>
|
|
||||||
<div style="background: #ecfdf5; color: #10b981; padding: 12px 16px; border-radius: 8px; margin-bottom: 24px; font-size: 0.875rem; border: 1px solid #10b981;">
|
|
||||||
<?= htmlspecialchars($_GET['success']) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if (isset($_GET['error'])): ?>
|
|
||||||
<div style="background: #fef2f2; color: #ef4444; padding: 12px 16px; border-radius: 8px; margin-bottom: 24px; font-size: 0.875rem; border: 1px solid #ef4444;">
|
|
||||||
<?= htmlspecialchars($_GET['error']) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0 0 4px 0; font-size: 1.5rem; color: #1e293b;">Election Dashboard</h1>
|
|
||||||
<div class="welcome-msg">
|
|
||||||
Active Election: <strong><?= htmlspecialchars($election['title'] ?? 'None') ?></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!empty($allElections)): ?>
|
|
||||||
<!-- Election Control Center -->
|
|
||||||
<div class="content-section animate-stagger" style="margin-bottom: 32px;">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">Election Control Center</div>
|
|
||||||
<button class="btn-new-election" id="btnNewElection" style="border: none; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
||||||
<i data-lucide="plus"></i> New Election
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<table class="election-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Election Title</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Current End Time</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($allElections as $e): ?>
|
|
||||||
<tr>
|
|
||||||
<td style="font-weight: 500;"><a href="view_election.php?id=<?= $e['id'] ?>" style="color: #6366f1; text-decoration: none;"><?= htmlspecialchars($e['title']) ?></a></td>
|
|
||||||
<td>
|
|
||||||
<span class="status-badge status-<?= strtolower($e['status']) ?>">
|
|
||||||
<?= htmlspecialchars($e['status']) ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="color: #64748b; font-size: 0.8rem;">
|
|
||||||
<?= date('M d, H:i', strtotime($e['end_date_and_time'])) ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="quick-actions">
|
|
||||||
<?php if ($e['status'] === 'Preparing'): ?>
|
|
||||||
<form action="api/update_election_status.php" method="POST" style="display:inline;">
|
|
||||||
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
||||||
<input type="hidden" name="status" value="Ongoing">
|
|
||||||
<input type="hidden" name="redirect" value="../dashboard.php?success=Election started">
|
|
||||||
<button type="submit" class="btn-update" style="background: #10b981;">Start</button>
|
|
||||||
</form>
|
|
||||||
<?php elseif ($e['status'] === 'Ongoing'): ?>
|
|
||||||
<form action="api/update_election_status.php" method="POST" style="display:inline;">
|
|
||||||
<input type="hidden" name="id" value="<?= $e['id'] ?>">
|
|
||||||
<input type="hidden" name="status" value="Finished">
|
|
||||||
<input type="hidden" name="redirect" value="../dashboard.php?success=Election ended">
|
|
||||||
<button type="submit" class="btn-update" style="background: #ef4444;">End</button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (in_array($user['role'], ['Admin', 'Adviser'])): ?>
|
|
||||||
<button
|
|
||||||
class="btn-update btn-manage-election"
|
|
||||||
style="background: #6366f1;"
|
|
||||||
data-id="<?= $e['id'] ?>"
|
|
||||||
data-title="<?= htmlspecialchars($e['title']) ?>"
|
|
||||||
data-status="<?= $e['status'] ?>"
|
|
||||||
data-end="<?= $e['end_date_and_time'] ?>"
|
|
||||||
>Manage</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
|
||||||
<div class="stats-grid animate-stagger">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Total Voters</div>
|
|
||||||
<div class="stat-value"><?= number_format($totalVoters) ?></div>
|
|
||||||
<div class="stat-footer voters">
|
|
||||||
<i data-lucide="users-2" style="width: 14px;"></i>
|
|
||||||
Assigned Students
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Total Candidates</div>
|
|
||||||
<div class="stat-value"><?= number_format($totalCandidates) ?></div>
|
|
||||||
<div class="stat-footer candidates">
|
|
||||||
<i data-lucide="user-circle" style="width: 14px;"></i>
|
|
||||||
Validated Contestants
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Total Votes Cast</div>
|
|
||||||
<div class="stat-value"><?= number_format($totalVotes) ?></div>
|
|
||||||
<div class="stat-footer votes">
|
|
||||||
<i data-lucide="check-circle-2" style="width: 14px;"></i>
|
|
||||||
Verified Ballots
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Analytics Charts -->
|
|
||||||
<div class="analytics-row animate-stagger">
|
|
||||||
<div class="analytics-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-title">Votes Per Grade Level</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="gradeChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="analytics-card">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-title">Votes Per Track</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="trackChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="analytics-row animate-stagger">
|
|
||||||
<div class="analytics-card" style="grid-column: span 2;">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="card-title">Votes Per Section</div>
|
|
||||||
<select id="trackFilter" class="chart-filter">
|
|
||||||
<?php if (empty($tracks)): ?>
|
|
||||||
<option>No data</option>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php foreach ($tracks as $t): ?>
|
|
||||||
<option value="<?= htmlspecialchars($t) ?>"><?= htmlspecialchars($t) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="chart-container" style="height: 300px;">
|
|
||||||
<canvas id="sectionChart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
// Chart Data from PHP
|
|
||||||
const gradeData = <?= json_encode($gradeStats) ?>;
|
|
||||||
const trackData = <?= json_encode($trackStats) ?>;
|
|
||||||
const sectionData = <?= json_encode($sectionStats) ?>;
|
|
||||||
|
|
||||||
// Common Chart Options
|
|
||||||
const commonOptions = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false }
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: { beginAtZero: true, grid: { display: false } },
|
|
||||||
x: { grid: { display: false } }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Grade Level Bar Chart
|
|
||||||
if (gradeData.length) {
|
|
||||||
new Chart(document.getElementById('gradeChart'), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: gradeData.map(d => 'Grade ' + d.label),
|
|
||||||
datasets: [{
|
|
||||||
label: 'Votes',
|
|
||||||
data: gradeData.map(d => d.count),
|
|
||||||
backgroundColor: '#4f46e5',
|
|
||||||
borderRadius: 6
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: commonOptions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track Bar Chart
|
|
||||||
if (trackData.length) {
|
|
||||||
new Chart(document.getElementById('trackChart'), {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: trackData.map(d => d.label),
|
|
||||||
datasets: [{
|
|
||||||
label: 'Votes',
|
|
||||||
data: trackData.map(d => d.count),
|
|
||||||
backgroundColor: '#10b981',
|
|
||||||
borderRadius: 6
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: commonOptions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Section Chart
|
|
||||||
let sectionChart;
|
|
||||||
function updateSectionChart(track) {
|
|
||||||
const canvas = document.getElementById('sectionChart');
|
|
||||||
if (!canvas) return;
|
|
||||||
const filtered = sectionData.filter(d => d.track === track);
|
|
||||||
const data = {
|
|
||||||
labels: filtered.map(d => d.label),
|
|
||||||
datasets: [{
|
|
||||||
label: 'Votes',
|
|
||||||
data: filtered.map(d => d.count),
|
|
||||||
backgroundColor: '#4f46e5',
|
|
||||||
borderRadius: 6
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sectionChart) {
|
|
||||||
sectionChart.data = data;
|
|
||||||
sectionChart.update();
|
|
||||||
} else {
|
|
||||||
sectionChart = new Chart(canvas, {
|
|
||||||
type: 'bar',
|
|
||||||
data: data,
|
|
||||||
options: commonOptions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const trackFilter = document.getElementById('trackFilter');
|
|
||||||
if (trackFilter && trackData.length) {
|
|
||||||
trackFilter.addEventListener('change', (e) => {
|
|
||||||
updateSectionChart(e.target.value);
|
|
||||||
});
|
|
||||||
updateSectionChart(trackFilter.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Override/Manage Modal -->
|
|
||||||
<div id="manageElectionModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title" id="modalElectionTitle">Manage Election</div>
|
|
||||||
</div>
|
|
||||||
<form action="api/manage_election_action.php" method="POST">
|
|
||||||
<input type="hidden" name="id" id="modalElectionId">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Override Status</label>
|
|
||||||
<select name="status" id="modalElectionStatus" class="form-control">
|
|
||||||
<option value="Preparing">Preparing</option>
|
|
||||||
<option value="Ongoing">Ongoing</option>
|
|
||||||
<option value="Finished">Finished</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Change End Time</label>
|
|
||||||
<input type="datetime-local" name="end_time" id="modalElectionEndTime" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn-cancel" onclick="closeModal('manageElectionModal')">Cancel</button>
|
|
||||||
<button type="submit" class="btn-submit">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- New Election Modal -->
|
|
||||||
<div id="createElectionModal" class="modal">
|
|
||||||
<div class="modal-content" style="max-width: 500px;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">Create New Election</div>
|
|
||||||
</div>
|
|
||||||
<form action="api/create_election.php" method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<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>Description</label>
|
|
||||||
<textarea name="description" class="form-control" rows="3" placeholder="Briefly describe the purpose..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Start Date & Time</label>
|
|
||||||
<input type="datetime-local" name="start_date" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>End Date & Time</label>
|
|
||||||
<input type="datetime-local" name="end_date" class="form-control" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn-cancel" onclick="closeModal('createElectionModal')">Cancel</button>
|
|
||||||
<button type="submit" class="btn-submit">Create Election</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.getElementById('btnNewElection').addEventListener('click', function() {
|
|
||||||
document.getElementById('createElectionModal').style.display = 'flex';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll('.btn-manage-election').forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const id = this.getAttribute('data-id');
|
|
||||||
const title = this.getAttribute('data-title');
|
|
||||||
const status = this.getAttribute('data-status');
|
|
||||||
const end = this.getAttribute('data-end');
|
|
||||||
|
|
||||||
document.getElementById('modalElectionId').value = id;
|
|
||||||
document.getElementById('modalElectionTitle').innerText = 'Manage: ' + title;
|
|
||||||
document.getElementById('modalElectionStatus').value = status;
|
|
||||||
|
|
||||||
if (end) {
|
|
||||||
const date = new Date(end);
|
|
||||||
const offset = date.getTimezoneOffset() * 60000;
|
|
||||||
const localISODate = new Date(date.getTime() - offset).toISOString().slice(0, 16);
|
|
||||||
document.getElementById('modalElectionEndTime').value = localISODate;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('manageElectionModal').style.display = 'flex';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function closeModal(modalId) {
|
|
||||||
if (modalId) {
|
|
||||||
document.getElementById(modalId).style.display = 'none';
|
|
||||||
} else {
|
|
||||||
document.getElementById('manageElectionModal').style.display = 'none';
|
|
||||||
document.getElementById('createElectionModal').style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target.classList.contains('modal')) {
|
|
||||||
event.target.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -8,15 +8,10 @@ define('DB_PASS', 'c217529c-a428-4a97-8f31-773c420377a7');
|
|||||||
function db() {
|
function db() {
|
||||||
static $pdo;
|
static $pdo;
|
||||||
if (!$pdo) {
|
if (!$pdo) {
|
||||||
// Local MariaDB/MySQL
|
|
||||||
try {
|
|
||||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
]);
|
]);
|
||||||
} catch (PDOException $mysqlException) {
|
|
||||||
die("Connection failed: " . $mysqlException->getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
-- 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 'Testing')
|
|
||||||
INSERT INTO users (id, student_id, name, email, password_hash, role, access_level)
|
|
||||||
VALUES ('admin-uuid-1', '00-0000', 'Admin User', 'Admin@iloilonhs.edu.ph', '$2y$10$W70K9blIfzVSYbr/sEQUte3eyUejciAHmpubscltUNZbmpkPrF71K', 'Admin', 4);
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
-- Sample Election Data
|
|
||||||
INSERT INTO elections (id, title, description, status, start_date_and_time, end_date_and_time, created_by)
|
|
||||||
VALUES (
|
|
||||||
'sample-election-uuid',
|
|
||||||
'School Year 2028 Election',
|
|
||||||
'General student council elections for the upcoming school year.',
|
|
||||||
'Ongoing',
|
|
||||||
'2026-02-11 08:00:00',
|
|
||||||
'2026-02-18 17:00:00',
|
|
||||||
'admin-uuid-1'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Sample Voters (to match the "8" in the screenshot)
|
|
||||||
INSERT INTO users (id, student_id, name, email, password_hash, role) VALUES
|
|
||||||
('voter-1', '21-0001', 'John Doe', 'john@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-2', '21-0002', 'Jane Smith', 'jane@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-3', '21-0003', 'Bob Johnson', 'bob@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-4', '21-0004', 'Alice Brown', 'alice@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-5', '21-0005', 'Charlie Davis', 'charlie@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-6', '21-0006', 'Eve Wilson', 'eve@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-7', '21-0007', 'Frank Miller', 'frank@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter'),
|
|
||||||
('voter-8', '21-0008', 'Grace Lee', 'grace@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter');
|
|
||||||
|
|
||||||
-- Sample Candidates (to match the "15" in the screenshot)
|
|
||||||
-- We'll need some positions first
|
|
||||||
INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES
|
|
||||||
('pos-1', 'sample-election-uuid', 'President', 1, 1),
|
|
||||||
('pos-2', 'sample-election-uuid', 'Vice President', 1, 2);
|
|
||||||
|
|
||||||
-- Insert 15 candidates (reusing voters for simplicity in this mockup)
|
|
||||||
INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES
|
|
||||||
('cand-1', 'sample-election-uuid', 'pos-1', 'voter-1', 'Unity Party', 1),
|
|
||||||
('cand-2', 'sample-election-uuid', 'pos-1', 'voter-2', 'Progress Party', 1),
|
|
||||||
('cand-3', 'sample-election-uuid', 'pos-2', 'voter-3', 'Unity Party', 1),
|
|
||||||
('cand-4', 'sample-election-uuid', 'pos-2', 'voter-4', 'Progress Party', 1),
|
|
||||||
('cand-5', 'sample-election-uuid', 'pos-1', 'voter-5', 'Independent', 1),
|
|
||||||
('cand-6', 'sample-election-uuid', 'pos-2', 'voter-6', 'Independent', 1),
|
|
||||||
('cand-7', 'sample-election-uuid', 'pos-1', 'voter-7', 'Students First', 1),
|
|
||||||
('cand-8', 'sample-election-uuid', 'pos-2', 'voter-8', 'Students First', 1),
|
|
||||||
('cand-9', 'sample-election-uuid', 'pos-1', 'admin-uuid-1', 'Faculty Choice', 1),
|
|
||||||
('cand-10', 'sample-election-uuid', 'pos-2', 'admin-uuid-1', 'Faculty Choice', 1),
|
|
||||||
-- Adding more to reach 15
|
|
||||||
('cand-11', 'sample-election-uuid', 'pos-1', 'voter-1', 'Extra 1', 1),
|
|
||||||
('cand-12', 'sample-election-uuid', 'pos-2', 'voter-2', 'Extra 2', 1),
|
|
||||||
('cand-13', 'sample-election-uuid', 'pos-1', 'voter-3', 'Extra 3', 1),
|
|
||||||
('cand-14', 'sample-election-uuid', 'pos-2', 'voter-4', 'Extra 4', 1),
|
|
||||||
('cand-15', 'sample-election-uuid', 'pos-1', 'voter-5', 'Extra 5', 1);
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
-- Update sample voters with grade level, track, and section
|
|
||||||
UPDATE users SET grade_level = 11, track = 'STEM', section = 'A' WHERE id = 'voter-1';
|
|
||||||
UPDATE users SET grade_level = 11, track = 'STEM', section = 'B' WHERE id = 'voter-2';
|
|
||||||
UPDATE users SET grade_level = 11, track = 'ABM', section = 'C' WHERE id = 'voter-3';
|
|
||||||
UPDATE users SET grade_level = 12, track = 'ABM', section = 'D' WHERE id = 'voter-4';
|
|
||||||
UPDATE users SET grade_level = 12, track = 'HUMSS', section = 'E' WHERE id = 'voter-5';
|
|
||||||
UPDATE users SET grade_level = 12, track = 'HUMSS', section = 'F' WHERE id = 'voter-6';
|
|
||||||
UPDATE users SET grade_level = 11, track = 'GAS', section = 'G' WHERE id = 'voter-7';
|
|
||||||
UPDATE users SET grade_level = 12, track = 'TVL', section = 'H' WHERE id = 'voter-8';
|
|
||||||
|
|
||||||
-- Insert some dummy votes so the charts aren't empty
|
|
||||||
-- We need to find the position and candidate IDs
|
|
||||||
-- President candidates: cand-1, cand-2, cand-5, cand-7, cand-9, cand-11, cand-13, cand-15
|
|
||||||
-- Vice President candidates: cand-3, cand-4, cand-6, cand-8, cand-10, cand-12, cand-14
|
|
||||||
|
|
||||||
-- voter-1 votes for cand-1 (President)
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-1', 'voter-1', '2026-02-11 10:00:00');
|
|
||||||
-- voter-2 votes for cand-2
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-2', 'voter-2', '2026-02-12 11:00:00');
|
|
||||||
-- voter-3 votes for cand-1
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-1', 'voter-3', '2026-02-13 09:00:00');
|
|
||||||
-- voter-4 votes for cand-5
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-5', 'voter-4', '2026-02-14 10:00:00');
|
|
||||||
-- voter-5 votes for cand-7
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-7', 'voter-5', '2026-02-15 11:00:00');
|
|
||||||
-- voter-6 votes for cand-1
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-1', 'voter-6', '2026-02-15 12:00:00');
|
|
||||||
-- voter-7 votes for cand-9
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-9', 'voter-7', '2026-02-15 13:00:00');
|
|
||||||
-- voter-8 votes for cand-11
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at)
|
|
||||||
VALUES (REPLACE(UUID(), '-', ''), 'sample-election-uuid', 'pos-1', 'cand-11', 'voter-8', '2026-02-15 14:00:00');
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
-- Past Elections for History
|
|
||||||
INSERT INTO elections (id, title, description, status, start_date_and_time, end_date_and_time, created_by) VALUES
|
|
||||||
('hist-uuid-1', 'School Year 2027-2028 Election', 'Previous year elections.', 'Finished', '2027-02-10 08:00:00', '2027-02-11 17:00:00', 'admin-uuid-1'),
|
|
||||||
('hist-uuid-2', 'School Year 2026-2027 Election', 'Elections from 2 years ago.', 'Finished', '2026-02-10 08:00:00', '2026-02-11 17:00:00', 'admin-uuid-1'),
|
|
||||||
('hist-uuid-3', 'School Year 2022-2023 Election', 'Older elections.', 'Finished', '2022-09-01 08:00:00', '2022-09-02 17:00:00', 'admin-uuid-1'),
|
|
||||||
('hist-uuid-4', 'School Year 2020-2021 Election', 'Archived elections.', 'Finished', '2020-09-01 08:00:00', '2020-09-02 17:00:00', 'admin-uuid-1');
|
|
||||||
|
|
||||||
-- Positions for 2027-2028
|
|
||||||
INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES
|
|
||||||
('pos-hist-1', 'hist-uuid-1', 'President', 1, 1),
|
|
||||||
('pos-hist-2', 'hist-uuid-1', 'Vice President', 1, 2);
|
|
||||||
|
|
||||||
-- Candidates for 2027-2028
|
|
||||||
INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES
|
|
||||||
('cand-hist-1', 'hist-uuid-1', 'pos-hist-1', 'voter-1', 'Unity Party', 1),
|
|
||||||
('cand-hist-2', 'hist-uuid-1', 'pos-hist-1', 'voter-2', 'Progress Party', 1);
|
|
||||||
|
|
||||||
-- Votes for 2027-2028
|
|
||||||
INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id, casted_at) VALUES
|
|
||||||
(REPLACE(UUID(), '-', ''), 'hist-uuid-1', 'pos-hist-1', 'cand-hist-1', 'voter-1', '2027-02-10 09:00:00'),
|
|
||||||
(REPLACE(UUID(), '-', ''), 'hist-uuid-1', 'pos-hist-1', 'cand-hist-1', 'voter-2', '2027-02-10 10:00:00'),
|
|
||||||
(REPLACE(UUID(), '-', ''), 'hist-uuid-1', 'pos-hist-1', 'cand-hist-2', 'voter-3', '2027-02-10 11:00:00');
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
-- Migration 005: Additional Voter Data for Management View
|
|
||||||
INSERT INTO users (id, student_id, name, email, password_hash, grade_level, track, section, role) VALUES
|
|
||||||
(UUID(), '28-0001', 'John Doe', 'john.doe@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'STEM', 'A', 'Voter'),
|
|
||||||
(UUID(), '28-0002', 'Jane Smith', 'jane.smith@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'ABM', 'A', 'Voter'),
|
|
||||||
(UUID(), '28-0003', 'Bob Wilson', 'bob.wilson@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'GAS', 'A', 'Voter'),
|
|
||||||
(UUID(), '28-0004', 'Alice Brown', 'alice.brown@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'HE', 'F', 'Voter'),
|
|
||||||
(UUID(), '28-0005', 'Charlie Davis', 'charlie.davis@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'HUMSS', 'A', 'Voter'),
|
|
||||||
(UUID(), '28-0006', 'Diana Prince', 'diana.prince@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'ICT', 'A', 'Voter'),
|
|
||||||
(UUID(), '28-0007', 'Edward Norton', 'edward.norton@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'STEM', 'A', 'Voter'),
|
|
||||||
(UUID(), '28-0008', 'Fiona Gallagher', 'fiona.gallagher@shs.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 12, 'ICT', 'A', 'Voter');
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
-- Refined 006
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
DELETE FROM candidates;
|
|
||||||
DELETE FROM positions;
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES
|
|
||||||
('pos-gov', 'sample-election-uuid', 'Governor', 1, 1),
|
|
||||||
('pos-vgov', 'sample-election-uuid', 'Vice Governor', 1, 2),
|
|
||||||
('pos-sec', 'sample-election-uuid', 'Secretary', 1, 3),
|
|
||||||
('pos-pio', 'sample-election-uuid', 'PIO', 1, 4),
|
|
||||||
('pos-bm', 'sample-election-uuid', 'Board Member', 4, 5);
|
|
||||||
|
|
||||||
-- Using different student IDs to avoid conflicts with 005
|
|
||||||
INSERT INTO users (id, student_id, name, email, password_hash, role, grade_level, track, section)
|
|
||||||
VALUES
|
|
||||||
('cand-1-uuid', '29-7832', 'Kurt Leovince Tse Wing', 'kurtleovince06@gmail.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'A'),
|
|
||||||
('cand-2-uuid', '29-0002', 'Noah Padilla', 'noah.p@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'A'),
|
|
||||||
('cand-3-uuid', '29-0003', 'Liam Garcia', 'liam.g@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'B'),
|
|
||||||
('cand-4-uuid', '29-0004', 'Emma Wilson', 'emma.w@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ABM', 'C'),
|
|
||||||
('cand-5-uuid', '29-0005', 'Olivia Martinez', 'olivia.m@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'HUMSS', 'D'),
|
|
||||||
('cand-6-uuid', '29-0006', 'James Brown', 'james.b@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'B'),
|
|
||||||
('cand-7-uuid', '29-0007', 'Sophia Davis', 'sophia.d@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'A'),
|
|
||||||
('cand-8-uuid', '29-0008', 'Mason Rodriguez', 'mason.r@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ABM', 'A'),
|
|
||||||
('cand-9-uuid', '29-0009', 'Isabella Lopez', 'isabella.l@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'HUMSS', 'A'),
|
|
||||||
('cand-10-uuid', '29-0010', 'Ethan Wilson', 'ethan.w@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'C'),
|
|
||||||
('cand-11-uuid', '29-0011', 'Ava Moore', 'ava.m@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'C'),
|
|
||||||
('cand-12-uuid', '29-0012', 'Lucas Taylor', 'lucas.t@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ABM', 'B'),
|
|
||||||
('cand-13-uuid', '29-0013', 'Mia Anderson', 'mia.a@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'HUMSS', 'B'),
|
|
||||||
('cand-14-uuid', '29-0014', 'Alexander Thomas', 'alex.t@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'ICT', 'D'),
|
|
||||||
('cand-15-uuid', '29-0015', 'Charlotte Jackson', 'charlotte.j@example.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Voter', 12, 'STEM', 'D')
|
|
||||||
ON DUPLICATE KEY UPDATE name=VALUES(name);
|
|
||||||
|
|
||||||
INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-bm', 'cand-1-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-bm', 'cand-2-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-bm', 'cand-3-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-gov', 'cand-4-uuid', 'Uswag', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-gov', 'cand-5-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-gov', 'cand-6-uuid', 'Uswag', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-vgov', 'cand-7-uuid', 'Uswag', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-vgov', 'cand-8-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-vgov', 'cand-9-uuid', 'Uswag', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-sec', 'cand-10-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-sec', 'cand-11-uuid', 'Uswag', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-pio', 'cand-12-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-pio', 'cand-13-uuid', 'Uswag', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-sec', 'cand-14-uuid', 'Maligaya', 1),
|
|
||||||
(UUID(), 'sample-election-uuid', 'pos-pio', 'cand-15-uuid', 'Uswag', 1);
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
-- Add Sample Officers for Management View
|
|
||||||
INSERT INTO users (id, student_id, name, email, password_hash, role, access_level)
|
|
||||||
VALUES
|
|
||||||
('officer-uuid-1', '23-5443', 'Jay Orly Mil Santiago', 'jay44296@gmail.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Adviser', 3),
|
|
||||||
('officer-uuid-2', '23-1111', 'Ma. Elena Santos', 'elena.santos@school.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Officer', 2),
|
|
||||||
('officer-uuid-3', '23-2222', 'Robert Chen', 'robert.chen@school.edu', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Officer', 2);
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
-- Add details column to audit_logs for human-readable descriptions
|
|
||||||
ALTER TABLE audit_logs ADD COLUMN details TEXT AFTER action;
|
|
||||||
|
|
||||||
-- Clear existing logs for a fresh start in development
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
TRUNCATE TABLE audit_logs;
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
-- Insert sample audit data matching the UI design
|
|
||||||
INSERT INTO audit_logs (id, user_id, action, details, created_at) VALUES
|
|
||||||
('a1', 'admin-uuid-1', 'Login', 'Successful login to the system', '2026-02-15 18:22:30'),
|
|
||||||
('a2', 'admin-uuid-1', 'Logout', 'User logged out of the system', '2026-02-11 04:09:53'),
|
|
||||||
('a3', 'admin-uuid-1', 'Update Election Status', 'Changed SY 2028 Election status from Preparing to Ongoing', '2026-02-11 04:09:43'),
|
|
||||||
('a4', 'admin-uuid-1', 'Remove Position', 'Removed position: Position_Name', '2026-02-11 04:08:34'),
|
|
||||||
('a5', 'admin-uuid-1', 'Add Position', 'Added new position: Position_Name (Uniform)', '2026-02-11 04:08:30'),
|
|
||||||
('a6', 'admin-uuid-1', 'Add Voter', 'Registered new voter: jay44296@gmail.com for Election ID: 6. User ID: 12-3456', '2026-02-11 04:07:57'),
|
|
||||||
('a7', 'admin-uuid-1', 'Login', 'Successful login to the system', '2026-02-11 04:03:00'),
|
|
||||||
('a8', 'admin-uuid-1', 'Logout', 'User logged out of the system', '2026-02-11 04:02:25'),
|
|
||||||
('a9', 'admin-uuid-1', 'Delete Voter', 'Deleted voter: jay44296@gmail.com (ID: 12-3456)', '2026-02-11 03:58:30'),
|
|
||||||
('a10', 'admin-uuid-1', 'Login', 'Successful login to the system', '2026-02-11 03:53:37'),
|
|
||||||
('a11', 'admin-uuid-1', 'Logout', 'User logged out of the system', '2026-02-11 03:49:56'),
|
|
||||||
('a12', 'admin-uuid-1', 'Add Candidate', 'Added candidate Vanessa Ortega for Board Member. ID: 25-7916', '2026-02-11 03:49:30'),
|
|
||||||
('a13', 'admin-uuid-1', 'Add Candidate', 'Added candidate Noah Padilla for Board Member. ID: 77-4683', '2026-02-11 03:49:10');
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
-- Migration to support multi-election and enhanced candidate management
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
-- Create parties table for definition
|
|
||||||
CREATE TABLE IF NOT EXISTS parties (
|
|
||||||
id CHAR(36) PRIMARY KEY,
|
|
||||||
election_id CHAR(36) NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
logo_url VARCHAR(255),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Add election_id to audit_logs if not exists
|
|
||||||
-- Check if column exists is not directly possible in standard SQL without procedural, but we can try to add it.
|
|
||||||
-- Since this is a fresh migration for polishing, we assume it's okay.
|
|
||||||
ALTER TABLE audit_logs ADD COLUMN election_id CHAR(36) NULL;
|
|
||||||
ALTER TABLE audit_logs ADD CONSTRAINT fk_audit_election FOREIGN KEY (election_id) REFERENCES elections(id) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
-- Ensure election_assignments is used correctly
|
|
||||||
-- We don't need to change the schema here, but we will update the logic.
|
|
||||||
|
|
||||||
-- Add some sample parties for the existing elections
|
|
||||||
INSERT INTO parties (id, election_id, name, description)
|
|
||||||
SELECT UUID(), id, 'PROGRESSIVE PARTY', 'Committed to innovation and change.' FROM elections;
|
|
||||||
INSERT INTO parties (id, election_id, name, description)
|
|
||||||
SELECT UUID(), id, 'UNITY ALLIANCE', 'Together for a better future.' FROM elections;
|
|
||||||
INSERT INTO parties (id, election_id, name, description)
|
|
||||||
SELECT UUID(), id, 'YOUTH VOICE', 'Empowering the next generation.' FROM elections;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
ALTER TABLE positions ADD COLUMN type ENUM('Uniform', 'Track Specific') DEFAULT 'Uniform' AFTER name;
|
|
||||||
137
db/mock_data.php
@ -1,137 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/config.php';
|
|
||||||
|
|
||||||
function generate_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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
||||||
echo "Connected using driver: $driver\n";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo->beginTransaction();
|
|
||||||
|
|
||||||
echo "Clearing existing data (preserving Admin)...\n";
|
|
||||||
|
|
||||||
// Order matters for foreign keys
|
|
||||||
$pdo->exec("DELETE FROM audit_logs");
|
|
||||||
$pdo->exec("DELETE FROM votes");
|
|
||||||
$pdo->exec("DELETE FROM candidates");
|
|
||||||
$pdo->exec("DELETE FROM positions");
|
|
||||||
$pdo->exec("DELETE FROM election_assignments");
|
|
||||||
$pdo->exec("DELETE FROM elections");
|
|
||||||
$pdo->exec("DELETE FROM users WHERE role != 'Admin'");
|
|
||||||
|
|
||||||
// Ensure admin exists
|
|
||||||
$admin = $pdo->query("SELECT id FROM users WHERE role = 'Admin' LIMIT 1")->fetch();
|
|
||||||
if (!$admin) {
|
|
||||||
$adminId = generate_uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (id, student_id, name, email, password_hash, role, access_level) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$adminId, '00-0000', 'Admin User', 'Admin@iloilonhs.edu.ph', password_hash('Testing', PASSWORD_DEFAULT), 'Admin', 4]);
|
|
||||||
echo "Created default admin.\n";
|
|
||||||
} else {
|
|
||||||
$adminId = $admin['id'];
|
|
||||||
echo "Preserved existing admin: " . $adminId . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Adding mock elections and positions...\n";
|
|
||||||
$electionId = generate_uuid();
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO elections (id, title, description, status, start_date_and_time, end_date_and_time, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([
|
|
||||||
$electionId,
|
|
||||||
'SSG General Elections 2026',
|
|
||||||
'Annual election for Supreme Student Government.',
|
|
||||||
'Ongoing',
|
|
||||||
date('Y-m-d H:i:s', strtotime('-1 day')),
|
|
||||||
date('Y-m-d H:i:s', strtotime('+7 days')),
|
|
||||||
$adminId
|
|
||||||
]);
|
|
||||||
|
|
||||||
$positions = [
|
|
||||||
['President', 1, 1],
|
|
||||||
['Vice President', 1, 2],
|
|
||||||
['Secretary', 1, 3],
|
|
||||||
['Treasurer', 1, 4]
|
|
||||||
];
|
|
||||||
$posIds = [];
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO positions (id, election_id, name, max_votes, sort_order) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
foreach ($positions as $p) {
|
|
||||||
$id = generate_uuid();
|
|
||||||
$stmt->execute([$id, $electionId, $p[0], $p[1], $p[2]]);
|
|
||||||
$posIds[$p[0]] = $id;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Adding mock students (voters)...\n";
|
|
||||||
$tracks = ['STEM', 'ABM', 'HUMSS', 'GAS', 'TVL'];
|
|
||||||
$sections = ['A', 'B', 'C', 'D'];
|
|
||||||
$voters = [];
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO users (id, student_id, name, email, password_hash, grade_level, track, section, role) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
||||||
$assignStmt = $pdo->prepare("INSERT INTO election_assignments (id, election_id, user_id, role_in_election, assigned_by) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
|
|
||||||
for ($i = 1; $i <= 40; $i++) {
|
|
||||||
$id = generate_uuid();
|
|
||||||
$studentId = sprintf('26-%04d', $i);
|
|
||||||
$track = $tracks[array_rand($tracks)];
|
|
||||||
$grade = rand(11, 12);
|
|
||||||
$section = $sections[array_rand($sections)];
|
|
||||||
|
|
||||||
$stmt->execute([
|
|
||||||
$id,
|
|
||||||
$studentId,
|
|
||||||
"Student $i",
|
|
||||||
"student$i@iloilonhs.edu.ph",
|
|
||||||
password_hash('password', PASSWORD_DEFAULT),
|
|
||||||
$grade,
|
|
||||||
$track,
|
|
||||||
$section,
|
|
||||||
'Voter'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$assignStmt->execute([generate_uuid(), $electionId, $id, 'Voter', $adminId]);
|
|
||||||
$voters[] = ['id' => $id, 'grade' => $grade, 'track' => $track];
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Adding candidates...\n";
|
|
||||||
$candidateIds = [];
|
|
||||||
foreach ($posIds as $posName => $posId) {
|
|
||||||
for ($c = 1; $c <= 2; $c++) {
|
|
||||||
$voter = array_shift($voters);
|
|
||||||
$userId = $voter['id'];
|
|
||||||
$candId = generate_uuid();
|
|
||||||
|
|
||||||
$candStmt = $pdo->prepare("INSERT INTO candidates (id, election_id, position_id, user_id, party_name, approved) VALUES (?, ?, ?, ?, ?, ?)");
|
|
||||||
$candStmt->execute([$candId, $electionId, $posId, $userId, ($c == 1 ? 'Alpha Party' : 'Beta Party'), true]);
|
|
||||||
$candidateIds[$posId][] = $candId;
|
|
||||||
|
|
||||||
$updStmt = $pdo->prepare("UPDATE election_assignments SET role_in_election = 'Candidate' WHERE election_id = ? AND user_id = ?");
|
|
||||||
$updStmt->execute([$electionId, $userId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Generating mock votes...\n";
|
|
||||||
$voteStmt = $pdo->prepare("INSERT INTO votes (id, election_id, position_id, candidate_id, voter_id) VALUES (?, ?, ?, ?, ?)");
|
|
||||||
foreach ($voters as $v) {
|
|
||||||
// 85% turnout
|
|
||||||
if (rand(1, 100) <= 85) {
|
|
||||||
foreach ($posIds as $posId) {
|
|
||||||
$candId = $candidateIds[$posId][array_rand($candidateIds[$posId])];
|
|
||||||
$voteStmt->execute([generate_uuid(), $electionId, $posId, $candId, $v['id']]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo->commit();
|
|
||||||
echo "Done! Mock data successfully generated.\n";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
|
||||||
echo "FATAL ERROR: " . $e->getMessage() . "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
@ -1,326 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// Fetch all elections for the history
|
|
||||||
$elections = $pdo->query("SELECT * FROM elections WHERE archived = FALSE ORDER BY start_date_and_time DESC")->fetchAll();
|
|
||||||
|
|
||||||
// Extract years for the "Jump to School Year" dropdown
|
|
||||||
$years = [];
|
|
||||||
foreach ($elections as $e) {
|
|
||||||
$year = date('Y', strtotime($e['start_date_and_time']));
|
|
||||||
$years[$year] = $year;
|
|
||||||
}
|
|
||||||
krsort($years);
|
|
||||||
|
|
||||||
$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>Election History | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<style>
|
|
||||||
.history-controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.history-title-area h1 {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
.history-title-area p {
|
|
||||||
margin: 0;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.year-selector {
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
background: white;
|
|
||||||
color: #4b5563;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-history-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item-header {
|
|
||||||
padding: 20px 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item-header:hover {
|
|
||||||
background: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item-title {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #2563eb;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge {
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
.status-ongoing { background: #fffbeb; color: #d97706; }
|
|
||||||
.status-finished { background: #ecfdf5; color: #10b981; }
|
|
||||||
|
|
||||||
.election-item-body {
|
|
||||||
display: none;
|
|
||||||
padding: 24px;
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item.active .election-item-body {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.election-item.active .chevron-icon {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 24px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-card {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-sub {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #1e293b;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-section {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-title {
|
|
||||||
padding: 16px 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #1e293b;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-table th {
|
|
||||||
background: #f9fafb;
|
|
||||||
padding: 10px 20px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #64748b;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.results-table td {
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="history-controls">
|
|
||||||
<div class="history-title-area">
|
|
||||||
<h1>Election History</h1>
|
|
||||||
<p>Voter turnout and candidate results per election year</p>
|
|
||||||
</div>
|
|
||||||
<select class="year-selector" onchange="jumpToYear(this.value)">
|
|
||||||
<option value="">Jump to School Year</option>
|
|
||||||
<?php foreach ($years as $y): ?>
|
|
||||||
<option value="year-<?= $y ?>">SY <?= $y ?>-<?= $y+1 ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="election-history-list animate-stagger">
|
|
||||||
<?php foreach ($elections as $election):
|
|
||||||
$electionYear = date('Y', strtotime($election['start_date_and_time']));
|
|
||||||
$voterCount = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
|
|
||||||
$voterCount->execute([$election['id']]);
|
|
||||||
$totalVoters = $voterCount->fetchColumn();
|
|
||||||
|
|
||||||
$results = $pdo->prepare("
|
|
||||||
SELECT c.*, u.name as candidate_name, p.name as position_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
|
|
||||||
JOIN positions p ON c.position_id = p.id
|
|
||||||
WHERE c.election_id = ?
|
|
||||||
ORDER BY p.sort_order, vote_count DESC
|
|
||||||
");
|
|
||||||
$results->execute([$election['id']]);
|
|
||||||
$candidates = $results->fetchAll();
|
|
||||||
?>
|
|
||||||
<div class="election-item" id="year-<?= $electionYear ?>">
|
|
||||||
<div class="election-item-header" onclick="toggleAccordion(this)">
|
|
||||||
<div class="election-item-title"><?= htmlspecialchars($election['title']) ?></div>
|
|
||||||
<div class="election-item-right">
|
|
||||||
<a href="view_results.php?id=<?= $election['id'] ?>" class="btn-generate" onclick="event.stopPropagation()" style="background: #4f46e5; color: white; padding: 6px 12px; border-radius: 6px; font-size: 0.75rem; font-weight: 600; text-decoration: none; display: flex; align-items: center; gap: 6px;">
|
|
||||||
<i data-lucide="file-text" style="width: 14px;"></i>
|
|
||||||
GENERATE RESULTS
|
|
||||||
</a>
|
|
||||||
<span class="status-badge status-<?= strtolower($election['status']) ?>">
|
|
||||||
<?= htmlspecialchars($election['status']) ?>
|
|
||||||
</span>
|
|
||||||
<i data-lucide="chevron-down" class="chevron-icon" style="width: 18px; color: #2563eb;"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="election-item-body">
|
|
||||||
<div class="details-grid">
|
|
||||||
<div class="detail-card">
|
|
||||||
<div class="detail-label">Total Voters</div>
|
|
||||||
<div class="detail-value"><?= number_format($totalVoters) ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-card">
|
|
||||||
<div class="detail-label">Election Period</div>
|
|
||||||
<div class="detail-sub">
|
|
||||||
<?= date('M d, Y', strtotime($election['start_date_and_time'])) ?> to
|
|
||||||
<?= date('M d, Y', strtotime($election['end_date_and_time'])) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="results-section">
|
|
||||||
<div class="results-title">Candidate Results</div>
|
|
||||||
<table class="results-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Candidate Name</th>
|
|
||||||
<th>Position</th>
|
|
||||||
<th>Party</th>
|
|
||||||
<th>Votes</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($candidates)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="text-align: center; padding: 20px; color: #94a3b8;">
|
|
||||||
No candidate results available for this election.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($candidates as $cand): ?>
|
|
||||||
<tr>
|
|
||||||
<td style="font-weight: 500; color: #1e293b;"><?= htmlspecialchars($cand['candidate_name']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($cand['position_name']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($cand['party_name'] ?? 'Independent') ?></td>
|
|
||||||
<td style="font-weight: 600; color: #2563eb;"><?= number_format($cand['vote_count']) ?></td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
function toggleAccordion(header) {
|
|
||||||
const item = header.parentElement;
|
|
||||||
const isActive = item.classList.contains('active');
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
item.classList.remove('active');
|
|
||||||
} else {
|
|
||||||
item.classList.add('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function jumpToYear(id) {
|
|
||||||
if (!id) return;
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) {
|
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
el.classList.add('active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
$regions = [
|
|
||||||
'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2',
|
|
||||||
'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2',
|
|
||||||
'eu-central-1', 'eu-west-1', 'eu-west-2', 'eu-west-3',
|
|
||||||
'sa-east-1', 'ca-central-1', 'me-south-1', 'af-south-1'
|
|
||||||
];
|
|
||||||
|
|
||||||
$pass = 'gA82h8K80T5QUAwi';
|
|
||||||
$projectRef = 'siqeqnizegizxemrfgkf';
|
|
||||||
|
|
||||||
foreach ($regions as $region) {
|
|
||||||
$host = "aws-0-$region.pooler.supabase.com";
|
|
||||||
$user = "postgres.$projectRef";
|
|
||||||
echo "Testing $region ($host)... ";
|
|
||||||
try {
|
|
||||||
$dsn = "pgsql:host=$host;port=6543;dbname=postgres;connect_timeout=5";
|
|
||||||
$pdo = new PDO($dsn, $user, $pass);
|
|
||||||
echo "SUCCESS!\n";
|
|
||||||
exit(0);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
if (strpos($e->getMessage(), 'Tenant or user not found') !== false) {
|
|
||||||
echo "Not here.\n";
|
|
||||||
} else {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
class SupabaseAuth {
|
|
||||||
private static function request(string $method, string $endpoint, array $data = [], bool $useServiceKey = false): array {
|
|
||||||
$url = rtrim(SUPABASE_URL, '/') . $endpoint;
|
|
||||||
$key = $useServiceKey ? SUPABASE_SERVICE_ROLE_KEY : SUPABASE_SERVICE_ROLE_KEY; // Always use service key for admin actions
|
|
||||||
|
|
||||||
$ch = curl_init($url);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'Content-Type: application/json',
|
|
||||||
'apikey: ' . $key,
|
|
||||||
'Authorization: Bearer ' . $key
|
|
||||||
];
|
|
||||||
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
||||||
|
|
||||||
if (!empty($data)) {
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$decoded = json_decode((string)$response, true);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => $httpCode,
|
|
||||||
'data' => $decoded,
|
|
||||||
'error' => $httpCode >= 400 ? ($decoded['msg'] ?? $decoded['error_description'] ?? 'Unknown error') : null
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function createUser(string $email, string $password): array {
|
|
||||||
// Use Admin API to create user without email verification
|
|
||||||
return self::request('POST', '/auth/v1/admin/users', [
|
|
||||||
'email' => $email,
|
|
||||||
'password' => $password,
|
|
||||||
'email_confirm' => true
|
|
||||||
], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function signIn(string $email, string $password): array {
|
|
||||||
return self::request('POST', '/auth/v1/token?grant_type=password', [
|
|
||||||
'email' => $email,
|
|
||||||
'password' => $password
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function updateUserPassword(string $uid, string $password): array {
|
|
||||||
return self::request('PUT', '/auth/v1/admin/users/' . $uid, [
|
|
||||||
'password' => $password
|
|
||||||
], true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getUserByEmail(string $email): ?array {
|
|
||||||
$res = self::request('GET', '/auth/v1/admin/users', [], true);
|
|
||||||
if ($res['status'] === 200 && isset($res['data']['users'])) {
|
|
||||||
foreach ($res['data']['users'] as $user) {
|
|
||||||
if ($user['email'] === $email) return $user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<header class="top-header">
|
|
||||||
<form action="search_results.php" method="GET" class="search-bar">
|
|
||||||
<i data-lucide="search" style="width: 16px; color: #94a3b8;"></i>
|
|
||||||
<input type="text" name="q" placeholder="Search for voters, candidates, or records..." value="<?= htmlspecialchars($_GET['q'] ?? '') ?>">
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="user-profile">
|
|
||||||
<div class="user-info">
|
|
||||||
<div class="user-name"><?= htmlspecialchars($user['name'] ?? 'System Administrator') ?></div>
|
|
||||||
<div class="user-role"><?= htmlspecialchars($user['role'] ?? 'Admin') ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="user-avatar">
|
|
||||||
<?= strtoupper(substr($user['name'] ?? 'S', 0, 1)) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
$activeElectionId = get_active_election_id();
|
|
||||||
$allElections = get_all_elections();
|
|
||||||
$currentElection = get_active_election();
|
|
||||||
$currentPage = basename($_SERVER['PHP_SELF']);
|
|
||||||
?>
|
|
||||||
<aside class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<div class="sidebar-brand">CLICK TO VOTE</div>
|
|
||||||
<div class="sidebar-subtitle">Administrator Portal</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="election-selector-container" style="padding: 0 20px; margin-bottom: 20px;">
|
|
||||||
<label style="font-size: 10px; font-weight: 700; color: #94a3b8; text-transform: uppercase; margin-bottom: 8px; display: block;">ACTIVE ELECTION</label>
|
|
||||||
<select onchange="window.location.href='?set_election_id=' + this.value" style="width: 100%; padding: 8px; border-radius: 6px; border: 1px solid #e2e8f0; background: #f8fafc; font-size: 12px; font-weight: 500; color: #1e293b; cursor: pointer;">
|
|
||||||
<?php foreach ($allElections as $e): ?>
|
|
||||||
<option value="<?= $e['id'] ?>" <?= $activeElectionId === $e['id'] ? 'selected' : '' ?>>
|
|
||||||
<?= htmlspecialchars($e['title']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($allElections)): ?>
|
|
||||||
<option disabled>No elections found</option>
|
|
||||||
<?php endif; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="sidebar-nav">
|
|
||||||
<a href="dashboard.php" class="nav-item <?= $currentPage === 'dashboard.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="layout-dashboard"></i>
|
|
||||||
Election Dashboard
|
|
||||||
</a>
|
|
||||||
<a href="election_history.php" class="nav-item <?= $currentPage === 'election_history.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="history"></i>
|
|
||||||
Election History
|
|
||||||
</a>
|
|
||||||
<a href="voter_management.php" class="nav-item <?= $currentPage === 'voter_management.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="users"></i>
|
|
||||||
Voter Management
|
|
||||||
</a>
|
|
||||||
<a href="candidate_management.php" class="nav-item <?= $currentPage === 'candidate_management.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="user-square-2"></i>
|
|
||||||
Candidate Management
|
|
||||||
</a>
|
|
||||||
<a href="officers_management.php" class="nav-item <?= $currentPage === 'officers_management.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="shield-check"></i>
|
|
||||||
Officers Management
|
|
||||||
</a>
|
|
||||||
<a href="reports_audit.php" class="nav-item <?= $currentPage === 'reports_audit.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="file-text"></i>
|
|
||||||
Reports & Audit
|
|
||||||
</a>
|
|
||||||
<a href="settings.php" class="nav-item <?= $currentPage === 'settings.php' ? 'active' : '' ?>">
|
|
||||||
<i data-lucide="settings"></i>
|
|
||||||
Settings
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
|
||||||
<a href="logout.php" class="nav-item" style="color: #ef4444;">
|
|
||||||
<i data-lucide="log-out"></i>
|
|
||||||
Logout
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
335
index.php
@ -1,233 +1,150 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once 'auth_helper.php';
|
declare(strict_types=1);
|
||||||
$user = get_user();
|
@ini_set('display_errors', '1');
|
||||||
|
@error_reporting(E_ALL);
|
||||||
|
@date_default_timezone_set('UTC');
|
||||||
|
|
||||||
if (!$user) {
|
$phpVersion = PHP_VERSION;
|
||||||
include 'landing.php';
|
$now = date('Y-m-d H:i:s');
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])) {
|
|
||||||
include 'dashboard.php';
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
|
|
||||||
// Voter redirection logic
|
|
||||||
if ($user['role'] === 'Voter') {
|
|
||||||
// Find ongoing elections that this voter is assigned to
|
|
||||||
$stmt = $pdo->prepare("
|
|
||||||
SELECT e.* FROM elections e
|
|
||||||
JOIN election_assignments ea ON e.id = ea.election_id
|
|
||||||
WHERE ea.user_id = ?
|
|
||||||
AND e.status = 'Ongoing'
|
|
||||||
AND e.archived = FALSE
|
|
||||||
AND e.end_date_and_time > CURRENT_TIMESTAMP
|
|
||||||
");
|
|
||||||
$stmt->execute([$user['id']]);
|
|
||||||
$activeElections = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Filter out elections where the user has already voted
|
|
||||||
$votedElectionsStmt = $pdo->prepare("SELECT election_id FROM votes WHERE voter_id = ?");
|
|
||||||
$votedElectionsStmt->execute([$user['id']]);
|
|
||||||
$votedIds = $votedElectionsStmt->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
$eligibleElections = array_filter($activeElections, function($e) use ($votedIds) {
|
|
||||||
return !in_array($e['id'], $votedIds);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (count($eligibleElections) === 1) {
|
|
||||||
$singleElection = reset($eligibleElections);
|
|
||||||
header("Location: ballot.php?id=" . $singleElection['id']);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no eligible elections but they were assigned to some active ones, they've already voted
|
|
||||||
if (count($eligibleElections) === 0 && count($activeElections) > 0) {
|
|
||||||
if (!isset($_GET['success']) && !isset($_GET['error'])) {
|
|
||||||
header("Location: index.php?error=already_voted");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For voters, only show their assigned elections in the list
|
|
||||||
$elections = $activeElections;
|
|
||||||
} else {
|
|
||||||
$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>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Election Dashboard | <?= htmlspecialchars($projectDescription) ?></title>
|
<title>New Style</title>
|
||||||
<meta name="description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
<?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; ?>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="assets/css/style.css?v=<?= time() ?>">
|
|
||||||
</head>
|
|
||||||
<?php $isVotedModalActive = (isset($_GET['success']) && $_GET['success'] === 'voted') || (isset($_GET['error']) && $_GET['error'] === 'already_voted'); ?>
|
|
||||||
<body class="<?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
<nav class="navbar" <?= $isVotedModalActive ? 'style="display: none;"' : '' ?>>
|
|
||||||
<a href="index.php" class="brand">Click to Vote</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>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container" <?= $isVotedModalActive ? 'style="display: none;"' : '' ?>>
|
|
||||||
<?php if (isset($_GET['success']) && $_GET['success'] !== 'voted'): ?>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Voted Modal -->
|
|
||||||
<div id="votedModal" class="modal-overlay <?= (isset($_GET['success']) && $_GET['success'] === 'voted') || (isset($_GET['error']) && $_GET['error'] === 'already_voted') ? 'active' : '' ?>">
|
|
||||||
<div class="confirm-modal" style="text-align: center;">
|
|
||||||
<div class="modal-icon" style="background: #f0fdf4; color: #10b981; margin: 0 auto 24px;">
|
|
||||||
<svg style="width: 32px; height: 32px;" 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="3" d="M5 13l4 4L19 7"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2>Already Voted</h2>
|
|
||||||
<p>You have already submitted your vote for this election. Thank you for participating!</p>
|
|
||||||
<div style="margin-top: 32px;">
|
|
||||||
<a href="logout.php" class="btn btn-primary" style="display: block; width: 100%; text-decoration: none; padding: 12px 0; background: #4f46e5; border-radius: 12px; font-weight: 700; color: white;">Back to Login</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Modal Styles */
|
:root {
|
||||||
.modal-overlay {
|
--bg-color-start: #6a11cb;
|
||||||
display: none;
|
--bg-color-end: #2575fc;
|
||||||
position: fixed;
|
--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;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(15, 23, 42, 0.6);
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
z-index: 2000;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-overlay.active {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.confirm-modal {
|
|
||||||
background: white;
|
|
||||||
border-radius: 20px;
|
|
||||||
max-width: 450px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 32px;
|
height: 100%;
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
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: modalIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
animation: bg-pan 20s linear infinite;
|
||||||
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
@keyframes bg-pan {
|
||||||
@keyframes modalIn {
|
0% { background-position: 0% 0%; }
|
||||||
from { transform: scale(0.9) translateY(20px); opacity: 0; }
|
100% { background-position: 100% 100%; }
|
||||||
to { transform: scale(1) translateY(0); opacity: 1; }
|
|
||||||
}
|
}
|
||||||
|
main {
|
||||||
.modal-icon {
|
padding: 2rem;
|
||||||
width: 64px;
|
}
|
||||||
height: 64px;
|
.card {
|
||||||
background: #eef2ff;
|
background: var(--card-bg-color);
|
||||||
color: #4f46e5;
|
border: 1px solid var(--card-border-color);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
display: flex;
|
padding: 2rem;
|
||||||
align-items: center;
|
backdrop-filter: blur(20px);
|
||||||
justify-content: center;
|
-webkit-backdrop-filter: blur(20px);
|
||||||
margin-bottom: 24px;
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
.loader {
|
||||||
.confirm-modal h2 {
|
margin: 1.25rem auto 1.25rem;
|
||||||
margin: 0 0 12px 0;
|
width: 48px;
|
||||||
font-size: 1.5rem;
|
height: 48px;
|
||||||
font-weight: 800;
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||||
color: #1e293b;
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
}
|
}
|
||||||
|
@keyframes spin {
|
||||||
.confirm-modal p {
|
from { transform: rotate(0deg); }
|
||||||
margin: 0 0 12px 0;
|
to { transform: rotate(360deg); }
|
||||||
color: #64748b;
|
}
|
||||||
font-size: 1rem;
|
.hint {
|
||||||
line-height: 1.6;
|
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>
|
</style>
|
||||||
|
</head>
|
||||||
<footer style="text-align: center; color: var(--text-muted); padding: 2rem; <?= $isVotedModalActive ? 'display: none;' : '' ?>">
|
<body>
|
||||||
© <?= date('Y') ?> Click to Vote | High School Online Election System
|
<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>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer>
|
||||||
|
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
170
landing.php
@ -1,170 +0,0 @@
|
|||||||
<!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>
|
|
||||||
170
login.php
@ -1,170 +0,0 @@
|
|||||||
<?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'] ?? '';
|
|
||||||
|
|
||||||
// 1. Find user locally to verify student_id, email and role match
|
|
||||||
$stmt = db()->prepare("SELECT * FROM users WHERE student_id = ? AND email = ? AND role = ?");
|
|
||||||
$stmt->execute([$student_id, $email, $role]);
|
|
||||||
$user = $stmt->fetch();
|
|
||||||
|
|
||||||
if ($user) {
|
|
||||||
// 2. Authenticate with Supabase
|
|
||||||
$auth = SupabaseAuth::signIn($email, $password);
|
|
||||||
|
|
||||||
if ($auth['error']) {
|
|
||||||
// Check if user exists locally with this password but not in Supabase yet
|
|
||||||
if (!empty($user['password_hash']) && password_verify($password, $user['password_hash'])) {
|
|
||||||
// Migrate to Supabase
|
|
||||||
$supabaseUser = SupabaseAuth::createUser($email, $password);
|
|
||||||
if (!$supabaseUser['error']) {
|
|
||||||
$auth = SupabaseAuth::signIn($email, $password);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$auth['error']) {
|
|
||||||
// Update supabase_uid if missing
|
|
||||||
if (empty($user['supabase_uid'])) {
|
|
||||||
$supabase_uid = $auth['data']['user']['id'] ?? null;
|
|
||||||
$upd = db()->prepare("UPDATE users SET supabase_uid = ? WHERE id = ?");
|
|
||||||
$upd->execute([$supabase_uid, $user['id']]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$_SESSION['user_id'] = $user['id'];
|
|
||||||
$_SESSION['user_role'] = $user['role'];
|
|
||||||
header('Location: index.php');
|
|
||||||
exit;
|
|
||||||
} else {
|
|
||||||
$error = 'Authentication failed: ' . $auth['error'];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$error = 'Invalid Credentials. Please check your UID, Email, and Role.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($error && isset($_POST['role']) && str_contains($_SERVER['HTTP_REFERER'] ?? '', 'index.php')) {
|
|
||||||
// Only redirect back if we actually came from landing page modal
|
|
||||||
header('Location: index.php?error=' . urlencode($error));
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Login - Iloilo National High School</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() ?>">
|
|
||||||
<style>
|
|
||||||
.login-page-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="landing-page" style="background-image: url('assets/images/background.jpg?v=<?= filemtime('assets/images/background.jpg') ?>');">
|
|
||||||
<div class="login-page-container">
|
|
||||||
<div class="login-modal" style="display: block; position: static; animation: none;">
|
|
||||||
<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>Election Login</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<?php if ($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($error) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<form 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>
|
|
||||||
</form>
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<small style="color: var(--landing-text-muted); font-size: 0.8rem;">Don't have an account? <a href="signup.php" style="color: var(--landing-primary); font-weight: 600; text-decoration: none;">Register here</a></small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?php
|
|
||||||
session_start();
|
|
||||||
session_destroy();
|
|
||||||
header('Location: index.php');
|
|
||||||
exit;
|
|
||||||
@ -1,162 +0,0 @@
|
|||||||
<?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 rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0; font-size: 1.75rem; color: #1e293b;">Candidates for <?= htmlspecialchars($position['name']) ?></h1>
|
|
||||||
<p class="welcome-msg" style="margin-top: 4px;"><?= htmlspecialchars($position['election_title']) ?></p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="view_election.php?id=<?= $position['election_id'] ?>" style="display: flex; align-items: center; gap: 8px; color: #4f46e5; font-weight: 600; text-decoration: none; background: #ffffff; padding: 10px 20px; border-radius: 8px; border: 1px solid #e2e8f0;">
|
|
||||||
<i data-lucide="arrow-left" style="width: 16px;"></i>
|
|
||||||
Back to Election
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 24px;">
|
|
||||||
<div style="background: #ffffff; border: 1px solid #f3f4f6; border-radius: 16px; padding: 24px; height: fit-content;">
|
|
||||||
<h3 style="margin-top: 0; font-size: 1.1rem; font-weight: 700; color: #1e293b; margin-bottom: 20px;">Add New 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 style="margin-bottom: 16px;">
|
|
||||||
<label style="display: block; font-size: 0.75rem; font-weight: 600; color: #64748b; margin-bottom: 6px;">Select Student</label>
|
|
||||||
<select name="user_id" style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; outline: none; background: #ffffff; font-size: 0.875rem;" 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 style="margin-bottom: 16px;">
|
|
||||||
<label style="display: block; font-size: 0.75rem; font-weight: 600; color: #64748b; margin-bottom: 6px;">Party Name</label>
|
|
||||||
<input type="text" name="party_name" style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; outline: none; font-size: 0.875rem;" placeholder="e.g. Independent">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 24px;">
|
|
||||||
<label style="display: block; font-size: 0.75rem; font-weight: 600; color: #64748b; margin-bottom: 6px;">Manifesto</label>
|
|
||||||
<textarea name="manifesto" rows="4" style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; outline: none; font-size: 0.875rem; font-family: inherit;" placeholder="Briefly describe the candidate's goals..."></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" style="width: 100%; background: #4f46e5; color: white; border: none; padding: 12px; border-radius: 8px; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 8px;">
|
|
||||||
<i data-lucide="user-plus" style="width: 18px;"></i>
|
|
||||||
Register Candidate
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: #ffffff; border: 1px solid #f3f4f6; border-radius: 16px; overflow: hidden;">
|
|
||||||
<div style="padding: 24px; border-bottom: 1px solid #f3f4f6;">
|
|
||||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 700; color: #1e293b;">Registered Candidates</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background: #f9fafb; border-bottom: 1px solid #f3f4f6;">
|
|
||||||
<th style="padding: 12px 24px; text-align: left; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Student Info</th>
|
|
||||||
<th style="padding: 12px 24px; text-align: left; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Party Affiliation</th>
|
|
||||||
<th style="padding: 12px 24px; text-align: left; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Status</th>
|
|
||||||
<th style="padding: 12px 24px; text-align: right; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($candidates)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="padding: 48px; text-align: center; color: #94a3b8;">
|
|
||||||
<div style="display: flex; flex-direction: column; align-items: center; gap: 12px;">
|
|
||||||
<i data-lucide="users" style="width: 48px; height: 48px; color: #e2e8f0;"></i>
|
|
||||||
<span>No candidates have been registered for this position yet.</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($candidates as $c): ?>
|
|
||||||
<tr style="border-bottom: 1px solid #f3f4f6; transition: background 0.2s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
|
|
||||||
<td style="padding: 16px 24px;">
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px;">
|
|
||||||
<div style="width: 36px; height: 36px; background: #e0e7ff; color: #4f46e5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 0.875rem;">
|
|
||||||
<?= strtoupper(substr($c['name'], 0, 1)) ?>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="font-weight: 600; color: #1e293b;"><?= htmlspecialchars($c['name']) ?></div>
|
|
||||||
<div style="font-size: 0.75rem; color: #64748b;"><?= $c['student_id'] ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 16px 24px;">
|
|
||||||
<span style="background: #f1f5f9; padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 600; color: #475569;">
|
|
||||||
<?= htmlspecialchars($c['party_name'] ?: 'Independent') ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 16px 24px;">
|
|
||||||
<span style="padding: 4px 12px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; background: <?= $c['approved'] ? '#dcfce7' : '#f1f5f9' ?>; color: <?= $c['approved'] ? '#166534' : '#475569' ?>;">
|
|
||||||
<?= $c['approved'] ? 'Approved' : 'Pending' ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 16px 24px; text-align: right;">
|
|
||||||
<form action="api/toggle_candidate_approval.php" method="POST" style="display:inline;">
|
|
||||||
<input type="hidden" name="id" value="<?= $c['id'] ?>">
|
|
||||||
<button type="submit" style="background: <?= $c['approved'] ? '#fef2f2' : '#f0f9ff' ?>; color: <?= $c['approved'] ? '#ef4444' : '#0ea5e9' ?>; border: 1px solid <?= $c['approved'] ? '#fee2e2' : '#e0f2fe' ?>; padding: 6px 12px; border-radius: 6px; font-size: 0.75rem; font-weight: 700; cursor: pointer; transition: all 0.2s;">
|
|
||||||
<?= $c['approved'] ? 'Revoke Approval' : 'Approve' ?>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
23
migrate.php
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$pdo = db();
|
|
||||||
$migrationFiles = glob(__DIR__ . '/db/migrations/*.sql');
|
|
||||||
sort($migrationFiles);
|
|
||||||
|
|
||||||
foreach ($migrationFiles as $file) {
|
|
||||||
$sql = file_get_contents($file);
|
|
||||||
$statements = explode(';', $sql);
|
|
||||||
foreach ($statements as $statement) {
|
|
||||||
$statement = trim($statement);
|
|
||||||
if ($statement) {
|
|
||||||
$pdo->exec($statement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
echo "Executed: " . basename($file) . "\n";
|
|
||||||
}
|
|
||||||
echo "All migrations completed successfully!\n";
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Migration failed: " . $e->getMessage() . "\n";
|
|
||||||
}
|
|
||||||
@ -1,273 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$election = get_active_election();
|
|
||||||
|
|
||||||
// Fetch officers assigned to this election grouped by role
|
|
||||||
$query = "SELECT u.* FROM users u
|
|
||||||
JOIN election_assignments ea ON u.id = ea.user_id
|
|
||||||
WHERE ea.election_id = ? AND u.deleted_at IS NULL";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($query . " AND u.role = 'Admin' ORDER BY u.name");
|
|
||||||
$stmt->execute([$electionId]);
|
|
||||||
$admins = $stmt->fetchAll();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($query . " AND u.role = 'Adviser' ORDER BY u.name");
|
|
||||||
$stmt->execute([$electionId]);
|
|
||||||
$advisers = $stmt->fetchAll();
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($query . " AND u.role = 'Officer' ORDER BY u.name");
|
|
||||||
$stmt->execute([$electionId]);
|
|
||||||
$officers = $stmt->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>Officer Management | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<link rel="stylesheet" href="assets/css/officers_management.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<div style="display: flex; align-items: center; gap: 16px;">
|
|
||||||
<div class="header-icon-container">
|
|
||||||
<i data-lucide="shield-check" style="width: 24px; color: #4f46e5;"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0; font-size: 1.5rem; color: var(--text-main);">Officer Management</h1>
|
|
||||||
<p style="margin: 4px 0 0 0; color: var(--text-muted); font-size: 0.875rem;">Personnel for <?= htmlspecialchars($election['title'] ?? 'Selected Election') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Register New Officer Form -->
|
|
||||||
<section class="registration-section animate-stagger">
|
|
||||||
<div class="registration-header">
|
|
||||||
<i data-lucide="plus-circle" style="width: 20px; color: #2563eb;"></i>
|
|
||||||
Assign New Officer to Election
|
|
||||||
</div>
|
|
||||||
<form class="registration-form" action="api/add_officer.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Full Name</label>
|
|
||||||
<input type="text" name="name" placeholder="Enter name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Student ID / Personnel ID</label>
|
|
||||||
<input type="text" name="student_id" placeholder="XX-XXXX" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Email</label>
|
|
||||||
<input type="email" name="email" placeholder="email@school.edu" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Role</label>
|
|
||||||
<select name="role" required>
|
|
||||||
<option value="Admin">Admin</option>
|
|
||||||
<option value="Adviser">Adviser</option>
|
|
||||||
<option value="Officer" selected>Officer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Password</label>
|
|
||||||
<input type="text" name="password" value="iloilohns" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 20px; display: flex; justify-content: flex-end;">
|
|
||||||
<button type="submit" class="btn-save-officer">
|
|
||||||
<i data-lucide="save" style="width: 18px;"></i>
|
|
||||||
ASSIGN TO ELECTION
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Officer Categories Grid -->
|
|
||||||
<div class="officer-management-grid animate-fade-in">
|
|
||||||
<!-- Admins -->
|
|
||||||
<div class="officer-category-card">
|
|
||||||
<div class="category-header">
|
|
||||||
<div class="category-title">
|
|
||||||
<i data-lucide="user-cog" style="width: 18px; color: #4f46e5;"></i>
|
|
||||||
Admins
|
|
||||||
</div>
|
|
||||||
<span class="active-count"><?= count($admins) ?> ACTIVE</span>
|
|
||||||
</div>
|
|
||||||
<div class="officer-list">
|
|
||||||
<?php foreach ($admins as $o): ?>
|
|
||||||
<div class="officer-item">
|
|
||||||
<div class="officer-main-info">
|
|
||||||
<div class="officer-avatar"><?= strtoupper(substr($o['name'], 0, 1)) ?></div>
|
|
||||||
<div class="officer-details">
|
|
||||||
<span class="officer-name"><?= htmlspecialchars($o['name']) ?></span>
|
|
||||||
<span class="officer-meta"><?= htmlspecialchars($o['student_id']) ?> | <?= htmlspecialchars($o['email']) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="officer-actions">
|
|
||||||
<button title="Edit" onclick='editOfficer(<?= json_encode($o) ?>)'><i data-lucide="edit-3" style="width: 16px;"></i></button>
|
|
||||||
<button title="Delete" style="color: #ef4444;" onclick="deleteOfficer('<?= $o['id'] ?>', '<?= htmlspecialchars($o['name']) ?>')"><i data-lucide="trash-2" style="width: 16px;"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($admins)): ?>
|
|
||||||
<div class="empty-state">No admins assigned.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMEA Advisers -->
|
|
||||||
<div class="officer-category-card">
|
|
||||||
<div class="category-header">
|
|
||||||
<div class="category-title">
|
|
||||||
<i data-lucide="user-check" style="width: 18px; color: #f97316;"></i>
|
|
||||||
Advisers
|
|
||||||
</div>
|
|
||||||
<span class="active-count" style="background: #fff7ed; color: #ea580c;"><?= count($advisers) ?> ACTIVE</span>
|
|
||||||
</div>
|
|
||||||
<div class="officer-list">
|
|
||||||
<?php foreach ($advisers as $o): ?>
|
|
||||||
<div class="officer-item">
|
|
||||||
<div class="officer-main-info">
|
|
||||||
<div class="officer-avatar" style="background: #eff6ff; color: #2563eb;"><?= strtoupper(substr($o['name'], 0, 1)) ?></div>
|
|
||||||
<div class="officer-details">
|
|
||||||
<span class="officer-name"><?= htmlspecialchars($o['name']) ?></span>
|
|
||||||
<span class="officer-meta"><?= htmlspecialchars($o['student_id']) ?> | <?= htmlspecialchars($o['email']) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="officer-actions">
|
|
||||||
<button title="Edit" onclick='editOfficer(<?= json_encode($o) ?>)'><i data-lucide="edit-3" style="width: 16px;"></i></button>
|
|
||||||
<button title="Delete" style="color: #ef4444;" onclick="deleteOfficer('<?= $o['id'] ?>', '<?= htmlspecialchars($o['name']) ?>')"><i data-lucide="trash-2" style="width: 16px;"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($advisers)): ?>
|
|
||||||
<div class="empty-state">No advisers assigned.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- COMEA Officers -->
|
|
||||||
<div class="officer-category-card">
|
|
||||||
<div class="category-header">
|
|
||||||
<div class="category-title">
|
|
||||||
<i data-lucide="users" style="width: 18px; color: #10b981;"></i>
|
|
||||||
COMEA Officers
|
|
||||||
</div>
|
|
||||||
<span class="active-count" style="background: #f0fdf4; color: #16a34a;"><?= count($officers) ?> ACTIVE</span>
|
|
||||||
</div>
|
|
||||||
<div class="officer-list">
|
|
||||||
<?php foreach ($officers as $o): ?>
|
|
||||||
<div class="officer-item">
|
|
||||||
<div class="officer-main-info">
|
|
||||||
<div class="officer-avatar" style="background: #ecfdf5; color: #059669;"><?= strtoupper(substr($o['name'], 0, 1)) ?></div>
|
|
||||||
<div class="officer-details">
|
|
||||||
<span class="officer-name"><?= htmlspecialchars($o['name']) ?></span>
|
|
||||||
<span class="officer-meta"><?= htmlspecialchars($o['student_id']) ?> | <?= htmlspecialchars($o['email']) ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="officer-actions">
|
|
||||||
<button title="Edit" onclick='editOfficer(<?= json_encode($o) ?>)'><i data-lucide="edit-3" style="width: 16px;"></i></button>
|
|
||||||
<button title="Delete" style="color: #ef4444;" onclick="deleteOfficer('<?= $o['id'] ?>', '<?= htmlspecialchars($o['name']) ?>')"><i data-lucide="trash-2" style="width: 16px;"></i></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php if (empty($officers)): ?>
|
|
||||||
<div class="empty-state">No officers assigned.</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Edit Officer Modal -->
|
|
||||||
<div id="editOfficerModal" class="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:1000; align-items:center; justify-content:center;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 style="margin:0; font-size:1.25rem;">Edit Officer</h2>
|
|
||||||
<button onclick="closeModal('editOfficerModal')" style="border:none; background:none; cursor:pointer; color: var(--text-muted);"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/update_officer.php" method="POST">
|
|
||||||
<input type="hidden" name="id" id="edit_officer_id">
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>FULL NAME</label>
|
|
||||||
<input type="text" name="name" id="edit_officer_name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>EMAIL ADDRESS</label>
|
|
||||||
<input type="email" name="email" id="edit_officer_email" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>ROLE</label>
|
|
||||||
<select name="role" id="edit_officer_role" required>
|
|
||||||
<option value="Admin">Admin</option>
|
|
||||||
<option value="Adviser">Adviser</option>
|
|
||||||
<option value="Officer">Officer</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:24px;">
|
|
||||||
<label>NEW PASSWORD (OPTIONAL)</label>
|
|
||||||
<input type="password" name="password" placeholder="Leave blank to keep current">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('editOfficerModal')" class="btn-cancel">Cancel</button>
|
|
||||||
<button type="submit" class="btn-submit" style="background: var(--accent-color); color: white;">Update Officer</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
function openModal(id) {
|
|
||||||
document.getElementById(id).style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal(id) {
|
|
||||||
document.getElementById(id).style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function editOfficer(officer) {
|
|
||||||
document.getElementById('edit_officer_id').value = officer.id;
|
|
||||||
document.getElementById('edit_officer_name').value = officer.name;
|
|
||||||
document.getElementById('edit_officer_email').value = officer.email;
|
|
||||||
document.getElementById('edit_officer_role').value = officer.role;
|
|
||||||
openModal('editOfficerModal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteOfficer(id, name) {
|
|
||||||
if (confirm(`Are you sure you want to remove ${name} from this election?`)) {
|
|
||||||
window.location.href = `api/delete_officer.php?id=${id}&election_id=<?= $electionId ?>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target.classList.contains('modal')) {
|
|
||||||
event.target.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$election = get_active_election();
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
$search = $_GET['search'] ?? '';
|
|
||||||
|
|
||||||
// Query Construction
|
|
||||||
$query = "SELECT l.*, u.student_id, u.role, u.name as user_name
|
|
||||||
FROM audit_logs l
|
|
||||||
LEFT JOIN users u ON l.user_id = u.id
|
|
||||||
WHERE (l.election_id = ? OR l.election_id IS NULL)";
|
|
||||||
|
|
||||||
$params = [$electionId];
|
|
||||||
|
|
||||||
if ($search) {
|
|
||||||
$query .= " AND (l.action LIKE ? OR l.details LIKE ? OR u.student_id LIKE ? OR u.name LIKE ?)";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
}
|
|
||||||
|
|
||||||
$query .= " ORDER BY l.created_at DESC LIMIT 100";
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($query);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$logs = $stmt->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>Reports & Audit | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<link rel="stylesheet" href="assets/css/reports_audit.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<div style="display: flex; align-items: center; gap: 16px;">
|
|
||||||
<div class="header-icon-container">
|
|
||||||
<i data-lucide="file-text" style="width: 24px; color: #4f46e5;"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0; font-size: 1.5rem; color: #1e293b;">Reports & Audit Trail</h1>
|
|
||||||
<p style="margin: 4px 0 0 0; color: #64748b; font-size: 0.875rem;">Monitoring activity for <?= htmlspecialchars($election['title'] ?? 'Selected Election') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters & Table Section -->
|
|
||||||
<div class="content-section animate-fade-in">
|
|
||||||
|
|
||||||
|
|
||||||
<table class="audit-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>TIMESTAMP</th>
|
|
||||||
<th>USER</th>
|
|
||||||
<th>ACTION</th>
|
|
||||||
<th>DETAILS</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($logs)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="text-align: center; color: #94a3b8; padding: 32px;">No activity logs found for this election.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($logs as $log): ?>
|
|
||||||
<tr>
|
|
||||||
<td style="white-space: nowrap;"><?= date('M d, Y H:i:s', strtotime($log['created_at'])) ?></td>
|
|
||||||
<td>
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<div class="user-avatar-small" style="width: 24px; height: 24px; font-size: 10px; background: #f1f5f9; color: #475569; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 600;">
|
|
||||||
<?= strtoupper(substr($log['user_name'] ?? 'S', 0, 1)) ?>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style="font-weight: 600; font-size: 13px;"><?= htmlspecialchars($log['user_name'] ?? 'SYSTEM') ?></div>
|
|
||||||
<div style="font-size: 11px; color: #94a3b8;"><?= htmlspecialchars($log['role'] ?? 'SYSTEM') ?> (<?= htmlspecialchars($log['student_id'] ?? 'N/A') ?>)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="action-badge"><?= strtoupper(htmlspecialchars($log['action'])) ?></span>
|
|
||||||
</td>
|
|
||||||
<td style="color: #64748b; font-size: 13px;">
|
|
||||||
<?= htmlspecialchars($log['details'] ?? 'No additional details') ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$query = $_GET['q'] ?? '';
|
|
||||||
|
|
||||||
$voters = [];
|
|
||||||
$candidates = [];
|
|
||||||
$logs = [];
|
|
||||||
|
|
||||||
if ($query) {
|
|
||||||
$search = "%$query%";
|
|
||||||
|
|
||||||
// Search Voters
|
|
||||||
$stmt = $pdo->prepare("SELECT u.* FROM users u
|
|
||||||
JOIN election_assignments ea ON u.id = ea.user_id
|
|
||||||
WHERE ea.election_id = ? AND ea.role_in_election = 'Voter'
|
|
||||||
AND (u.name LIKE ? OR u.email LIKE ? OR u.student_id LIKE ?)");
|
|
||||||
$stmt->execute([$electionId, $search, $search, $search]);
|
|
||||||
$voters = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Search Candidates
|
|
||||||
$stmt = $pdo->prepare("SELECT c.*, u.name as user_name, u.student_id
|
|
||||||
FROM candidates c
|
|
||||||
JOIN users u ON c.user_id = u.id
|
|
||||||
WHERE c.election_id = ?
|
|
||||||
AND (u.name LIKE ? OR c.position LIKE ? OR c.platform LIKE ?)");
|
|
||||||
$stmt->execute([$electionId, $search, $search, $search]);
|
|
||||||
$candidates = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Search Records (Audit Logs)
|
|
||||||
$stmt = $pdo->prepare("SELECT l.*, u.name as user_name
|
|
||||||
FROM audit_logs l
|
|
||||||
LEFT JOIN users u ON l.user_id = u.id
|
|
||||||
WHERE (l.election_id = ? OR l.election_id IS NULL)
|
|
||||||
AND (l.action LIKE ? OR l.details LIKE ? OR u.name LIKE ?)");
|
|
||||||
$stmt->execute([$electionId, $search, $search, $search]);
|
|
||||||
$logs = $stmt->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>Search Results | <?= htmlspecialchars($projectDescription) ?></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/dashboard.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<style>
|
|
||||||
.search-results-container { padding: 24px; }
|
|
||||||
.result-section { margin-bottom: 40px; }
|
|
||||||
.result-section h2 { font-size: 1.25rem; color: #1e293b; margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
|
||||||
.result-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 16px; }
|
|
||||||
.result-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; transition: transform 0.2s; }
|
|
||||||
.result-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
|
|
||||||
.result-title { font-weight: 600; color: #1e293b; margin-bottom: 4px; }
|
|
||||||
.result-meta { font-size: 0.875rem; color: #64748b; }
|
|
||||||
.no-results { padding: 40px; text-align: center; color: #94a3b8; background: #f8fafc; border-radius: 12px; border: 2px dashed #e2e8f0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content">
|
|
||||||
<div class="search-results-container">
|
|
||||||
<div style="margin-bottom: 24px;">
|
|
||||||
<h1 style="font-size: 1.875rem; color: #1e293b;">Search Results</h1>
|
|
||||||
<p style="color: #64748b;">Showing results for "<strong><?= htmlspecialchars($query) ?></strong>"</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if (!$query): ?>
|
|
||||||
<div class="no-results">
|
|
||||||
<i data-lucide="search" style="width: 48px; height: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
|
|
||||||
<p>Enter a search term to find voters, candidates, or records.</p>
|
|
||||||
</div>
|
|
||||||
<?php elseif (empty($voters) && empty($candidates) && empty($logs)): ?>
|
|
||||||
<div class="no-results">
|
|
||||||
<i data-lucide="frown" style="width: 48px; height: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
|
|
||||||
<p>No results found for "<?= htmlspecialchars($query) ?>".</p>
|
|
||||||
</div>
|
|
||||||
<?php else: ?>
|
|
||||||
|
|
||||||
<?php if (!empty($voters)): ?>
|
|
||||||
<div class="result-section">
|
|
||||||
<h2><i data-lucide="users"></i> Voters (<?= count($voters) ?>)</h2>
|
|
||||||
<div class="result-grid">
|
|
||||||
<?php foreach ($voters as $v): ?>
|
|
||||||
<div class="result-card">
|
|
||||||
<div class="result-title"><?= htmlspecialchars($v['name']) ?></div>
|
|
||||||
<div class="result-meta"><?= htmlspecialchars($v['student_id']) ?> • <?= htmlspecialchars($v['email']) ?></div>
|
|
||||||
<div style="margin-top: 12px;">
|
|
||||||
<a href="voter_management.php?search=<?= urlencode($v['student_id']) ?>" style="font-size: 12px; color: #4f46e5; font-weight: 500; text-decoration: none;">View in Voters List →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($candidates)): ?>
|
|
||||||
<div class="result-section">
|
|
||||||
<h2><i data-lucide="user-square"></i> Candidates (<?= count($candidates) ?>)</h2>
|
|
||||||
<div class="result-grid">
|
|
||||||
<?php foreach ($candidates as $c): ?>
|
|
||||||
<div class="result-card">
|
|
||||||
<div class="result-title"><?= htmlspecialchars($c['user_name']) ?></div>
|
|
||||||
<div class="result-meta"><?= htmlspecialchars($c['position']) ?> • <?= htmlspecialchars($c['student_id']) ?></div>
|
|
||||||
<div style="margin-top: 12px;">
|
|
||||||
<a href="candidate_management.php?search=<?= urlencode($c['user_name']) ?>" style="font-size: 12px; color: #4f46e5; font-weight: 500; text-decoration: none;">View in Candidates →</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($logs)): ?>
|
|
||||||
<div class="result-section">
|
|
||||||
<h2><i data-lucide="file-text"></i> Audit Records (<?= count($logs) ?>)</h2>
|
|
||||||
<div class="result-grid">
|
|
||||||
<?php foreach ($logs as $l): ?>
|
|
||||||
<div class="result-card">
|
|
||||||
<div class="result-title"><?= htmlspecialchars($l['action']) ?></div>
|
|
||||||
<div class="result-meta"><?= date('M d, Y H:i', strtotime($l['created_at'])) ?> by <?= htmlspecialchars($l['user_name'] ?? 'SYSTEM') ?></div>
|
|
||||||
<div style="margin-top: 8px; font-size: 13px; color: #64748b; line-height: 1.5;">
|
|
||||||
<?= htmlspecialchars($l['details']) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<script>lucide.createIcons();</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
164
settings.php
@ -1,164 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$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>Settings | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
<style>
|
|
||||||
.settings-card {
|
|
||||||
background: white;
|
|
||||||
padding: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
.settings-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid #f1f5f9;
|
|
||||||
}
|
|
||||||
.settings-group:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.settings-info h3 {
|
|
||||||
margin: 0 0 4px 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
.settings-info p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
/* Toggle Switch */
|
|
||||||
.switch {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
width: 50px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
.switch input {
|
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
.slider {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
transition: .4s;
|
|
||||||
border-radius: 24px;
|
|
||||||
}
|
|
||||||
.slider:before {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
left: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
background-color: white;
|
|
||||||
transition: .4s;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
input:checked + .slider {
|
|
||||||
background-color: #4f46e5;
|
|
||||||
}
|
|
||||||
input:checked + .slider:before {
|
|
||||||
transform: translateX(26px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode preview styles (optional, can be integrated into global CSS later) */
|
|
||||||
.dark-mode-preview {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 12px;
|
|
||||||
background: #1e293b;
|
|
||||||
color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0 0 4px 0; font-size: 1.5rem; color: #1e293b;">Settings</h1>
|
|
||||||
<div class="welcome-msg">Manage your account preferences</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="settings-card">
|
|
||||||
<div class="settings-group">
|
|
||||||
<div class="settings-info">
|
|
||||||
<h3>Theme Toggle</h3>
|
|
||||||
<p>Switch between Light and Dark mode</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" id="themeToggle" <?= ($user['theme'] ?? 'light') === 'dark' ? 'checked' : '' ?>>
|
|
||||||
<span class="slider"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
document.getElementById('themeToggle').addEventListener('change', function() {
|
|
||||||
const theme = this.checked ? 'dark' : 'light';
|
|
||||||
|
|
||||||
// Apply theme immediately to body
|
|
||||||
if (theme === 'dark') {
|
|
||||||
document.body.classList.add('dark-theme');
|
|
||||||
} else {
|
|
||||||
document.body.classList.remove('dark-theme');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to database
|
|
||||||
fetch('api/update_theme.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ theme: theme }),
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (!data.success) {
|
|
||||||
console.error('Failed to update theme:', data.error);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
158
signup.php
@ -1,158 +0,0 @@
|
|||||||
<?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 {
|
|
||||||
// 1. Create user in Supabase
|
|
||||||
$supabaseUser = SupabaseAuth::createUser($email, $password);
|
|
||||||
$supabase_uid = null;
|
|
||||||
if ($supabaseUser['error']) {
|
|
||||||
if (str_contains(strtolower($supabaseUser['error']), 'already registered')) {
|
|
||||||
$sbUser = SupabaseAuth::getUserByEmail($email);
|
|
||||||
$supabase_uid = $sbUser['id'] ?? null;
|
|
||||||
} else {
|
|
||||||
throw new Exception("Supabase Error: " . $supabaseUser['error']);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$supabase_uid = $supabaseUser['data']['id'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = uuid();
|
|
||||||
$stmt = db()->prepare("INSERT INTO users (id, supabase_uid, student_id, name, email, role) VALUES (?, ?, ?, ?, ?, ?)");
|
|
||||||
$stmt->execute([$id, $supabase_uid, $student_id, $name, $email, $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();
|
|
||||||
}
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$error = $e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Registration - Iloilo National High School</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() ?>">
|
|
||||||
<style>
|
|
||||||
.signup-page-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
padding: 2rem 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="landing-page" style="background-image: url('assets/images/background.jpg?v=<?= filemtime('assets/images/background.jpg') ?>');">
|
|
||||||
<div class="signup-page-container">
|
|
||||||
<div class="login-modal" style="display: block; position: static; animation: none; max-width: 500px;">
|
|
||||||
<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="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><polyline points="17 11 19 13 23 9"></polyline></svg>
|
|
||||||
</div>
|
|
||||||
<h2>Voter Registration</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<?php if ($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($error) ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<form method="POST">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Full Name</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>
|
|
||||||
<input type="text" name="name" placeholder="Juan Dela Cruz" required>
|
|
||||||
</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="Create a password" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<button type="submit" class="modal-btn-login">
|
|
||||||
REGISTER
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<small style="color: var(--landing-text-muted); font-size: 0.8rem;">Already have an account? <a href="login.php" style="color: var(--landing-primary); font-weight: 600; text-decoration: none;">Login here</a></small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/db/config.php';
|
|
||||||
|
|
||||||
// This script migrates the local MariaDB database to Supabase PostgreSQL.
|
|
||||||
// It requires the Supabase Database Password.
|
|
||||||
|
|
||||||
if (php_sapi_name() !== 'cli') {
|
|
||||||
die("This script must be run from the command line.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$dbPassword = $argv[1] ?? '';
|
|
||||||
if (!$dbPassword) {
|
|
||||||
echo "Usage: php supabase_migration.php [SUPABASE_DB_PASSWORD]\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
$supabaseHost = "aws-1-ap-southeast-1.pooler.supabase.com";
|
|
||||||
$supabaseUser = "postgres.siqeqnizegizxemrfgkf";
|
|
||||||
$supabaseDb = "postgres";
|
|
||||||
$supabasePort = "6543";
|
|
||||||
|
|
||||||
try {
|
|
||||||
echo "Connecting to local MariaDB...\n";
|
|
||||||
$localPdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
]);
|
|
||||||
|
|
||||||
echo "Connecting to Supabase PostgreSQL...\n";
|
|
||||||
$dsn = "pgsql:host=$supabaseHost;port=$supabasePort;dbname=$supabaseDb";
|
|
||||||
$supabasePdo = new PDO($dsn, $supabaseUser, $dbPassword, [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
||||||
]);
|
|
||||||
|
|
||||||
echo "Converting and creating tables in Supabase...\n";
|
|
||||||
|
|
||||||
// Define tables and their PostgreSQL schemas
|
|
||||||
$schemas = [
|
|
||||||
"users" => "CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
supabase_uid VARCHAR(255),
|
|
||||||
student_id VARCHAR(10) UNIQUE NOT NULL,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
|
||||||
password_hash VARCHAR(255),
|
|
||||||
grade_level INTEGER,
|
|
||||||
track VARCHAR(100),
|
|
||||||
section VARCHAR(100),
|
|
||||||
role VARCHAR(50) DEFAULT 'Voter',
|
|
||||||
access_level INTEGER DEFAULT 0,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted_at TIMESTAMP
|
|
||||||
)",
|
|
||||||
"elections" => "CREATE TABLE IF NOT EXISTS elections (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
title VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
status VARCHAR(50) DEFAULT 'Preparing',
|
|
||||||
start_date_and_time TIMESTAMP NOT NULL,
|
|
||||||
end_date_and_time TIMESTAMP NOT NULL,
|
|
||||||
created_by VARCHAR(255) REFERENCES users(id),
|
|
||||||
archived BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)",
|
|
||||||
"positions" => "CREATE TABLE IF NOT EXISTS positions (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
type VARCHAR(50) DEFAULT 'Uniform',
|
|
||||||
max_votes INTEGER DEFAULT 1,
|
|
||||||
sort_order INTEGER DEFAULT 0
|
|
||||||
)",
|
|
||||||
"candidates" => "CREATE TABLE IF NOT EXISTS candidates (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
|
||||||
position_id VARCHAR(255) REFERENCES positions(id) ON DELETE CASCADE,
|
|
||||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
party_name VARCHAR(255),
|
|
||||||
manifesto TEXT,
|
|
||||||
approved BOOLEAN DEFAULT FALSE,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)",
|
|
||||||
"votes" => "CREATE TABLE IF NOT EXISTS votes (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
election_id VARCHAR(255) REFERENCES elections(id),
|
|
||||||
position_id VARCHAR(255) REFERENCES positions(id),
|
|
||||||
candidate_id VARCHAR(255) REFERENCES candidates(id),
|
|
||||||
voter_id VARCHAR(255) REFERENCES users(id),
|
|
||||||
casted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
ip_address VARCHAR(45),
|
|
||||||
user_agent TEXT,
|
|
||||||
UNIQUE (election_id, position_id, voter_id)
|
|
||||||
)",
|
|
||||||
"election_assignments" => "CREATE TABLE IF NOT EXISTS election_assignments (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
|
||||||
user_id VARCHAR(255) REFERENCES users(id) ON DELETE CASCADE,
|
|
||||||
role_in_election VARCHAR(50) DEFAULT 'Voter',
|
|
||||||
assigned_by VARCHAR(255) REFERENCES users(id),
|
|
||||||
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)",
|
|
||||||
"parties" => "CREATE TABLE IF NOT EXISTS parties (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
logo_url TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
||||||
)",
|
|
||||||
"audit_logs" => "CREATE TABLE IF NOT EXISTS audit_logs (
|
|
||||||
id VARCHAR(255) PRIMARY KEY,
|
|
||||||
user_id VARCHAR(255) REFERENCES users(id),
|
|
||||||
action VARCHAR(255) NOT NULL,
|
|
||||||
details TEXT,
|
|
||||||
table_name VARCHAR(100),
|
|
||||||
record_id VARCHAR(255),
|
|
||||||
old_values TEXT,
|
|
||||||
new_values TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
election_id VARCHAR(255) REFERENCES elections(id) ON DELETE CASCADE
|
|
||||||
)"
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($schemas as $tableName => $sql) {
|
|
||||||
echo "Creating table: $tableName...\n";
|
|
||||||
$supabasePdo->exec($sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Migrating data...\n";
|
|
||||||
|
|
||||||
$tables = array_keys($schemas);
|
|
||||||
// Order matters for foreign keys: users, elections, positions, candidates, assignments, votes, audit_logs
|
|
||||||
$orderedTables = ["users", "elections", "positions", "election_assignments", "parties", "candidates", "votes", "audit_logs"];
|
|
||||||
|
|
||||||
foreach ($orderedTables as $table) {
|
|
||||||
echo "Migrating data for $table...\n";
|
|
||||||
$stmt = $localPdo->query("SELECT * FROM $table");
|
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
if (empty($rows)) {
|
|
||||||
echo "No data for $table.\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$columns = array_keys($rows[0]);
|
|
||||||
$placeholders = implode(',', array_fill(0, count($columns), '?'));
|
|
||||||
$insertSql = "INSERT INTO $table (" . implode(',', $columns) . ") VALUES ($placeholders) ON CONFLICT (id) DO NOTHING";
|
|
||||||
|
|
||||||
$insertStmt = $supabasePdo->prepare($insertSql);
|
|
||||||
$count = 0;
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$insertStmt->execute(array_values($row));
|
|
||||||
$count++;
|
|
||||||
}
|
|
||||||
echo "Migrated $count rows for $table.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "Migration completed successfully!\n";
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
echo "Error: " . $e->getMessage() . "\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
@ -1,293 +0,0 @@
|
|||||||
<?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/dashboard.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<div class="dashboard-header" style="align-items: center;">
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0; font-size: 1.75rem; color: #1e293b;"><?= htmlspecialchars($election['title']) ?></h1>
|
|
||||||
<p class="welcome-msg" style="margin-top: 4px;"><?= htmlspecialchars($election['description']) ?></p>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 12px;">
|
|
||||||
<?php if (in_array($user['role'], ['Admin', 'Adviser', 'Officer'])): ?>
|
|
||||||
<button class="btn-action" style="background: #ffffff; border: 1px solid #e2e8f0; color: #4b5563; padding: 10px 20px; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px;" onclick="document.getElementById('editElectionModal').style.display='flex'">
|
|
||||||
<i data-lucide="edit-3" style="width: 16px;"></i>
|
|
||||||
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-action" style="background: #166534; color: white; padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
||||||
<i data-lucide="play" style="width: 16px;"></i>
|
|
||||||
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-action" style="background: #ef4444; color: white; padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px;">
|
|
||||||
<i data-lucide="square" style="width: 16px;"></i>
|
|
||||||
End Election
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats-grid" style="grid-template-columns: repeat(3, 1fr); margin-bottom: 24px;">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
|
||||||
<div style="background: #eef2ff; padding: 10px; border-radius: 10px; color: #4f46e5;">
|
|
||||||
<i data-lucide="check-square"></i>
|
|
||||||
</div>
|
|
||||||
<span style="font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em;">Status</span>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<span class="status-indicator <?= strtolower($election['status']) ?>" style="padding: 4px 12px; border-radius: 9999px; font-size: 0.875rem; font-weight: 600; background: <?= $election['status'] === 'Ongoing' ? '#dcfce7' : ($election['status'] === 'Preparing' ? '#fef9c3' : '#f1f5f9') ?>; color: <?= $election['status'] === 'Ongoing' ? '#166534' : ($election['status'] === 'Preparing' ? '#854d0e' : '#475569') ?>;">
|
|
||||||
<?= $election['status'] ?>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
|
||||||
<div style="background: #fff7ed; padding: 10px; border-radius: 10px; color: #f97316;">
|
|
||||||
<i data-lucide="calendar"></i>
|
|
||||||
</div>
|
|
||||||
<span style="font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em;">Election Period</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 0.875rem; font-weight: 600; color: #1e293b;">
|
|
||||||
<?= date('M d, H:i', strtotime($election['start_date_and_time'])) ?> — <?= date('M d, H:i', strtotime($election['end_date_and_time'])) ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stat-card">
|
|
||||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
|
|
||||||
<div style="background: #ecfdf5; padding: 10px; border-radius: 10px; color: #10b981;">
|
|
||||||
<i data-lucide="users"></i>
|
|
||||||
</div>
|
|
||||||
<span style="font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em;">Total Participation</span>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
$vStmt = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
|
|
||||||
$vStmt->execute([$id]);
|
|
||||||
$votesCount = $vStmt->fetchColumn();
|
|
||||||
?>
|
|
||||||
<div style="font-size: 1.5rem; font-weight: 800; color: #1e293b;"><?= $votesCount ?> <small style="font-size: 0.875rem; color: #64748b; font-weight: 500;">Votes Cast</small></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px;">
|
|
||||||
<div style="background: #ffffff; border: 1px solid #f3f4f6; border-radius: 16px; overflow: hidden;">
|
|
||||||
<div style="padding: 24px; border-bottom: 1px solid #f3f4f6; display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<h2 style="margin: 0; font-size: 1.1rem; color: #1e293b; font-weight: 700;">Positions & Structure</h2>
|
|
||||||
<?php if ($election['status'] === 'Preparing'): ?>
|
|
||||||
<button onclick="document.getElementById('addPositionForm').style.display='block'" style="background: #4f46e5; color: white; border: none; padding: 8px 16px; border-radius: 8px; font-size: 0.875rem; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 6px;">
|
|
||||||
<i data-lucide="plus" style="width: 14px;"></i>
|
|
||||||
Add Position
|
|
||||||
</button>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="addPositionForm" style="display:none; margin: 24px; padding: 20px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px;">
|
|
||||||
<form action="api/add_position.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $election['id'] ?>">
|
|
||||||
<div style="display: grid; grid-template-columns: 2fr 1fr auto; gap: 16px; align-items: flex-end;">
|
|
||||||
<div>
|
|
||||||
<label style="display:block; font-size: 0.75rem; font-weight: 600; color: #64748b; margin-bottom: 6px;">Position Name</label>
|
|
||||||
<input type="text" name="name" style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; outline: none;" placeholder="e.g. President" required>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label style="display:block; font-size: 0.75rem; font-weight: 600; color: #64748b; margin-bottom: 6px;">Max Votes</label>
|
|
||||||
<input type="number" name="max_votes" style="width: 100%; padding: 10px; border: 1px solid #e2e8f0; border-radius: 8px; outline: none;" value="1" min="1">
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 8px;">
|
|
||||||
<button type="submit" style="background: #2563eb; color: white; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 600; cursor: pointer;">Save</button>
|
|
||||||
<button type="button" onclick="document.getElementById('addPositionForm').style.display='none'" style="background: #f1f5f9; color: #475569; border: none; padding: 10px 20px; border-radius: 8px; font-weight: 600; cursor: pointer;">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="overflow-x: auto;">
|
|
||||||
<table style="width: 100%; border-collapse: collapse;">
|
|
||||||
<thead>
|
|
||||||
<tr style="background: #f9fafb; border-bottom: 1px solid #f3f4f6;">
|
|
||||||
<th style="padding: 12px 24px; text-align: left; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Position Name</th>
|
|
||||||
<th style="padding: 12px 24px; text-align: left; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Max Votes</th>
|
|
||||||
<th style="padding: 12px 24px; text-align: left; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Candidates</th>
|
|
||||||
<th style="padding: 12px 24px; text-align: right; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase;">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($positions)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" style="padding: 48px; text-align: center; color: #94a3b8;">
|
|
||||||
<div style="display: flex; flex-direction: column; align-items: center; gap: 12px;">
|
|
||||||
<i data-lucide="layers" style="width: 48px; height: 48px; color: #e2e8f0;"></i>
|
|
||||||
<span>No positions defined yet. Start by adding one.</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($positions as $pos):
|
|
||||||
$cStmt = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE position_id = ?");
|
|
||||||
$cStmt->execute([$pos['id']]);
|
|
||||||
$cCount = $cStmt->fetchColumn();
|
|
||||||
?>
|
|
||||||
<tr style="border-bottom: 1px solid #f3f4f6; transition: background 0.2s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
|
|
||||||
<td style="padding: 16px 24px; font-weight: 600; color: #1e293b;"><?= htmlspecialchars($pos['name']) ?></td>
|
|
||||||
<td style="padding: 16px 24px; color: #4b5563;">
|
|
||||||
<span style="background: #f1f5f9; padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 600;"><?= $pos['max_votes'] ?> Vote<?= $pos['max_votes'] > 1 ? 's' : '' ?></span>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 16px 24px;">
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<div style="width: 8px; height: 8px; border-radius: 50%; background: #10b981;"></div>
|
|
||||||
<span style="font-weight: 600; color: #1e293b;"><?= $cCount ?></span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 16px 24px; text-align: right;">
|
|
||||||
<a href="manage_candidates.php?position_id=<?= $pos['id'] ?>" style="color: #4f46e5; text-decoration: none; font-size: 0.875rem; font-weight: 600; display: inline-flex; align-items: center; gap: 6px;">
|
|
||||||
Manage Candidates
|
|
||||||
<i data-lucide="arrow-right" style="width: 14px;"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 24px;">
|
|
||||||
<div style="background: #ffffff; border: 1px solid #f3f4f6; border-radius: 16px; padding: 24px;">
|
|
||||||
<h2 style="margin-top: 0; font-size: 1.1rem; color: #1e293b; font-weight: 700; margin-bottom: 20px;">Quick Stats</h2>
|
|
||||||
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<div style="font-size: 0.7rem; color: #64748b; text-transform: uppercase; font-weight: 700; letter-spacing: 0.05em;">Total Candidates</div>
|
|
||||||
<?php
|
|
||||||
$canStmt = $pdo->prepare("SELECT COUNT(*) FROM candidates WHERE election_id = ?");
|
|
||||||
$canStmt->execute([$id]);
|
|
||||||
$candidatesTotal = $canStmt->fetchColumn();
|
|
||||||
?>
|
|
||||||
<div style="font-size: 1.5rem; font-weight: 800; color: #1e293b;"><?= $candidatesTotal ?></div>
|
|
||||||
</div>
|
|
||||||
<div style="background: #fef2f2; padding: 12px; border-radius: 12px; color: #ef4444;">
|
|
||||||
<i data-lucide="user-check"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
||||||
<div>
|
|
||||||
<div style="font-size: 0.7rem; color: #64748b; text-transform: uppercase; font-weight: 700; letter-spacing: 0.05em;">Voter Turnout</div>
|
|
||||||
<div style="font-size: 1.5rem; font-weight: 800; color: #1e293b;"><?= $votesCount ?></div>
|
|
||||||
</div>
|
|
||||||
<div style="background: #f0fdf4; padding: 12px; border-radius: 12px; color: #22c55e;">
|
|
||||||
<i data-lucide="bar-chart-3"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: linear-gradient(135deg, #4f46e5 0%, #3730a3 100%); border-radius: 16px; padding: 24px; color: white;">
|
|
||||||
<h3 style="margin: 0; font-size: 1rem; font-weight: 700; margin-bottom: 12px;">Need Help?</h3>
|
|
||||||
<p style="font-size: 0.875rem; opacity: 0.9; margin-bottom: 20px;">Manage your election structure by adding positions and assigning candidates to them. Once ready, you can launch the election.</p>
|
|
||||||
<a href="reports_audit.php" style="display: block; width: 100%; text-align: center; background: white; color: #4f46e5; padding: 12px; border-radius: 8px; font-weight: 700; text-decoration: none; font-size: 0.875rem;">View Reports</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="editElectionModal" class="modal">
|
|
||||||
<div class="modal-content animate-scale-in" style="max-width: 550px;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 style="font-weight: 800;">Edit Election Details</h2>
|
|
||||||
<button type="button" onclick="document.getElementById('editElectionModal').style.display='none'" class="close-btn">×</button>
|
|
||||||
</div>
|
|
||||||
<form action="api/update_election.php" method="POST">
|
|
||||||
<input type="hidden" name="id" value="<?= $election['id'] ?>">
|
|
||||||
<div class="form-group" style="margin-bottom: 20px;">
|
|
||||||
<label>Election Title</label>
|
|
||||||
<input type="text" name="title" value="<?= htmlspecialchars($election['title']) ?>" style="width: 100%; padding: 12px; border: 1px solid #e2e8f0; border-radius: 10px; outline: none;" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom: 20px;">
|
|
||||||
<label>Description</label>
|
|
||||||
<textarea name="description" rows="3" style="width: 100%; padding: 12px; border: 1px solid #e2e8f0; border-radius: 10px; outline: none; font-family: inherit;"><?= htmlspecialchars($election['description']) ?></textarea>
|
|
||||||
</div>
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 32px;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Start Date & Time</label>
|
|
||||||
<input type="datetime-local" name="start_date" value="<?= date('Y-m-d\TH:i', strtotime($election['start_date_and_time'])) ?>" style="width: 100%; padding: 12px; border: 1px solid #e2e8f0; border-radius: 10px; outline: none;" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>End Date & Time</label>
|
|
||||||
<input type="datetime-local" name="end_date" value="<?= date('Y-m-d\TH:i', strtotime($election['end_date_and_time'])) ?>" style="width: 100%; padding: 12px; border: 1px solid #e2e8f0; border-radius: 10px; outline: none;" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="document.getElementById('editElectionModal').style.display='none'" class="btn-cancel">Cancel</button>
|
|
||||||
<button type="submit" class="btn-submit" style="background: #4f46e5;">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target.id === 'editElectionModal') {
|
|
||||||
event.target.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
114
view_results.php
@ -1,114 +0,0 @@
|
|||||||
<?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">Click to Vote</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 $index => $res):
|
|
||||||
$percent = $posTotal > 0 ? round(($res['vote_count'] / $posTotal) * 100, 1) : 0;
|
|
||||||
$isWinner = ($index === 0 && $res['vote_count'] > 0);
|
|
||||||
?>
|
|
||||||
<div class="candidate-result" style="<?= $isWinner ? 'border-color: #10b981; background: #f0fdf4;' : '' ?>">
|
|
||||||
<div style="display: flex; justify-content: space-between; font-weight: 600;">
|
|
||||||
<span style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<?= htmlspecialchars($res['name']) ?> (<?= htmlspecialchars($res['party_name'] ?: 'Ind.') ?>)
|
|
||||||
<?php if ($isWinner): ?>
|
|
||||||
<span style="background: #10b981; color: white; font-size: 0.65rem; padding: 2px 8px; border-radius: 4px; text-transform: uppercase;">Winner</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</span>
|
|
||||||
<span><?= $res['vote_count'] ?> votes (<?= $percent ?>%)</span>
|
|
||||||
</div>
|
|
||||||
<div class="result-bar-container">
|
|
||||||
<div class="result-bar" style="width: <?= $percent ?>%; <?= $isWinner ? 'background: #10b981;' : '' ?>"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,418 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once 'auth_helper.php';
|
|
||||||
require_login();
|
|
||||||
$user = get_user();
|
|
||||||
|
|
||||||
$pdo = db();
|
|
||||||
$electionId = get_active_election_id();
|
|
||||||
$election = get_active_election();
|
|
||||||
|
|
||||||
// Statistics (Filtered by Election if possible, otherwise global)
|
|
||||||
// For now, let's assume we want to see voters assigned to the current election
|
|
||||||
$totalVoters = $pdo->prepare("SELECT COUNT(*) FROM users u JOIN election_assignments ea ON u.id = ea.user_id WHERE ea.election_id = ? AND ea.role_in_election = 'Voter'");
|
|
||||||
$totalVoters->execute([$electionId]);
|
|
||||||
$totalVoters = $totalVoters->fetchColumn();
|
|
||||||
|
|
||||||
$votedCount = $pdo->prepare("SELECT COUNT(DISTINCT voter_id) FROM votes WHERE election_id = ?");
|
|
||||||
$votedCount->execute([$electionId]);
|
|
||||||
$votedCount = $votedCount->fetchColumn();
|
|
||||||
|
|
||||||
$notVotedCount = $totalVoters - $votedCount;
|
|
||||||
|
|
||||||
// Distribution (Filtered by Election)
|
|
||||||
$trackStats = $pdo->prepare("SELECT u.track, COUNT(*) as count FROM users u JOIN election_assignments ea ON u.id = ea.user_id WHERE ea.election_id = ? AND ea.role_in_election = 'Voter' GROUP BY u.track ORDER BY u.track");
|
|
||||||
$trackStats->execute([$electionId]);
|
|
||||||
$trackStats = $trackStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
$gradeStats = $pdo->prepare("SELECT u.grade_level, COUNT(*) as count FROM users u JOIN election_assignments ea ON u.id = ea.user_id WHERE ea.election_id = ? AND ea.role_in_election = 'Voter' GROUP BY u.grade_level ORDER BY u.grade_level");
|
|
||||||
$gradeStats->execute([$electionId]);
|
|
||||||
$gradeStats = $gradeStats->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
$search = $_GET['search'] ?? '';
|
|
||||||
$filterTrack = $_GET['track'] ?? 'All Tracks';
|
|
||||||
$filterGrade = $_GET['grade'] ?? 'All Grades';
|
|
||||||
|
|
||||||
// Query Construction
|
|
||||||
$query = "SELECT u.*,
|
|
||||||
(SELECT COUNT(*) FROM votes v WHERE v.voter_id = u.id AND v.election_id = ?) as has_voted
|
|
||||||
FROM users u
|
|
||||||
JOIN election_assignments ea ON u.id = ea.user_id
|
|
||||||
WHERE ea.election_id = ? AND ea.role_in_election = 'Voter'";
|
|
||||||
|
|
||||||
$params = [$electionId, $electionId];
|
|
||||||
|
|
||||||
if ($search) {
|
|
||||||
$query .= " AND (u.email LIKE ? OR u.name LIKE ? OR u.student_id LIKE ?)";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
$params[] = "%$search%";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filterTrack !== 'All Tracks') {
|
|
||||||
$query .= " AND u.track = ?";
|
|
||||||
$params[] = $filterTrack;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($filterGrade !== 'All Grades') {
|
|
||||||
$query .= " AND u.grade_level = ?";
|
|
||||||
$params[] = $filterGrade;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = $pdo->prepare($query);
|
|
||||||
$stmt->execute($params);
|
|
||||||
$voters = $stmt->fetchAll();
|
|
||||||
|
|
||||||
// Get unique values for filters
|
|
||||||
$tracks = $pdo->query("SELECT DISTINCT track FROM users WHERE track IS NOT NULL ORDER BY track")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
$grades = $pdo->query("SELECT DISTINCT grade_level FROM users WHERE grade_level IS NOT NULL ORDER BY grade_level")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
|
|
||||||
$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>Voter Management | <?= htmlspecialchars($projectDescription) ?></title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="assets/css/dashboard.css?v=<?= time() ?>">
|
|
||||||
<link rel="stylesheet" href="assets/css/voter_management.css?v=<?= time() ?>">
|
|
||||||
<script src="https://unpkg.com/lucide@latest"></script>
|
|
||||||
</head>
|
|
||||||
<body class="dashboard-body <?= ($user['theme'] ?? 'light') === 'dark' ? 'dark-theme' : '' ?>">
|
|
||||||
|
|
||||||
<?php require_once 'includes/sidebar.php'; ?>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="main-wrapper">
|
|
||||||
<?php require_once 'includes/header.php'; ?>
|
|
||||||
|
|
||||||
<main class="dashboard-content animate-fade-in">
|
|
||||||
<?php if (isset($_GET['success'])): ?>
|
|
||||||
<?php if ($_GET['success'] === 'voter_added'): ?>
|
|
||||||
<div style="background: #ecfdf5; color: #065f46; padding: 16px; border-radius: 12px; border: 1px solid #a7f3d0; margin-bottom: 24px; display: flex; align-items: center; gap: 12px;">
|
|
||||||
<i data-lucide="check-circle" style="width: 20px;"></i>
|
|
||||||
<span style="font-weight: 500;">Voter successfully registered!</span>
|
|
||||||
</div>
|
|
||||||
<?php elseif ($_GET['success'] === 'import_complete'): ?>
|
|
||||||
<div style="background: #ecfdf5; color: #065f46; padding: 16px; border-radius: 12px; border: 1px solid #a7f3d0; margin-bottom: 24px; display: flex; align-items: center; gap: 12px;">
|
|
||||||
<i data-lucide="check-circle" style="width: 20px;"></i>
|
|
||||||
<div>
|
|
||||||
<span style="font-weight: 600; display: block;">Import completed successfully!</span>
|
|
||||||
<span style="font-size: 0.875rem;">
|
|
||||||
<?= (int)$_GET['imported'] ?> new voters added, <?= (int)$_GET['updated'] ?> existing records updated.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<div class="dashboard-header">
|
|
||||||
<div style="display: flex; align-items: center; gap: 16px;">
|
|
||||||
<div class="header-icon-container">
|
|
||||||
<i data-lucide="users" style="width: 24px; color: #4f46e5;"></i>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 style="margin: 0; font-size: 1.5rem; color: var(--text-main);">Voters List</h1>
|
|
||||||
<p style="margin: 4px 0 0 0; color: var(--text-muted); font-size: 0.875rem;">Managing voters for <?= htmlspecialchars($election['title'] ?? 'Selected Election') ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats Grid -->
|
|
||||||
<div class="voter-stats-grid animate-stagger">
|
|
||||||
<div class="voter-stat-card">
|
|
||||||
<div class="voter-stat-label">TOTAL VOTERS</div>
|
|
||||||
<div class="voter-stat-value" style="color: var(--accent-color);"><?= number_format($totalVoters) ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="voter-stat-card">
|
|
||||||
<div class="voter-stat-label">VOTERS WHO VOTED</div>
|
|
||||||
<div class="voter-stat-value" style="color: var(--text-muted);"><?= number_format($votedCount) ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="voter-stat-card">
|
|
||||||
<div class="voter-stat-label" style="color: #ef4444;">VOTERS WHO HAVEN'T VOTED</div>
|
|
||||||
<div class="voter-stat-value" style="color: #ef4444;"><?= number_format($notVotedCount) ?></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 12px; margin-bottom: 24px;" class="animate-stagger">
|
|
||||||
<button class="btn-action btn-add" onclick="openModal('addVoterModal')">
|
|
||||||
<i data-lucide="plus" style="width: 14px;"></i>
|
|
||||||
ADD VOTER
|
|
||||||
</button>
|
|
||||||
<button class="btn-action btn-import" onclick="openModal('importModal')">
|
|
||||||
<i data-lucide="upload" style="width: 14px;"></i>
|
|
||||||
Import CSV
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters & Table Section -->
|
|
||||||
<div class="content-section animate-fade-in">
|
|
||||||
<form id="filterForm" method="GET" class="filter-bar">
|
|
||||||
<div class="filter-group" style="flex: 2;">
|
|
||||||
<label>SEARCH</label>
|
|
||||||
<div class="search-input-wrapper">
|
|
||||||
<i data-lucide="search" style="width: 14px; color: #94a3b8;"></i>
|
|
||||||
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Search by email">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>TRACK</label>
|
|
||||||
<select name="track" onchange="this.form.submit()">
|
|
||||||
<option>All Tracks</option>
|
|
||||||
<?php foreach ($tracks as $t): ?>
|
|
||||||
<option value="<?= htmlspecialchars($t) ?>" <?= $filterTrack === $t ? 'selected' : '' ?>><?= htmlspecialchars($t) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="filter-group">
|
|
||||||
<label>GRADE</label>
|
|
||||||
<select name="grade" onchange="this.form.submit()">
|
|
||||||
<option>All Grades</option>
|
|
||||||
<?php foreach ($grades as $g): ?>
|
|
||||||
<option value="<?= htmlspecialchars($g) ?>" <?= $filterGrade == $g ? 'selected' : '' ?>>Grade <?= htmlspecialchars($g) ?></option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<table class="voters-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>USER ID</th>
|
|
||||||
<th>NAME</th>
|
|
||||||
<th>EMAIL</th>
|
|
||||||
<th>TRACK</th>
|
|
||||||
<th>GRADE</th>
|
|
||||||
<th>SECTION</th>
|
|
||||||
<th>STATUS</th>
|
|
||||||
<th>ACTIONS</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php if (empty($voters)): ?>
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" style="text-align: center; color: #94a3b8; padding: 32px;">No voters assigned to this election.</td>
|
|
||||||
</tr>
|
|
||||||
<?php else: ?>
|
|
||||||
<?php foreach ($voters as $voter): ?>
|
|
||||||
<tr>
|
|
||||||
<td><?= htmlspecialchars($voter['student_id']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($voter['name']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($voter['email']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($voter['track']) ?></td>
|
|
||||||
<td>Grade <?= htmlspecialchars($voter['grade_level']) ?></td>
|
|
||||||
<td><?= htmlspecialchars($voter['section'] ?? 'N/A') ?></td>
|
|
||||||
<td>
|
|
||||||
<span class="status-indicator <?= $voter['has_voted'] ? 'voted' : 'pending' ?>">
|
|
||||||
<?= $voter['has_voted'] ? 'Voted' : 'Pending' ?>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td class="actions-cell">
|
|
||||||
<button title="Edit" onclick='editVoter(<?= json_encode($voter) ?>)'><i data-lucide="edit-2"></i></button>
|
|
||||||
<button title="Delete" style="color: #ef4444;" onclick="deleteVoter('<?= $voter['id'] ?>', '<?= htmlspecialchars($voter['name']) ?>')"><i data-lucide="trash-2"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modals -->
|
|
||||||
<div id="addVoterModal" class="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:1000; align-items:center; justify-content:center;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Register New Voter</h2>
|
|
||||||
<button onclick="closeModal('addVoterModal')" style="border:none; background:none; cursor:pointer; color: var(--text-muted);"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/add_voter.php" method="POST">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>FULL NAME</label>
|
|
||||||
<input type="text" name="name" placeholder="Enter student's full name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>STUDENT ID</label>
|
|
||||||
<input type="text" name="student_id" placeholder="XX-XXXX" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>EMAIL ADDRESS</label>
|
|
||||||
<input type="email" name="email" placeholder="student@school.edu" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>PASSWORD</label>
|
|
||||||
<input type="text" name="password" value="iloilohns" required>
|
|
||||||
<small style="color: var(--text-muted); font-size: 11px;">Default is iloilohns</small>
|
|
||||||
</div>
|
|
||||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:16px; margin-bottom:24px;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>TRACK</label>
|
|
||||||
<select name="track" required>
|
|
||||||
<option value="STEM">STEM</option>
|
|
||||||
<option value="ABM">ABM</option>
|
|
||||||
<option value="HUMSS">HUMSS</option>
|
|
||||||
<option value="GAS">GAS</option>
|
|
||||||
<option value="TVL">TVL</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>GRADE LEVEL</label>
|
|
||||||
<select name="grade_level" required>
|
|
||||||
<option value="11">Grade 11</option>
|
|
||||||
<option value="12">Grade 12</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:24px;">
|
|
||||||
<label>SECTION</label>
|
|
||||||
<input type="text" name="section" placeholder="Enter section (e.g. Einstein, Newton)" required>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('addVoterModal')" class="btn-cancel">Cancel</button>
|
|
||||||
<button type="submit" class="btn-action btn-add">Register Voter</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="editVoterModal" class="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:1000; align-items:center; justify-content:center;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Edit Voter</h2>
|
|
||||||
<button onclick="closeModal('editVoterModal')" style="border:none; background:none; cursor:pointer; color: var(--text-muted);"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/update_voter.php" method="POST">
|
|
||||||
<input type="hidden" name="id" id="edit_voter_id">
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>FULL NAME</label>
|
|
||||||
<input type="text" name="name" id="edit_voter_name" placeholder="Enter student's full name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>STUDENT ID</label>
|
|
||||||
<input type="text" name="student_id" id="edit_voter_student_id" placeholder="XX-XXXX" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>EMAIL ADDRESS</label>
|
|
||||||
<input type="email" name="email" id="edit_voter_email" placeholder="student@school.edu" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:16px;">
|
|
||||||
<label>NEW PASSWORD (OPTIONAL)</label>
|
|
||||||
<input type="password" name="password" placeholder="Leave blank to keep current">
|
|
||||||
</div>
|
|
||||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:16px; margin-bottom:24px;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>TRACK</label>
|
|
||||||
<select name="track" id="edit_voter_track" required>
|
|
||||||
<option value="STEM">STEM</option>
|
|
||||||
<option value="ABM">ABM</option>
|
|
||||||
<option value="HUMSS">HUMSS</option>
|
|
||||||
<option value="GAS">GAS</option>
|
|
||||||
<option value="TVL">TVL</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>GRADE LEVEL</label>
|
|
||||||
<select name="grade_level" id="edit_voter_grade_level" required>
|
|
||||||
<option value="11">Grade 11</option>
|
|
||||||
<option value="12">Grade 12</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group" style="margin-bottom:24px;">
|
|
||||||
<label>SECTION</label>
|
|
||||||
<input type="text" name="section" id="edit_voter_section" placeholder="Enter section" required>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('editVoterModal')" class="btn-cancel">Cancel</button>
|
|
||||||
<button type="submit" class="btn-action btn-add">Update Voter</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="importModal" class="modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; z-index:1000; align-items:center; justify-content:center;">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>Import Voters from CSV</h2>
|
|
||||||
<button onclick="closeModal('importModal')" style="border:none; background:none; cursor:pointer; color: var(--text-muted);"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<form action="api/import_voters.php" method="POST" enctype="multipart/form-data">
|
|
||||||
<input type="hidden" name="election_id" value="<?= $electionId ?>">
|
|
||||||
<div class="form-group" style="margin-bottom:24px;">
|
|
||||||
<label>SELECT CSV FILE</label>
|
|
||||||
<div style="border: 2px dashed var(--border-color); padding: 32px; border-radius: 12px; text-align: center; cursor: pointer;" onclick="document.getElementById('csvFile').click()">
|
|
||||||
<i data-lucide="upload-cloud" style="width: 32px; height: 32px; color: var(--text-muted); margin-bottom: 12px;"></i>
|
|
||||||
<p style="margin: 0; font-size: 0.875rem; color: var(--text-muted);">Click to upload or drag and drop</p>
|
|
||||||
<p style="margin: 4px 0 0 0; font-size: 0.75rem; color: var(--text-muted);">CSV files only (Max 5MB)</p>
|
|
||||||
<input type="file" id="csvFile" name="csv_file" accept=".csv" style="display: none;" onchange="updateFileName(this)">
|
|
||||||
<div id="fileName" style="margin-top: 12px; font-weight: 500; color: var(--accent-color); font-size: 0.875rem;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="background: var(--bg-color); padding: 16px; border-radius: 8px; margin-bottom: 24px;">
|
|
||||||
<p style="margin: 0 0 8px 0; font-size: 0.75rem; font-weight: 600; color: var(--text-muted);">CSV FORMAT REQUIREMENTS:</p>
|
|
||||||
<p style="margin: 0; font-size: 0.75rem; color: var(--text-muted); line-height: 1.5;">
|
|
||||||
Columns: <code>student_id, name, email, track, grade_level, section</code><br>
|
|
||||||
Example: <code>20-1234, John Doe, john@example.com, STEM, 12, Einstein</code>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" onclick="closeModal('importModal')" class="btn-cancel">Cancel</button>
|
|
||||||
<button type="submit" class="btn-action btn-add">Start Import</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
lucide.createIcons();
|
|
||||||
|
|
||||||
function updateFileName(input) {
|
|
||||||
const fileNameDisplay = document.getElementById('fileName');
|
|
||||||
if (input.files && input.files.length > 0) {
|
|
||||||
fileNameDisplay.textContent = input.files[0].name;
|
|
||||||
} else {
|
|
||||||
fileNameDisplay.textContent = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openModal(id) {
|
|
||||||
document.getElementById(id).style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeModal(id) {
|
|
||||||
document.getElementById(id).style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
function editVoter(voter) {
|
|
||||||
document.getElementById('edit_voter_id').value = voter.id;
|
|
||||||
document.getElementById('edit_voter_name').value = voter.name;
|
|
||||||
document.getElementById('edit_voter_student_id').value = voter.student_id;
|
|
||||||
document.getElementById('edit_voter_email').value = voter.email;
|
|
||||||
document.getElementById('edit_voter_track').value = voter.track;
|
|
||||||
document.getElementById('edit_voter_grade_level').value = voter.grade_level;
|
|
||||||
document.getElementById('edit_voter_section').value = voter.section || '';
|
|
||||||
openModal('editVoterModal');
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteVoter(id, name) {
|
|
||||||
if (confirm(`Are you sure you want to remove ${name} from this election?`)) {
|
|
||||||
window.location.href = `api/delete_voter.php?id=${id}&election_id=<?= $electionId ?>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target.classList.contains('modal')) {
|
|
||||||
event.target.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||