Source Code Aplikasi E-Voting Ketua Osis dengan Appscript
Source Code Aplikasi E-Voting Ketua OSIS
Solusi Digital Modern untuk Demokrasi Sekolah
Apa itu OSIS?
OSIS (Organisasi Siswa Intra Sekolah) adalah satu-satunya wadah organisasi siswa yang sah di sekolah. OSIS berperan sebagai penggerak kegiatan kesiswaan, tempat belajar kepemimpinan, dan penyambung aspirasi antara siswa dan pihak sekolah. Pemilihan Ketua OSIS merupakan momen krusial untuk menentukan arah kemajuan kreativitas siswa selama satu tahun ke depan.
Pemilihan Konvensional vs E-Voting
Mengenal Google Apps Script
Google Apps Script adalah platform pengkodean berbasis JavaScript yang disediakan oleh Google untuk mengotomatiskan tugas-tugas di seluruh produk Google (seperti Sheets, Docs, dan Drive).
Manfaat untuk E-Voting:
- Gratis: Tidak perlu membayar hosting atau domain.
- Integrasi: Data pemilih dan hasil suara otomatis tersimpan di Google Sheets.
- Mudah Diakses: Bisa dibuka melalui smartphone maupun laptop siswa.
Langkah-langkah Persiapan
1. Persiapan Spreadsheet: Buat Google Sheets baru.
2. Buka AppScript: Klik Menu Extensions > Apps Script.
3. Copy-Paste Kode: Masukkan kode
Code.gs dan index.html di bawah.
4. Deploy: Klik Deploy > New Deployment > Web App. Pilih akses "Anyone".
1. File: Code.gs
/**
* E-Voting System 2026 - Code.gs
* Backend logic for Google Apps Script
*/
// --- KONFIGURASI NAMA SHEET ---
const DB_CONFIG = "Konfigurasi";
const DB_SISWA = "Data Siswa";
const DB_KANDIDAT = "Data Kandidat";
function doGet() {
setupDatabase(); // Auto-setup saat dibuka
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('E-Voting OSIS Premium')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// --- FUNGSI DATABASE AUTO-SETUP ---
function setupDatabase() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 1. Setup Sheet Konfigurasi
let sheetConfig = ss.getSheetByName(DB_CONFIG);
if (!sheetConfig) {
sheetConfig = ss.insertSheet(DB_CONFIG);
sheetConfig.appendRow(["Key", "Value"]);
sheetConfig.appendRow(["Nama Sekolah", "SMA Negeri Future"]);
sheetConfig.appendRow(["Status Voting", "BUKA"]);
}
// 2. Setup Sheet Data Siswa
let sheetSiswa = ss.getSheetByName(DB_SISWA);
if (!sheetSiswa) {
sheetSiswa = ss.insertSheet(DB_SISWA);
sheetSiswa.appendRow(["NIS", "Nama", "Kelas", "Status Pemilihan"]);
}
// 3. Setup Sheet Kandidat
let sheetKandidat = ss.getSheetByName(DB_KANDIDAT);
if (!sheetKandidat) {
sheetKandidat = ss.insertSheet(DB_KANDIDAT);
sheetKandidat.appendRow(["ID", "Ketua", "Wakil", "Visi", "Misi", "FotoURL", "Jumlah Suara"]);
}
}
// --- FUNGSI API (Dipanggil dari HTML) ---
function getInitialData() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const config = ss.getSheetByName(DB_CONFIG).getDataRange().getValues();
const sheetKandidat = ss.getSheetByName(DB_KANDIDAT);
// Handle jika sheet kosong
let candidates = [];
if (sheetKandidat.getLastRow() > 1) {
candidates = sheetKandidat.getDataRange().getValues();
}
let candidateList = [];
if (candidates.length > 1) {
for (let i = 1; i < candidates.length; i++) {
candidateList.push({
id: candidates[i][0],
ketua: candidates[i][1],
wakil: candidates[i][2],
visi: candidates[i][3],
misi: candidates[i][4],
foto: candidates[i][5],
suara: candidates[i][6]
});
}
}
let schoolName = "Sekolah";
config.forEach(row => {
if(row[0] === "Nama Sekolah") schoolName = row[1];
});
return { schoolName, candidates: candidateList };
}
function saveSchoolConfig(name) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(DB_CONFIG);
const data = sheet.getDataRange().getValues();
for(let i=0; i<data.length; i++){
if(data[i][0] === "Nama Sekolah"){
sheet.getRange(i+1, 2).setValue(name);
return "Nama sekolah berhasil disimpan";
}
}
return "Gagal menyimpan";
}
function addStudentsBulk(studentList) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(DB_SISWA);
const lastRow = sheet.getLastRow();
const existingNIS = lastRow > 1 ? sheet.getRange(2, 1, lastRow-1).getValues().flat() : [];
let newRows = [];
let duplicateCount = 0;
studentList.forEach(s => {
// Validasi NIS harus string agar tidak error match
if (!existingNIS.includes(String(s.nis)) && !existingNIS.includes(Number(s.nis))) {
newRows.push([s.nis, s.nama, s.kelas, "BELUM"]);
} else {
duplicateCount++;
}
});
if (newRows.length > 0) {
sheet.getRange(lastRow + 1, 1, newRows.length, 4).setValues(newRows);
}
return { success: true, added: newRows.length, skipped: duplicateCount };
}
// REVISI: Fungsi Tambah Kandidat (Sekarang menerima String URL, bukan upload file)
function addCandidate(data) {
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(DB_KANDIDAT);
const newId = new Date().getTime(); // ID unik pakai timestamp
// Simpan URL langsung dari input user
sheet.appendRow([
newId,
data.namaKetua,
data.namaWakil,
data.visi,
data.misi,
data.fotoUrl, // URL Link
0 // Suara awal 0
]);
return { success: true };
} catch (e) {
return { success: false, error: e.toString() };
}
}
function submitVote(nis, candidateId) {
const lock = LockService.getScriptLock();
try {
// Lock untuk mencegah race condition (vote ganda dalam milidetik yang sama)
lock.waitLock(10000);
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheetSiswa = ss.getSheetByName(DB_SISWA);
const sheetKandidat = ss.getSheetByName(DB_KANDIDAT);
// Cek Siswa
const dataSiswa = sheetSiswa.getDataRange().getValues();
let studentRow = -1;
for (let i = 1; i < dataSiswa.length; i++) {
if (String(dataSiswa[i][0]) === String(nis)) { // Compare as string
studentRow = i + 1;
if (dataSiswa[i][3] === "SUDAH") {
return { success: false, message: "Anda sudah menggunakan hak pilih sebelumnya." };
}
break;
}
}
if (studentRow === -1) {
return { success: false, message: "NIS tidak ditemukan dalam daftar pemilih." };
}
// Update Suara Kandidat
const dataKandidat = sheetKandidat.getDataRange().getValues();
let candidateRow = -1;
for (let i = 1; i < dataKandidat.length; i++) {
// Compare ID
if (String(dataKandidat[i][0]) === String(candidateId)) {
candidateRow = i + 1;
break;
}
}
if (candidateRow !== -1) {
// 1. Tandai Siswa Sudah Memilih
sheetSiswa.getRange(studentRow, 4).setValue("SUDAH");
// 2. Tambah Hitungan Suara
const currentVote = sheetKandidat.getRange(candidateRow, 7).getValue();
sheetKandidat.getRange(candidateRow, 7).setValue(currentVote + 1);
return { success: true, message: "Voting Berhasil! Terima kasih." };
} else {
return { success: false, message: "Data kandidat error." };
}
} catch (e) {
return { success: false, message: "Error: " + e.toString() };
} finally {
lock.releaseLock();
}
}
function resetVotes() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheetSiswa = ss.getSheetByName(DB_SISWA);
const sheetKandidat = ss.getSheetByName(DB_KANDIDAT);
const lastRowSiswa = sheetSiswa.getLastRow();
if(lastRowSiswa > 1) {
sheetSiswa.getRange(2, 4, lastRowSiswa - 1, 1).setValue("BELUM");
}
const lastRowKandidat = sheetKandidat.getLastRow();
if(lastRowKandidat > 1) {
sheetKandidat.getRange(2, 7, lastRowKandidat - 1, 1).setValue(0);
}
return "Sistem berhasil di-reset ke 0.";
}
2. File: index.html
<!DOCTYPE html>
<html lang="id">
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>E-Voting OSIS Premium</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
/* ULTRA PREMIUM STYLING */
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-bg: rgba(255, 255, 255, 0.95);
--glass-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
}
body {
background-color: #f0f2f5;
font-family: 'Poppins', sans-serif;
color: #333;
overflow-x: hidden;
}
/* --- REVISI: HEADER LEBIH KECIL (COMPACT) --- */
.premium-header {
background: var(--primary-gradient);
color: white;
padding: 25px 0 20px; /* Padding dikurangi drastis */
border-radius: 0 0 25px 25px; /* Lengkungan diperkecil */
margin-bottom: 25px; /* Jarak ke menu diperdekat */
position: relative;
z-index: 1;
animation: fadeInDown 0.8s ease-out;
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.2);
}
/* Z-Index Fix untuk Menu */
.nav-pills-container {
position: relative;
z-index: 100;
}
/* Content Animation */
.fade-in-content {
animation: fadeInUp 1s ease-out forwards;
opacity: 0;
}
/* Glass Card Styling */
.premium-card {
background: var(--card-bg);
backdrop-filter: blur(4px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 15px; /* Sedikit diperkecil radiusnya */
box-shadow: var(--glass-shadow);
transition: transform 0.3s ease, box-shadow 0.3s ease;
overflow: hidden;
}
.premium-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(31, 38, 135, 0.2);
}
.candidate-img-wrapper {
width: 100%;
height: 250px; /* Tinggi foto sedikit dikurangi agar proporsional */
overflow: hidden;
position: relative;
background-color: #ddd;
}
.candidate-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.premium-card:hover .candidate-img {
transform: scale(1.05);
}
.vote-btn {
background: var(--primary-gradient);
border: none;
width: 100%;
padding: 12px;
font-weight: 600;
letter-spacing: 1px;
text-transform: uppercase;
transition: opacity 0.3s;
font-size: 0.9rem;
}
.vote-btn:hover {
opacity: 0.9;
}
/* Custom Tabs */
.nav-pills .nav-link {
color: #555;
border-radius: 50px;
padding: 8px 20px; /* Padding tombol menu diperkecil sedikit */
font-weight: 500;
font-size: 0.95rem;
transition: all 0.3s;
margin: 0 5px;
cursor: pointer;
}
.nav-pills .nav-link.active {
background: var(--primary-gradient);
color: white;
box-shadow: 0 4px 10px rgba(118, 75, 162, 0.3);
}
/* Animations */
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.loader {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255,255,255,0.9); z-index: 9999;
justify-content: center; align-items: center;
flex-direction: column;
}
</style>
</head>
<body>
<div class="loader" id="loader">
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status"></div>
<p class="mt-3 fw-bold text-primary">Memproses...</p>
</div>
<div class="container-fluid p-0">
<div class="premium-header text-center">
<h1 class="fw-bold display-6 mb-0" id="schoolNameDisplay">E-Voting System</h1>
<p class="small opacity-75 mt-1 mb-0">Suara Anda Menentukan Masa Depan</p>
</div>
<div class="container mb-5 fade-in-content" style="animation-delay: 0.2s;">
<div class="nav-pills-container">
<ul class="nav nav-pills mb-4 justify-content-center" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="pills-vote-tab" data-bs-toggle="pill" data-bs-target="#pills-vote" type="button" role="tab">🗳️ Bilik Suara</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-result-tab" data-bs-toggle="pill" data-bs-target="#pills-result" type="button" role="tab">📊 Hasil Live</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="pills-admin-tab" data-bs-toggle="pill" data-bs-target="#pills-admin" type="button" role="tab">⚙️ Panel Admin</button>
</li>
</ul>
</div>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-vote" role="tabpanel">
<div class="row g-4" id="candidateContainer">
<div class="col-12 text-center py-5">
<div class="spinner-border text-secondary" role="status"></div>
<p>Memuat Kandidat...</p>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-result" role="tabpanel">
<div class="card premium-card border-0">
<div class="card-body p-3">
<h5 class="card-title text-center fw-bold mb-3" style="color: #764ba2;">Hasil Perolehan Suara</h5>
<div style="height: 350px; position: relative;">
<canvas id="voteChart"></canvas>
</div>
<div class="mt-3 text-center">
<button class="btn btn-outline-primary btn-sm rounded-pill px-4" onclick="loadCharts()">🔄 Segarkan Data</button>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="pills-admin" role="tabpanel">
<div class="row g-3">
<div class="col-md-6">
<div class="card premium-card h-100 border-0">
<div class="card-body">
<h6 class="fw-bold mb-2">🏫 Konfigurasi Sekolah</h6>
<div class="mb-2">
<label class="form-label text-muted small">Nama Sekolah</label>
<input type="text" class="form-control form-control-sm" id="adminSchoolName">
</div>
<button class="btn btn-primary btn-sm w-100 rounded-pill" onclick="saveConfig()">Simpan</button>
<hr class="my-3">
<button class="btn btn-danger btn-sm w-100 rounded-pill" onclick="resetAllVotes()">⚠️ Reset Sistem</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card premium-card h-100 border-0">
<div class="card-body">
<h6 class="fw-bold mb-2">👥 Data Pemilih (Siswa)</h6>
<div class="alert alert-light border py-1 px-2 mb-2" style="font-size: 0.75rem;">
Format: <b>NIS, Nama, Kelas</b> (Enter per baris)
</div>
<textarea class="form-control form-control-sm mb-2" id="studentBulkInput" rows="3" placeholder="1001, Ahmad, XII-A"></textarea>
<button class="btn btn-success btn-sm w-100 rounded-pill" onclick="uploadStudents()">Import Siswa</button>
</div>
</div>
</div>
<div class="col-12">
<div class="card premium-card border-0">
<div class="card-body">
<h6 class="fw-bold mb-3 text-warning">⭐ Tambah Paslon Baru</h6>
<form id="formCandidate">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label small">Ketua</label>
<input type="text" class="form-control form-control-sm" id="candKetua" required>
</div>
<div class="col-md-6">
<label class="form-label small">Wakil</label>
<input type="text" class="form-control form-control-sm" id="candWakil" required>
</div>
<div class="col-12">
<label class="form-label small fw-bold">Link Foto (Google Drive ID)</label>
<input type="text" class="form-control form-control-sm" id="candPhotoUrl" placeholder="https://lh3.googleusercontent.com/d/..." required>
</div>
<div class="col-12">
<label class="form-label small">Visi</label>
<textarea class="form-control form-control-sm" id="candVisi" rows="2"></textarea>
</div>
<div class="col-12">
<label class="form-label small">Misi</label>
<textarea class="form-control form-control-sm" id="candMisi" rows="2"></textarea>
</div>
</div>
<button type="submit" class="btn btn-warning btn-sm w-100 rounded-pill mt-3 fw-bold">Simpan Paslon</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<footer class="text-center py-3 bg-white border-top mt-4">
<small class="text-muted fw-bold" style="font-size: 0.75rem;">© 2026 E-Voting System. Built with Google Apps Script.</small>
</footer>
</div>
<div class="modal fade" id="voteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow-lg" style="border-radius: 20px;">
<div class="modal-header bg-primary text-white border-0" style="border-radius: 20px 20px 0 0;">
<h5 class="modal-title fs-6">Konfirmasi Pilihan</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4 text-center">
<p class="mb-2 small">Anda yakin memilih:</p>
<h5 class="fw-bold text-primary mb-3" id="selectedCandName"></h5>
<div class="form-floating mb-3">
<input type="number" class="form-control" id="inputNIS" placeholder="NIS">
<label for="inputNIS">Masukkan NIS</label>
</div>
<input type="hidden" id="selectedCandId">
<small class="text-danger" style="font-size: 0.75rem;">*Pilihan tidak dapat diubah setelah dikirim.</small>
</div>
<div class="modal-footer border-0 justify-content-center pb-4">
<button type="button" class="btn btn-light btn-sm rounded-pill px-4" data-bs-dismiss="modal">Batal</button>
<button type="button" class="btn btn-primary btn-sm rounded-pill px-5" onclick="confirmVote()">Kirim</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="detailModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content border-0" style="border-radius: 20px;">
<div class="modal-header border-0">
<h5 class="modal-title fw-bold fs-6" id="detailTitle"></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<h6 class="fw-bold text-primary small">VISI</h6>
<p id="detailVisi" class="text-muted fst-italic mb-3 small"></p>
<h6 class="fw-bold text-primary small">MISI</h6>
<p id="detailMisi" class="small" style="white-space: pre-line;"></p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let myChart = null;
document.addEventListener('DOMContentLoaded', function () {
loadInitialData();
var resultTabEl = document.getElementById('pills-result-tab')
resultTabEl.addEventListener('shown.bs.tab', function (event) {
loadCharts();
})
});
function showLoader(show) {
document.getElementById('loader').style.display = show ? 'flex' : 'none';
}
function loadInitialData() {
google.script.run.withSuccessHandler(renderHome).getInitialData();
}
function renderHome(data) {
if(data.schoolName) {
document.getElementById('schoolNameDisplay').innerText = data.schoolName;
document.getElementById('adminSchoolName').value = data.schoolName;
}
const container = document.getElementById('candidateContainer');
container.innerHTML = '';
if (!data.candidates || data.candidates.length === 0) {
container.innerHTML = '<div class="col-12 text-center text-muted"><h4>Belum ada kandidat.</h4></div>';
return;
}
data.candidates.forEach((c, index) => {
const imgUrl = c.foto ? c.foto : 'https://via.placeholder.com/400x300?text=No+Image';
const card = `
<div class="col-md-6 col-lg-4">
<div class="premium-card h-100">
<div class="candidate-img-wrapper">
<img src="${imgUrl}" class="candidate-img" alt="Foto" onerror="this.onerror=null;this.src='https://via.placeholder.com/400x300?text=Error';">
<span class="position-absolute top-0 start-0 bg-warning text-dark px-3 py-1 fw-bold rounded-end mt-3 shadow" style="font-size: 0.8rem">No. ${index + 1}</span>
</div>
<div class="card-body text-center p-3">
<h5 class="fw-bold mb-1">${c.ketua}</h5>
<h6 class="text-muted mb-2 small">& ${c.wakil}</h6>
<button class="btn btn-sm btn-outline-secondary rounded-pill px-3 mb-2" style="font-size: 0.8rem" onclick="showDetail('${c.ketua}', '${c.wakil}', \`${c.visi}\`, \`${c.misi}\`)">Lihat Visi Misi</button>
</div>
<button class="btn btn-primary vote-btn text-white" onclick="openVoteModal('${c.id}', '${c.ketua} & ${c.wakil}')">PILIH PASLON INI</button>
</div>
</div>
`;
container.innerHTML += card;
});
}
// --- VOTING ---
function openVoteModal(id, name) {
document.getElementById('selectedCandId').value = id;
document.getElementById('selectedCandName').innerText = name;
document.getElementById('inputNIS').value = '';
new bootstrap.Modal(document.getElementById('voteModal')).show();
}
function confirmVote() {
const nis = document.getElementById('inputNIS').value;
const id = document.getElementById('selectedCandId').value;
if (!nis) return Swal.fire('Error', 'Masukkan NIS!', 'error');
showLoader(true);
bootstrap.Modal.getInstance(document.getElementById('voteModal')).hide();
google.script.run
.withSuccessHandler(res => {
showLoader(false);
if (res.success) {
Swal.fire({ title: 'Sukses!', text: res.message, icon: 'success' });
} else {
Swal.fire('Gagal', res.message, 'error');
}
})
.withFailureHandler(e => { showLoader(false); Swal.fire('Error', e.message, 'error'); })
.submitVote(nis, id);
}
function showDetail(ketua, wakil, visi, misi) {
document.getElementById('detailTitle').innerText = ketua + " & " + wakil;
document.getElementById('detailVisi').innerText = visi;
document.getElementById('detailMisi').innerText = misi;
new bootstrap.Modal(document.getElementById('detailModal')).show();
}
// --- ADMIN ---
function saveConfig() {
const name = document.getElementById('adminSchoolName').value;
showLoader(true);
google.script.run.withSuccessHandler(res => {
showLoader(false);
Swal.fire('Info', res, 'success').then(() => loadInitialData());
}).saveSchoolConfig(name);
}
function uploadStudents() {
const rawText = document.getElementById('studentBulkInput').value;
if (!rawText) return Swal.fire('Warning', 'Data kosong', 'warning');
const lines = rawText.split('\n');
let students = [];
lines.forEach(line => {
const parts = line.split(',');
if (parts.length >= 2) {
students.push({ nis: parts[0].trim(), nama: parts[1].trim(), kelas: parts[2] ? parts[2].trim() : '-' });
}
});
if (students.length === 0) return Swal.fire('Error', 'Format salah', 'error');
showLoader(true);
google.script.run.withSuccessHandler(res => {
showLoader(false);
Swal.fire('Import Selesai', `Ditambahkan: ${res.added}, Duplikat: ${res.skipped}`, 'info');
document.getElementById('studentBulkInput').value = '';
}).addStudentsBulk(students);
}
document.getElementById('formCandidate').addEventListener('submit', function(e) {
e.preventDefault();
const payload = {
namaKetua: document.getElementById('candKetua').value,
namaWakil: document.getElementById('candWakil').value,
visi: document.getElementById('candVisi').value,
misi: document.getElementById('candMisi').value,
fotoUrl: document.getElementById('candPhotoUrl').value
};
showLoader(true);
google.script.run.withSuccessHandler(res => {
showLoader(false);
if(res.success){
Swal.fire('Sukses', 'Kandidat disimpan!', 'success').then(() => {
document.getElementById('formCandidate').reset();
loadInitialData();
});
} else {
Swal.fire('Gagal', res.error, 'error');
}
}).addCandidate(payload);
});
function resetAllVotes() {
Swal.fire({
title: 'Reset Total?',
text: "Hapus semua suara?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
confirmButtonText: 'Ya'
}).then((result) => {
if (result.isConfirmed) {
showLoader(true);
google.script.run.withSuccessHandler(res => {
showLoader(false);
Swal.fire('Reset', res, 'success');
loadCharts();
}).resetVotes();
}
});
}
// --- CHART ---
function loadCharts() {
google.script.run.withSuccessHandler(data => {
const labels = data.candidates.map(c => c.ketua);
const votes = data.candidates.map(c => c.suara);
const ctx = document.getElementById('voteChart').getContext('2d');
if (myChart) myChart.destroy();
myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Suara',
data: votes,
backgroundColor: ['#667eea', '#764ba2', '#ff6b6b', '#feca57'],
borderRadius: 5
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }
}
});
}).getInitialData();
}
</script>
</body>
</html>
