Tutorial Membuat CBT Anti Kecurangan
Berbasis Google Apps Script (Gratis & Serverless)
Apa itu GAS CBT?
CBT (Computer Based Test) ini dibangun menggunakan Google Apps Script sebagai backend. Ini memungkinkan Anda memiliki sistem ujian online tanpa biaya sewa hosting mahal.
Sistem ini dapat mendeteksi "kecurangan" sederhana seperti:
- Mendeteksi perpindahan tab (blur event).
- Mencegah klik kanan.
- Timer server-side yang akurat.
- Token ujian unik.
Pilihan Metode
1. Server Google (Web App)
Frontend dan Backend berjalan sepenuhnya di ekosistem Google. URL menggunakan script.google.com.
2. Server Luar / Google Sites
Backend tetap di Google (Database Spreadsheet), tapi tampilan (Frontend) dipasang di Hosting (cPanel) atau di-embed di Google Sites agar tampilan lebih custom dan URL lebih profesional.
Metode 1: Menggunakan Server Google Langsung
Persiapan Spreadsheet
Buatlah Spreadsheet baru dengan struktur sheet sebagai berikut:
| Nama Sheet | Kolom (Header Baris 1) |
|---|---|
| DATA | No, NISN, Nama, Jenjang Kelas, Kelas, Status |
| SOAL | No, Jenjang Kelas, Mata Pelajaran, Link Soal, Token, Waktu Ujian, Mulai, Selesai, Kelas |
| AL | Username, Password, Nama Pengguna |
| LOGO | Link Logo, Nama Sekolah |
Buka Apps Script
Di Spreadsheet, klik menu Ekstensi (Extensions) → Apps Script.
Isi File Code.gs
Hapus semua kode yang ada, lalu copy kode di bawah ini:
Pada bagian bawah kode ini, cari baris: const SPREADSHEET_ID = "COPY_SPEADSHEET_ID_DI_SINI";.
Ganti tulisan di dalam tanda kutip dengan ID SPREADSHEET yang Anda dapatkan dari url file spreadsheet.
// Code.gs (Versi Server Google - Full Fitur + Kunci Kelas)
const SPREADSHEET_ID = 'COPY_SPEADSHEET_ID_DI_SINI';
function doGet(e){
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('Ujian Online')
.addMetaTag('viewport','width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/* ---------- Utility ---------- */
function _open() {
return SpreadsheetApp.openById(SPREADSHEET_ID);
}
function _getSheetByName(name){
const ss = _open();
const sheet = ss.getSheetByName(name);
if(!sheet) throw new Error('Sheet "'+name+'" tidak ditemukan.');
return sheet;
}
/* ---------- Public Helpers ---------- */
function getLogoAndTitles(){
const sheet = _getSheetByName('LOGO');
const a2 = sheet.getRange('A2').getValue() || '';
const b2 = sheet.getRange('B2').getValue() || ''; // B2 sekarang jadi Judul Utama (Nama Sekolah)
return {
logoUrl: a2,
title: b2 || 'Ujian Online', // Jika B2 kosong, default 'Ujian Online'
subtitle: 'Designed by Kang Rohma Rohmadi' // FIXED: Tidak bisa diubah
};
}
// Fungsi update konfigurasi (Judul & Logo)
function updateAppConfig(newTitle, newLogo){
const sheet = _getSheetByName('LOGO');
sheet.getRange('B2').setValue(newTitle); // Menyimpan Nama Sekolah ke B2
sheet.getRange('A2').setValue(newLogo);
return { success: true };
}
function getLevels(){
const sheet = _getSheetByName('DATA');
const last = Math.max( sheet.getLastRow(), 1 );
if(last < 2) return [];
const values = sheet.getRange(2,4,last-1,1).getValues().flat();
const uniq = [...new Set(values.filter(v => v!==''))];
return uniq;
}
function getClassesForLevel(jkelas){
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(), 1);
if(last < 2) return [];
const cols = sheet.getRange(2,4,last-1,2).getValues();
const classes = [];
cols.forEach(r => {
if(String(r[0]) === String(jkelas) && r[1] && r[1] !== '') classes.push(r[1]);
});
return [...new Set(classes)];
}
function getSubjectsForLevel(jkelas, kelas){
const sheet = _getSheetByName('SOAL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return [];
const rows = sheet.getRange(2,1,last-1,9).getValues();
const subjects = [];
rows.forEach(r => {
const level = r[1];
const subject = r[2];
const rKelas = r[8];
if(String(level) === String(jkelas) && String(rKelas) === String(kelas) && subject && subject !== '') {
subjects.push(subject);
}
});
return [...new Set(subjects)];
}
/* LOGIKA JADWAL & KELAS UJIAN */
function getSoalMeta(jkelas, mata, kelas){
const sheet = _getSheetByName('SOAL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return null;
const rows = sheet.getRange(2,1,last-1,9).getValues();
for(let i=0;i<rows.length;i++){
const r = rows[i];
if(String(r[1]) === String(jkelas) && String(r[2]) === String(mata) && String(r[8]) === String(kelas)){
const now = new Date();
const start = r[6] ? new Date(r[6]) : null;
const end = r[7] ? new Date(r[7]) : null;
if (start && now < start) {
return { error: 'Ujian belum dimulai. Jadwal: ' + formatDate(start) };
}
if (end && now > end) {
return { error: 'Ujian sudah berakhir pada: ' + formatDate(end) };
}
return {
link: r[3] || '',
token: r[4] || '',
durationMinutes: Number(r[5]) || 0
};
}
}
return null;
}
function formatDate(d) {
if(!d) return '';
return Utilities.formatDate(d, "Asia/Jakarta", "dd/MM/yyyy HH:mm");
}
/* LOGIKA LOGIN SISWA */
function authenticateStudent(nisn, jkelas, kelas, mataPelajaran){
nisn = String(nisn || '').trim();
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return { success:false, message:'Tidak ada data siswa.' };
const rows = sheet.getRange(2,1,last-1,sheet.getLastColumn()).getValues();
for(let i=0;i<rows.length;i++){
const r = rows[i];
const nisnCell = String(r[1] || '').trim();
if(nisnCell === nisn){
const nama = r[2] || '';
const jkelasCell = String(r[3] || '');
const kelasCell = String(r[4] || '');
const status = String(r[5] || '');
if(jkelasCell !== String(jkelas)) return { success:false, message:'Jenjang Kelas tidak sesuai.' };
if(kelasCell !== String(kelas)) return { success:false, message:'Kelas tidak sesuai.' };
if(status !== 'Aktif') {
return { success:false, message: 'Status Ujian : ' + status };
}
const meta = getSoalMeta(jkelas, mataPelajaran, kelas);
if(!meta) return { success:false, message:'Soal tidak ditemukan untuk kelas Anda.' };
if(meta.error) return { success:false, message: meta.error };
return {
success:true,
data: {
nama: nama,
nisn: nisnCell,
jkelas: jkelasCell,
kelas: kelasCell,
mataPelajaran: mataPelajaran,
waktuMenit: meta.durationMinutes,
linkSoal: meta.link
}
};
}
}
return { success:false, message:'NISN tidak ditemukan.' };
}
function validateToken(jkelas, mataPelajaran, token, kelas){
const meta = getSoalMeta(jkelas, mataPelajaran, kelas);
if(!meta) return { success:false, message:'Soal tidak ditemukan.' };
if(meta.error) return { success:false, message: meta.error };
if(String(meta.token) === String(token)) return { success:true, durationMinutes: meta.durationMinutes, link: meta.link };
return { success:false, message:'Token salah.' };
}
function setStudentStatus(nisn, newStatus){
nisn = String(nisn || '').trim();
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return { success:false, message:'Tidak ada data.' };
const range = sheet.getRange(2,2,last-1,1);
const vals = range.getValues();
for(let i=0;i<vals.length;i++){
if(String(vals[i][0]||'').trim() === nisn){
sheet.getRange(i+2,6).setValue(newStatus);
return { success:true };
}
}
return { success:false, message:'NISN tidak ditemukan.' };
}
/* ---------- Admin Functions ---------- */
function authenticateAdmin(username, password){
username = String(username || '').trim();
password = String(password || '').trim();
const sheet = _getSheetByName('AL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return { success:false, message:'Data admin kosong.' };
const rows = sheet.getRange(2,1,last-1,3).getValues();
for(let i=0;i<rows.length;i++){
if(String(rows[i][0]||'').trim() === username && String(rows[i][1]||'').trim() === password){
return { success:true, name: rows[i][2] || username };
}
}
return { success:false, message:'Username / password salah.' };
}
function updateAdminProfile(username, newPass, newName){
const sheet = _getSheetByName('AL');
const last = Math.max(sheet.getLastRow(),1);
const rows = sheet.getRange(2,1,last-1,1).getValues().flat();
for(let i=0; i<rows.length; i++){
if(String(rows[i]) === username){
if(newPass) sheet.getRange(i+2, 2).setValue(newPass);
if(newName) sheet.getRange(i+2, 3).setValue(newName);
return { success: true };
}
}
return { success: false, message: 'User tidak ditemukan' };
}
function getAdminCounts(){
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return { total:0, aktif:0, tidakAktif:0 };
const allF = sheet.getRange(2,6,last-1,1).getValues().flat();
const total = allF.length;
const aktif = allF.filter(v => String(v||'').trim() === 'Aktif').length;
const tidakAktif = total - aktif;
return { total: total, aktif: aktif, tidakAktif: tidakAktif };
}
function getStudentsAdmin(){
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return [];
const rows = sheet.getRange(2,1,last-1,6).getValues();
const out = [];
for(let i=0;i<rows.length;i++){
const r = rows[i];
out.push({
actualRow: i+2,
colA: r[0] || '',
nisn: String(r[1] || ''),
nama: r[2] || '',
jkelas: r[3] || '',
kelas: r[4] || '',
status: r[5] || ''
});
}
return out;
}
function updateStudentAdmin(actualRow, data){
actualRow = Number(actualRow);
if(!actualRow || actualRow < 2) return { success:false };
const sheet = _getSheetByName('DATA');
const nisnValue = "'" + String(data.nisn || '');
sheet.getRange(actualRow,1).setValue(data.colA || '');
sheet.getRange(actualRow,2).setValue(nisnValue);
sheet.getRange(actualRow,3).setValue(data.nama || '');
sheet.getRange(actualRow,4).setValue(data.jkelas || '');
sheet.getRange(actualRow,5).setValue(data.kelas || '');
sheet.getRange(actualRow,6).setValue(data.status || '');
return { success:true };
}
function deleteStudentAdmin(actualRow){
actualRow = Number(actualRow);
if(!actualRow || actualRow < 2) return { success:false };
const sheet = _getSheetByName('DATA');
sheet.deleteRow(actualRow);
return { success:true };
}
function addStudentAdmin(data) {
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
const newNisn = String(data.nisn || '').trim();
const allNisn = sheet.getRange(2,2,last,1).getValues().flat().map(String);
if(allNisn.includes(newNisn)){
return { success: false, message: 'NISN sudah terdaftar!' };
}
const newRow = [
data.colA || '',
"'" + newNisn,
data.nama || '',
data.jkelas || '',
data.kelas || '',
data.status || 'Aktif'
];
sheet.appendRow(newRow);
return { success: true, message: 'Data peserta berhasil ditambahkan.' };
}
/* ---------- Admin Soal ---------- */
function getSoalAdmin(){
const sheet = _getSheetByName('SOAL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return [];
const rows = sheet.getRange(2,1,last-1,9).getValues();
const out = [];
for(let i=0;i<rows.length;i++){
const r = rows[i];
out.push({
actualRow: i+2,
colA: r[0] || '',
jkelas: r[1] || '',
mata: r[2] || '',
link: r[3] || '',
token: r[4] || '',
waktu: r[5] || '',
mulai: r[6] ? formatISOLike(r[6]) : '',
selesai: r[7] ? formatISOLike(r[7]) : '',
kelas: r[8] || ''
});
}
return out;
}
function formatISOLike(dateObj) {
if(!dateObj || !(dateObj instanceof Date)) return '';
return Utilities.formatDate(dateObj, "Asia/Jakarta", "yyyy-MM-dd'T'HH:mm");
}
function updateSoalAdmin(actualRow, data){
actualRow = Number(actualRow);
if(!actualRow || actualRow < 2) return { success:false };
const sheet = _getSheetByName('SOAL');
sheet.getRange(actualRow,2).setValue(data.jkelas || '');
sheet.getRange(actualRow,3).setValue(data.mata || '');
sheet.getRange(actualRow,4).setValue(data.link || '');
sheet.getRange(actualRow,5).setValue(data.token || '');
sheet.getRange(actualRow,6).setValue(data.waktu || '');
sheet.getRange(actualRow,7).setValue(data.mulai || '');
sheet.getRange(actualRow,8).setValue(data.selesai || '');
sheet.getRange(actualRow,9).setValue(data.kelas || '');
return { success:true };
}
function addSoalAdmin(data) {
const sheet = _getSheetByName('SOAL');
const newRow = [
'',
data.jkelas || '',
data.mata || '',
data.link || '',
data.token || '',
data.waktu || '',
data.mulai || '',
data.selesai || '',
data.kelas || ''
];
sheet.appendRow(newRow);
return { success: true, message: 'Data soal berhasil ditambahkan.' };
}
function deleteSoalAdmin(actualRow){
actualRow = Number(actualRow);
if(!actualRow || actualRow < 2) return { success:false };
const sheet = _getSheetByName('SOAL');
sheet.deleteRow(actualRow);
return { success:true };
}
Buat File index.html
Klik tanda (+) pilih HTML, beri nama index. Isi dengan kode berikut:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Ujian Online</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body, html {
height: 100%;
margin: 0;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
font-family: 'Segoe UI', sans-serif;
}
/* Login Box */
#loginBox {
max-width:420px;
width:94%;
margin:auto;
position: absolute;
top:50%;
left:50%;
transform: translate(-50%, -50%);
background: #ffffff;
border-radius: 16px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
padding: 40px;
}
#loginBox img.logo { max-width:100px; display:block; margin:0 auto 15px auto; }
.small-muted { font-size:0.9rem; color:#6c757d; }
.center { text-align:center; }
/* Loading overlay */
.overlay-loading {
position:fixed; inset:0; display:flex; align-items:center; justify-content:center;
background:rgba(255,255,255,0.8); z-index:9999; display:none;
}
/* Konten siswa full layar */
#studentContent {
display:none;
height:100vh;
width:100%;
overflow-y:auto;
padding:20px;
background: #f0f2f5;
}
.navbar-top {
background: #ffffff;
color: #333;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
position: relative;
}
.navbar-top h5 { margin: 0; font-weight: 700; color: #2c3e50; }
.card-identitas {
background:white;
border-radius:16px;
box-shadow:0 4px 20px rgba(0,0,0,0.05);
padding:25px;
margin-top:30px;
transition: all 0.3s ease;
}
.identitas-grid {
display:grid;
grid-template-columns: repeat(auto-fit, minmax(200px,1fr));
gap:15px;
}
.identitas-item {
background:#f8f9fa;
border-radius:10px;
padding:12px 16px;
border-left: 4px solid #4facfe;
}
.identitas-item strong {
display:block;
color:#6c757d;
font-size:0.8rem;
margin-bottom:4px;
text-transform: uppercase;
}
.identitas-item span {
color:#2c3e50;
font-weight:600;
font-size: 1.05rem;
}
/* Area soal */
#examArea { margin-top:20px; }
.iframe-wrapper {
height:78vh;
min-height:400px;
border-radius:12px;
overflow:hidden;
box-shadow:0 4px 15px rgba(0,0,0,0.1);
background: #fff;
}
.iframe-wrapper iframe {
width: 100%; height: 100%; border: none;
}
/* --- ADMIN LAYOUT --- */
#adminShell { display: none; height: 100vh; overflow: hidden; }
/* SIDEBAR */
#adminSidebar {
width: 260px;
height: 100vh;
background: #1e293b;
color: #fff;
position: fixed;
left: 0; top: 0; bottom: 0;
padding: 20px 10px;
box-shadow: 3px 0 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
z-index: 1051;
display: flex; flex-direction: column;
}
#adminSidebar.collapsed { width: 70px; padding: 20px 5px; }
#adminSidebar.collapsed .brand-text { display: none; }
#adminSidebar.collapsed .nav-link span { display: none; }
#adminSidebar.collapsed .nav-link { justify-content: center; padding: 12px 0; }
#adminSidebar.collapsed .nav-link i { margin-right: 0 !important; font-size: 1.2rem; }
#adminSidebar.collapsed .logout-wrap span { display: none; }
#adminSidebar .brand {
display: flex; flex-direction: column;
align-items: center; justify-content: center;
text-align: center; margin-bottom: 25px;
}
#adminSidebar .brand i { font-size: 2rem; color: #4facfe; margin-bottom: 8px; }
#adminSidebar .brand-text {
font-weight: 700; font-size: 1rem; color: #fff;
letter-spacing: 0.5px; text-transform: uppercase;
transition: opacity 0.2s;
}
#adminSidebar .nav-link {
color: #cbd5e1; padding: 12px 15px;
border-radius: 8px; margin-bottom: 5px;
font-size: 0.95rem; transition: all 0.2s ease;
display: flex; align-items: center; text-decoration: none;
}
#adminSidebar .nav-link:hover { background: rgba(255,255,255,0.1); color: #fff; }
#adminSidebar .nav-link.active { background: #4facfe; color: #fff; font-weight: 600; box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4); }
#adminSidebar .nav-link i { margin-right: 12px; width: 20px; text-align: center; }
#adminMain {
margin-left: 260px; padding: 20px;
transition: margin-left 0.3s ease;
overflow-y: auto; height: 100vh;
background: #f1f5f9;
}
#adminMain.full { margin-left: 70px; }
.admin-topbar {
display: flex; align-items: center; justify-content: space-between;
background-color: #fff; color: #333;
padding: 12px 20px; border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.03); margin-bottom: 20px;
}
.stat-card {
border-radius: 12px; padding: 20px;
background: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.02);
border-bottom: 4px solid #4facfe;
}
.filter-container {
background: #fff; padding: 15px; border-radius: 8px;
border: 1px solid #e2e8f0; margin-bottom: 15px;
}
.table-wrap { overflow-x: auto; background: #fff; border-radius: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.05); }
.table { margin-bottom: 0; }
.table thead th { background: #f8fafc; border-bottom: 2px solid #e2e8f0; color: #475569; font-weight: 600; font-size: 0.85rem; padding: 12px; }
.table tbody td { padding: 12px; vertical-align: middle; font-size: 0.9rem; border-bottom: 1px solid #f1f5f9; }
/* Responsive Mobile */
@media (max-width: 768px) {
#adminSidebar { transform: translateX(-100%); width: 240px; }
#adminSidebar.show { transform: translateX(0); }
#adminMain, #adminMain.full { margin-left: 0; }
#adminSidebar.collapsed { transform: translateX(-100%); }
#sidebarOverlay.show { display: block; position: fixed; inset:0; background:rgba(0,0,0,0.5); z-index:1050; }
}
/* Header Exam */
.exam-header { background: #fff; padding: 15px; border-radius: 10px; margin-bottom: 15px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
#timerDisplay { font-family: 'Courier New', monospace; font-weight: 700; font-size: 1.5rem; color: #e74c3c; }
.fade-out { opacity: 0; transform: translateY(-10px); }
.fade-in { opacity: 1; transform: translateY(0); }
.card-identitas, #examArea { transition: opacity 0.5s ease, transform 0.5s ease; }
</style>
</head>
<body>
<div class="overlay-loading" id="loadingOverlay">
<div class="text-center">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2 fw-bold text-dark">Memproses...</div>
</div>
</div>
<div id="loginBox" class="text-center">
<a id="logoLink" target="_blank"><img src="" alt="Logo" class="logo" id="appLogo" /></a>
<h4 id="appTitle" class="mb-1 text-primary fw-bold">Ujian Online</h4>
<p id="appSubtitle" class="small-muted mb-4"></p>
<div class="mb-3 text-start">
<select id="roleSelect" class="form-select border-primary bg-light">
<option value="siswa" selected>Login Siswa</option>
<option value="admin">Login Admin</option>
</select>
</div>
<div id="formSiswa">
<div class="mb-2 text-start">
<label class="form-label small fw-bold">NISN</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-id-card"></i></span>
<input id="nisnInput" class="form-control" placeholder="Masukkan NISN" />
</div>
</div>
<div class="mb-2 text-start">
<label class="form-label small fw-bold">Jenjang Kelas</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-layer-group"></i></span>
<select id="jkelasSelect" class="form-select">
<option value="">-- Pilih Jenjang --</option>
</select>
</div>
</div>
<div class="mb-2 text-start">
<label class="form-label small fw-bold">Kelas</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-school"></i></span>
<select id="kelasSelect" class="form-select">
<option value="">-- Pilih Kelas --</option>
</select>
</div>
</div>
<div class="mb-4 text-start">
<label class="form-label small fw-bold">Mata Pelajaran</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-book"></i></span>
<select id="mataSelect" class="form-select">
<option value="">-- Pilih Mapel --</option>
</select>
</div>
</div>
<button id="loginSiswaBtn" class="btn btn-primary w-100 py-2 fw-bold shadow-sm">
<i class="fa fa-sign-in-alt me-2"></i> MASUK
</button>
</div>
<div id="formAdmin" style="display:none;">
<div class="mb-2 text-start">
<label class="form-label small fw-bold">Username</label>
<div class="input-group">
<span class="input-group-text bg-white text-danger"><i class="fa fa-user-shield"></i></span>
<input id="adminUser" type="text" class="form-control" placeholder="Username" />
</div>
</div>
<div class="mb-4 text-start">
<label class="form-label small fw-bold">Password</label>
<div class="input-group">
<span class="input-group-text bg-white text-danger"><i class="fa fa-lock"></i></span>
<input id="adminPass" type="password" class="form-control" placeholder="Password" />
</div>
</div>
<button id="loginAdminBtn" class="btn btn-danger w-100 py-2 fw-bold shadow-sm">
<i class="fa fa-shield-alt me-2"></i> LOGIN ADMIN
</button>
</div>
</div>
<div id="studentContent" class="container-fluid">
<div class="navbar-top">
<h5><i class="fa-solid fa-user-graduate me-2 text-primary"></i>Panel Ujian</h5>
<div class="d-flex align-items-center">
<span class="student-name me-3" id="studentName">Siswa</span>
<button id="logoutBtn" class="btn btn-sm btn-outline-danger px-3"><i class="fa-solid fa-power-off me-1"></i> Keluar</button>
</div>
</div>
<div class="card-identitas">
<h5 class="mb-3 text-primary"><i class="fa-solid fa-circle-info me-2"></i>Data Identitas Peserta</h5>
<div class="identitas-grid">
<div class="identitas-item"><strong>Nama Lengkap</strong><span id="dNama">-</span></div>
<div class="identitas-item"><strong>NISN</strong><span id="dNISN">-</span></div>
<div class="identitas-item"><strong>Jenjang Kelas</strong><span id="dTingkat">-</span></div>
<div class="identitas-item"><strong>Kelas</strong><span id="dKelas">-</span></div>
<div class="identitas-item"><strong>Mata Pelajaran</strong><span id="dMata">-</span></div>
<div class="identitas-item"><strong>Durasi Ujian</strong><span id="dWaktu">0</span> Menit</div>
</div>
<div class="text-center mt-4">
<button id="mulaiUjianBtn" class="btn btn-primary btn-lg shadow px-5 rounded-pill">
<i class="fa fa-play me-2"></i> MULAI UJIAN
</button>
</div>
</div>
<div id="examArea" style="display:none;">
<div class="exam-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
<div class="bg-light p-2 rounded"><i class="fa-solid fa-clock text-danger fs-3"></i></div>
<div>
<div class="fw-bold text-secondary small">SISA WAKTU</div>
<div id="timerDisplay">--:--:--</div>
</div>
</div>
<button id="finishBtn" class="btn btn-danger px-4 rounded-pill fw-bold shadow-sm">
<i class="fa-solid fa-flag-checkered me-2"></i> SELESAI
</button>
</div>
<div class="progress mb-3" style="height: 6px;">
<div id="timeProgress" class="progress-bar bg-success" style="width: 100%; transition: width 1s linear;"></div>
</div>
<div class="iframe-wrapper">
<iframe id="soalIframe" src="" frameborder="0"></iframe>
</div>
</div>
</div>
<div id="adminShell">
<div id="sidebarOverlay" onclick="closeSidebar()"></div>
<div id="adminSidebar">
<div class="d-flex justify-content-end d-lg-none mb-2">
<button id="closeSidebarBtn" class="text-white bg-transparent border-0"><i class="fa fa-times fs-4"></i></button>
</div>
<div class="brand">
<i class="fa fa-graduation-cap"></i>
<div class="brand-text mt-2">CBT ADMIN</div>
</div>
<hr class="divider bg-secondary opacity-25" />
<nav class="nav flex-column">
<a href="#" class="nav-link active" id="menuDashboard">
<i class="fa fa-tachometer-alt"></i> <span>Dashboard</span>
</a>
<a href="#" class="nav-link" id="menuDataPeserta">
<i class="fa fa-users"></i> <span>Data Siswa</span>
</a>
<a href="#" class="nav-link" id="menuDataSoal">
<i class="fa fa-file-alt"></i> <span>Data Soal</span>
</a>
<a href="#" class="nav-link" id="menuPengaturan">
<i class="fa fa-cogs"></i> <span>Pengaturan</span>
</a>
</nav>
<div class="logout-wrap mt-auto">
<button id="logoutAdminBtn" class="btn btn-outline-light w-100 d-flex align-items-center justify-content-center">
<i class="fa fa-sign-out-alt"></i> <span class="ms-2">Logout</span>
</button>
</div>
</div>
<div id="adminMain">
<div class="admin-topbar">
<div class="d-flex align-items-center">
<button id="hamburgerBtn" class="btn btn-light me-3 border"><i class="fa fa-bars text-secondary"></i></button>
<strong id="topbarTitle" class="fs-5">Dashboard</strong>
</div>
<div class="d-flex align-items-center gap-2">
<div class="bg-light rounded-circle p-2"><i class="fa fa-user text-primary"></i></div>
<span id="adminWelcome" class="fw-bold text-secondary">Admin</span>
</div>
</div>
<div id="adminContentArea">
<div id="adminDashboard">
<div class="row g-4">
<div class="col-md-4">
<div class="stat-card border-bottom border-primary">
<h6 class="text-muted text-uppercase mb-2">Total Siswa</h6>
<div class="fs-2 fw-bold text-dark" id="countTotal">0</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card border-bottom border-success">
<h6 class="text-muted text-uppercase mb-2">Siswa Aktif</h6>
<div class="fs-2 fw-bold text-success" id="countAktif">0</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card border-bottom border-danger">
<h6 class="text-muted text-uppercase mb-2">Tidak Aktif</h6>
<div class="fs-2 fw-bold text-danger" id="countTidakAktif">0</div>
</div>
</div>
</div>
</div>
<div id="adminDataPeserta" style="display:none;">
<div class="card p-4 shadow-sm border-0 rounded-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap">
<h5 class="mb-0 fw-bold text-secondary">Manajemen Data Siswa</h5>
<button class="btn btn-primary rounded-pill px-4" id="btnTambahPeserta">
<i class="fa fa-plus me-2"></i> Tambah Siswa
</button>
</div>
<div class="filter-container">
<div class="row g-2">
<div class="col-md-4">
<label class="small fw-bold text-muted">Cari Nama/NISN</label>
<input type="text" id="filterSearch" class="form-control" placeholder="Ketik nama...">
</div>
<div class="col-md-4">
<label class="small fw-bold text-muted">Filter Jenjang</label>
<select id="filterJenjang" class="form-select">
<option value="">Semua Jenjang</option>
</select>
</div>
<div class="col-md-4">
<label class="small fw-bold text-muted">Filter Kelas</label>
<select id="filterKelas" class="form-select">
<option value="">Semua Kelas</option>
</select>
</div>
</div>
</div>
<div id="tablePesertaWrap" class="table-wrap"></div>
</div>
</div>
<div id="adminDataSoal" style="display:none;">
<div class="card p-4 shadow-sm border-0 rounded-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap">
<h5 class="mb-0 fw-bold text-secondary">Bank Soal & Jadwal</h5>
<button class="btn btn-primary rounded-pill px-4" id="btnTambahSoal">
<i class="fa fa-plus me-2"></i> Tambah Soal
</button>
</div>
<div id="tableSoalWrap" class="table-wrap"></div>
</div>
</div>
<div id="adminPengaturan" style="display:none;">
<div class="row g-4">
<div class="col-md-6">
<div class="card p-4 border-0 shadow-sm rounded-4 h-100">
<h5 class="fw-bold mb-3">Profil Admin</h5>
<div class="mb-3">
<label class="form-label small">Nama Tampilan</label>
<input type="text" id="confAdminName" class="form-control">
</div>
<div class="mb-3">
<label class="form-label small">Password Baru (Kosongkan jika tidak ubah)</label>
<input type="password" id="confAdminPass" class="form-control">
</div>
<button id="saveProfileBtn" class="btn btn-primary w-100">Simpan Profil</button>
</div>
</div>
<div class="col-md-6">
<div class="card p-4 border-0 shadow-sm rounded-4 h-100">
<h5 class="fw-bold mb-3">Identitas Aplikasi</h5>
<div class="mb-3">
<label class="form-label small">Nama Aplikasi / Sekolah</label>
<input type="text" id="confAppTitle" class="form-control">
</div>
<div class="mb-3">
<label class="form-label small">URL Logo</label>
<input type="text" id="confAppLogo" class="form-control">
</div>
<button id="saveConfigBtn" class="btn btn-secondary w-100">Simpan Konfigurasi</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modalTambahPeserta" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Tambah Siswa</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-2"><label class="small">NISN</label><input id="peserta_nisn" class="form-control"></div>
<div class="mb-2"><label class="small">Nama</label><input id="peserta_nama" class="form-control"></div>
<div class="mb-2">
<label class="small">Jenjang Kelas</label>
<select id="peserta_jkelas" class="form-select">
<option value="">-- Pilih --</option>
<option value="Kelas 1">Kelas 1</option><option value="Kelas 2">Kelas 2</option><option value="Kelas 3">Kelas 3</option>
<option value="Kelas 4">Kelas 4</option><option value="Kelas 5">Kelas 5</option><option value="Kelas 6">Kelas 6</option>
<option value="Kelas 7">Kelas 7</option><option value="Kelas 8">Kelas 8</option><option value="Kelas 9">Kelas 9</option>
<option value="Kelas 10">Kelas 10</option><option value="Kelas 11">Kelas 11</option><option value="Kelas 12">Kelas 12</option>
</select>
</div>
<div class="mb-2"><label class="small">Kelas</label><input id="peserta_kelas" class="form-control"></div>
<div class="mb-2"><label class="small">Status</label><select id="peserta_status" class="form-select"><option value="Aktif" selected>Aktif</option><option value="Tidak Aktif">Tidak Aktif</option></select></div>
</div>
<div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="simpanPesertaBaru">Simpan</button></div>
</div>
</div>
</div>
<div class="modal fade" id="modalTambahSoal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Tambah Soal</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-6 mb-2">
<label class="small">Jenjang Kelas</label>
<select id="soal_jkelas" class="form-select">
<option value="">-- Pilih --</option>
<option value="Kelas 1">Kelas 1</option><option value="Kelas 2">Kelas 2</option><option value="Kelas 3">Kelas 3</option>
<option value="Kelas 4">Kelas 4</option><option value="Kelas 5">Kelas 5</option><option value="Kelas 6">Kelas 6</option>
<option value="Kelas 7">Kelas 7</option><option value="Kelas 8">Kelas 8</option><option value="Kelas 9">Kelas 9</option>
<option value="Kelas 10">Kelas 10</option><option value="Kelas 11">Kelas 11</option><option value="Kelas 12">Kelas 12</option>
</select>
</div>
<div class="col-6 mb-2">
<label class="small">Kelas Khusus</label>
<input id="soal_kelas" class="form-control" placeholder="Contoh: X IPA 1">
</div>
</div>
<div class="mb-2"><label class="small">Mata Pelajaran</label><input id="soal_mata" class="form-control"></div>
<div class="mb-2"><label class="small">Link Soal</label><input id="soal_link" class="form-control"></div>
<div class="row">
<div class="col-6 mb-2"><label class="small">Token</label><input id="soal_token" class="form-control"></div>
<div class="col-6 mb-2"><label class="small">Waktu (Menit)</label><input id="soal_waktu" type="number" class="form-control"></div>
</div>
<div class="row">
<div class="col-6 mb-2"><label class="small fw-bold">Jadwal Mulai</label><input id="soal_mulai" type="datetime-local" class="form-control"></div>
<div class="col-6 mb-2"><label class="small fw-bold">Jadwal Selesai</label><input id="soal_selesai" type="datetime-local" class="form-control"></div>
</div>
</div>
<div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="simpanSoalBaru">Simpan</button></div>
</div>
</div>
</div>
<div class="modal fade" id="editPesertaModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title">Edit Peserta</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<input type="hidden" id="editPesertaRow" />
<div class="mb-2"><label class="small">NISN (Tidak dapat diubah)</label><input id="edit_nisn" class="form-control bg-light" readonly/></div>
<div class="mb-2"><label class="small">Nama</label><input id="edit_nama" class="form-control"/></div>
<div class="mb-2">
<label class="small">Jenjang Kelas</label>
<select id="edit_jkelas" class="form-select">
<option value="">-- Pilih --</option>
<option value="Kelas 1">Kelas 1</option><option value="Kelas 2">Kelas 2</option><option value="Kelas 3">Kelas 3</option>
<option value="Kelas 4">Kelas 4</option><option value="Kelas 5">Kelas 5</option><option value="Kelas 6">Kelas 6</option>
<option value="Kelas 7">Kelas 7</option><option value="Kelas 8">Kelas 8</option><option value="Kelas 9">Kelas 9</option>
<option value="Kelas 10">Kelas 10</option><option value="Kelas 11">Kelas 11</option><option value="Kelas 12">Kelas 12</option>
</select>
</div>
<div class="mb-2"><label class="small">Kelas</label><input id="edit_kelas" class="form-control"/></div>
<div class="mb-2"><label class="small">Status</label><select id="edit_status" class="form-select"><option>Aktif</option><option>Tidak Aktif</option><option>Selesai</option><option>Curang</option><option>Waktu Habis</option></select></div>
</div>
<div class="modal-footer"><button id="savePesertaBtn" class="btn btn-primary">Update</button></div>
</div>
</div>
</div>
<div class="modal fade" id="editSoalModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header"><h5 class="modal-title">Edit Soal</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<input type="hidden" id="editSoalRow" />
<input type="hidden" id="edit_soal_colA" />
<div class="row">
<div class="col-6 mb-2">
<label class="small">Jenjang Kelas</label>
<select id="edit_soal_jkelas" class="form-select">
<option value="">-- Pilih --</option>
<option value="Kelas 1">Kelas 1</option><option value="Kelas 2">Kelas 2</option><option value="Kelas 3">Kelas 3</option>
<option value="Kelas 4">Kelas 4</option><option value="Kelas 5">Kelas 5</option><option value="Kelas 6">Kelas 6</option>
<option value="Kelas 7">Kelas 7</option><option value="Kelas 8">Kelas 8</option><option value="Kelas 9">Kelas 9</option>
<option value="Kelas 10">Kelas 10</option><option value="Kelas 11">Kelas 11</option><option value="Kelas 12">Kelas 12</option>
</select>
</div>
<div class="col-6 mb-2">
<label class="small">Kelas Khusus</label>
<input id="edit_soal_kelas" class="form-control">
</div>
</div>
<div class="mb-2"><label class="small">Mata Pelajaran</label><input id="edit_soal_mata" class="form-control"/></div>
<div class="mb-2"><label class="small">Link Soal</label><input id="edit_soal_link" class="form-control"/></div>
<div class="row">
<div class="col-6 mb-2"><label class="small">Token</label><input id="edit_soal_token" class="form-control"/></div>
<div class="col-6 mb-2"><label class="small">Waktu</label><input id="edit_soal_waktu" class="form-control"/></div>
</div>
<div class="row">
<div class="col-6 mb-2"><label class="small fw-bold">Mulai</label><input id="edit_soal_mulai" type="datetime-local" class="form-control"></div>
<div class="col-6 mb-2"><label class="small fw-bold">Selesai</label><input id="edit_soal_selesai" type="datetime-local" class="form-control"></div>
</div>
</div>
<div class="modal-footer"><button id="saveSoalBtn" class="btn btn-primary">Update</button></div>
</div>
</div>
</div>
<div class="modal fade" id="tokenModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered"><div class="modal-content">
<div class="modal-header"><h5 class="modal-title">Masukkan Token</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="alert alert-danger small mb-3">
<i class="fa fa-exclamation-triangle me-1"></i> <strong>PERINGATAN KERAS!</strong><br>
Dilarang membuka tab lain, berpindah aplikasi, atau meminimalkan layar ujian. Jika terdeteksi, <strong>Ujian Anda akan otomatis diblokir dengan status CURANG.</strong>
</div>
<input id="tokenInput" class="form-control form-control-lg text-center text-uppercase fw-bold" placeholder="KETIK TOKEN DI SINI" style="letter-spacing: 2px;"/>
</div>
<div class="modal-footer"><button id="tokenSubmitBtn" class="btn btn-primary w-100">MULAI UJIAN</button></div>
</div></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script>
let currentStudent = null;
let examTimer = null;
let remainingSeconds = 0;
let examStarted = false;
let adminName = '';
let allStudentsData = [];
document.addEventListener('DOMContentLoaded', function(){
showLoading(true);
google.script.run.withSuccessHandler(initUI).getLogoAndTitles();
google.script.run.withSuccessHandler(populateLevels).getLevels();
showLoading(false);
// Enter Key Handler
document.addEventListener('keypress', function(e){
if(e.key === 'Enter'){
if(document.getElementById('loginBox').style.display !== 'none'){
if(document.getElementById('roleSelect').value === 'siswa'){
onLoginSiswa();
} else {
onLoginAdmin();
}
}
}
});
// role toggler
document.getElementById('roleSelect').addEventListener('change', function(){
const r = this.value;
if(r === 'siswa'){
document.getElementById('formSiswa').style.display = '';
document.getElementById('formAdmin').style.display = 'none';
} else {
document.getElementById('formSiswa').style.display = 'none';
document.getElementById('formAdmin').style.display = '';
}
});
// Jenjang change
document.getElementById('jkelasSelect').addEventListener('change', function(){
const t = this.value;
if(!t) return;
showLoading(true);
google.script.run.withSuccessHandler(function(classes){
fillSelect('kelasSelect', classes, '-- Pilih Kelas --');
fillSelect('mataSelect', [], '-- Pilih Mapel --');
showLoading(false);
}).getClassesForLevel(t);
});
// Kelas change
document.getElementById('kelasSelect').addEventListener('change', function(){
const jkelas = document.getElementById('jkelasSelect').value;
const kelas = this.value;
if(!jkelas || !kelas) return;
showLoading(true);
google.script.run.withSuccessHandler(function(subjects){
fillSelect('mataSelect', subjects, '-- Pilih Mata Pelajaran --');
showLoading(false);
}).getSubjectsForLevel(jkelas, kelas);
});
// Button Events
document.getElementById('loginSiswaBtn').addEventListener('click', onLoginSiswa);
document.getElementById('loginAdminBtn').addEventListener('click', onLoginAdmin);
document.getElementById('mulaiUjianBtn').addEventListener('click', onMulaiUjian);
document.getElementById('tokenSubmitBtn').addEventListener('click', submitToken);
document.getElementById('logoutBtn').addEventListener('click', logoutToLogin);
document.getElementById('logoutAdminBtn').addEventListener('click', logoutToLogin);
document.getElementById('finishBtn').addEventListener('click', finishExam);
// Admin UI Events
document.getElementById('hamburgerBtn').addEventListener('click', toggleSidebar);
document.getElementById('menuDashboard').addEventListener('click', showDashboard);
document.getElementById('menuDataPeserta').addEventListener('click', showDataPeserta);
document.getElementById('menuDataSoal').addEventListener('click', showDataSoal);
document.getElementById('menuPengaturan').addEventListener('click', showPengaturan);
// Save/Edit Modals
document.getElementById('savePesertaBtn').addEventListener('click', savePesertaEdit);
document.getElementById('saveSoalBtn').addEventListener('click', saveSoalEdit);
document.getElementById('saveProfileBtn').addEventListener('click', saveAdminProfile);
document.getElementById('saveConfigBtn').addEventListener('click', saveAppConfig);
// Filter Events
document.getElementById('filterSearch').addEventListener('input', applyStudentFilters);
document.getElementById('filterJenjang').addEventListener('change', applyStudentFilters);
document.getElementById('filterKelas').addEventListener('change', applyStudentFilters);
});
function setButtonLoading(btnId, isLoading, defaultText = 'Simpan') {
const btn = document.getElementById(btnId);
if (isLoading) {
btn.disabled = true;
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Memproses...`;
} else {
btn.disabled = false;
btn.innerHTML = defaultText;
}
}
function showLoading(show){
document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
}
function initUI(data){
if(data){
document.getElementById('appLogo').src = data.logoUrl || '';
document.getElementById('logoLink').href = data.logoUrl || '#';
document.getElementById('appTitle').textContent = data.title || 'Ujian Online';
document.getElementById('appSubtitle').textContent = data.subtitle || '';
document.getElementById('confAppTitle').value = data.title === 'Ujian Online' ? '' : data.title;
document.getElementById('confAppLogo').value = data.logoUrl || '';
}
}
function populateLevels(levels){
fillSelect('jkelasSelect', levels, '-- Pilih Jenjang --');
}
function fillSelect(id, arr, placeholder){
const sel = document.getElementById(id);
sel.innerHTML = '';
const opt0 = document.createElement('option');
opt0.value = ''; opt0.textContent = placeholder || '-- Pilih --';
sel.appendChild(opt0);
(arr||[]).forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
}
/* ---------- Login Logic ---------- */
function onLoginSiswa(){
const nisn = document.getElementById('nisnInput').value.trim();
const jkelas = document.getElementById('jkelasSelect').value;
const kelas = document.getElementById('kelasSelect').value;
const mata = document.getElementById('mataSelect').value;
if(!nisn || !jkelas || !kelas || !mata){
Swal.fire({ icon:'warning', title:'Form Belum Lengkap', text:'Harap lengkapi semua data.' });
return;
}
showLoading(true);
google.script.run.withSuccessHandler(function(resp){
showLoading(false);
if(resp.success){
currentStudent = resp.data;
document.getElementById('studentName').textContent = currentStudent.nama;
document.getElementById('loginBox').style.display = 'none';
document.getElementById('studentContent').style.display = 'block';
document.getElementById('adminShell').style.display = 'none';
Swal.fire({ icon:'success', title:'Selamat Datang', text: currentStudent.nama, timer: 1500, showConfirmButton:false });
document.getElementById('dNama').textContent = currentStudent.nama;
document.getElementById('dNISN').textContent = currentStudent.nisn;
document.getElementById('dTingkat').textContent = currentStudent.jkelas;
document.getElementById('dKelas').textContent = currentStudent.kelas;
document.getElementById('dMata').textContent = currentStudent.mataPelajaran;
document.getElementById('dWaktu').textContent = currentStudent.waktuMenit || 0;
} else {
Swal.fire({ icon:'error', title:'Akses Ditolak', text: resp.message });
}
}).authenticateStudent(nisn, jkelas, kelas, mata);
}
function onLoginAdmin(){
const user = document.getElementById('adminUser').value.trim();
const pass = document.getElementById('adminPass').value || '';
if(!user || !pass){
Swal.fire({ icon:'warning', title:'Form Belum Lengkap', text:'Masukkan username & password.' });
return;
}
showLoading(true);
google.script.run.withSuccessHandler(function(resp){
showLoading(false);
if(resp.success){
adminName = resp.name || user;
document.getElementById('loginBox').style.display = 'none';
document.getElementById('studentContent').style.display = 'none';
document.getElementById('adminShell').style.display = 'block';
document.getElementById('adminMain').classList.remove('full');
document.getElementById('adminSidebar').classList.remove('collapsed');
document.getElementById('adminWelcome').textContent = adminName;
document.getElementById('confAdminName').value = adminName;
loadDashboardCounts();
showDashboard();
} else {
Swal.fire({ icon:'error', title:'Gagal Login', text: resp.message });
}
}).authenticateAdmin(user, pass);
}
function logoutToLogin(){
currentStudent = null;
adminName = '';
examStarted = false;
if(examTimer){ clearInterval(examTimer); examTimer = null; }
document.getElementById('studentContent').style.display = 'none';
document.getElementById('adminShell').style.display = 'none';
document.getElementById('loginBox').style.display = 'block';
document.getElementById('adminUser').value = '';
document.getElementById('adminPass').value = '';
document.getElementById('nisnInput').value = '';
document.getElementById('soalIframe').src = '';
document.getElementById('examArea').style.display = 'none';
document.getElementById('tokenInput').value = '';
google.script.run.withSuccessHandler(initUI).getLogoAndTitles();
google.script.run.withSuccessHandler(populateLevels).getLevels();
}
/* ---------- Exam Flows ---------- */
function onMulaiUjian(){
if(!currentStudent) return;
new bootstrap.Modal(document.getElementById('tokenModal')).show();
}
function submitToken(){
const token = document.getElementById('tokenInput').value.trim();
if(!token){ Swal.fire('Token Kosong', 'Masukkan token ujian', 'warning'); return; }
showLoading(true);
google.script.run.withSuccessHandler(function(resp){
showLoading(false);
if(resp.success){
startExam(resp.durationMinutes, resp.link);
bootstrap.Modal.getInstance(document.getElementById('tokenModal')).hide();
} else {
Swal.fire('Gagal', resp.message, 'error');
}
}).validateToken(currentStudent.jkelas, currentStudent.mataPelajaran, token, currentStudent.kelas);
}
function startExam(durationMinutes, link) {
totalSeconds = Math.max(0, Math.floor(Number(durationMinutes) * 60));
remainingSeconds = totalSeconds;
if (!link) {
Swal.fire('Error', 'Link soal tidak tersedia.', 'error');
return;
}
const identitasCard = document.querySelector('.card-identitas');
const examArea = document.getElementById('examArea');
identitasCard.classList.add('fade-out');
setTimeout(() => {
identitasCard.style.display = 'none';
examArea.style.display = '';
examArea.classList.add('fade-in');
document.getElementById('soalIframe').src = link;
examStarted = true;
updateTimerDisplay();
if (examTimer) clearInterval(examTimer);
examTimer = setInterval(function () {
remainingSeconds--;
if (remainingSeconds <= 0) {
clearInterval(examTimer);
examTimer = null;
finishExam(false);
} else {
updateTimerDisplay();
}
}, 1000);
}, 500);
}
function updateTimerDisplay() {
const h = Math.floor(remainingSeconds / 3600);
const m = Math.floor((remainingSeconds % 3600) / 60);
const s = remainingSeconds % 60;
const str = `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
document.getElementById('timerDisplay').textContent = str;
const progress = (remainingSeconds / totalSeconds) * 100;
const bar = document.getElementById('timeProgress');
bar.style.width = `${progress}%`;
bar.classList.remove('high-time', 'medium-time', 'low-time');
if (progress > 60) bar.classList.add('high-time');
else if (progress > 30) bar.classList.add('medium-time');
else bar.classList.add('low-time');
}
function finishExam(manual = true) {
if (manual) {
Swal.fire({
title: 'Selesaikan Ujian?',
text: 'Anda tidak dapat kembali setelah ini.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Ya, Selesai'
}).then((result) => {
if (result.isConfirmed) {
if (examTimer) clearInterval(examTimer);
examTimer = null;
examStarted = false;
if (currentStudent) google.script.run.setStudentStatus(currentStudent.nisn, 'Selesai');
Swal.fire('Selesai', 'Terima kasih telah mengikuti ujian.', 'success');
setTimeout(() => logoutToLogin(), 2000);
}
});
} else {
if (examTimer) clearInterval(examTimer);
examTimer = null;
examStarted = false;
if (currentStudent) google.script.run.setStudentStatus(currentStudent.nisn, 'Waktu Habis');
Swal.fire('Waktu Habis', 'Ujian berakhir otomatis.', 'warning');
setTimeout(() => logoutToLogin(), 2000);
}
}
document.addEventListener('visibilitychange', function(){
if(document.hidden && examStarted){
if(!currentStudent) return;
document.getElementById('soalIframe').style.display = 'none';
examStarted = false;
if(examTimer) clearInterval(examTimer);
google.script.run.withSuccessHandler(()=>{
Swal.fire('Pelanggaran', 'Anda terdeteksi pindah tab. Status Anda diubah menjadi CURANG.', 'error')
.then(()=> logoutToLogin());
}).setStudentStatus(currentStudent.nisn, 'Curang');
}
});
/* ---------- ADMIN Logic ---------- */
function toggleSidebar(){
const sb = document.getElementById('adminSidebar');
const main = document.getElementById('adminMain');
if (window.innerWidth < 992) {
sb.classList.toggle('show');
document.getElementById('sidebarOverlay').classList.toggle('show');
} else {
sb.classList.toggle('collapsed');
main.classList.toggle('full');
}
}
function closeSidebar(){
document.getElementById('adminSidebar').classList.remove('show');
document.getElementById('sidebarOverlay').classList.remove('show');
}
function showDashboard(e){
if(e) e.preventDefault();
switchAdminTab('adminDashboard');
loadDashboardCounts();
}
function showDataPeserta(e){
if(e) e.preventDefault();
switchAdminTab('adminDataPeserta');
loadStudentsTable();
}
function showDataSoal(e){
if(e) e.preventDefault();
switchAdminTab('adminDataSoal');
loadSoalTable();
}
function showPengaturan(e){
if(e) e.preventDefault();
switchAdminTab('adminPengaturan');
}
function switchAdminTab(id){
['adminDashboard','adminDataPeserta','adminDataSoal','adminPengaturan'].forEach(tid => {
document.getElementById(tid).style.display = (tid===id) ? '' : 'none';
});
document.querySelectorAll('#adminSidebar .nav-link').forEach(l=>l.classList.remove('active'));
const map = {'adminDashboard':'menuDashboard', 'adminDataPeserta':'menuDataPeserta', 'adminDataSoal':'menuDataSoal', 'adminPengaturan':'menuPengaturan'};
if(map[id]) document.getElementById(map[id]).classList.add('active');
}
function loadDashboardCounts(){
google.script.run.withSuccessHandler(function(c){
document.getElementById('countTotal').textContent = c.total;
document.getElementById('countAktif').textContent = c.aktif;
document.getElementById('countTidakAktif').textContent = c.tidakAktif;
}).getAdminCounts();
}
function loadStudentsTable(){
showLoading(true);
google.script.run.withSuccessHandler(function(rows){
showLoading(false);
allStudentsData = rows || [];
populateFilterOptions();
applyStudentFilters();
}).getStudentsAdmin();
}
function populateFilterOptions(){
const jenjangSet = new Set(), kelasSet = new Set();
allStudentsData.forEach(r => {
if(r.jkelas) jenjangSet.add(r.jkelas);
if(r.kelas) kelasSet.add(r.kelas);
});
const selJ = document.getElementById('filterJenjang');
const selK = document.getElementById('filterKelas');
const currJ = selJ.value, currK = selK.value;
selJ.innerHTML = '<option value="">Semua Jenjang</option>';
selK.innerHTML = '<option value="">Semua Kelas</option>';
[...jenjangSet].sort().forEach(j => selJ.add(new Option(j,j)));
[...kelasSet].sort().forEach(k => selK.add(new Option(k,k)));
selJ.value = currJ; selK.value = currK;
}
function applyStudentFilters(){
const txt = document.getElementById('filterSearch').value.toLowerCase();
const fJ = document.getElementById('filterJenjang').value;
const fK = document.getElementById('filterKelas').value;
const filtered = allStudentsData.filter(r => {
const matchTxt = (r.nama && r.nama.toLowerCase().includes(txt)) || (r.nisn && String(r.nisn).includes(txt));
const matchJ = fJ === "" || r.jkelas === fJ;
const matchK = fK === "" || r.kelas === fK;
return matchTxt && matchJ && matchK;
});
renderStudentTable(filtered);
}
function renderStudentTable(rows){
const wrap = document.getElementById('tablePesertaWrap');
if(!rows.length){ wrap.innerHTML = '<div class="alert alert-info text-center">Data tidak ditemukan.</div>'; return; }
let html = '<table class="table table-hover align-middle"><thead><tr><th width="5%">NO</th><th>NISN</th><th>NAMA</th><th>JENJANG</th><th>KELAS</th><th>STATUS</th><th width="15%">AKSI</th></tr></thead><tbody>';
rows.forEach((r,i)=>{
let statusClass = 'text-dark';
if(r.status==='Aktif') statusClass = 'text-success fw-bold';
else if(r.status==='Curang') statusClass = 'text-danger fw-bold';
else if(r.status==='Selesai') statusClass = 'text-primary fw-bold';
html += `<tr>
<td>${i+1}</td>
<td>${escapeHtml(r.nisn)}</td>
<td>${escapeHtml(r.nama)}</td>
<td>${escapeHtml(r.jkelas)}</td>
<td>${escapeHtml(r.kelas)}</td>
<td class="${statusClass}">${escapeHtml(r.status)}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="openEditPeserta(${r.actualRow})"><i class="fa fa-edit"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="confirmDeletePeserta(${r.actualRow})"><i class="fa fa-trash"></i></button>
</td>
</tr>`;
});
html += '</tbody></table>';
wrap.innerHTML = html;
}
function openEditPeserta(row){
const found = allStudentsData.find(r => r.actualRow === row);
if(!found) return;
document.getElementById('editPesertaRow').value = found.actualRow;
document.getElementById('edit_nisn').value = found.nisn;
document.getElementById('edit_nama').value = found.nama;
document.getElementById('edit_jkelas').value = found.jkelas;
document.getElementById('edit_kelas').value = found.kelas;
document.getElementById('edit_status').value = found.status;
new bootstrap.Modal(document.getElementById('editPesertaModal')).show();
}
function savePesertaEdit(){
setButtonLoading('savePesertaBtn', true, 'Update');
const data = {
colA: '',
nisn: document.getElementById('edit_nisn').value,
nama: document.getElementById('edit_nama').value,
jkelas: document.getElementById('edit_jkelas').value,
kelas: document.getElementById('edit_kelas').value,
status: document.getElementById('edit_status').value
};
const row = document.getElementById('editPesertaRow').value;
google.script.run.withSuccessHandler(()=>{
setButtonLoading('savePesertaBtn', false, 'Update');
Swal.fire('Berhasil','Data diupdate','success');
bootstrap.Modal.getInstance(document.getElementById('editPesertaModal')).hide();
loadStudentsTable();
}).updateStudentAdmin(row, data);
}
function confirmDeletePeserta(row){
Swal.fire({ title:'Hapus?', icon:'warning', showCancelButton:true }).then(r=>{
if(r.isConfirmed){
showLoading(true);
google.script.run.withSuccessHandler(()=>{
showLoading(false);
loadStudentsTable();
loadDashboardCounts();
}).deleteStudentAdmin(row);
}
});
}
document.getElementById('btnTambahPeserta').addEventListener('click', ()=> new bootstrap.Modal(document.getElementById('modalTambahPeserta')).show());
document.getElementById('simpanPesertaBaru').addEventListener('click', ()=>{
setButtonLoading('simpanPesertaBaru', true, 'Simpan');
const data = {
colA: '',
nisn: document.getElementById('peserta_nisn').value,
nama: document.getElementById('peserta_nama').value,
jkelas: document.getElementById('peserta_jkelas').value,
kelas: document.getElementById('peserta_kelas').value,
status: document.getElementById('peserta_status').value
};
google.script.run.withSuccessHandler(res=>{
setButtonLoading('simpanPesertaBaru', false, 'Simpan');
if(res.success){
Swal.fire('Berhasil', res.message, 'success');
bootstrap.Modal.getInstance(document.getElementById('modalTambahPeserta')).hide();
loadStudentsTable();
} else {
Swal.fire('Gagal', res.message, 'error');
}
}).addStudentAdmin(data);
});
function loadSoalTable(){
showLoading(true);
google.script.run.withSuccessHandler(rows=>{
showLoading(false);
const wrap = document.getElementById('tableSoalWrap');
if(!rows.length){ wrap.innerHTML = '<div class="alert alert-info">Belum ada soal.</div>'; return; }
let html = '<div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>No</th><th>Jenjang</th><th>Kelas</th><th>Mapel</th><th>Token</th><th>Jadwal</th><th>Aksi</th></tr></thead><tbody>';
rows.forEach((r,i)=>{
const jadwalStr = (r.mulai && r.selesai) ? `<small>${r.mulai.replace('T',' ')}<br>s.d<br>${r.selesai.replace('T',' ')}</small>` : '-';
html += `<tr>
<td>${i+1}</td>
<td>${escapeHtml(r.jkelas)}</td>
<td><span class="badge bg-info text-dark">${escapeHtml(r.kelas)}</span></td>
<td>${escapeHtml(r.mata)}</td>
<td><code class="fw-bold text-primary">${escapeHtml(r.token)}</code></td>
<td>${jadwalStr}</td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="openEditSoal(${r.actualRow})"><i class="fa fa-edit"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteSoal(${r.actualRow})"><i class="fa fa-trash"></i></button>
</td>
</tr>`;
});
html += '</tbody></table></div>';
wrap.innerHTML = html;
}).getSoalAdmin();
}
function openEditSoal(row){
showLoading(true);
google.script.run.withSuccessHandler(rows=>{
showLoading(false);
const found = rows.find(r=>r.actualRow === row);
if(!found) return;
document.getElementById('editSoalRow').value = found.actualRow;
document.getElementById('edit_soal_colA').value = 'Auto';
document.getElementById('edit_soal_jkelas').value = found.jkelas;
document.getElementById('edit_soal_kelas').value = found.kelas;
document.getElementById('edit_soal_mata').value = found.mata;
document.getElementById('edit_soal_link').value = found.link;
document.getElementById('edit_soal_token').value = found.token;
document.getElementById('edit_soal_waktu').value = found.waktu;
document.getElementById('edit_soal_mulai').value = found.mulai;
document.getElementById('edit_soal_selesai').value = found.selesai;
new bootstrap.Modal(document.getElementById('editSoalModal')).show();
}).getSoalAdmin();
}
function saveSoalEdit(){
setButtonLoading('saveSoalBtn', true, 'Update');
const data = {
jkelas: document.getElementById('edit_soal_jkelas').value,
kelas: document.getElementById('edit_soal_kelas').value,
mata: document.getElementById('edit_soal_mata').value,
link: document.getElementById('edit_soal_link').value,
token: document.getElementById('edit_soal_token').value,
waktu: document.getElementById('edit_soal_waktu').value,
mulai: document.getElementById('edit_soal_mulai').value,
selesai: document.getElementById('edit_soal_selesai').value
};
google.script.run.withSuccessHandler(()=>{
setButtonLoading('saveSoalBtn', false, 'Update');
bootstrap.Modal.getInstance(document.getElementById('editSoalModal')).hide();
loadSoalTable();
}).updateSoalAdmin(document.getElementById('editSoalRow').value, data);
}
function confirmDeleteSoal(row){
Swal.fire({title:'Hapus Soal?', icon:'warning', showCancelButton:true}).then(r=>{
if(r.isConfirmed) google.script.run.withSuccessHandler(loadSoalTable).deleteSoalAdmin(row);
});
}
document.getElementById('btnTambahSoal').addEventListener('click', ()=> new bootstrap.Modal(document.getElementById('modalTambahSoal')).show());
document.getElementById('simpanSoalBaru').addEventListener('click', ()=>{
setButtonLoading('simpanSoalBaru', true, 'Simpan');
const data = {
jkelas: document.getElementById('soal_jkelas').value,
kelas: document.getElementById('soal_kelas').value,
mata: document.getElementById('soal_mata').value,
link: document.getElementById('soal_link').value,
token: document.getElementById('soal_token').value,
waktu: document.getElementById('soal_waktu').value,
mulai: document.getElementById('soal_mulai').value,
selesai: document.getElementById('soal_selesai').value
};
google.script.run.withSuccessHandler(()=>{
setButtonLoading('simpanSoalBaru', false, 'Simpan');
bootstrap.Modal.getInstance(document.getElementById('modalTambahSoal')).hide();
loadSoalTable();
}).addSoalAdmin(data);
});
function saveAdminProfile(){
setButtonLoading('saveProfileBtn', true, 'Simpan Profil');
const newName = document.getElementById('confAdminName').value;
const newPass = document.getElementById('confAdminPass').value;
const currentUser = document.getElementById('adminUser').value;
google.script.run.withSuccessHandler(r=>{
setButtonLoading('saveProfileBtn', false, 'Simpan Profil');
if(r.success) Swal.fire('Sukses','Profil admin diupdate','success');
else Swal.fire('Gagal', r.message, 'error');
}).updateAdminProfile(currentUser, newPass, newName);
}
function saveAppConfig(){
setButtonLoading('saveConfigBtn', true, 'Simpan Konfigurasi');
const t = document.getElementById('confAppTitle').value;
const l = document.getElementById('confAppLogo').value;
google.script.run.withSuccessHandler(r=>{
setButtonLoading('saveConfigBtn', false, 'Simpan Konfigurasi');
Swal.fire('Sukses','Konfigurasi tersimpan. Refresh halaman untuk melihat hasil.','success');
}).updateAppConfig(t, l);
}
function escapeHtml(s){ return s ? String(s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])) : ''; }
function escapeAttr(s){ return s ? String(s).replace(/"/g,'"') : ''; }
</script>
</body>
</html>
Cara Deploy (Mendapatkan URL)
Agar aplikasi bisa diakses, lakukan langkah berikut di editor Apps Script:
- Klik tombol Deploy (biasanya berwarna biru) di pojok kanan atas.
- Pilih New Deployment.
- Klik ikon roda gigi (Select type) di kiri, pilih Web App.
- Isi Description: Ujian V1 (atau bebas).
- Execute as: Pilih Me (email anda).
- Who has access: Pilih Anyone (Penting! agar siswa bisa akses tanpa login Google).
- Klik tombol Deploy.
- Salin link yang ada di bawah tulisan Web App URL.
Catatan tambahan : untuk menyingkat/meringkas url, bisa menggunakan https://s.id/ atau https://bit.ly/
Metode 2: Server Luar (cPanel / Google Sites)
Metode ini memisahkan Database (Spreadsheet) dengan Tampilan (Frontend). GAS berfungsi sebagai API.
Persiapan Spreadsheet
Buatlah Spreadsheet baru dengan struktur sheet sebagai berikut:
| Nama Sheet | Kolom (Header Baris 1) |
|---|---|
| DATA | No, NISN, Nama, Jenjang Kelas, Kelas, Status |
| SOAL | No, Jenjang Kelas, Mata Pelajaran, Link Soal, Token, Waktu Ujian, Mulai, Selesai, Kelas |
| AL | Username, Password, Nama Pengguna |
| LOGO | Link Logo, Nama Sekolah |
Kode API (Code.gs)
Gunakan kode ini agar GAS bisa diakses dari luar (CORS diaktifkan via JSON output). Copy paste ke Code.gs:
Pada bagian bawah kode ini, cari baris: const SPREADSHEET_ID = "COPY_SPEADSHEET_ID_DI_SINI";.
Ganti tulisan di dalam tanda kutip dengan ID SPREADSHEET yang Anda dapatkan dari url file spreadsheet.
// Code.gs (updated)
// Backend untuk aplikasi Ujian Online
const SPREADSHEET_ID = 'COPY_SPEADSHEET_ID_DI_SINI';
// Handle Request dari Luar (CORS Handled)
function doPost(e) {
return handleRequest(e);
}
function doGet(e) {
// Hanya info jika dibuka langsung di browser
return ContentService.createTextOutput("Server CBT Aktif. Gunakan POST request.");
}
function handleRequest(e) {
var output = {};
try {
// Parsing data yang dikirim dari fetch()
var params = JSON.parse(e.postData.contents);
var action = params.action;
var data = params.data || {};
// Router Aksi
switch (action) {
case 'getLogoAndTitles': output = getLogoAndTitles(); break;
case 'updateAppConfig': output = updateAppConfig(data.title, data.logo); break;
case 'getLevels': output = getLevels(); break;
case 'getClassesForLevel': output = getClassesForLevel(data.jkelas); break;
case 'getSubjectsForLevel': output = getSubjectsForLevel(data.jkelas, data.kelas); break;
case 'authenticateStudent': output = authenticateStudent(data.nisn, data.jkelas, data.kelas, data.mata); break;
case 'validateToken': output = validateToken(data.jkelas, data.mata, data.token, data.kelas); break;
case 'setStudentStatus': output = setStudentStatus(data.nisn, data.status); break;
case 'authenticateAdmin': output = authenticateAdmin(data.user, data.pass); break;
case 'updateAdminProfile': output = updateAdminProfile(data.username, data.newPass, data.newName); break;
case 'getAdminCounts': output = getAdminCounts(); break;
case 'getStudentsAdmin': output = getStudentsAdmin(); break;
case 'updateStudentAdmin': output = updateStudentAdmin(data.row, data.values); break;
case 'addStudentAdmin': output = addStudentAdmin(data.values); break;
case 'deleteStudentAdmin': output = deleteStudentAdmin(data.row); break;
case 'getSoalAdmin': output = getSoalAdmin(); break;
case 'updateSoalAdmin': output = updateSoalAdmin(data.row, data.values); break;
case 'addSoalAdmin': output = addSoalAdmin(data.values); break;
case 'deleteSoalAdmin': output = deleteSoalAdmin(data.row); break;
default: output = { success: false, message: "Aksi tidak dikenal" };
}
} catch (err) {
output = { success: false, message: err.toString() };
}
// Return JSON Text Output
return ContentService.createTextOutput(JSON.stringify(output))
.setMimeType(ContentService.MimeType.JSON);
}
/* ================== LOGIKA BISNIS (Sama seperti sebelumnya) ================== */
function _open() { return SpreadsheetApp.openById(SPREADSHEET_ID); }
function _getSheetByName(name){
const ss = _open();
const sheet = ss.getSheetByName(name);
if(!sheet) throw new Error('Sheet "'+name+'" tidak ditemukan.');
return sheet;
}
function getLogoAndTitles(){
const sheet = _getSheetByName('LOGO');
const a2 = sheet.getRange('A2').getValue() || '';
const b2 = sheet.getRange('B2').getValue() || '';
return {
logoUrl: a2,
title: b2 || 'Ujian Online',
subtitle: 'Designed by Kang Rohma Rohmadi'
};
}
function updateAppConfig(newTitle, newLogo){
const sheet = _getSheetByName('LOGO');
sheet.getRange('B2').setValue(newTitle);
sheet.getRange('A2').setValue(newLogo);
return { success: true };
}
function getLevels(){
const sheet = _getSheetByName('DATA');
const last = Math.max( sheet.getLastRow(), 1 );
if(last < 2) return [];
const values = sheet.getRange(2,4,last-1,1).getValues().flat();
return [...new Set(values.filter(v => v!==''))];
}
function getClassesForLevel(jkelas){
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(), 1);
if(last < 2) return [];
const cols = sheet.getRange(2,4,last-1,2).getValues();
const classes = [];
cols.forEach(r => {
if(String(r[0]) === String(jkelas) && r[1]) classes.push(r[1]);
});
return [...new Set(classes)];
}
function getSubjectsForLevel(jkelas, kelas){
const sheet = _getSheetByName('SOAL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return [];
const rows = sheet.getRange(2,1,last-1,9).getValues();
const subjects = [];
rows.forEach(r => {
if(String(r[1]) === String(jkelas) && String(r[8]) === String(kelas) && r[2]) {
subjects.push(r[2]);
}
});
return [...new Set(subjects)];
}
function getSoalMeta(jkelas, mata, kelas){
const sheet = _getSheetByName('SOAL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return null;
const rows = sheet.getRange(2,1,last-1,9).getValues();
for(let i=0;i<rows.length;i++){
const r = rows[i];
if(String(r[1]) === String(jkelas) && String(r[2]) === String(mata) && String(r[8]) === String(kelas)){
const now = new Date();
const start = r[6] ? new Date(r[6]) : null;
const end = r[7] ? new Date(r[7]) : null;
if (start && now < start) return { error: 'Ujian belum dimulai. Jadwal: ' + formatDate(start) };
if (end && now > end) return { error: 'Ujian sudah berakhir pada: ' + formatDate(end) };
return { link: r[3] || '', token: r[4] || '', durationMinutes: Number(r[5]) || 0 };
}
}
return null;
}
function formatDate(d) {
if(!d) return '';
return Utilities.formatDate(d, "Asia/Jakarta", "dd/MM/yyyy HH:mm");
}
function authenticateStudent(nisn, jkelas, kelas, mataPelajaran){
nisn = String(nisn || '').trim();
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return { success:false, message:'Tidak ada data siswa.' };
const rows = sheet.getRange(2,1,last-1,sheet.getLastColumn()).getValues();
for(let i=0;i<rows.length;i++){
const r = rows[i];
if(String(r[1] || '').trim() === nisn){
if(String(r[3]) !== String(jkelas)) return { success:false, message:'Jenjang Kelas tidak sesuai.' };
if(String(r[4]) !== String(kelas)) return { success:false, message:'Kelas tidak sesuai.' };
if(String(r[5]) !== 'Aktif') return { success:false, message: 'Status Ujian : ' + r[5] };
const meta = getSoalMeta(jkelas, mataPelajaran, kelas);
if(!meta) return { success:false, message:'Soal tidak ditemukan.' };
if(meta.error) return { success:false, message: meta.error };
return {
success:true,
data: {
nama: r[2], nisn: nisn, jkelas: r[3], kelas: r[4],
mataPelajaran: mataPelajaran, waktuMenit: meta.durationMinutes, linkSoal: meta.link
}
};
}
}
return { success:false, message:'NISN tidak ditemukan.' };
}
function validateToken(jkelas, mataPelajaran, token, kelas){
const meta = getSoalMeta(jkelas, mataPelajaran, kelas);
if(!meta) return { success:false, message:'Soal tidak ditemukan.' };
if(meta.error) return { success:false, message: meta.error };
if(String(meta.token) === String(token)) return { success:true, durationMinutes: meta.durationMinutes, link: meta.link };
return { success:false, message:'Token salah.' };
}
function setStudentStatus(nisn, newStatus){
nisn = String(nisn || '').trim();
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
const range = sheet.getRange(2,2,last-1,1);
const vals = range.getValues();
for(let i=0;i<vals.length;i++){
if(String(vals[i][0]||'').trim() === nisn){
sheet.getRange(i+2,6).setValue(newStatus);
return { success:true };
}
}
return { success:false, message:'NISN tidak ditemukan.' };
}
function authenticateAdmin(username, password){
username = String(username || '').trim();
password = String(password || '').trim();
const sheet = _getSheetByName('AL');
const last = Math.max(sheet.getLastRow(),1);
const rows = sheet.getRange(2,1,last-1,3).getValues();
for(let i=0;i<rows.length;i++){
if(String(rows[i][0]).trim() === username && String(rows[i][1]).trim() === password){
return { success:true, name: rows[i][2] || username };
}
}
return { success:false, message:'Username / password salah.' };
}
function updateAdminProfile(username, newPass, newName){
const sheet = _getSheetByName('AL');
const last = Math.max(sheet.getLastRow(),1);
const rows = sheet.getRange(2,1,last-1,1).getValues().flat();
for(let i=0; i<rows.length; i++){
if(String(rows[i]) === username){
if(newPass) sheet.getRange(i+2, 2).setValue(newPass);
if(newName) sheet.getRange(i+2, 3).setValue(newName);
return { success: true };
}
}
return { success: false, message: 'User tidak ditemukan' };
}
function getAdminCounts(){
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return { total:0, aktif:0, tidakAktif:0 };
const allF = sheet.getRange(2,6,last-1,1).getValues().flat();
const total = allF.length;
const aktif = allF.filter(v => String(v||'').trim() === 'Aktif').length;
const tidakAktif = total - aktif;
return { total: total, aktif: aktif, tidakAktif: tidakAktif };
}
function getStudentsAdmin(){
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return [];
const rows = sheet.getRange(2,1,last-1,6).getValues();
const out = [];
for(let i=0;i<rows.length;i++){
const r = rows[i];
out.push({
actualRow: i+2, colA: r[0]||'', nisn: String(r[1]||''), nama: r[2]||'',
jkelas: r[3]||'', kelas: r[4]||'', status: r[5]||''
});
}
return out;
}
function updateStudentAdmin(actualRow, data){
const sheet = _getSheetByName('DATA');
const nisnValue = "'" + String(data.nisn || '');
sheet.getRange(actualRow,2).setValue(nisnValue);
sheet.getRange(actualRow,3).setValue(data.nama);
sheet.getRange(actualRow,4).setValue(data.jkelas);
sheet.getRange(actualRow,5).setValue(data.kelas);
sheet.getRange(actualRow,6).setValue(data.status);
return { success:true };
}
function addStudentAdmin(data) {
const sheet = _getSheetByName('DATA');
const last = Math.max(sheet.getLastRow(),1);
const newNisn = String(data.nisn || '').trim();
const allNisn = sheet.getRange(2,2,last,1).getValues().flat().map(String);
if(allNisn.includes(newNisn)) return { success: false, message: 'NISN sudah terdaftar!' };
sheet.appendRow(['', "'" + newNisn, data.nama, data.jkelas, data.kelas, data.status]);
return { success: true, message: 'Data peserta berhasil ditambahkan.' };
}
function deleteStudentAdmin(actualRow){
_getSheetByName('DATA').deleteRow(Number(actualRow));
return { success:true };
}
function getSoalAdmin(){
const sheet = _getSheetByName('SOAL');
const last = Math.max(sheet.getLastRow(),1);
if(last < 2) return [];
const rows = sheet.getRange(2,1,last-1,9).getValues();
const out = [];
for(let i=0;i<rows.length;i++){
const r = rows[i];
out.push({
actualRow: i+2, jkelas: r[1]||'', mata: r[2]||'', link: r[3]||'',
token: r[4]||'', waktu: r[5]||'', mulai: r[6]?formatISOLike(r[6]):'',
selesai: r[7]?formatISOLike(r[7]):'', kelas: r[8]||''
});
}
return out;
}
function formatISOLike(dateObj) {
if(!dateObj || !(dateObj instanceof Date)) return '';
return Utilities.formatDate(dateObj, "Asia/Jakarta", "yyyy-MM-dd'T'HH:mm");
}
function updateSoalAdmin(actualRow, data){
const sheet = _getSheetByName('SOAL');
sheet.getRange(actualRow,2).setValue(data.jkelas);
sheet.getRange(actualRow,3).setValue(data.mata);
sheet.getRange(actualRow,4).setValue(data.link);
sheet.getRange(actualRow,5).setValue(data.token);
sheet.getRange(actualRow,6).setValue(data.waktu);
sheet.getRange(actualRow,7).setValue(data.mulai);
sheet.getRange(actualRow,8).setValue(data.selesai);
sheet.getRange(actualRow,9).setValue(data.kelas);
return { success:true };
}
function addSoalAdmin(data) {
const sheet = _getSheetByName('SOAL');
sheet.appendRow(['', data.jkelas, data.mata, data.link, data.token, data.waktu, data.mulai, data.selesai, data.kelas]);
return { success: true, message: 'Data soal berhasil ditambahkan.' };
}
function deleteSoalAdmin(actualRow){
_getSheetByName('SOAL').deleteRow(Number(actualRow));
return { success:true };
}
Kode Frontend (index.html)
File ini yang akan diunggah ke cPanel atau ditempel di Google Sites.
Menghubungkan Script Google
Pada bagian bawah kode ini, cari baris: const SCRIPT_URL = "MASUKKAN_URL_WEB_APP_ANDA_DISINI";.
Ganti tulisan di dalam tanda kutip dengan Web App URL yang Anda dapatkan dari langkah deploy Code.gs sebelumnya.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Ujian Online</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body, html {
height: 100%; margin: 0;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
font-family: 'Segoe UI', sans-serif;
}
#loginBox {
max-width:420px; width:94%; margin:auto; position: absolute; top:50%; left:50%;
transform: translate(-50%, -50%); background: #ffffff; border-radius: 16px;
box-shadow: 0 15px 35px rgba(0,0,0,0.2); padding: 40px;
}
#loginBox img.logo { max-width:100px; display:block; margin:0 auto 15px auto; }
.small-muted { font-size:0.9rem; color:#6c757d; }
.overlay-loading {
position:fixed; inset:0; display:none; align-items:center; justify-content:center;
background:rgba(255,255,255,0.8); z-index:9999;
}
#studentContent, #adminShell { display:none; height:100vh; width:100%; overflow-y:auto; }
#studentContent { padding:20px; background: #f0f2f5; }
/* Navbar & Student UI */
.navbar-top {
background: #ffffff; color: #333; padding: 12px 20px; display: flex;
justify-content: space-between; align-items: center; border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05); position: relative;
}
.card-identitas {
background:white; border-radius:16px; box-shadow:0 4px 20px rgba(0,0,0,0.05);
padding:25px; margin-top:30px; transition: all 0.3s ease;
}
.identitas-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(200px,1fr)); gap:15px; }
.identitas-item { background:#f8f9fa; border-radius:10px; padding:12px 16px; border-left: 4px solid #4facfe; }
.identitas-item strong { display:block; color:#6c757d; font-size:0.8rem; margin-bottom:4px; text-transform: uppercase; }
.identitas-item span { color:#2c3e50; font-weight:600; font-size: 1.05rem; }
/* Exam Area */
#examArea { margin-top:20px; }
.iframe-wrapper { height:78vh; min-height:400px; border-radius:12px; overflow:hidden; box-shadow:0 4px 15px rgba(0,0,0,0.1); background: #fff; }
.iframe-wrapper iframe { width: 100%; height: 100%; border: none; }
.fade-out { opacity: 0; transform: translateY(-10px); }
.fade-in { opacity: 1; transform: translateY(0); }
.card-identitas, #examArea { transition: opacity 0.5s ease, transform 0.5s ease; }
/* Admin UI */
#adminSidebar {
width: 260px; height: 100vh; background: #1e293b; color: #fff; position: fixed;
left: 0; top: 0; bottom: 0; padding: 20px 10px; z-index: 1051; display: flex; flex-direction: column;
transition: all 0.3s ease;
}
#adminSidebar.collapsed { width: 70px; padding: 20px 5px; }
#adminSidebar.collapsed .brand-text, #adminSidebar.collapsed .nav-link span, #adminSidebar.collapsed .logout-wrap span { display: none; }
#adminSidebar.collapsed .nav-link { justify-content: center; padding: 12px 0; }
#adminSidebar.collapsed .nav-link i { margin-right: 0 !important; font-size: 1.2rem; }
#adminSidebar .brand { display: flex; flex-direction: column; align-items: center; text-align: center; margin-bottom: 25px; }
#adminSidebar .brand i { font-size: 2rem; color: #4facfe; margin-bottom: 8px; }
#adminSidebar .brand-text { font-weight: 700; font-size: 1rem; color: #fff; text-transform: uppercase; }
#adminSidebar .nav-link { color: #cbd5e1; padding: 12px 15px; border-radius: 8px; margin-bottom: 5px; text-decoration: none; display: flex; align-items: center; }
#adminSidebar .nav-link:hover { background: rgba(255,255,255,0.1); color: #fff; }
#adminSidebar .nav-link.active { background: #4facfe; color: #fff; font-weight: 600; box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4); }
#adminSidebar .nav-link i { margin-right: 12px; width: 20px; text-align: center; }
#adminMain { margin-left: 260px; padding: 20px; transition: margin-left 0.3s ease; overflow-y: auto; height: 100vh; background: #f1f5f9; }
#adminMain.full { margin-left: 70px; }
.admin-topbar { display: flex; align-items: center; justify-content: space-between; background-color: #fff; color: #333; padding: 12px 20px; border-radius: 12px; margin-bottom: 20px; }
.stat-card { border-radius: 12px; padding: 20px; background: #fff; border-bottom: 4px solid #4facfe; margin-bottom: 15px; }
.filter-container { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid #e2e8f0; margin-bottom: 15px; }
.table-wrap { overflow-x: auto; background: #fff; border-radius: 10px; }
.table tbody td { vertical-align: middle; }
@media (max-width: 768px) {
#adminSidebar { transform: translateX(-100%); width: 240px; }
#adminSidebar.show { transform: translateX(0); }
#adminMain, #adminMain.full { margin-left: 0; }
#sidebarOverlay.show { display: block; position: fixed; inset:0; background:rgba(0,0,0,0.5); z-index:1050; }
}
</style>
</head>
<body>
<div class="overlay-loading" id="loadingOverlay">
<div class="text-center">
<div class="spinner-border text-primary" role="status"></div>
<div class="mt-2 fw-bold text-dark">Memproses...</div>
</div>
</div>
<div id="loginBox" class="text-center">
<a id="logoLink" target="_blank"><img src="" alt="Logo" class="logo" id="appLogo" /></a>
<h4 id="appTitle" class="mb-1 text-primary fw-bold">Ujian Online</h4>
<p id="appSubtitle" class="small-muted mb-4"></p>
<div class="mb-3 text-start">
<select id="roleSelect" class="form-select border-primary bg-light">
<option value="siswa" selected>Login Siswa</option>
<option value="admin">Login Admin</option>
</select>
</div>
<div id="formSiswa">
<div class="mb-2 text-start">
<label class="form-label small fw-bold">NISN</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-id-card"></i></span>
<input id="nisnInput" class="form-control" placeholder="Masukkan NISN" />
</div>
</div>
<div class="mb-2 text-start">
<label class="form-label small fw-bold">Jenjang Kelas</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-layer-group"></i></span>
<select id="jkelasSelect" class="form-select"><option value="">-- Pilih Jenjang --</option></select>
</div>
</div>
<div class="mb-2 text-start">
<label class="form-label small fw-bold">Kelas</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-school"></i></span>
<select id="kelasSelect" class="form-select"><option value="">-- Pilih Kelas --</option></select>
</div>
</div>
<div class="mb-4 text-start">
<label class="form-label small fw-bold">Mata Pelajaran</label>
<div class="input-group">
<span class="input-group-text bg-white text-primary"><i class="fa fa-book"></i></span>
<select id="mataSelect" class="form-select"><option value="">-- Pilih Mapel --</option></select>
</div>
</div>
<button id="loginSiswaBtn" class="btn btn-primary w-100 py-2 fw-bold shadow-sm"><i class="fa fa-sign-in-alt me-2"></i> MASUK</button>
</div>
<div id="formAdmin" style="display:none;">
<div class="mb-2 text-start">
<label class="form-label small fw-bold">Username</label>
<div class="input-group">
<span class="input-group-text bg-white text-danger"><i class="fa fa-user-shield"></i></span>
<input id="adminUser" type="text" class="form-control" placeholder="Username" />
</div>
</div>
<div class="mb-4 text-start">
<label class="form-label small fw-bold">Password</label>
<div class="input-group">
<span class="input-group-text bg-white text-danger"><i class="fa fa-lock"></i></span>
<input id="adminPass" type="password" class="form-control" placeholder="Password" />
</div>
</div>
<button id="loginAdminBtn" class="btn btn-danger w-100 py-2 fw-bold shadow-sm"><i class="fa fa-shield-alt me-2"></i> LOGIN ADMIN</button>
</div>
</div>
<div id="studentContent">
<div class="navbar-top">
<h5><i class="fa-solid fa-user-graduate me-2 text-primary"></i>Panel Ujian</h5>
<div class="d-flex align-items-center">
<span class="student-name me-3" id="studentName">Siswa</span>
<button id="logoutBtn" class="btn btn-sm btn-outline-danger px-3"><i class="fa-solid fa-power-off me-1"></i> Keluar</button>
</div>
</div>
<div class="card-identitas">
<h5 class="mb-3 text-primary"><i class="fa-solid fa-circle-info me-2"></i>Data Identitas Peserta</h5>
<div class="identitas-grid">
<div class="identitas-item"><strong>Nama Lengkap</strong><span id="dNama">-</span></div>
<div class="identitas-item"><strong>NISN</strong><span id="dNISN">-</span></div>
<div class="identitas-item"><strong>Jenjang Kelas</strong><span id="dTingkat">-</span></div>
<div class="identitas-item"><strong>Kelas</strong><span id="dKelas">-</span></div>
<div class="identitas-item"><strong>Mata Pelajaran</strong><span id="dMata">-</span></div>
<div class="identitas-item"><strong>Durasi Ujian</strong><span id="dWaktu">0</span> Menit</div>
</div>
<div class="text-center mt-4">
<button id="mulaiUjianBtn" class="btn btn-primary btn-lg shadow px-5 rounded-pill"><i class="fa fa-play me-2"></i> MULAI UJIAN</button>
</div>
</div>
<div id="examArea" style="display:none;">
<div class="exam-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-3">
<div class="bg-light p-2 rounded"><i class="fa-solid fa-clock text-danger fs-3"></i></div>
<div><div class="fw-bold text-secondary small">SISA WAKTU</div><div id="timerDisplay">--:--:--</div></div>
</div>
<button id="finishBtn" class="btn btn-danger px-4 rounded-pill fw-bold shadow-sm"><i class="fa-solid fa-flag-checkered me-2"></i> SELESAI</button>
</div>
<div class="progress mb-3" style="height: 6px;">
<div id="timeProgress" class="progress-bar bg-success" style="width: 100%; transition: width 1s linear;"></div>
</div>
<div class="iframe-wrapper"><iframe id="soalIframe" src="" frameborder="0"></iframe></div>
</div>
</div>
<div id="adminShell">
<div id="sidebarOverlay" onclick="closeSidebar()"></div>
<div id="adminSidebar">
<div class="d-flex justify-content-end d-lg-none mb-2"><button id="closeSidebarBtn" class="text-white bg-transparent border-0"><i class="fa fa-times fs-4"></i></button></div>
<div class="brand"><i class="fa fa-graduation-cap"></i><div class="brand-text mt-2">CBT ADMIN</div></div>
<hr class="divider bg-secondary opacity-25" />
<nav class="nav flex-column">
<a href="#" class="nav-link active" id="menuDashboard"><i class="fa fa-tachometer-alt"></i> <span>Dashboard</span></a>
<a href="#" class="nav-link" id="menuDataPeserta"><i class="fa fa-users"></i> <span>Data Siswa</span></a>
<a href="#" class="nav-link" id="menuDataSoal"><i class="fa fa-file-alt"></i> <span>Data Soal</span></a>
<a href="#" class="nav-link" id="menuPengaturan"><i class="fa fa-cogs"></i> <span>Pengaturan</span></a>
</nav>
<div class="logout-wrap mt-auto"><button id="logoutAdminBtn" class="btn btn-outline-light w-100"><i class="fa fa-sign-out-alt"></i> <span class="ms-2">Logout</span></button></div>
</div>
<div id="adminMain">
<div class="admin-topbar">
<div class="d-flex align-items-center"><button id="hamburgerBtn" class="btn btn-light me-3 border"><i class="fa fa-bars text-secondary"></i></button><strong id="topbarTitle" class="fs-5">Dashboard</strong></div>
<div class="d-flex align-items-center gap-2"><div class="bg-light rounded-circle p-2"><i class="fa fa-user text-primary"></i></div><span id="adminWelcome" class="fw-bold text-secondary">Admin</span></div>
</div>
<div id="adminContentArea">
<div id="adminDashboard">
<div class="row g-4">
<div class="col-md-4"><div class="stat-card border-bottom border-primary"><h6 class="text-muted text-uppercase mb-2">Total Siswa</h6><div class="fs-2 fw-bold text-dark" id="countTotal">0</div></div></div>
<div class="col-md-4"><div class="stat-card border-bottom border-success"><h6 class="text-muted text-uppercase mb-2">Siswa Aktif</h6><div class="fs-2 fw-bold text-success" id="countAktif">0</div></div></div>
<div class="col-md-4"><div class="stat-card border-bottom border-danger"><h6 class="text-muted text-uppercase mb-2">Tidak Aktif</h6><div class="fs-2 fw-bold text-danger" id="countTidakAktif">0</div></div></div>
</div>
</div>
<div id="adminDataPeserta" style="display:none;">
<div class="card p-4 shadow-sm border-0 rounded-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap"><h5 class="mb-0 fw-bold text-secondary">Manajemen Data Siswa</h5><button class="btn btn-primary rounded-pill px-4" id="btnTambahPeserta"><i class="fa fa-plus me-2"></i> Tambah Siswa</button></div>
<div class="filter-container">
<div class="row g-2">
<div class="col-md-4"><label class="small fw-bold text-muted">Cari Nama/NISN</label><input type="text" id="filterSearch" class="form-control" placeholder="Ketik nama..."></div>
<div class="col-md-4"><label class="small fw-bold text-muted">Filter Jenjang</label><select id="filterJenjang" class="form-select"><option value="">Semua Jenjang</option></select></div>
<div class="col-md-4"><label class="small fw-bold text-muted">Filter Kelas</label><select id="filterKelas" class="form-select"><option value="">Semua Kelas</option></select></div>
</div>
</div>
<div id="tablePesertaWrap" class="table-wrap"></div>
</div>
</div>
<div id="adminDataSoal" style="display:none;">
<div class="card p-4 shadow-sm border-0 rounded-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap"><h5 class="mb-0 fw-bold text-secondary">Bank Soal & Jadwal</h5><button class="btn btn-primary rounded-pill px-4" id="btnTambahSoal"><i class="fa fa-plus me-2"></i> Tambah Soal</button></div>
<div id="tableSoalWrap" class="table-wrap"></div>
</div>
</div>
<div id="adminPengaturan" style="display:none;">
<div class="row g-4">
<div class="col-md-6"><div class="card p-4 border-0 shadow-sm rounded-4 h-100"><h5 class="fw-bold mb-3">Profil Admin</h5><div class="mb-3"><label class="form-label small">Nama Tampilan</label><input type="text" id="confAdminName" class="form-control"></div><div class="mb-3"><label class="form-label small">Password Baru</label><input type="password" id="confAdminPass" class="form-control"></div><button id="saveProfileBtn" class="btn btn-primary w-100">Simpan Profil</button></div></div>
<div class="col-md-6"><div class="card p-4 border-0 shadow-sm rounded-4 h-100"><h5 class="fw-bold mb-3">Identitas Aplikasi</h5><div class="mb-3"><label class="form-label small">Nama Sekolah</label><input type="text" id="confAppTitle" class="form-control"></div><div class="mb-3"><label class="form-label small">URL Logo</label><input type="text" id="confAppLogo" class="form-control"></div><button id="saveConfigBtn" class="btn btn-secondary w-100">Simpan Konfigurasi</button></div></div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="modalTambahPeserta" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header bg-primary text-white"><h5 class="modal-title">Tambah Siswa</h5><button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button></div><div class="modal-body">
<div class="mb-2"><label class="small">NISN</label><input id="peserta_nisn" class="form-control"></div><div class="mb-2"><label class="small">Nama</label><input id="peserta_nama" class="form-control"></div><div class="mb-2"><label class="small">Jenjang Kelas</label><select id="peserta_jkelas" class="form-select"><option value="">-- Pilih --</option><option>Kelas 1</option><option>Kelas 2</option><option>Kelas 3</option><option>Kelas 4</option><option>Kelas 5</option><option>Kelas 6</option><option>Kelas 7</option><option>Kelas 8</option><option>Kelas 9</option><option>Kelas 10</option><option>Kelas 11</option><option>Kelas 12</option></select></div><div class="mb-2"><label class="small">Kelas</label><input id="peserta_kelas" class="form-control"></div><div class="mb-2"><label class="small">Status</label><select id="peserta_status" class="form-select"><option value="Aktif" selected>Aktif</option><option>Tidak Aktif</option></select></div>
</div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="simpanPesertaBaru">Simpan</button></div></div></div></div>
<div class="modal fade" id="modalTambahSoal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header bg-primary text-white"><h5 class="modal-title">Tambah Soal</h5><button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button></div><div class="modal-body">
<div class="row"><div class="col-6 mb-2"><label class="small">Jenjang Kelas</label><select id="soal_jkelas" class="form-select"><option value="">-- Pilih --</option><option>Kelas 1</option><option>Kelas 2</option><option>Kelas 3</option><option>Kelas 4</option><option>Kelas 5</option><option>Kelas 6</option><option>Kelas 7</option><option>Kelas 8</option><option>Kelas 9</option><option>Kelas 10</option><option>Kelas 11</option><option>Kelas 12</option></select></div><div class="col-6 mb-2"><label class="small">Kelas Khusus</label><input id="soal_kelas" class="form-control"></div></div>
<div class="mb-2"><label class="small">Mapel</label><input id="soal_mata" class="form-control"></div><div class="mb-2"><label class="small">Link</label><input id="soal_link" class="form-control"></div>
<div class="row"><div class="col-6 mb-2"><label class="small">Token</label><input id="soal_token" class="form-control"></div><div class="col-6 mb-2"><label class="small">Waktu</label><input id="soal_waktu" type="number" class="form-control"></div></div>
<div class="row"><div class="col-6 mb-2"><label class="small fw-bold">Mulai</label><input id="soal_mulai" type="datetime-local" class="form-control"></div><div class="col-6 mb-2"><label class="small fw-bold">Selesai</label><input id="soal_selesai" type="datetime-local" class="form-control"></div></div>
</div><div class="modal-footer"><button class="btn btn-secondary" data-bs-dismiss="modal">Batal</button><button class="btn btn-primary" id="simpanSoalBaru">Simpan</button></div></div></div></div>
<div class="modal fade" id="editPesertaModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Edit Peserta</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body">
<input type="hidden" id="editPesertaRow" /><div class="mb-2"><label class="small">NISN</label><input id="edit_nisn" class="form-control bg-light" readonly/></div><div class="mb-2"><label class="small">Nama</label><input id="edit_nama" class="form-control"/></div><div class="mb-2"><label class="small">Jenjang</label><select id="edit_jkelas" class="form-select"><option value="">-- Pilih --</option><option>Kelas 1</option><option>Kelas 2</option><option>Kelas 3</option><option>Kelas 4</option><option>Kelas 5</option><option>Kelas 6</option><option>Kelas 7</option><option>Kelas 8</option><option>Kelas 9</option><option>Kelas 10</option><option>Kelas 11</option><option>Kelas 12</option></select></div><div class="mb-2"><label class="small">Kelas</label><input id="edit_kelas" class="form-control"/></div><div class="mb-2"><label class="small">Status</label><select id="edit_status" class="form-select"><option>Aktif</option><option>Tidak Aktif</option><option>Selesai</option><option>Curang</option><option>Waktu Habis</option></select></div>
</div><div class="modal-footer"><button id="savePesertaBtn" class="btn btn-primary">Update</button></div></div></div></div>
<div class="modal fade" id="editSoalModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Edit Soal</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body">
<input type="hidden" id="editSoalRow" /><div class="row"><div class="col-6 mb-2"><label class="small">Jenjang</label><select id="edit_soal_jkelas" class="form-select"><option value="">-- Pilih --</option><option>Kelas 1</option><option>Kelas 2</option><option>Kelas 3</option><option>Kelas 4</option><option>Kelas 5</option><option>Kelas 6</option><option>Kelas 7</option><option>Kelas 8</option><option>Kelas 9</option><option>Kelas 10</option><option>Kelas 11</option><option>Kelas 12</option></select></div><div class="col-6 mb-2"><label class="small">Kelas Khusus</label><input id="edit_soal_kelas" class="form-control"></div></div>
<div class="mb-2"><label class="small">Mapel</label><input id="edit_soal_mata" class="form-control"/></div><div class="mb-2"><label class="small">Link</label><input id="edit_soal_link" class="form-control"/></div>
<div class="row"><div class="col-6 mb-2"><label class="small">Token</label><input id="edit_soal_token" class="form-control"/></div><div class="col-6 mb-2"><label class="small">Waktu</label><input id="edit_soal_waktu" class="form-control"/></div></div>
<div class="row"><div class="col-6 mb-2"><label class="small fw-bold">Mulai</label><input id="edit_soal_mulai" type="datetime-local" class="form-control"></div><div class="col-6 mb-2"><label class="small fw-bold">Selesai</label><input id="edit_soal_selesai" type="datetime-local" class="form-control"></div></div>
</div><div class="modal-footer"><button id="saveSoalBtn" class="btn btn-primary">Update</button></div></div></div></div>
<div class="modal fade" id="tokenModal" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Masukkan Token</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body">
<div class="alert alert-danger small mb-3"><i class="fa fa-exclamation-triangle me-1"></i> <strong>PERINGATAN KERAS!</strong><br>Dilarang membuka tab lain. Jika terdeteksi, <strong>Ujian Anda akan otomatis diblokir dengan status CURANG.</strong></div>
<input id="tokenInput" class="form-control form-control-lg text-center text-uppercase fw-bold" placeholder="KETIK TOKEN" style="letter-spacing: 2px;"/>
</div><div class="modal-footer"><button id="tokenSubmitBtn" class="btn btn-primary w-100">MULAI UJIAN</button></div></div></div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<script>
// ==========================================
// CONFIG: GANTI URL DI BAWAH INI DENGAN URL DEPLOY ANDA
const SCRIPT_URL = "MASUKKAN_URL_WEB_APP_ANDA_DISINI";
// ==========================================
let currentStudent = null, examTimer = null, remainingSeconds = 0, examStarted = false, adminName = '', allStudentsData = [];
// --- API HELPER (Pengganti google.script.run) ---
async function runApi(action, data = {}) {
try {
const response = await fetch(SCRIPT_URL, {
method: "POST",
body: JSON.stringify({ action: action, data: data })
});
return await response.json();
} catch (e) {
console.error(e);
return { success: false, message: "Koneksi Gagal: " + e.message };
}
}
document.addEventListener('DOMContentLoaded', function(){
showLoading(true);
runApi('getLogoAndTitles').then(d => { initUI(d); });
runApi('getLevels').then(d => { populateLevels(d); showLoading(false); });
document.addEventListener('keypress', function(e){
if(e.key === 'Enter' && document.getElementById('loginBox').style.display !== 'none'){
if(document.getElementById('roleSelect').value === 'siswa') onLoginSiswa();
else onLoginAdmin();
}
});
document.getElementById('roleSelect').addEventListener('change', function(){
if(this.value === 'siswa'){ document.getElementById('formSiswa').style.display=''; document.getElementById('formAdmin').style.display='none'; }
else { document.getElementById('formSiswa').style.display='none'; document.getElementById('formAdmin').style.display=''; }
});
document.getElementById('jkelasSelect').addEventListener('change', function(){
if(!this.value) return;
showLoading(true);
runApi('getClassesForLevel', { jkelas: this.value }).then(d => { fillSelect('kelasSelect', d, '-- Pilih Kelas --'); fillSelect('mataSelect', [], '-- Pilih Mapel --'); showLoading(false); });
});
document.getElementById('kelasSelect').addEventListener('change', function(){
if(!this.value) return;
showLoading(true);
runApi('getSubjectsForLevel', { jkelas: document.getElementById('jkelasSelect').value, kelas: this.value }).then(d => { fillSelect('mataSelect', d, '-- Pilih Mapel --'); showLoading(false); });
});
// Bind Buttons
document.getElementById('loginSiswaBtn').addEventListener('click', onLoginSiswa);
document.getElementById('loginAdminBtn').addEventListener('click', onLoginAdmin);
document.getElementById('mulaiUjianBtn').addEventListener('click', onMulaiUjian);
document.getElementById('tokenSubmitBtn').addEventListener('click', submitToken);
document.getElementById('logoutBtn').addEventListener('click', logoutToLogin);
document.getElementById('logoutAdminBtn').addEventListener('click', logoutToLogin);
document.getElementById('finishBtn').addEventListener('click', finishExam);
document.getElementById('hamburgerBtn').addEventListener('click', toggleSidebar);
document.getElementById('menuDashboard').addEventListener('click', showDashboard);
document.getElementById('menuDataPeserta').addEventListener('click', showDataPeserta);
document.getElementById('menuDataSoal').addEventListener('click', showDataSoal);
document.getElementById('menuPengaturan').addEventListener('click', showPengaturan);
document.getElementById('savePesertaBtn').addEventListener('click', savePesertaEdit);
document.getElementById('saveSoalBtn').addEventListener('click', saveSoalEdit);
document.getElementById('saveProfileBtn').addEventListener('click', saveAdminProfile);
document.getElementById('saveConfigBtn').addEventListener('click', saveAppConfig);
document.getElementById('filterSearch').addEventListener('input', applyStudentFilters);
document.getElementById('filterJenjang').addEventListener('change', applyStudentFilters);
document.getElementById('filterKelas').addEventListener('change', applyStudentFilters);
document.getElementById('btnTambahPeserta').addEventListener('click', ()=> new bootstrap.Modal(document.getElementById('modalTambahPeserta')).show());
document.getElementById('btnTambahSoal').addEventListener('click', ()=> new bootstrap.Modal(document.getElementById('modalTambahSoal')).show());
// Add Student
document.getElementById('simpanPesertaBaru').addEventListener('click', ()=>{
setButtonLoading('simpanPesertaBaru', true);
const data = { nisn: document.getElementById('peserta_nisn').value, nama: document.getElementById('peserta_nama').value, jkelas: document.getElementById('peserta_jkelas').value, kelas: document.getElementById('peserta_kelas').value, status: document.getElementById('peserta_status').value };
runApi('addStudentAdmin', { values: data }).then(res => {
setButtonLoading('simpanPesertaBaru', false, 'Simpan');
if(res.success){ Swal.fire('Berhasil', res.message, 'success'); bootstrap.Modal.getInstance(document.getElementById('modalTambahPeserta')).hide(); loadStudentsTable(); }
else Swal.fire('Gagal', res.message, 'error');
});
});
// Add Soal
document.getElementById('simpanSoalBaru').addEventListener('click', ()=>{
setButtonLoading('simpanSoalBaru', true);
const data = { jkelas: document.getElementById('soal_jkelas').value, kelas: document.getElementById('soal_kelas').value, mata: document.getElementById('soal_mata').value, link: document.getElementById('soal_link').value, token: document.getElementById('soal_token').value, waktu: document.getElementById('soal_waktu').value, mulai: document.getElementById('soal_mulai').value, selesai: document.getElementById('soal_selesai').value };
runApi('addSoalAdmin', { values: data }).then(res => {
setButtonLoading('simpanSoalBaru', false, 'Simpan');
bootstrap.Modal.getInstance(document.getElementById('modalTambahSoal')).hide(); loadSoalTable();
});
});
});
function setButtonLoading(btnId, isLoading, defaultText = 'Simpan') {
const btn = document.getElementById(btnId);
if (isLoading) { btn.disabled = true; btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Memproses...`; }
else { btn.disabled = false; btn.innerHTML = defaultText; }
}
function showLoading(show){ document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none'; }
function fillSelect(id, arr, placeholder){
const sel = document.getElementById(id); sel.innerHTML = '';
const opt0 = document.createElement('option'); opt0.value = ''; opt0.textContent = placeholder; sel.appendChild(opt0);
(arr||[]).forEach(v=>{ const o=document.createElement('option'); o.value=v; o.textContent=v; sel.appendChild(o); });
}
function escapeHtml(s){ return s ? String(s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])) : ''; }
function initUI(data){
if(data){
document.getElementById('appLogo').src = data.logoUrl || '';
document.getElementById('appTitle').textContent = data.title;
document.getElementById('appSubtitle').textContent = data.subtitle;
document.getElementById('confAppTitle').value = data.title === 'Ujian Online' ? '' : data.title;
document.getElementById('confAppLogo').value = data.logoUrl || '';
}
}
function populateLevels(levels){ fillSelect('jkelasSelect', levels, '-- Pilih Jenjang --'); }
/* AUTHENTICATION */
function onLoginSiswa(){
const d = { nisn: document.getElementById('nisnInput').value, jkelas: document.getElementById('jkelasSelect').value, kelas: document.getElementById('kelasSelect').value, mata: document.getElementById('mataSelect').value };
if(!d.nisn || !d.jkelas || !d.kelas || !d.mata){ Swal.fire('Info', 'Lengkapi data login.', 'warning'); return; }
showLoading(true);
runApi('authenticateStudent', d).then(resp => {
showLoading(false);
if(resp.success){
currentStudent = resp.data;
document.getElementById('studentName').textContent = currentStudent.nama;
document.getElementById('loginBox').style.display = 'none'; document.getElementById('studentContent').style.display = 'block'; document.getElementById('adminShell').style.display = 'none';
Swal.fire({ icon:'success', title:'Halo '+currentStudent.nama, showConfirmButton:false, timer:1500 });
['dNama','dNISN','dTingkat','dKelas','dMata','dWaktu'].forEach((id,i) => document.getElementById(id).textContent = Object.values(currentStudent)[i]);
} else { Swal.fire('Gagal', resp.message, 'error'); }
});
}
function onLoginAdmin(){
const u = document.getElementById('adminUser').value, p = document.getElementById('adminPass').value;
if(!u || !p){ Swal.fire('Info', 'Masukkan user & pass.', 'warning'); return; }
showLoading(true);
runApi('authenticateAdmin', { user:u, pass:p }).then(resp => {
showLoading(false);
if(resp.success){
adminName = resp.name;
document.getElementById('loginBox').style.display = 'none'; document.getElementById('adminShell').style.display = 'block';
document.getElementById('adminWelcome').textContent = adminName; document.getElementById('confAdminName').value = adminName;
showDashboard();
} else { Swal.fire('Gagal', resp.message, 'error'); }
});
}
function logoutToLogin(){
currentStudent = null; examStarted = false; if(examTimer) clearInterval(examTimer);
document.getElementById('studentContent').style.display='none'; document.getElementById('adminShell').style.display='none'; document.getElementById('loginBox').style.display='block';
document.getElementById('soalIframe').src = ''; document.getElementById('examArea').style.display = 'none'; document.getElementById('tokenInput').value = '';
runApi('getLogoAndTitles').then(d => initUI(d));
}
/* EXAM LOGIC */
function onMulaiUjian(){ new bootstrap.Modal(document.getElementById('tokenModal')).show(); }
function submitToken(){
const t = document.getElementById('tokenInput').value;
if(!t){ Swal.fire('Info','Masukkan Token','warning'); return; }
showLoading(true);
runApi('validateToken', { jkelas: currentStudent.jkelas, mata: currentStudent.mataPelajaran, kelas: currentStudent.kelas, token: t }).then(resp => {
showLoading(false);
if(resp.success){ startExam(resp.durationMinutes, resp.link); bootstrap.Modal.getInstance(document.getElementById('tokenModal')).hide(); }
else Swal.fire('Gagal', resp.message, 'error');
});
}
function startExam(dur, link){
totalSeconds = Math.max(0, Math.floor(Number(dur)*60)); remainingSeconds = totalSeconds;
document.querySelector('.card-identitas').classList.add('fade-out');
setTimeout(()=>{
document.querySelector('.card-identitas').style.display='none';
const ea = document.getElementById('examArea'); ea.style.display=''; ea.classList.add('fade-in');
document.getElementById('soalIframe').src = link; examStarted = true; updateTimerDisplay();
if(examTimer) clearInterval(examTimer);
examTimer = setInterval(()=>{ remainingSeconds--; if(remainingSeconds<=0) finishExam(false); else updateTimerDisplay(); }, 1000);
}, 500);
}
function updateTimerDisplay(){
const h=Math.floor(remainingSeconds/3600), m=Math.floor((remainingSeconds%3600)/60), s=remainingSeconds%60;
document.getElementById('timerDisplay').textContent = `${String(h).padStart(2,'0')}:${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
const p = (remainingSeconds/totalSeconds)*100; const b = document.getElementById('timeProgress');
b.style.width = p+'%'; b.className = `progress-bar bg-${p>60?'success':p>30?'warning':'danger'}`;
}
function finishExam(manual=true){
const act = () => {
if(examTimer) clearInterval(examTimer); examStarted = false;
runApi('setStudentStatus', { nisn: currentStudent.nisn, status: manual?'Selesai':'Waktu Habis' });
Swal.fire(manual?'Selesai':'Waktu Habis', 'Ujian berakhir.', manual?'success':'warning');
setTimeout(logoutToLogin, 2000);
};
if(manual) Swal.fire({title:'Selesai?', icon:'question', showCancelButton:true, confirmButtonText:'Ya'}).then(r=>{ if(r.isConfirmed) act(); });
else act();
}
document.addEventListener('visibilitychange', function(){
if(document.hidden && examStarted){
document.getElementById('soalIframe').style.display='none'; examStarted = false; if(examTimer) clearInterval(examTimer);
runApi('setStudentStatus', { nisn: currentStudent.nisn, status: 'Curang' });
Swal.fire('Pelanggaran','Anda pindah tab. Status: CURANG.','error').then(logoutToLogin);
}
});
/* ADMIN LOGIC */
function toggleSidebar(){ document.getElementById('adminSidebar').classList.toggle(window.innerWidth<992?'show':'collapsed'); document.getElementById('sidebarOverlay').classList.toggle('show'); if(window.innerWidth>=992) document.getElementById('adminMain').classList.toggle('full'); }
function closeSidebar(){ document.getElementById('adminSidebar').classList.remove('show'); document.getElementById('sidebarOverlay').classList.remove('show'); }
function showDashboard(e){ if(e)e.preventDefault(); switchTab('adminDashboard'); runApi('getAdminCounts').then(c=>{ document.getElementById('countTotal').textContent=c.total; document.getElementById('countAktif').textContent=c.aktif; document.getElementById('countTidakAktif').textContent=c.tidakAktif; }); }
function showDataPeserta(e){ if(e)e.preventDefault(); switchTab('adminDataPeserta'); loadStudentsTable(); }
function showDataSoal(e){ if(e)e.preventDefault(); switchTab('adminDataSoal'); loadSoalTable(); }
function showPengaturan(e){ if(e)e.preventDefault(); switchTab('adminPengaturan'); }
function switchTab(id){
['adminDashboard','adminDataPeserta','adminDataSoal','adminPengaturan'].forEach(tid => document.getElementById(tid).style.display = (tid===id)?'':'none');
}
function loadStudentsTable(){
showLoading(true);
runApi('getStudentsAdmin').then(rows => { showLoading(false); allStudentsData = rows; populateFilterOptions(); applyStudentFilters(); });
}
function populateFilterOptions(){
const sJ=document.getElementById('filterJenjang'), sK=document.getElementById('filterKelas'), jS=new Set(), kS=new Set();
allStudentsData.forEach(r=>{ if(r.jkelas)jS.add(r.jkelas); if(r.kelas)kS.add(r.kelas); });
const cJ=sJ.value, cK=sK.value; sJ.innerHTML='<option value="">Semua Jenjang</option>'; sK.innerHTML='<option value="">Semua Kelas</option>';
[...jS].sort().forEach(v=>sJ.add(new Option(v,v))); [...kS].sort().forEach(v=>sK.add(new Option(v,v)));
sJ.value=cJ; sK.value=cK;
}
function applyStudentFilters(){
const t=document.getElementById('filterSearch').value.toLowerCase(), fJ=document.getElementById('filterJenjang').value, fK=document.getElementById('filterKelas').value;
const filtered = allStudentsData.filter(r => ((r.nama&&r.nama.toLowerCase().includes(t))||(r.nisn&&String(r.nisn).includes(t))) && (fJ===""||r.jkelas===fJ) && (fK===""||r.kelas===fK));
renderStudentTable(filtered);
}
function renderStudentTable(rows){
const w = document.getElementById('tablePesertaWrap');
if(!rows.length){ w.innerHTML = '<div class="alert alert-info text-center">Data kosong.</div>'; return; }
let h = '<table class="table table-hover align-middle"><thead><tr><th>No</th><th>NISN</th><th>Nama</th><th>Jenjang</th><th>Kelas</th><th>Status</th><th>Aksi</th></tr></thead><tbody>';
rows.forEach((r,i)=>{
let cls = r.status==='Aktif'?'success':r.status==='Curang'?'danger':r.status==='Selesai'?'primary':'dark';
h += `<tr><td>${i+1}</td><td>${escapeHtml(r.nisn)}</td><td>${escapeHtml(r.nama)}</td><td>${escapeHtml(r.jkelas)}</td><td>${escapeHtml(r.kelas)}</td><td class="fw-bold text-${cls}">${escapeHtml(r.status)}</td>
<td><button class="btn btn-sm btn-outline-primary" onclick="editPeserta(${r.actualRow})"><i class="fa fa-edit"></i></button> <button class="btn btn-sm btn-outline-danger" onclick="delPeserta(${r.actualRow})"><i class="fa fa-trash"></i></button></td></tr>`;
});
w.innerHTML = h + '</tbody></table>';
}
window.editPeserta = function(row){
const f = allStudentsData.find(r=>r.actualRow===row); if(!f)return;
document.getElementById('editPesertaRow').value=row; document.getElementById('edit_nisn').value=f.nisn; document.getElementById('edit_nama').value=f.nama;
document.getElementById('edit_jkelas').value=f.jkelas; document.getElementById('edit_kelas').value=f.kelas; document.getElementById('edit_status').value=f.status;
new bootstrap.Modal(document.getElementById('editPesertaModal')).show();
}
function savePesertaEdit(){
setButtonLoading('savePesertaBtn', true, 'Update');
const d = { nisn: document.getElementById('edit_nisn').value, nama: document.getElementById('edit_nama').value, jkelas: document.getElementById('edit_jkelas').value, kelas: document.getElementById('edit_kelas').value, status: document.getElementById('edit_status').value };
runApi('updateStudentAdmin', { row: document.getElementById('editPesertaRow').value, values: d }).then(()=>{
setButtonLoading('savePesertaBtn', false, 'Update'); bootstrap.Modal.getInstance(document.getElementById('editPesertaModal')).hide(); loadStudentsTable();
});
}
window.delPeserta = function(row){
Swal.fire({title:'Hapus?', icon:'warning', showCancelButton:true}).then(r=>{ if(r.isConfirmed){ showLoading(true); runApi('deleteStudentAdmin', {row:row}).then(()=>{ showLoading(false); loadStudentsTable(); }); } });
}
function loadSoalTable(){
showLoading(true);
runApi('getSoalAdmin').then(rows=>{
showLoading(false); const w = document.getElementById('tableSoalWrap');
if(!rows.length){ w.innerHTML='<div class="alert alert-info">Kosong.</div>'; return; }
let h = '<div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th>No</th><th>Jenjang</th><th>Kelas</th><th>Mapel</th><th>Token</th><th>Jadwal</th><th>Aksi</th></tr></thead><tbody>';
rows.forEach((r,i)=>{
h += `<tr><td>${i+1}</td><td>${escapeHtml(r.jkelas)}</td><td><span class="badge bg-info text-dark">${escapeHtml(r.kelas)}</span></td><td>${escapeHtml(r.mata)}</td><td><code class="fw-bold text-primary">${escapeHtml(r.token)}</code></td><td><small>${r.mulai.replace('T',' ')}<br>${r.selesai.replace('T',' ')}</small></td>
<td><button class="btn btn-sm btn-outline-primary" onclick="editSoal(${r.actualRow})"><i class="fa fa-edit"></i></button> <button class="btn btn-sm btn-outline-danger" onclick="delSoal(${r.actualRow})"><i class="fa fa-trash"></i></button></td></tr>`;
});
w.innerHTML = h+'</tbody></table></div>';
window.soalData = rows;
});
}
window.editSoal = function(row){
const f = window.soalData.find(r=>r.actualRow===row); if(!f)return;
document.getElementById('editSoalRow').value=row; document.getElementById('edit_soal_jkelas').value=f.jkelas; document.getElementById('edit_soal_kelas').value=f.kelas;
document.getElementById('edit_soal_mata').value=f.mata; document.getElementById('edit_soal_link').value=f.link; document.getElementById('edit_soal_token').value=f.token;
document.getElementById('edit_soal_waktu').value=f.waktu; document.getElementById('edit_soal_mulai').value=f.mulai; document.getElementById('edit_soal_selesai').value=f.selesai;
new bootstrap.Modal(document.getElementById('editSoalModal')).show();
}
function saveSoalEdit(){
setButtonLoading('saveSoalBtn', true, 'Update');
const d = { jkelas: document.getElementById('edit_soal_jkelas').value, kelas: document.getElementById('edit_soal_kelas').value, mata: document.getElementById('edit_soal_mata').value, link: document.getElementById('edit_soal_link').value, token: document.getElementById('edit_soal_token').value, waktu: document.getElementById('edit_soal_waktu').value, mulai: document.getElementById('edit_soal_mulai').value, selesai: document.getElementById('edit_soal_selesai').value };
runApi('updateSoalAdmin', { row: document.getElementById('editSoalRow').value, values: d }).then(()=>{
setButtonLoading('saveSoalBtn', false, 'Update'); bootstrap.Modal.getInstance(document.getElementById('editSoalModal')).hide(); loadSoalTable();
});
}
window.delSoal = function(row){
Swal.fire({title:'Hapus?', icon:'warning', showCancelButton:true}).then(r=>{ if(r.isConfirmed) runApi('deleteSoalAdmin', {row:row}).then(loadSoalTable); });
}
function saveAdminProfile(){
setButtonLoading('saveProfileBtn', true);
runApi('updateAdminProfile', { username: document.getElementById('adminUser').value, newPass: document.getElementById('confAdminPass').value, newName: document.getElementById('confAdminName').value }).then(r=>{
setButtonLoading('saveProfileBtn', false);
if(r.success) Swal.fire('Sukses','Update berhasil','success'); else Swal.fire('Gagal',r.message,'error');
});
}
function saveAppConfig(){
setButtonLoading('saveConfigBtn', true);
runApi('updateAppConfig', { title: document.getElementById('confAppTitle').value, logo: document.getElementById('confAppLogo').value }).then(r=>{
setButtonLoading('saveConfigBtn', false); Swal.fire('Sukses','Tersimpan','success');
});
}
</script>
</body>
</html>
Mukhlis Rohmadi
Lecturer
Halaman ini didedikasikan untuk berbagi tutorial dan tools bermanfaat seputar teknologi pendidikan. Untuk informasi lebih lanjut mengenai profil, portfolio, dan kontak penulis, silakan kunjungi website resmi kami.
Kunjungi Profil Lengkap