🗳️ Membangun Aplikasi E-Voting Lengkap dengan PHP dan MySQL
Di era digital yang semakin maju, sistem pemungutan suara elektronik (E-Voting) menjadi solusi modern untuk menggantikan metode konvensional yang rentan terhadap kecurangan dan kesalahan penghitungan. E-Voting memungkinkan proses pemilihan dilakukan secara online, transparan, efisien, dan hasilnya dapat langsung diketahui secara real-time.
Pada artikel kali ini, kita akan membangun sebuah aplikasi E-Voting profesional menggunakan PHP dan MySQL dari awal. Aplikasi ini akan memiliki fitur lengkap seperti login multi-role (admin dan pemilih), manajemen kandidat, pemungutan suara elektronik, serta statistik hasil voting yang ditampilkan dalam bentuk grafik dan persentase.
✅ Login multi-role (Admin & Pemilih) dengan autentikasi aman
✅ Pendaftaran akun pemilih baru
✅ Manajemen kandidat (tambah, lihat, hapus) oleh admin
✅ Upload foto kandidat dengan validasi file
✅ Pemungutan suara elektronik (satu pemilih satu suara)
✅ Statistik hasil voting real-time dengan persentase
✅ Progress bar visual untuk setiap kandidat
✅ Antarmuka responsif (desktop & mobile)
✅ Keamanan password hashing (bcrypt)
✅ Proteksi SQL Injection dengan prepared statements
✅ Session management untuk keamanan akses halaman
📖 Daftar Isi
- 1. Pendahuluan
- 2. Persiapan Sistem
- 3. Struktur Database
- 4. Konfigurasi Database
- 5. Halaman Utama (index.php)
- 6. Registrasi (register.php)
- 7. Login (login.php)
- 8. Dashboard Pemilih (dashboard.php)
- 9. Panel Admin (admin.php)
- 10. Logout (logout.php)
- 11. Stylesheet (style.css)
- 12. Cara Menjalankan
- 13. Troubleshooting
- 14. Pengembangan Lebih Lanjut
1. Pendahuluan
Sistem E-Voting (Electronic Voting) adalah sistem pemungutan suara yang memanfaatkan teknologi informasi untuk melakukan proses pencatatan, penghitungan, dan pengumuman hasil suara secara digital. Dibandingkan dengan sistem konvensional, E-Voting menawarkan berbagai keunggulan:
- Efisiensi Waktu: Proses penghitungan suara yang semula memakan waktu berjam-jam dapat diselesaikan dalam hitungan detik.
- Akurasi Tinggi: Meminimalisir kesalahan manusia dalam penghitungan suara.
- Transparansi: Hasil dapat dilihat secara real-time oleh semua pemilih.
- Keamanan: Setiap pemilih hanya dapat memberikan satu suara karena sistem mencatat status voting.
- Aksesibilitas: Pemilih dapat memberikan suara dari mana saja menggunakan perangkat yang terhubung internet.
Aplikasi yang akan kita bangun ini dirancang untuk kebutuhan skala kecil hingga menengah, seperti pemilihan ketua organisasi kampus, pemilihan ketua RT/RW, atau polling internal perusahaan. Dengan memahami kode yang akan kita buat, Anda dapat mengembangkannya lebih lanjut sesuai kebutuhan spesifik.
2. Persiapan Sistem
🛠️ Software yang Dibutuhkan
Berikut adalah software yang perlu Anda instal sebelum memulai pembuatan aplikasi:
| Software | Versi | Fungsi |
|---|---|---|
| XAMPP | PHP 7.4+ atau 8.x | Web server (Apache) dan database server (MySQL) |
| Text Editor | VS Code, Sublime Text, atau Notepad++ | Menulis dan mengedit kode PHP, HTML, CSS |
| Web Browser | Chrome, Firefox, atau Edge | Menguji dan menjalankan aplikasi |
| phpMyAdmin | (bawaan XAMPP) | Mengelola database secara visual |
📁 Struktur Folder Proyek
Buat folder baru di dalam htdocs (XAMPP) atau www (WAMP) dengan nama evoting. Struktur folder akhir akan seperti ini:
evoting/ ├── config.php # Konfigurasi database ├── index.php # Halaman beranda ├── login.php # Halaman login ├── register.php # Halaman registrasi ├── dashboard.php # Dashboard pemilih & voting ├── admin.php # Panel admin ├── logout.php # Proses logout ├── style.css # File stylesheet ├── uploads/ # Folder untuk foto kandidat (buat manual) └── evoting.sql # File database (import ke phpMyAdmin)
3. Struktur Database
Kita membutuhkan 3 tabel utama untuk menyimpan data pengguna, kandidat, dan riwayat voting. Berikut penjelasan detail setiap tabel:
📋 Tabel users (Penyimpanan data pengguna)
Tabel ini menyimpan informasi semua pengguna sistem, baik admin maupun pemilih biasa. Kolom has_voted digunakan untuk mencegah pemilih memberikan suara lebih dari satu kali.
📋 Tabel candidates (Data kandidat)
Menyimpan informasi kandidat yang akan dipilih, termasuk nama, foto, deskripsi, dan jumlah suara yang diperoleh.
📋 Tabel votes (Rekam jejak voting)
Mencatat setiap transaksi voting untuk keperluan audit. Relasi foreign key memastikan integritas data.
-- =============================================
-- DATABASE: evoting_system
-- Aplikasi E-Voting dengan PHP MySQL
-- =============================================
CREATE DATABASE IF NOT EXISTS evoting_system;
USE evoting_system;
-- =============================================
-- Tabel 1: users (Penyimpanan data pengguna)
-- =============================================
-- Kolom:
-- - id: ID unik auto increment
-- - username: nama pengguna (unik)
-- - password: password terenkripsi (bcrypt)
-- - email: alamat email (unik)
-- - role: peran pengguna (admin/voter)
-- - has_voted: status apakah sudah memilih (TRUE/FALSE)
-- - created_at: waktu pendaftaran
-- =============================================
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
role ENUM('admin', 'voter') DEFAULT 'voter',
has_voted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- =============================================
-- Tabel 2: candidates (Data kandidat)
-- =============================================
-- Kolom:
-- - id: ID unik auto increment
-- - name: nama lengkap kandidat
-- - photo: path file foto kandidat
-- - description: visi misi atau deskripsi
-- - votes: jumlah suara yang diperoleh
-- - created_at: waktu pendaftaran kandidat
-- =============================================
CREATE TABLE candidates (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
photo VARCHAR(255),
description TEXT,
votes INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- =============================================
-- Tabel 3: votes (Rekam jejak voting)
-- =============================================
-- Kolom:
-- - id: ID unik auto increment
-- - user_id: ID pengguna yang memilih (foreign key)
-- - candidate_id: ID kandidat yang dipilih (foreign key)
-- - voted_at: waktu voting dilakukan
-- =============================================
CREATE TABLE votes (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
candidate_id INT NOT NULL,
voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (candidate_id) REFERENCES candidates(id) ON DELETE CASCADE
);
-- =============================================
-- Insert akun admin default
-- Password: 'password' (sudah di-hash menggunakan bcrypt)
-- =============================================
INSERT INTO users (username, email, password, role)
VALUES ('admin', 'admin@evoting.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin');
-- =============================================
-- Contoh data kandidat (opsional untuk testing)
-- =============================================
INSERT INTO candidates (name, description, votes) VALUES
('Kandidat Alpha', 'Mengusung visi perubahan digital, inovasi teknologi, dan peningkatan kualitas sumber daya manusia.', 0),
('Kandidat Beta', 'Fokus pada kesejahteraan masyarakat, pemerataan pendidikan, dan pengembangan ekonomi kreatif.', 0),
('Kandidat Gamma', 'Prioritas pada lingkungan berkelanjutan, kesehatan publik, dan pemberdayaan UMKM lokal.', 0);
4. Konfigurasi Database (config.php)
File config.php berfungsi sebagai pusat koneksi database dan inisialisasi session. File ini akan di-include di semua halaman lain.
<?php
// =============================================
// Konfigurasi Database
// =============================================
$host = 'localhost'; // Server database (biasanya localhost)
$dbname = 'evoting_system'; // Nama database yang sudah dibuat
$username = 'root'; // Username MySQL (default XAMPP: root)
$password = ''; // Password MySQL (default XAMPP: kosong)
try {
// Membuat koneksi PDO (PHP Data Objects)
// PDO lebih aman daripada mysqli karena mendukung prepared statements
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
// Mengatur mode error menjadi exception (agar mudah debugging)
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Mengatur mode fetch default menjadi assoc array
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
// Jika koneksi gagal, tampilkan pesan error dan hentikan script
die("Koneksi database gagal: " . $e->getMessage());
}
// =============================================
// Memulai Session Management
// Session digunakan untuk menyimpan data login user
// =============================================
session_start();
?>
PDO (PHP Data Objects): PDO adalah extension PHP yang menyediakan antarmuka yang konsisten untuk mengakses berbagai database. Keunggulan utamanya adalah prepared statements yang secara otomatis mencegah SQL Injection.
session_start(): Fungsi ini memulai atau melanjutkan session yang ada. Session digunakan untuk menyimpan data sementara seperti ID user, role, dan status login di seluruh halaman.
5. Halaman Utama (index.php)
Halaman ini adalah landing page pertama yang dilihat pengunjung. Jika pengguna sudah login, mereka akan langsung diarahkan ke dashboard.
<?php
require_once 'config.php';
// Redirect jika sudah login - langsung ke dashboard
if (isset($_SESSION['user_id'])) {
header('Location: dashboard.php');
exit;
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistem E-Voting Terpercaya</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container navbar">
<a href="index.php" class="logo">🗳️ E-Voting</a>
<div class="nav-links">
<a href="login.php">Login</a>
<a href="register.php">Daftar</a>
</div>
</div>
</header>
<main>
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1>Sistem Pemungutan Suara Elektronik</h1>
<p>Modern, Aman, dan Transparan. Berikan suara Anda untuk kandidat pilihan dengan mudah.</p>
<div class="hero-buttons">
<a href="login.php" class="btn">Mulai Memilih</a>
<a href="register.php" class="btn btn-outline">Daftar Akun</a>
</div>
</div>
</section>
<!-- Fitur Section -->
<section class="features">
<div class="container">
<h2 class="section-title">Mengapa Memilih E-Voting Kami?</h2>
<div class="grid">
<div class="card feature-card">
<div class="feature-icon">🔒</div>
<h3>Keamanan Tinggi</h3>
<p>Password dienkripsi dengan bcrypt dan setiap pemilih hanya bisa memilih satu kali.</p>
</div>
<div class="card feature-card">
<div class="feature-icon">📊</div>
<h3>Hasil Real-time</h3>
<p>Statistik suara dapat dilihat secara langsung oleh semua pemilih.</p>
</div>
<div class="card feature-card">
<div class="feature-icon">📱</div>
<h3>Responsif</h3>
<p>Dapat diakses dari desktop, tablet, maupun smartphone.</p>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<p>© <?php echo date('Y'); ?> E-Voting System. All rights reserved.</p>
</div>
</footer>
</body>
</html>
6. Registrasi (register.php)
Halaman ini digunakan oleh pemilih baru untuk membuat akun. Password akan di-hash menggunakan algoritma bcrypt sebelum disimpan ke database.
<?php
require_once 'config.php';
// Inisialisasi variabel pesan
$error = '';
$success = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username']);
$email = trim($_POST['email']);
$password = $_POST['password'];
$confirm_password = $_POST['confirm_password'];
// Validasi input
if (empty($username) || empty($email) || empty($password)) {
$error = "Semua field harus diisi!";
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = "Format email tidak valid!";
} elseif (strlen($password) < 6) {
$error = "Password minimal 6 karakter!";
} elseif ($password !== $confirm_password) {
$error = "Konfirmasi password tidak cocok!";
} else {
// Hash password menggunakan bcrypt (keamanan tinggi)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$role = 'voter'; // Default role adalah voter
try {
$stmt = $pdo->prepare("INSERT INTO users (username, email, password, role) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $email, $hashed_password, $role]);
$_SESSION['register_success'] = "Registrasi berhasil! Silakan login.";
header('Location: login.php');
exit;
} catch (PDOException $e) {
if ($e->getCode() == 23000) {
$error = "Username atau email sudah terdaftar!";
} else {
$error = "Terjadi kesalahan: " . $e->getMessage();
}
}
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daftar Akun - E-Voting</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container navbar">
<a href="index.php" class="logo">🗳️ E-Voting</a>
<div class="nav-links">
<a href="index.php">Beranda</a>
<a href="login.php">Login</a>
</div>
</div>
</header>
<main class="container">
<div class="auth-card">
<h2>Daftar Akun Baru</h2>
<p>Silakan isi formulir di bawah untuk mendaftar sebagai pemilih.</p>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<form action="register.php" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password (min. 6 karakter)</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label for="confirm_password">Konfirmasi Password</label>
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
</div>
<button type="submit" class="btn" style="width: 100%;">Daftar Sekarang</button>
</form>
<p class="auth-footer">Sudah punya akun? <a href="login.php">Login disini</a></p>
</div>
</main>
<footer>
<div class="container">
<p>© <?php echo date('Y'); ?> E-Voting System. All rights reserved.</p>
</div>
</footer>
</body>
</html>
7. Login (login.php)
Halaman login memverifikasi kredensial pengguna menggunakan password_verify() untuk membandingkan password input dengan hash di database.
<?php
require_once 'config.php';
// Redirect jika sudah login
if (isset($_SESSION['user_id'])) {
header('Location: dashboard.php');
exit;
}
$error = '';
$success = isset($_SESSION['register_success']) ? $_SESSION['register_success'] : '';
unset($_SESSION['register_success']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'];
$password = $_POST['password'];
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password'])) {
// Set session data
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['has_voted'] = (bool)$user['has_voted'];
header('Location: dashboard.php');
exit;
} else {
$error = "Username atau password salah!";
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - E-Voting</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container navbar">
<a href="index.php" class="logo">🗳️ E-Voting</a>
<div class="nav-links">
<a href="index.php">Beranda</a>
<a href="register.php">Daftar</a>
</div>
</div>
</header>
<main class="container">
<div class="auth-card">
<h2>Login ke Akun Anda</h2>
<p>Masukkan username dan password untuk melanjutkan.</p>
<?php if ($success): ?>
<div class="alert alert-success"><?php echo $success; ?></div>
<?php endif; ?>
<?php if ($error): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<form action="login.php" method="POST">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn" style="width: 100%;">Login</button>
</form>
<p class="auth-footer">Belum punya akun? <a href="register.php">Daftar disini</a></p>
</div>
</main>
<footer>
<div class="container">
<p>© <?php echo date('Y'); ?> E-Voting System. All rights reserved.</p>
</div>
</footer>
</body>
</html>
password_verify(): Fungsi ini membandingkan password plain text yang dimasukkan user dengan hash yang tersimpan di database. Hash bcrypt bersifat one-way, artinya tidak bisa didekripsi kembali. Ini adalah standar keamanan modern untuk menyimpan password.
Session: Setelah login berhasil, data user disimpan dalam session superglobal. Session ini akan tetap ada sampai user logout atau browser ditutup.
8. Dashboard Pemilih (dashboard.php)
Halaman ini adalah pusat aktivitas pemilih. Di sini mereka dapat melihat daftar kandidat, statistik suara, dan memberikan suara (jika belum memilih).
<?php
require_once 'config.php';
// Redirect jika belum login
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
// Ambil total suara keseluruhan
$total_votes = $pdo->query("SELECT COALESCE(SUM(votes), 0) as total FROM candidates")->fetch()['total'];
// Ambil data kandidat beserta persentase suara
// COALESCE digunakan untuk menghindari division by zero
$stmt = $pdo->query("
SELECT *,
ROUND(COALESCE(votes / NULLIF((SELECT SUM(votes) FROM candidates), 0) * 100, 0), 2) as percentage
FROM candidates
ORDER BY votes DESC
");
$candidates = $stmt->fetchAll();
// Proses voting
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['candidate_id']) && !$_SESSION['has_voted']) {
$candidate_id = $_POST['candidate_id'];
$user_id = $_SESSION['user_id'];
try {
$pdo->beginTransaction();
// 1. Tambah jumlah suara ke kandidat
$stmt = $pdo->prepare("UPDATE candidates SET votes = votes + 1 WHERE id = ?");
$stmt->execute([$candidate_id]);
// 2. Catat riwayat voting
$stmt = $pdo->prepare("INSERT INTO votes (user_id, candidate_id) VALUES (?, ?)");
$stmt->execute([$user_id, $candidate_id]);
// 3. Update status user menjadi sudah memilih
$stmt = $pdo->prepare("UPDATE users SET has_voted = TRUE WHERE id = ?");
$stmt->execute([$user_id]);
$pdo->commit();
$_SESSION['has_voted'] = true;
$_SESSION['success'] = "Terima kasih telah berpartisipasi! Suara Anda telah disimpan.";
header('Location: dashboard.php');
exit;
} catch (PDOException $e) {
$pdo->rollBack();
$error = "Terjadi kesalahan saat memproses suara: " . $e->getMessage();
}
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - E-Voting</title>
<link rel="stylesheet" href="style.css">
<style>
.progress-container {
width: 100%;
background-color: #e0e0e0;
border-radius: 10px;
margin: 10px 0;
overflow: hidden;
}
.progress-bar {
height: 20px;
border-radius: 10px;
background: linear-gradient(90deg, #4a6bff, #667eea);
text-align: center;
line-height: 20px;
color: white;
font-size: 11px;
transition: width 0.5s ease;
}
.vote-info {
display: flex;
justify-content: space-between;
margin-top: 5px;
font-size: 13px;
}
.candidate-card {
text-align: center;
}
.candidate-card img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 15px;
border: 3px solid #ddd;
}
.candidate-card h3 {
margin-bottom: 5px;
}
.btn-vote {
margin-top: 15px;
background-color: #28a745;
}
.btn-vote:hover {
background-color: #218838;
}
</style>
</head>
<body>
<header>
<div class="container navbar">
<a href="index.php" class="logo">🗳️ E-Voting</a>
<div class="nav-links">
<?php if ($_SESSION['role'] === 'admin'): ?>
<a href="admin.php">Admin Panel</a>
<?php endif; ?>
<a href="logout.php">Logout</a>
</div>
</div>
</header>
<main class="container">
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"><?php echo $_SESSION['success']; unset($_SESSION['success']); ?></div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<div class="welcome-card">
<h2>Selamat datang, <?php echo htmlspecialchars($_SESSION['username']); ?>!</h2>
<p>Silakan berikan suara Anda untuk kandidat pilihan di bawah ini.</p>
</div>
<?php if ($_SESSION['has_voted']): ?>
<div class="alert alert-success" style="text-align: center;">
<h3>✅ Anda sudah memberikan suara!</h3>
<p>Terima kasih telah berpartisipasi dalam pemungutan suara ini.</p>
</div>
<?php endif; ?>
<div class="stats-card">
<h3>📊 Statistik Pemilihan</h3>
<p>Total Suara Masuk: <strong><?php echo $total_votes; ?></strong> suara</p>
<?php if ($total_votes > 0): ?>
<p>Partisipasi pemilih: <strong><?php echo $total_votes; ?></strong> orang telah memberikan suara.</p>
<?php else: ?>
<p>Belum ada suara yang masuk. Jadilah pemilih pertama!</p>
<?php endif; ?>
</div>
<h3 class="section-title">Daftar Kandidat</h3>
<div class="grid">
<?php foreach ($candidates as $candidate): ?>
<div class="card candidate-card">
<?php if ($candidate['photo'] && file_exists($candidate['photo'])): ?>
<img src="<?php echo htmlspecialchars($candidate['photo']); ?>" alt="<?php echo htmlspecialchars($candidate['name']); ?>">
<?php else: ?>
<img src="https://ui-avatars.com/api/?name=<?php echo urlencode($candidate['name']); ?>&background=4a6bff&color=fff&size=120" alt="<?php echo htmlspecialchars($candidate['name']); ?>">
<?php endif; ?>
<h3><?php echo htmlspecialchars($candidate['name']); ?></h3>
<p><?php echo htmlspecialchars(substr($candidate['description'], 0, 100)); ?>...</p>
<div class="progress-container">
<div class="progress-bar" style="width: <?php echo $candidate['percentage']; ?>%;">
<?php echo $candidate['percentage']; ?>%
</div>
</div>
<div class="vote-info">
<span>🗳️ <?php echo $candidate['votes']; ?> Suara</span>
<span>📊 <?php echo $candidate['percentage']; ?>%</span>
</div>
<?php if (!$_SESSION['has_voted']): ?>
<form action="dashboard.php" method="POST" onsubmit="return confirm('Apakah Anda yakin ingin memilih kandidat ini? Suara tidak dapat diubah setelah dipilih.')">
<input type="hidden" name="candidate_id" value="<?php echo $candidate['id']; ?>">
<button type="submit" class="btn btn-vote">🗳️ Pilih Kandidat Ini</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</main>
<footer>
<div class="container">
<p>© <?php echo date('Y'); ?> E-Voting System. All rights reserved.</p>
</div>
</footer>
</body>
</html>
Transaction: Kode voting menggunakan
beginTransaction(), commit(), dan rollBack(). Ini memastikan bahwa semua tiga operasi (update suara, insert riwayat, update status user) berhasil semua atau gagal semua. Jika salah satu gagal, tidak akan ada perubahan parsial yang merusak integritas data.COALESCE dan NULLIF: Digunakan untuk menghindari division by zero saat menghitung persentase. Jika total suara = 0, persentase dihitung sebagai 0.
9. Panel Admin (admin.php)
Halaman ini hanya dapat diakses oleh user dengan role 'admin'. Admin dapat menambah kandidat baru, menghapus kandidat, dan melihat statistik lengkap pemilihan.
<?php
require_once 'config.php';
// Redirect jika bukan admin
if (!isset($_SESSION['user_id']) || $_SESSION['role'] !== 'admin') {
header('Location: login.php');
exit;
}
// =============================================
// TAMBAH KANDIDAT BARU
// =============================================
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_candidate'])) {
$name = trim($_POST['name']);
$description = trim($_POST['description']);
$photo = null;
// Handle upload foto
if (isset($_FILES['photo']) && $_FILES['photo']['error'] === UPLOAD_ERR_OK) {
$uploadDir = 'uploads/';
// Buat folder uploads jika belum ada
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
// Validasi tipe file (hanya gambar)
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$fileType = mime_content_type($_FILES['photo']['tmp_name']);
$fileSize = $_FILES['photo']['size']; // dalam bytes
if (!in_array($fileType, $allowedTypes)) {
$error = "Hanya file JPG, PNG, GIF, atau WEBP yang diizinkan.";
} elseif ($fileSize > 2 * 1024 * 1024) { // Maksimal 2MB
$error = "Ukuran file maksimal 2MB.";
} else {
// Generate nama file unik
$extension = pathinfo($_FILES['photo']['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $extension;
$destination = $uploadDir . $filename;
if (move_uploaded_file($_FILES['photo']['tmp_name'], $destination)) {
$photo = $destination;
} else {
$error = "Gagal menyimpan file foto.";
}
}
}
if (!isset($error)) {
try {
$stmt = $pdo->prepare("INSERT INTO candidates (name, photo, description) VALUES (?, ?, ?)");
$stmt->execute([$name, $photo, $description]);
$_SESSION['success'] = "Kandidat '{$name}' berhasil ditambahkan!";
header('Location: admin.php');
exit;
} catch (PDOException $e) {
$error = "Terjadi kesalahan database: " . $e->getMessage();
}
}
}
// =============================================
// HAPUS KANDIDAT
// =============================================
if (isset($_GET['delete'])) {
$id = (int)$_GET['delete'];
try {
// Ambil info foto sebelum menghapus
$stmt = $pdo->prepare("SELECT photo FROM candidates WHERE id = ?");
$stmt->execute([$id]);
$candidate = $stmt->fetch();
// Hapus file foto jika ada
if ($candidate && $candidate['photo'] && file_exists($candidate['photo'])) {
unlink($candidate['photo']);
}
// Hapus kandidat dari database
$stmt = $pdo->prepare("DELETE FROM candidates WHERE id = ?");
$stmt->execute([$id]);
$_SESSION['success'] = "Kandidat berhasil dihapus!";
header('Location: admin.php');
exit;
} catch (PDOException $e) {
$error = "Gagal menghapus kandidat: " . $e->getMessage();
}
}
// =============================================
// AMBIL DATA UNTUK DITAMPILKAN
// =============================================
$candidates = $pdo->query("SELECT * FROM candidates ORDER BY created_at DESC")->fetchAll();
$users = $pdo->query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll();
$total_voters = $pdo->query("SELECT COUNT(*) as total FROM users WHERE role = 'voter'")->fetch()['total'];
$voted = $pdo->query("SELECT COUNT(*) as total FROM users WHERE has_voted = TRUE")->fetch()['total'];
$vote_percentage = $total_voters > 0 ? round(($voted / $total_voters) * 100, 2) : 0;
// Data untuk grafik
$candidate_names = [];
$candidate_votes = [];
foreach ($candidates as $c) {
$candidate_names[] = $c['name'];
$candidate_votes[] = $c['votes'];
}
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Panel - E-Voting</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<header>
<div class="container navbar">
<a href="index.php" class="logo">🗳️ E-Voting</a>
<div class="nav-links">
<a href="dashboard.php">Dashboard</a>
<a href="logout.php">Logout</a>
</div>
</div>
</header>
<main class="container">
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success"><?php echo $_SESSION['success']; unset($_SESSION['success']); ?></div>
<?php endif; ?>
<?php if (isset($error)): ?>
<div class="alert alert-danger"><?php echo $error; ?></div>
<?php endif; ?>
<h2>👑 Admin Panel</h2>
<!-- Statistik & Form Tambah Kandidat -->
<div class="grid grid-2">
<div class="card">
<h3>📊 Statistik Voting</h3>
<div class="stat-item">
<span>Total Pemilih Terdaftar:</span>
<strong><?php echo $total_voters; ?></strong>
</div>
<div class="stat-item">
<span>Sudah Memilih:</span>
<strong><?php echo $voted; ?></strong>
</div>
<div class="stat-item">
<span>Persentase Partisipasi:</span>
<strong><?php echo $vote_percentage; ?>%</strong>
</div>
<div class="stat-item">
<span>Belum Memilih:</span>
<strong><?php echo $total_voters - $voted; ?></strong>
</div>
</div>
<div class="card">
<h3>➕ Tambah Kandidat Baru</h3>
<form action="admin.php" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="name">Nama Lengkap Kandidat *</label>
<input type="text" id="name" name="name" class="form-control" required>
</div>
<div class="form-group">
<label for="description">Deskripsi / Visi Misi *</label>
<textarea id="description" name="description" class="form-control" rows="4" required></textarea>
</div>
<div class="form-group">
<label for="photo">Foto Kandidat (Max 2MB, format JPG/PNG/GIF)</label>
<input type="file" id="photo" name="photo" class="form-control" accept="image/jpeg,image/png,image/gif,image/webp">
</div>
<button type="submit" name="add_candidate" class="btn">➕ Tambahkan Kandidat</button>
</form>
</div>
</div>
<!-- Grafik Hasil Voting -->
<div class="card">
<h3>📈 Grafik Hasil Voting</h3>
<canvas id="resultsChart" style="max-height: 400px;"></canvas>
</div>
<h3>📋 Daftar Kandidat</h3>
<div class="grid">
<?php foreach ($candidates as $candidate): ?>
<div class="card">
<div class="candidate-admin">
<?php if ($candidate['photo'] && file_exists($candidate['photo'])): ?>
<img src="<?php echo htmlspecialchars($candidate['photo']); ?>" alt="<?php echo htmlspecialchars($candidate['name']); ?>">
<?php else: ?>
<img src="https://ui-avatars.com/api/?name=<?php echo urlencode($candidate['name']); ?>&background=4a6bff&color=fff&size=80" alt="<?php echo htmlspecialchars($candidate['name']); ?>">
<?php endif; ?>
<div class="candidate-admin-info">
<h4><?php echo htmlspecialchars($candidate['name']); ?></h4>
<p>🗳️ <?php echo $candidate['votes']; ?> suara</p>
<p><?php echo htmlspecialchars(substr($candidate['description'], 0, 80)); ?>...</p>
</div>
</div>
<a href="admin.php?delete=<?php echo $candidate['id']; ?>" class="btn btn-danger" onclick="return confirm('Hapus kandidat ini? Semua data suara akan ikut terhapus.')">🗑️ Hapus</a>
</div>
<?php endforeach; ?>
</div>
<h3>👥 Daftar Pengguna Terdaftar</h3>
<div class="card">
<div class="table-responsive">
<table>
<thead>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Status Voting</th>
<th>Tanggal Daftar</th>
</thead>
<tbody>
<?php foreach ($users as $user): ?>
<tr>
<td><?php echo htmlspecialchars($user['username']); ?></td>
<td><?php echo htmlspecialchars($user['email']); ?></td>
<td><span class="badge badge-<?php echo $user['role']; ?>"><?php echo ucfirst($user['role']); ?></span></td>
<td>
<?php if ($user['has_voted']): ?>
<span class="badge badge-success">✅ Sudah Memilih</span>
<?php else: ?>
<span class="badge badge-warning">⏳ Belum Memilih</span>
<?php endif; ?>
</td>
<td><?php echo date('d/m/Y H:i', strtotime($user['created_at'])); ?></td>
<?php endforeach; ?>
</tbody>
</div>
</div>
</main>
<footer>
<div class="container">
<p>© <?php echo date('Y'); ?> E-Voting System. All rights reserved.</p>
</div>
</footer>
<script>
// Grafik menggunakan Chart.js
const ctx = document.getElementById('resultsChart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: {
labels: <?php echo json_encode($candidate_names); ?>,
datasets: [{
label: 'Jumlah Suara',
data: <?php echo json_encode($candidate_votes); ?>,
backgroundColor: ['#4a6bff', '#28a745', '#ffc107', '#dc3545', '#17a2b8'],
borderRadius: 8
}]
},
options: {
responsive: true,
plugins: {
legend: { position: 'top' },
tooltip: { callbacks: { label: function(context) { return context.raw + ' suara'; } } }
}
}
});
</script>
</body>
</html>
10. Logout (logout.php)
Halaman ini bertugas menghapus session pengguna dan mengarahkan kembali ke halaman login.
<?php
require_once 'config.php';
// Hapus semua data session
$_SESSION = array();
// Hapus session cookie jika ada
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
// Hancurkan session
session_destroy();
// Redirect ke halaman login
header('Location: login.php');
exit;
?>
11. Stylesheet (style.css)
File CSS ini mengatur tampilan seluruh halaman agar responsif dan menarik.
:root {
--primary-color: #4a6bff;
--primary-dark: #3a5bef;
--secondary-color: #f8f9fa;
--dark-color: #343a40;
--light-color: #ffffff;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--border-radius: 12px;
--box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
--transition: all 0.3s ease;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #e9ecef 100%);
color: var(--dark-color);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
}
/* Header & Navigation */
header {
background: var(--light-color);
box-shadow: var(--box-shadow);
position: sticky;
top: 0;
z-index: 100;
}
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: var(--primary-color);
text-decoration: none;
transition: var(--transition);
}
.logo:hover {
color: var(--primary-dark);
}
.nav-links {
display: flex;
gap: 28px;
}
.nav-links a {
text-decoration: none;
color: var(--dark-color);
font-weight: 500;
transition: var(--transition);
}
.nav-links a:hover {
color: var(--primary-color);
}
/* Buttons */
.btn {
display: inline-block;
padding: 12px 28px;
background: var(--primary-color);
color: var(--light-color);
border: none;
border-radius: var(--border-radius);
cursor: pointer;
text-decoration: none;
font-weight: 600;
transition: var(--transition);
font-size: 0.95rem;
}
.btn:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(74, 107, 255, 0.3);
}
.btn-outline {
background: transparent;
border: 2px solid var(--primary-color);
color: var(--primary-color);
}
.btn-outline:hover {
background: var(--primary-color);
color: var(--light-color);
transform: translateY(-2px);
}
.btn-danger {
background: var(--danger-color);
}
.btn-danger:hover {
background: #c82333;
}
/* Cards */
.card {
background: var(--light-color);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 24px;
transition: var(--transition);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
}
.auth-card {
background: var(--light-color);
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 32px;
max-width: 480px;
margin: 40px auto;
}
.auth-card h2 {
text-align: center;
margin-bottom: 8px;
}
.auth-card p {
text-align: center;
color: #666;
margin-bottom: 24px;
}
.auth-footer {
text-align: center;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid #eee;
}
/* Forms */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--dark-color);
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 1rem;
transition: var(--transition);
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 107, 255, 0.1);
}
/* Alerts */
.alert {
padding: 16px 20px;
border-radius: 10px;
margin-bottom: 24px;
}
.alert-success {
background: #d4edda;
color: #155724;
border-left: 4px solid var(--success-color);
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border-left: 4px solid var(--danger-color);
}
/* Grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
.grid-2 {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
/* Hero Section */
.hero {
text-align: center;
padding: 80px 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
margin-bottom: 40px;
border-radius: 0 0 40px 40px;
}
.hero h1 {
font-size: 2.8rem;
margin-bottom: 16px;
}
.hero p {
font-size: 1.2rem;
max-width: 600px;
margin: 0 auto 32px;
opacity: 0.95;
}
.hero-buttons {
display: flex;
gap: 16px;
justify-content: center;
}
.hero-buttons .btn-outline {
background: transparent;
border-color: white;
color: white;
}
.hero-buttons .btn-outline:hover {
background: white;
color: #667eea;
}
/* Section Titles */
.section-title {
text-align: center;
margin: 40px 0 24px;
font-size: 1.8rem;
color: var(--dark-color);
}
/* Feature Cards */
.feature-card {
text-align: center;
}
.feature-icon {
font-size: 3rem;
margin-bottom: 16px;
}
.feature-card h3 {
margin-bottom: 12px;
}
/* Welcome Card */
.welcome-card, .stats-card {
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
color: white;
padding: 24px;
border-radius: var(--border-radius);
margin-bottom: 24px;
}
.welcome-card h2, .stats-card h3 {
margin-bottom: 8px;
}
/* Candidate Admin */
.candidate-admin {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.candidate-admin img {
width: 70px;
height: 70px;
border-radius: 50%;
object-fit: cover;
}
.candidate-admin-info h4 {
margin-bottom: 4px;
}
/* Badges */
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
.badge-admin {
background: #dc3545;
color: white;
}
.badge-voter {
background: #28a745;
color: white;
}
.badge-success {
background: #28a745;
color: white;
}
.badge-warning {
background: #ffc107;
color: #333;
}
/* Table Responsive */
.table-responsive {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background: var(--secondary-color);
font-weight: 600;
}
/* Footer */
footer {
background: var(--dark-color);
color: var(--light-color);
text-align: center;
padding: 24px 0;
margin-top: 60px;
}
/* Stat Items */
.stat-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.stat-item:last-child {
border-bottom: none;
}
/* Responsive */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 12px;
}
.nav-links {
flex-wrap: wrap;
justify-content: center;
gap: 16px;
}
.hero h1 {
font-size: 1.8rem;
}
.hero-buttons {
flex-direction: column;
align-items: center;
}
.grid, .grid-2 {
grid-template-columns: 1fr;
}
.container {
padding: 0 16px;
}
.auth-card {
padding: 24px;
margin: 20px;
}
}
12. Cara Menjalankan Aplikasi
1. Install XAMPP
Unduh dan install XAMPP dari situs resmi Apache Friends. Jalankan Apache dan MySQL dari kontrol panel XAMPP.
2. Buat Folder Proyek
Buat folder baru bernama
evoting di dalam direktori htdocs (biasanya di C:\xampp\htdocs\).3. Import Database
- Buka browser dan akses
http://localhost/phpmyadmin- Buat database baru bernama
evoting_system- Buka tab SQL dan paste seluruh kode dari file
evoting.sql- Klik "Go" untuk menjalankan query
4. Letakkan File PHP
Simpan semua file PHP (config.php, index.php, login.php, register.php, dashboard.php, admin.php, logout.php) dan style.css ke folder
htdocs/evoting/5. Buat Folder Uploads
Buat folder
uploads di dalam htdocs/evoting/ dan atur permission-nya menjadi 755.6. Jalankan Aplikasi
Buka browser dan akses
http://localhost/evoting7. Login sebagai Admin
Username:
adminPassword:
password
13. Troubleshooting (Mengatasi Masalah Umum)
❌ Error "Koneksi database gagal"
Penyebab: MySQL tidak berjalan atau kredensial database salah.
Solusi: Pastikan MySQL menyala di XAMPP. Cek file config.php, pastikan username 'root' dan password kosong (default XAMPP).
❌ Error "Class 'PDO' not found"
Penyebab: Ekstensi PDO MySQL tidak aktif di php.ini.
Solusi: Buka file php.ini (di folder PHP XAMPP), cari ;extension=pdo_mysql dan hapus titik koma di depannya. Restart Apache.
❌ Error "Failed to open stream: No such file or directory" saat upload foto
Penyebab: Folder uploads tidak ada atau permission tidak cukup.
Solusi: Buat folder 'uploads' di direktori proyek dan beri permission 755 (di Linux/Mac) atau pastikan writeable (di Windows).
❌ Session tidak tersimpan setelah login
Penyebab: session_start() tidak dipanggil atau ada output sebelum session_start.
Solusi: Pastikan tidak ada spasi atau karakter apapun sebelum <?php di config.php. Letakkan session_start() di awal file.
14. Pengembangan Lebih Lanjut
- Lupa Password: Tambahkan fitur reset password dengan mengirimkan link ke email pengguna menggunakan PHPMailer.
- Ekspor Data: Tambahkan fitur ekspor hasil voting ke format Excel (PHPExcel/PhpSpreadsheet) atau PDF (DomPDF).
- Captcha: Implementasi Google reCAPTCHA pada halaman registrasi dan login untuk mencegah bot.
- HTTPS: Jika aplikasi di-deploy ke server online, pastikan menggunakan SSL/TLS untuk keamanan komunikasi data.
- Pembatasan Waktu Voting: Tambahkan pengaturan waktu mulai dan berakhirnya sesi pemilihan.
- Notifikasi Email: Kirim email konfirmasi setelah user berhasil memilih.
- Multi-level Admin: Buat sistem role yang lebih kompleks (super admin, admin biasa, operator).
- Log Aktivitas: Catat semua aktivitas pengguna (login, voting, perubahan data) ke dalam tabel logs.
- API Endpoint: Buat REST API untuk integrasi dengan aplikasi mobile atau sistem lain.
- Dashboard Analitik: Tambahkan grafik interaktif dan filter data berdasarkan periode waktu.

Tidak ada komentar:
Posting Komentar