<!DOCTYPE html>
<html>
<head>
<base target="_top">
<meta charset="UTF-8">
<style>
:root{
--glass-bg: rgba(255,255,255,0.55);
--glass-bg-strong: rgba(255,255,255,0.75);
--glass-border: rgba(255,255,255,0.65);
--text-main: #1D2433;
--text-sub: #5B6472;
--accent: #4C7EF3;
--accent-soft: rgba(76,126,243,0.15);
--danger: #F0506E;
--danger-soft: rgba(240,80,110,0.15);
--radius: 22px;
}
*{ box-sizing:border-box; }
html,body{
margin:0; padding:0; min-height:100vh;
font-family:-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Inter", "Segoe UI", sans-serif;
color:var(--text-main);
background:
radial-gradient(circle at 12% 15%, #FFD9E8 0%, transparent 45%),
radial-gradient(circle at 88% 10%, #C7E2FF 0%, transparent 50%),
radial-gradient(circle at 30% 90%, #E3D6FF 0%, transparent 50%),
radial-gradient(circle at 90% 85%, #CFF3E4 0%, transparent 45%),
#F4F6FA;
background-attachment: fixed;
-webkit-font-smoothing:antialiased;
}
/* ---------- LOGIN SCREEN ---------- */
.initial-loader{
position:fixed; inset:0; z-index:9999; display:flex; align-items:center; justify-content:center;
background:#F4F6FA;
}
.login-screen{
min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px;
}
.login-card{
width:100%; max-width:360px;
background:var(--glass-bg-strong);
backdrop-filter: blur(30px) saturate(180%);
-webkit-backdrop-filter: blur(30px) saturate(180%);
border:1px solid var(--glass-border);
border-radius:28px;
box-shadow:0 24px 60px rgba(20,25,40,0.18);
padding:32px 26px;
text-align:center;
}
.login-logo{ font-size:40px; margin-bottom:6px; }
.login-card h2{ margin:0 0 2px 0; font-size:20px; font-weight:800; letter-spacing:-0.02em; }
.login-sub{ font-size:12.5px; color:var(--text-sub); margin:0 0 20px 0; }
.login-error{
margin-top:12px; font-size:12.5px; color:var(--danger); min-height:16px; font-weight:600;
}
.login-hint{
margin-top:16px; font-size:11px; color:var(--text-sub); background:rgba(0,0,0,0.04);
border-radius:12px; padding:8px 10px; text-align:left; line-height:1.5;
}
.site-footer{
text-align:center; font-size:11.5px; color:var(--text-sub);
padding:22px 20px 28px 20px; letter-spacing:.02em;
}
/* ---------- NAVBAR ---------- */
.navbar{
position:sticky; top:0; z-index:20;
display:flex; align-items:center; justify-content:space-between;
gap:16px; padding:16px 22px;
background:var(--glass-bg);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border-bottom:1px solid var(--glass-border);
}
.navbar h1{ font-size:19px; font-weight:700; margin:0; letter-spacing:-0.02em; }
.navbar .sub{ font-size:12px; color:var(--text-sub); margin-top:2px; }
.search-wrap{ flex:1; max-width:380px; position:relative; }
.search-wrap input{
width:100%; padding:11px 16px 11px 40px; border-radius:14px;
border:1px solid var(--glass-border); background:rgba(255,255,255,0.6);
font-size:14px; color:var(--text-main); outline:none; transition:.2s;
}
.search-wrap input:focus{ background:rgba(255,255,255,0.9); box-shadow:0 0 0 3px var(--accent-soft); }
.search-wrap::before{
content:"⌕"; position:absolute; left:14px; top:50%; transform:translateY(-50%);
font-size:16px; color:var(--text-sub);
}
.nav-right{ display:flex; align-items:center; gap:8px; }
.role-badge{
font-size:11px; font-weight:700; padding:6px 12px; border-radius:999px;
background:var(--accent-soft); color:var(--accent); text-transform:uppercase; letter-spacing:.04em;
white-space:nowrap;
}
.nav-icon-btn{
width:36px; height:36px; border-radius:12px; border:none; background:rgba(0,0,0,0.05);
cursor:pointer; font-size:15px; display:flex; align-items:center; justify-content:center; transition:.15s;
}
.nav-icon-btn:hover{ background:rgba(0,0,0,0.1); }
/* ---------- LAYOUT ---------- */
.container{ max-width:1180px; margin:0 auto; padding:22px; }
.menu-tabs{ display:flex; gap:8px; margin-bottom:18px; }
.menu-tab{
padding:10px 18px; border-radius:999px; font-size:13px; font-weight:600;
background:rgba(255,255,255,0.5); border:1px solid var(--glass-border);
cursor:pointer; color:var(--text-sub); transition:.15s;
}
.menu-tab:hover{ background:rgba(255,255,255,0.75); }
.menu-tab.active{ background:var(--accent); color:#fff; border-color:var(--accent); }
.stats-row{ display:flex; gap:14px; flex-wrap:wrap; margin-bottom:18px; }
.stat-card{
flex:1; min-width:140px; background:var(--glass-bg);
backdrop-filter: blur(20px) saturate(160%); -webkit-backdrop-filter: blur(20px) saturate(160%);
border:1px solid var(--glass-border); border-radius:var(--radius); padding:16px 18px;
box-shadow:0 8px 24px rgba(31,41,64,0.06);
}
.stat-card .num{ font-size:26px; font-weight:800; letter-spacing:-0.02em; }
.stat-card .lbl{ font-size:12px; color:var(--text-sub); margin-top:2px; }
.panel{
background:var(--glass-bg); backdrop-filter: blur(22px) saturate(170%); -webkit-backdrop-filter: blur(22px) saturate(170%);
border:1px solid var(--glass-border); border-radius:var(--radius);
box-shadow:0 10px 30px rgba(31,41,64,0.07); overflow:hidden;
}
.panel-head{
display:flex; align-items:center; justify-content:space-between;
padding:16px 20px; border-bottom:1px solid rgba(255,255,255,0.5);
}
.panel-head h2{ font-size:15px; margin:0; font-weight:700; }
.export-btn{
display:flex; align-items:center; gap:6px; padding:8px 14px; border-radius:999px; font-size:12.5px; font-weight:600;
background:rgba(76,126,243,0.12); color:var(--accent); border:none; cursor:pointer; transition:.15s;
}
.export-btn:hover{ background:rgba(76,126,243,0.2); }
/* ---------- FILTER BAR ---------- */
.filter-bar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:14px 20px; border-bottom:1px solid rgba(255,255,255,0.5); }
.filter-bar .filter-label{ font-size:11.5px; color:var(--text-sub); font-weight:700; text-transform:uppercase; letter-spacing:.05em; margin-right:2px; }
.filter-select{
padding:8px 30px 8px 14px; border-radius:999px; font-size:13px; border:1px solid var(--glass-border);
background:rgba(255,255,255,0.65); color:var(--text-main); cursor:pointer; outline:none;
appearance:none; -webkit-appearance:none;
background-image: linear-gradient(45deg, transparent 50%, var(--text-sub) 50%), linear-gradient(135deg, var(--text-sub) 50%, transparent 50%);
background-position: calc(100% - 15px) center, calc(100% - 10px) center;
background-size: 5px 5px, 5px 5px; background-repeat:no-repeat; transition:.15s;
}
.filter-select:hover{ background-color:rgba(255,255,255,0.9); }
.filter-select.active{ background-color:var(--accent-soft); border-color:var(--accent); color:var(--accent); font-weight:600; }
.filter-reset{
padding:8px 14px; border-radius:999px; font-size:12.5px; font-weight:600;
background:var(--danger-soft); color:var(--danger); border:none; cursor:pointer; display:none;
}
.filter-reset.show{ display:inline-block; }
.table-scroll{ overflow-x:auto; overflow-y:auto; max-height:480px; }
table{ width:100%; border-collapse:collapse; min-width:600px; }
table.table-compact{ width:100%; min-width:0; }
.rekap-grid{ display:grid; grid-template-columns:1fr; gap:18px; align-items:start; }
@media (min-width:960px){
.rekap-grid{ grid-template-columns:1fr 1fr; }
}
thead{ position:sticky; top:0; z-index:5; }
thead th{
text-align:left; font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--text-sub);
padding:10px 18px; white-space:nowrap; border-bottom:1px solid rgba(0,0,0,0.08);
background:rgba(255,255,255,0.85);
backdrop-filter: blur(10px) saturate(160%); -webkit-backdrop-filter: blur(10px) saturate(160%);
}
.table-compact thead th, .table-compact tbody td{ padding:10px 22px; }
.table-compact thead th:last-child, .table-compact tbody td:last-child{ text-align:right; }
.sortable-th{ cursor:pointer; user-select:none; transition:color .15s; }
.sortable-th:hover{ color:var(--accent); }
.sortable-th.active{ color:var(--accent); font-weight:700; }
.sort-icon{ opacity:0.45; font-size:10px; margin-left:2px; }
.sortable-th.active .sort-icon{ opacity:1; }
tbody td{ padding:12px 18px; font-size:13.5px; white-space:nowrap; border-bottom:1px solid rgba(0,0,0,0.04); }
.col-narrow{ width:56px !important; max-width:56px; padding-left:10px !important; padding-right:10px !important; text-align:center; }
tbody tr{ transition:background .15s; cursor:pointer; }
tbody tr:hover{ background:rgba(255,255,255,0.55); }
tbody tr:last-child td{ border-bottom:none; }
.total-row td{
font-weight:800; background:rgba(0,0,0,0.035);
border-top:2px solid rgba(0,0,0,0.1); color:var(--text-main);
}
.actions-cell{ display:flex; gap:6px; }
.icon-btn{
border:none; background:rgba(0,0,0,0.05); width:30px; height:30px; border-radius:10px;
font-size:13px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:.15s;
}
.icon-btn:hover{ background:rgba(0,0,0,0.1); }
.icon-btn.danger:hover{ background:var(--danger-soft); color:var(--danger); }
.empty-state{ padding:60px 20px; text-align:center; color:var(--text-sub); }
.fab{
position:fixed; bottom:28px; right:28px; z-index:30; width:58px; height:58px; border-radius:50%;
background:linear-gradient(135deg, var(--accent), #7AA2FF); color:#fff; font-size:26px; border:none; cursor:pointer;
box-shadow:0 10px 26px rgba(76,126,243,0.45), inset 0 1px 1px rgba(255,255,255,0.6);
display:flex; align-items:center; justify-content:center;
transition:transform .25s cubic-bezier(.34,1.56,.64,1), box-shadow .25s;
}
.fab:hover{ transform:scale(1.08) rotate(90deg); box-shadow:0 14px 32px rgba(76,126,243,0.55); }
.overlay{
position:fixed; inset:0; background:rgba(20,25,40,0.35);
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
display:none; align-items:center; justify-content:center; z-index:50; padding:20px;
}
.overlay.show{ display:flex; }
.modal{
width:100%; max-width:480px; max-height:85vh; overflow-y:auto;
background:var(--glass-bg-strong); backdrop-filter: blur(30px) saturate(180%); -webkit-backdrop-filter: blur(30px) saturate(180%);
border:1px solid var(--glass-border); border-radius:26px; box-shadow:0 20px 60px rgba(20,25,40,0.25); padding:22px;
}
.modal.wide{ max-width:620px; }
.modal h3{ margin:0 0 16px 0; font-size:17px; font-weight:700; }
.modal h4{ margin:18px 0 10px 0; font-size:13px; font-weight:700; color:var(--text-sub); text-transform:uppercase; letter-spacing:.04em; }
.field{ margin-bottom:12px; }
.field label{ font-size:11.5px; color:var(--text-sub); font-weight:600; text-transform:uppercase; letter-spacing:.04em; }
.field input, .field select{
width:100%; margin-top:5px; padding:10px 13px; border-radius:12px; border:1px solid rgba(0,0,0,0.1);
background:rgba(255,255,255,0.7); font-size:14px; outline:none;
}
.field input:focus, .field select:focus{ box-shadow:0 0 0 3px var(--accent-soft); background:#fff; }
.modal-actions{ display:flex; gap:10px; margin-top:18px; flex-wrap:wrap; }
.btn{ flex:1; padding:12px; border-radius:14px; border:none; font-size:14px; font-weight:600; cursor:pointer; transition:.15s; min-width:100px; }
.btn-primary{ background:var(--accent); color:#fff; }
.btn-primary:hover{ background:#3D6BDB; }
.btn-ghost{ background:rgba(0,0,0,0.06); color:var(--text-main); }
.btn-ghost:hover{ background:rgba(0,0,0,0.1); }
.btn-danger{ background:var(--danger); color:#fff; }
.btn-ghost.small{ flex:none; padding:6px 12px; font-size:12px; border-radius:999px; }
.btn.full{ flex:none; width:100%; }
.detail-row{ display:flex; justify-content:space-between; gap:16px; padding:9px 0; border-bottom:1px dashed rgba(0,0,0,0.08); font-size:13.5px; }
.detail-row .k{ color:var(--text-sub); }
.detail-row .v{ font-weight:600; text-align:right; }
.toast{
position:fixed; bottom:100px; right:28px; z-index:60; background:var(--text-main); color:#fff; padding:12px 18px;
border-radius:14px; font-size:13.5px; box-shadow:0 10px 24px rgba(0,0,0,0.25); opacity:0; transform:translateY(10px); transition:.25s;
}
.toast.show{ opacity:1; transform:translateY(0); }
.loader{
display:inline-block; width:16px; height:16px; border:2px solid rgba(255,255,255,0.5);
border-top-color:#fff; border-radius:50%; animation:spin .7s linear infinite;
}
@keyframes spin{ to{ transform:rotate(360deg);} }
/* ---------- SETTINGS MODAL ---------- */
.settings-section{ margin-bottom:6px; }
.col-check-list{
max-height:200px; overflow-y:auto; border:1px solid rgba(0,0,0,0.08); border-radius:14px;
padding:6px 12px; background:rgba(255,255,255,0.4); margin-bottom:8px;
}
.col-check-item{ display:flex; align-items:center; gap:9px; padding:7px 4px; font-size:13px; border-bottom:1px dashed rgba(0,0,0,0.07); }
.col-check-item:last-child{ border-bottom:none; }
.col-check-item input{ width:15px; height:15px; accent-color: var(--accent); cursor:pointer; }
.user-row{
display:flex; align-items:center; justify-content:space-between; padding:9px 4px;
border-bottom:1px dashed rgba(0,0,0,0.07); font-size:13px;
}
.user-row:last-child{ border-bottom:none; }
.user-row .uname{ font-weight:700; }
.user-row .urole{
font-size:10.5px; text-transform:uppercase; font-weight:700; color:var(--accent);
background:var(--accent-soft); padding:3px 9px; border-radius:999px; margin-left:8px;
}
.user-list{ max-height:180px; overflow-y:auto; border:1px solid rgba(0,0,0,0.08); border-radius:14px; padding:6px 12px; background:rgba(255,255,255,0.4); margin-bottom:10px; }
.del-user-btn{ border:none; background:var(--danger-soft); color:var(--danger); border-radius:8px; width:26px; height:26px; cursor:pointer; }
.add-user-form{ display:grid; grid-template-columns:1fr 1fr 100px; gap:8px; align-items:end; }
.add-user-form .field{ margin-bottom:0; }
.color-row{ display:flex; align-items:center; gap:10px; }
.color-row input[type=color]{ width:44px; height:36px; padding:2px; margin-top:5px; }
@media (max-width:600px){
.navbar{ flex-wrap:wrap; }
.stats-row{ flex-wrap:wrap; }
.add-user-form{ grid-template-columns:1fr; }
}
</style>
</head>
<body>
<!-- ==================== LOGIN SCREEN ==================== -->
<div class="initial-loader" id="initialLoader"><span class="loader" style="width:28px;height:28px;border-width:3px;border-color:rgba(76,126,243,.2);border-top-color:var(--accent);"></span></div>
<div class="login-screen" id="loginScreen" style="display:none;">
<div class="login-card">
<div class="login-logo" id="loginLogo">🎓</div>
<h2 id="loginTitle">Database Siswa</h2>
<p class="login-sub" id="loginSubtitleText">Masuk untuk melanjutkan</p>
<div class="field" style="text-align:left;">
<label>Username</label>
<input id="loginUsername" type="text" placeholder="username" autocomplete="username">
</div>
<div class="field" style="text-align:left;">
<label>Password</label>
<input id="loginPassword" type="password" placeholder="••••••••" autocomplete="current-password"
onkeydown="if(event.key==='Enter') doLogin()">
</div>
<button class="btn btn-primary full" id="loginBtn" onclick="doLogin()">Masuk</button>
<div class="login-error" id="loginError"></div>
<div class="site-footer" id="loginFooter" style="padding:16px 0 0 0;">© 2026 Database Siswa. All rights reserved.</div>
</div>
</div>
<!-- ==================== MAIN APP ==================== -->
<div id="appRoot" style="display:none;">
<div class="navbar">
<div>
<h1 id="siteTitleText"><span id="navLogo">🎓</span> Database Siswa</h1>
<div class="sub" id="taglineText">Liquid glass interface</div>
</div>
<div class="search-wrap">
<input id="searchInput" type="text" placeholder="Cari nama, kelas, atau apa saja...">
</div>
<div class="nav-right">
<span class="role-badge" id="roleBadge">-</span>
<button class="nav-icon-btn" id="settingsBtn" title="Pengaturan" onclick="openSettingsModal()" style="display:none;">⚙</button>
<button class="nav-icon-btn" title="Keluar" onclick="doLogout()">⎋</button>
</div>
</div>
<div class="container">
<div class="menu-tabs">
<button class="menu-tab active" id="tabDataBtn" onclick="switchMenu('data')">📋 Data Siswa</button>
<button class="menu-tab" id="tabRekapBtn" onclick="switchMenu('rekap')">📊 Rekap</button>
</div>
<!-- ===== MENU: DATA SISWA ===== -->
<div id="dataMenuPanel">
<div class="stats-row" id="statsRow"></div>
<div class="panel">
<div class="panel-head">
<h2>Daftar Siswa</h2>
<div style="display:flex; align-items:center; gap:10px;">
<span id="countLabel" style="font-size:12px;color:var(--text-sub);"></span>
<button class="export-btn" onclick="openExportModal()">⭳ Ekspor</button>
</div>
</div>
<div class="filter-bar" id="filterBar" style="display:none;">
<span class="filter-label">Filter:</span>
<div id="filterSelects" style="display:flex; gap:10px; flex-wrap:wrap;"></div>
<button class="filter-reset" id="resetBtn" onclick="resetFilters()">Reset filter</button>
</div>
<div class="table-scroll">
<table id="dataTable">
<thead><tr id="tableHead"></tr></thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div id="emptyState" class="empty-state" style="display:none;">Belum ada data siswa 📭</div>
</div>
</div>
<!-- ===== MENU: REKAP ===== -->
<div id="rekapMenuPanel" style="display:none;">
<div class="stats-row" id="rekapStatsRow"></div>
<div class="rekap-grid">
<div class="panel">
<div class="panel-head">
<h2>Rekap per Kelas & Rombel</h2>
<span id="rekapKRCount" style="font-size:12px;color:var(--text-sub);"></span>
</div>
<div class="filter-bar" id="rekapKRFilterBar">
<span class="filter-label">Filter:</span>
<div id="rekapKRFilterSelects" style="display:flex; gap:10px; flex-wrap:wrap;"></div>
</div>
<div class="table-scroll" style="max-height:420px;">
<table class="table-compact">
<thead><tr id="rekapKRHead"></tr></thead>
<tbody id="rekapKRBody"></tbody>
</table>
</div>
<div id="rekapKREmpty" class="empty-state" style="display:none;">Belum ada data 📭</div>
</div>
<div class="panel">
<div class="panel-head">
<h2>Rekap per Status & JK</h2>
<span id="rekapSJCount" style="font-size:12px;color:var(--text-sub);"></span>
</div>
<div class="filter-bar" id="rekapSJFilterBar">
<span class="filter-label">Filter:</span>
<div id="rekapSJFilterSelects" style="display:flex; gap:10px; flex-wrap:wrap;"></div>
</div>
<div class="table-scroll" style="max-height:420px;">
<table class="table-compact">
<thead><tr id="rekapSJHead"></tr></thead>
<tbody id="rekapSJBody"></tbody>
</table>
</div>
<div id="rekapSJEmpty" class="empty-state" style="display:none;">Belum ada data 📭</div>
</div>
</div>
</div>
</div>
<div class="site-footer" id="appFooter">© 2026 Database Siswa. All rights reserved.</div>
<button class="fab" id="fabAdd" onclick="openAddModal()">+</button>
</div>
<!-- MODAL: Add / Edit -->
<div class="overlay" id="formOverlay">
<div class="modal">
<h3 id="formTitle">Tambah Siswa</h3>
<div id="formFields"></div>
<div class="modal-actions">
<button class="btn btn-ghost" onclick="closeFormModal()">Batal</button>
<button class="btn btn-primary" id="saveBtn" onclick="saveForm()">Simpan</button>
</div>
</div>
</div>
<!-- MODAL: Detail -->
<div class="overlay" id="detailOverlay">
<div class="modal">
<h3>Detail Siswa</h3>
<div id="detailFields"></div>
<div class="modal-actions">
<button class="btn btn-ghost" onclick="closeDetailModal()">Tutup</button>
<button class="btn btn-primary" id="editFromDetailBtn" onclick="editFromDetail()">Edit</button>
</div>
</div>
</div>
<!-- MODAL: Export -->
<div class="overlay" id="exportOverlay">
<div class="modal">
<h3>Ekspor Data</h3>
<div class="export-count" style="font-size:12px;color:var(--text-sub);margin-bottom:12px;">
Mengikuti filter & pencarian yang aktif — <b id="exportCount">0</b> siswa akan diekspor
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px;">
<span class="filter-label" style="margin:0;">Pilih kolom:</span>
<div style="display:flex; gap:8px;">
<button class="btn-ghost small" onclick="toggleAllExportCols(true)">Pilih semua</button>
<button class="btn-ghost small" onclick="toggleAllExportCols(false)">Kosongkan</button>
</div>
</div>
<div class="col-check-list" id="exportColList"></div>
<div class="modal-actions">
<button class="btn btn-ghost" onclick="closeExportModal()">Batal</button>
<button class="btn btn-primary" onclick="exportData('xlsx')">📊 Excel</button>
<button class="btn btn-primary" onclick="exportData('pdf')">📄 PDF</button>
</div>
</div>
</div>
<!-- MODAL: Settings (admin only) -->
<div class="overlay" id="settingsOverlay">
<div class="modal wide">
<h3>⚙ Pengaturan</h3>
<div class="settings-section">
<h4>Tampilan</h4>
<div class="field">
<label>Judul Web (navbar)</label>
<input id="settingSiteTitle" type="text" placeholder="Database Siswa">
</div>
<div class="field">
<label>Tagline (di bawah judul navbar)</label>
<input id="settingTagline" type="text" placeholder="Liquid glass interface">
</div>
<div class="field">
<label>Logo (emoji atau URL gambar)</label>
<input id="settingLogo" type="text" placeholder="🎓 atau https://...">
</div>
<div class="field">
<label>Warna Aksen</label>
<div class="color-row">
<input id="settingAccentColor" type="color" value="#4C7EF3">
<span style="font-size:12px;color:var(--text-sub);">Warna tombol & sorotan utama</span>
</div>
</div>
</div>
<div class="settings-section">
<h4>Halaman Login</h4>
<div class="field">
<label>Judul di halaman login</label>
<input id="settingLoginHeading" type="text" placeholder="Database Siswa">
</div>
<div class="field">
<label>Subjudul di halaman login</label>
<input id="settingLoginSubtitle" type="text" placeholder="Masuk untuk melanjutkan">
</div>
</div>
<div class="settings-section">
<h4>Footer</h4>
<div class="field">
<label>Teks Copyright (nama yang muncul setelah "© {tahun}")</label>
<input id="settingCopyrightText" type="text" placeholder="Database Siswa">
<div style="font-size:11px;color:var(--text-sub);margin-top:5px;">
Contoh hasil: © 2026 [teks ini]. All rights reserved. — tahun otomatis ngikutin tahun sekarang.
</div>
</div>
</div>
<div class="settings-section">
<h4>Kolom yang boleh dilihat — Role User</h4>
<div class="col-check-list" id="colListUser"></div>
</div>
<div class="settings-section">
<h4>Kolom yang boleh dilihat — Role Tamu</h4>
<div class="col-check-list" id="colListTamu"></div>
</div>
<button class="btn btn-primary full" onclick="saveSiteSettings()">Simpan Tampilan & Kolom</button>
<div class="settings-section" style="margin-top:20px;">
<h4>Kelola Akun</h4>
<div class="user-list" id="userList"></div>
<div class="add-user-form">
<div class="field"><label>Username</label><input id="newUserUsername" type="text"></div>
<div class="field"><label>Password</label><input id="newUserPassword" type="password"></div>
<div class="field">
<label>Role</label>
<select id="newUserRole">
<option value="user">User</option>
<option value="tamu">Tamu</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<button class="btn btn-primary full" style="margin-top:8px;" onclick="addNewUser()">+ Tambah Akun</button>
</div>
<div class="modal-actions">
<button class="btn btn-ghost full" onclick="closeSettingsModal()">Tutup</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
<script>
let TOKEN = null;
let ROLE = null;
let USERNAME = null;
let ALL_HEADERS = [];
let ALL_ROWS = [];
let FILTERED = [];
let PREVIEW_COLS = [];
let CURRENT_DETAIL_ROW = null;
let EDIT_MODE = false;
let FILTER_COLS = [];
let ACTIVE_FILTERS = {};
let SEARCH_QUERY = '';
let EXPORT_SELECTED = new Set();
let CAN_ADD = false, CAN_EDIT = false, CAN_DELETE = false;
let ADMIN_SETTINGS = null; // { siteTitle, accentColor, columnVisibility, allHeaders }
let SORT_COL = null;
let SORT_DIR = 'asc';
function showToast(msg){
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(()=>t.classList.remove('show'), 2400);
}
/* ================= AUTH / SESSION ================= */
function initFromSession(){
TOKEN = sessionStorage.getItem('dbs_token');
ROLE = sessionStorage.getItem('dbs_role');
USERNAME = sessionStorage.getItem('dbs_username');
google.script.run
.withSuccessHandler(function(branding){
applyTheme(branding);
hideInitialLoader_();
if (TOKEN) showApp(); else showLogin();
})
.withFailureHandler(function(){
hideInitialLoader_();
if (TOKEN) showApp(); else showLogin();
})
.getPublicBranding();
}
function hideInitialLoader_(){
const el = document.getElementById('initialLoader');
if (el) el.style.display = 'none';
}
function showLogin(){
document.getElementById('loginScreen').style.display = 'flex';
document.getElementById('appRoot').style.display = 'none';
}
function showApp(){
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('appRoot').style.display = 'block';
initApp();
}
function doLogin(){
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
const errEl = document.getElementById('loginError');
errEl.textContent = '';
if (!username || !password){ errEl.textContent = 'Isi username dan password dulu ya'; return; }
const btn = document.getElementById('loginBtn');
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span>';
google.script.run
.withSuccessHandler(function(res){
btn.disabled = false;
btn.textContent = 'Masuk';
if (res.success){
sessionStorage.setItem('dbs_token', res.token);
sessionStorage.setItem('dbs_role', res.role);
sessionStorage.setItem('dbs_username', res.username);
TOKEN = res.token; ROLE = res.role; USERNAME = res.username;
showApp();
} else {
errEl.textContent = res.message || 'Login gagal';
}
})
.withFailureHandler(function(err){
btn.disabled = false;
btn.textContent = 'Masuk';
errEl.textContent = err.message || 'Terjadi kesalahan, coba lagi';
})
.login(username, password);
}
function resetToLoginScreen(){
TOKEN = null; ROLE = null; USERNAME = null;
ADMIN_SETTINGS = null;
const u = document.getElementById('loginUsername');
const p = document.getElementById('loginPassword');
if (u) u.value = '';
if (p) p.value = '';
const errEl = document.getElementById('loginError');
if (errEl) errEl.textContent = '';
const btn = document.getElementById('loginBtn');
if (btn){ btn.disabled = false; btn.textContent = 'Masuk'; }
showLogin();
}
function doLogout(){
if (TOKEN) google.script.run.logout(TOKEN);
sessionStorage.removeItem('dbs_token');
sessionStorage.removeItem('dbs_role');
sessionStorage.removeItem('dbs_username');
resetToLoginScreen();
}
function onSessionExpired(){
showToast('Sesi habis, silakan login ulang 🔒');
sessionStorage.clear();
setTimeout(resetToLoginScreen, 1000);
}
/* ================= INIT APP ================= */
function initApp(){
document.getElementById('roleBadge').textContent = ROLE;
document.getElementById('settingsBtn').style.display = (ROLE === 'admin') ? 'flex' : 'none';
google.script.run
.withSuccessHandler(function(settings){
applyTheme(settings);
if (ROLE === 'admin') ADMIN_SETTINGS = settings;
loadData();
})
.withFailureHandler(function(err){
if (String(err.message||'').indexOf('Sesi') !== -1){ onSessionExpired(); return; }
showToast('Gagal memuat pengaturan: ' + (err.message||err));
loadData();
})
.getSettings(TOKEN);
}
function applyTheme(settings){
if (!settings) return;
if (settings.siteTitle){
document.title = settings.siteTitle;
const navTitleHtml = '<span id="navLogo">' + renderLogo_(settings.logo) + '</span> ' + escapeHtml(settings.siteTitle);
document.getElementById('siteTitleText').innerHTML = navTitleHtml;
}
if (settings.logo){
document.getElementById('loginLogo').innerHTML = renderLogo_(settings.logo);
}
if (settings.tagline !== undefined){
document.getElementById('taglineText').textContent = settings.tagline;
}
if (settings.loginHeading){
document.getElementById('loginTitle').textContent = settings.loginHeading;
}
if (settings.loginSubtitle !== undefined){
document.getElementById('loginSubtitleText').textContent = settings.loginSubtitle;
}
if (settings.accentColor){
document.documentElement.style.setProperty('--accent', settings.accentColor);
const rgb = hexToRgb_(settings.accentColor);
if (rgb) document.documentElement.style.setProperty('--accent-soft', 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',0.15)');
}
if (settings.copyrightText !== undefined){
const year = new Date().getFullYear();
const text = '© ' + year + ' ' + settings.copyrightText + '. All rights reserved.';
const loginFooter = document.getElementById('loginFooter');
const appFooter = document.getElementById('appFooter');
if (loginFooter) loginFooter.textContent = text;
if (appFooter) appFooter.textContent = text;
}
}
// Logo bisa berupa emoji/teks pendek, atau URL gambar (kalau diawali http)
function renderLogo_(logo){
if (!logo) return '🎓';
if (/^https?:\/\//i.test(logo.trim())){
return `<img src="${logo.trim()}" alt="logo" style="height:1em;vertical-align:-0.15em;border-radius:4px;">`;
}
return escapeHtml(logo);
}
function hexToRgb_(hex){
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!m) return null;
return { r: parseInt(m[1],16), g: parseInt(m[2],16), b: parseInt(m[3],16) };
}
/* ================= DATA LOAD ================= */
function loadData(){
google.script.run
.withSuccessHandler(onDataLoaded)
.withFailureHandler(onError)
.getStudents(TOKEN);
}
function onError(err){
const msg = err && err.message ? err.message : String(err);
if (msg.indexOf('Sesi') !== -1){ onSessionExpired(); return; }
showToast('Gagal memuat data: ' + msg);
}
function onDataLoaded(res){
ALL_HEADERS = res.headers;
ALL_ROWS = res.rows;
CAN_ADD = res.canAdd;
CAN_EDIT = res.canEdit;
CAN_DELETE = res.canDelete;
document.getElementById('fabAdd').style.display = CAN_ADD ? 'flex' : 'none';
PREVIEW_COLS = pickPreviewColumns();
EXPORT_SELECTED = new Set(ALL_HEADERS);
FILTER_COLS = detectFilterColumns();
ACTIVE_FILTERS = {};
renderFilterBar();
renderStats();
applyFilters();
}
function pickPreviewColumns(){
const cols = [];
['NAMA SISWA', 'NISN'].forEach(w => {
const h = ALL_HEADERS.find(hh => hh.toUpperCase() === w);
if (h) cols.push(h);
});
// NIK siswa = kemunculan pertama header "NIK" (bukan NIK ayah/ibu/wali yang di-dedupe jadi "NIK (2)" dst)
const nik = ALL_HEADERS.find(hh => hh.toUpperCase() === 'NIK');
if (nik) cols.push(nik);
// TTL (tempat lahir) & TTL(dd/mm/yyyy) (tanggal lahir) — plain "TTL" duluan baru versi tanggalnya
const ttlCols = ALL_HEADERS.filter(hh => hh.toUpperCase().startsWith('TTL')).sort((a,b)=>a.length-b.length);
ttlCols.forEach(h => cols.push(h));
// Kelas & Rombel (kemunculan pertama, bukan riwayat kelas 7/8/9 yang ke-dedupe)
['KELAS', 'ROM'].forEach(w => {
const h = ALL_HEADERS.find(hh => hh.toUpperCase() === w);
if (h) cols.push(h);
});
return cols.length ? cols : ALL_HEADERS.slice(0, 2);
}
function detectFilterColumns(){
const wanted = ['TP MASUK', 'STATUS SISWA', 'KELAS', 'ROM', 'JK'];
return wanted.map(w => ALL_HEADERS.find(h => h.toUpperCase() === w)).filter(Boolean);
}
function renderStats(){
const statsRow = document.getElementById('statsRow');
const statusHeader = ALL_HEADERS.find(h => h.toUpperCase() === 'STATUS SISWA');
let cards = `<div class="stat-card"><div class="num">${ALL_ROWS.length}</div><div class="lbl">Total Siswa</div></div>`;
if (statusHeader){
const aktif = ALL_ROWS.filter(r => String(r[statusHeader] ?? '').trim().toUpperCase() === 'AKTIF').length;
cards += `<div class="stat-card"><div class="num">${aktif}</div><div class="lbl">Siswa Aktif</div></div>`;
}
cards += `<div class="stat-card"><div class="num">${ALL_HEADERS.length}</div><div class="lbl">Kolom Terlihat</div></div>`;
statsRow.innerHTML = cards;
}
function sortBy(header){
if (SORT_COL === header){
SORT_DIR = SORT_DIR === 'asc' ? 'desc' : 'asc';
} else {
SORT_COL = header;
SORT_DIR = 'asc';
}
renderTable();
}
function getSortedRows_(){
if (!SORT_COL) return FILTERED;
const sorted = [...FILTERED].sort((a, b) => {
const va = a[SORT_COL] ?? '';
const vb = b[SORT_COL] ?? '';
const na = parseFloat(va), nb = parseFloat(vb);
const bothNumeric = !isNaN(na) && !isNaN(nb) && String(va).trim() !== '' && String(vb).trim() !== '';
const cmp = bothNumeric ? (na - nb) : String(va).localeCompare(String(vb), 'id');
return SORT_DIR === 'asc' ? cmp : -cmp;
});
return sorted;
}
function renderTable(){
const head = document.getElementById('tableHead');
const body = document.getElementById('tableBody');
const empty = document.getElementById('emptyState');
const countLabel = document.getElementById('countLabel');
const showActions = CAN_EDIT || CAN_DELETE;
const narrowCols = ['KELAS', 'ROM'];
head.innerHTML = PREVIEW_COLS.map(h => {
const icon = (SORT_COL === h) ? (SORT_DIR === 'asc' ? '↑' : '↓') : '↕';
const activeClass = (SORT_COL === h) ? ' active' : '';
const narrowClass = narrowCols.includes(h.toUpperCase()) ? ' col-narrow' : '';
return `<th class="sortable-th${activeClass}${narrowClass}" onclick="sortBy('${h.replace(/'/g,"\\'")}')">${escapeHtml(h)} <span class="sort-icon">${icon}</span></th>`;
}).join('') + (showActions ? '<th>Aksi</th>' : '');
countLabel.textContent = FILTERED.length + ' data';
if (FILTERED.length === 0){ body.innerHTML=''; empty.style.display='block'; return; }
empty.style.display = 'none';
const rows = getSortedRows_();
body.innerHTML = rows.map(row => {
const cells = PREVIEW_COLS.map(h => {
const narrowClass = narrowCols.includes(h.toUpperCase()) ? ' class="col-narrow"' : '';
return `<td${narrowClass}>${escapeHtml(row[h])}</td>`;
}).join('');
let actions = '';
if (showActions){
actions = '<td class="actions-cell" onclick="event.stopPropagation()">';
if (CAN_EDIT) actions += `<button class="icon-btn" title="Edit" onclick="openEditModal(${row.__rowIndex})">✎</button>`;
if (CAN_DELETE) actions += `<button class="icon-btn danger" title="Hapus" onclick="confirmDelete(${row.__rowIndex})">🗑</button>`;
actions += '</td>';
}
return `<tr onclick="openDetail(${row.__rowIndex})">${cells}${actions}</tr>`;
}).join('');
}
function escapeHtml(v){
if (v === undefined || v === null) return '';
return String(v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
document.getElementById('searchInput').addEventListener('input', function(e){
SEARCH_QUERY = e.target.value.toLowerCase().trim();
applyFilters();
});
function findRow(rowIndex){ return ALL_ROWS.find(r => r.__rowIndex === rowIndex); }
/* ================= FILTER BAR ================= */
function renderFilterBar(){
const bar = document.getElementById('filterBar');
const wrap = document.getElementById('filterSelects');
if (FILTER_COLS.length === 0){ bar.style.display = 'none'; return; }
bar.style.display = 'flex';
wrap.innerHTML = FILTER_COLS.map(h => {
const vals = [...new Set(ALL_ROWS.map(r => String(r[h] ?? '').trim()).filter(v => v !== ''))].sort();
const safeId = 'filter_' + h.replace(/[^a-zA-Z0-9]/g, '_');
const options = vals.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('');
return `<select class="filter-select" id="${safeId}" onchange="onFilterChange('${h.replace(/'/g,"\\'")}', this)">
<option value="">Semua ${escapeHtml(h)}</option>${options}
</select>`;
}).join('');
}
function onFilterChange(header, selectEl){
const val = selectEl.value;
if (val){ ACTIVE_FILTERS[header] = val; selectEl.classList.add('active'); }
else { delete ACTIVE_FILTERS[header]; selectEl.classList.remove('active'); }
document.getElementById('resetBtn').classList.toggle('show', Object.keys(ACTIVE_FILTERS).length > 0);
applyFilters();
}
function resetFilters(){
ACTIVE_FILTERS = {};
document.querySelectorAll('.filter-select').forEach(sel => { sel.value=''; sel.classList.remove('active'); });
document.getElementById('resetBtn').classList.remove('show');
applyFilters();
}
function applyFilters(){
let result = ALL_ROWS;
if (SEARCH_QUERY){
result = result.filter(row => ALL_HEADERS.some(h => String(row[h] ?? '').toLowerCase().includes(SEARCH_QUERY)));
}
Object.entries(ACTIVE_FILTERS).forEach(([h, v]) => {
result = result.filter(row => String(row[h] ?? '').trim() === v);
});
FILTERED = result;
renderTable();
}
/* ================= DETAIL MODAL ================= */
function openDetail(rowIndex){
const row = findRow(rowIndex);
if (!row) return;
CURRENT_DETAIL_ROW = row;
document.getElementById('detailFields').innerHTML = ALL_HEADERS.map(h =>
`<div class="detail-row"><span class="k">${h}</span><span class="v">${escapeHtml(row[h]) || '—'}</span></div>`
).join('');
document.getElementById('editFromDetailBtn').style.display = CAN_EDIT ? 'block' : 'none';
document.getElementById('detailOverlay').classList.add('show');
}
function closeDetailModal(){ document.getElementById('detailOverlay').classList.remove('show'); }
function editFromDetail(){ closeDetailModal(); openEditModal(CURRENT_DETAIL_ROW.__rowIndex); }
/* ================= FORM MODAL (Add/Edit) ================= */
function buildFormFields(existingRow){
document.getElementById('formFields').innerHTML = ALL_HEADERS.map(h => {
const val = existingRow ? escapeHtml(existingRow[h]) : '';
const safeId = 'f_' + h.replace(/[^a-zA-Z0-9]/g,'_');
return `<div class="field"><label>${h}</label><input id="${safeId}" data-header="${h}" value="${val}"></div>`;
}).join('');
}
function openAddModal(){
if (!CAN_ADD){ showToast('Akun kamu ga punya izin nambah data'); return; }
EDIT_MODE = false; CURRENT_DETAIL_ROW = null;
document.getElementById('formTitle').textContent = 'Tambah Siswa';
buildFormFields(null);
document.getElementById('formOverlay').classList.add('show');
}
function openEditModal(rowIndex){
if (!CAN_EDIT){ showToast('Akun kamu ga punya izin edit data'); return; }
const row = findRow(rowIndex);
if (!row) return;
EDIT_MODE = true; CURRENT_DETAIL_ROW = row;
document.getElementById('formTitle').textContent = 'Edit Siswa';
buildFormFields(row);
document.getElementById('formOverlay').classList.add('show');
}
function closeFormModal(){ document.getElementById('formOverlay').classList.remove('show'); }
function saveForm(){
const inputs = document.querySelectorAll('#formFields input');
const data = {};
inputs.forEach(inp => { data[inp.dataset.header] = inp.value; });
const btn = document.getElementById('saveBtn');
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span>';
if (EDIT_MODE){
google.script.run.withSuccessHandler(()=>afterSave('Data berhasil diupdate ✅')).withFailureHandler(onSaveError)
.updateStudent(TOKEN, CURRENT_DETAIL_ROW.__rowIndex, data);
} else {
google.script.run.withSuccessHandler(()=>afterSave('Siswa baru berhasil ditambahkan ✅')).withFailureHandler(onSaveError)
.addStudent(TOKEN, data);
}
}
function onSaveError(err){
const btn = document.getElementById('saveBtn');
btn.disabled = false; btn.textContent = 'Simpan';
const msg = err && err.message ? err.message : String(err);
if (msg.indexOf('Sesi') !== -1){ onSessionExpired(); return; }
showToast('Gagal menyimpan: ' + msg);
}
function afterSave(msg){
const btn = document.getElementById('saveBtn');
btn.disabled = false; btn.textContent = 'Simpan';
closeFormModal();
showToast(msg);
loadData();
}
function confirmDelete(rowIndex){
if (!CAN_DELETE){ showToast('Cuma admin yang bisa hapus data'); return; }
const row = findRow(rowIndex);
const label = row ? (row[ALL_HEADERS[0]] || 'data ini') : 'data ini';
if (!confirm('Hapus data "' + label + '"? Tindakan ini tidak bisa dibatalkan.')) return;
google.script.run.withSuccessHandler(()=>{ showToast('Data dihapus 🗑'); loadData(); }).withFailureHandler(onError)
.deleteStudent(TOKEN, rowIndex);
}
/* ================= EXPORT ================= */
function openExportModal(){
document.getElementById('exportCount').textContent = FILTERED.length;
renderExportColList();
document.getElementById('exportOverlay').classList.add('show');
}
function closeExportModal(){ document.getElementById('exportOverlay').classList.remove('show'); }
function renderExportColList(){
document.getElementById('exportColList').innerHTML = ALL_HEADERS.map(h => {
const checked = EXPORT_SELECTED.has(h) ? 'checked' : '';
const safeId = 'exp_' + h.replace(/[^a-zA-Z0-9]/g, '_');
return `<label class="col-check-item" for="${safeId}">
<input type="checkbox" id="${safeId}" ${checked} onchange="onExportColToggle('${h.replace(/'/g,"\\'")}', this)">
${escapeHtml(h)}</label>`;
}).join('');
}
function onExportColToggle(header, el){
if (el.checked) EXPORT_SELECTED.add(header); else EXPORT_SELECTED.delete(header);
}
function toggleAllExportCols(state){
if (state) ALL_HEADERS.forEach(h => EXPORT_SELECTED.add(h)); else EXPORT_SELECTED.clear();
renderExportColList();
}
function exportData(type){
const cols = ALL_HEADERS.filter(h => EXPORT_SELECTED.has(h));
if (cols.length === 0){ showToast('Pilih minimal 1 kolom dulu ya'); return; }
if (FILTERED.length === 0){ showToast('Ga ada data buat diekspor (cek filter kamu)'); return; }
const bodyRows = FILTERED.map(row => cols.map(c => (row[c] ?? '')));
if (type === 'xlsx'){
const aoa = [cols, ...bodyRows];
const ws = XLSX.utils.aoa_to_sheet(aoa);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'Siswa');
XLSX.writeFile(wb, 'data-siswa.xlsx');
showToast('Excel berhasil diunduh 📊');
} else {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' });
doc.setFontSize(12);
doc.text('Database Siswa', 30, 24);
doc.autoTable({
head: [cols], body: bodyRows, startY: 34,
styles: { fontSize: 7, cellPadding: 3, overflow: 'linebreak' },
headStyles: { fillColor: [76, 126, 243], textColor: 255 },
margin: { top: 34, left: 20, right: 20 }
});
doc.save('data-siswa.pdf');
showToast('PDF berhasil diunduh 📄');
}
closeExportModal();
}
/* ================= MENU SWITCH & REKAP ================= */
let CURRENT_MENU = 'data';
let REKAP_LOADED = false;
let REKAP_KR_ALL = [], REKAP_KR_FILTERED = [], REKAP_KR_FILTERS = {};
let REKAP_KR_SORT_COL = null, REKAP_KR_SORT_DIR = 'asc';
let REKAP_SJ_ALL = [], REKAP_SJ_FILTERED = [], REKAP_SJ_FILTERS = {};
let REKAP_SJ_SORT_COL = null, REKAP_SJ_SORT_DIR = 'asc';
function switchMenu(menu){
CURRENT_MENU = menu;
document.getElementById('tabDataBtn').classList.toggle('active', menu === 'data');
document.getElementById('tabRekapBtn').classList.toggle('active', menu === 'rekap');
document.getElementById('dataMenuPanel').style.display = menu === 'data' ? 'block' : 'none';
document.getElementById('rekapMenuPanel').style.display = menu === 'rekap' ? 'block' : 'none';
document.getElementById('fabAdd').style.display = (menu === 'data' && CAN_ADD) ? 'flex' : 'none';
if (menu === 'rekap' && !REKAP_LOADED) loadRekap();
}
function loadRekap(){
google.script.run.withSuccessHandler(onRekapLoaded).withFailureHandler(onError).getRekap(TOKEN);
}
function onRekapLoaded(res){
REKAP_LOADED = true;
REKAP_KR_ALL = res.perKelasRombel;
REKAP_SJ_ALL = res.perStatusJk;
renderRekapStatCards(res.totalPerStatus);
renderRekapFilters();
applyRekapKRFilters();
applyRekapSJFilters();
}
function renderRekapStatCards(totals){
const wrap = document.getElementById('rekapStatsRow');
const keys = Object.keys(totals);
if (keys.length === 0){ wrap.innerHTML = ''; return; }
wrap.innerHTML = keys.map(status =>
`<div class="stat-card"><div class="num">${totals[status]}</div><div class="lbl">${escapeHtml(status)}</div></div>`
).join('');
}
function renderRekapFilters(){
const kelasVals = [...new Set(REKAP_KR_ALL.map(r => r.kelas).filter(v => v !== ''))].sort();
const statusValsKR = [...new Set(REKAP_KR_ALL.map(r => r.status).filter(v => v !== ''))].sort();
document.getElementById('rekapKRFilterSelects').innerHTML = `
<select class="filter-select" onchange="onRekapFilterChange('kr','kelas',this)">
<option value="">Semua Kelas</option>
${kelasVals.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('')}
</select>
<select class="filter-select" onchange="onRekapFilterChange('kr','status',this)">
<option value="">Semua Status</option>
${statusValsKR.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('')}
</select>
`;
const statusValsSJ = [...new Set(REKAP_SJ_ALL.map(r => r.status).filter(v => v !== ''))].sort();
const kelasValsSJ = [...new Set(REKAP_SJ_ALL.map(r => r.kelas).filter(v => v !== ''))].sort();
const rombelValsSJ = [...new Set(REKAP_SJ_ALL.map(r => r.rombel).filter(v => v !== ''))].sort();
document.getElementById('rekapSJFilterSelects').innerHTML = `
<select class="filter-select" onchange="onRekapFilterChange('sj','kelas',this)">
<option value="">Semua Kelas</option>
${kelasValsSJ.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('')}
</select>
<select class="filter-select" onchange="onRekapFilterChange('sj','rombel',this)">
<option value="">Semua Rombel</option>
${rombelValsSJ.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('')}
</select>
<select class="filter-select" onchange="onRekapFilterChange('sj','status',this)">
<option value="">Semua Status</option>
${statusValsSJ.map(v => `<option value="${escapeHtml(v)}">${escapeHtml(v)}</option>`).join('')}
</select>
`;
}
function onRekapFilterChange(table, field, el){
const val = el.value;
const filters = table === 'kr' ? REKAP_KR_FILTERS : REKAP_SJ_FILTERS;
if (val) filters[field] = val; else delete filters[field];
el.classList.toggle('active', !!val);
if (table === 'kr') applyRekapKRFilters(); else applyRekapSJFilters();
}
function applyRekapKRFilters(){
let result = REKAP_KR_ALL;
Object.entries(REKAP_KR_FILTERS).forEach(([k, v]) => { result = result.filter(r => String(r[k]) === v); });
REKAP_KR_FILTERED = result;
renderRekapKRTable();
}
function applyRekapSJFilters(){
let result = REKAP_SJ_ALL;
Object.entries(REKAP_SJ_FILTERS).forEach(([k, v]) => { result = result.filter(r => String(r[k]) === v); });
// group ulang jadi per status+jk (biar total tetap benar walau difilter kelas/rombel)
const grouped = {};
result.forEach(r => {
const key = r.status + '||' + r.jk;
grouped[key] = (grouped[key] || 0) + r.jumlah;
});
REKAP_SJ_FILTERED = Object.keys(grouped).map(key => {
const parts = key.split('||');
return { status: parts[0], jk: parts[1], jumlah: grouped[key] };
});
renderRekapSJTable();
}
function sortRekap_(rows, col, dir){
if (!col) return rows;
return [...rows].sort((a, b) => {
const va = a[col] ?? '', vb = b[col] ?? '';
const na = parseFloat(va), nb = parseFloat(vb);
const bothNumeric = !isNaN(na) && !isNaN(nb) && String(va).trim() !== '' && String(vb).trim() !== '';
const cmp = bothNumeric ? (na - nb) : String(va).localeCompare(String(vb), 'id');
return dir === 'asc' ? cmp : -cmp;
});
}
function sortRekapKR(col){
if (REKAP_KR_SORT_COL === col) REKAP_KR_SORT_DIR = REKAP_KR_SORT_DIR === 'asc' ? 'desc' : 'asc';
else { REKAP_KR_SORT_COL = col; REKAP_KR_SORT_DIR = 'asc'; }
renderRekapKRTable();
}
function sortRekapSJ(col){
if (REKAP_SJ_SORT_COL === col) REKAP_SJ_SORT_DIR = REKAP_SJ_SORT_DIR === 'asc' ? 'desc' : 'asc';
else { REKAP_SJ_SORT_COL = col; REKAP_SJ_SORT_DIR = 'asc'; }
renderRekapSJTable();
}
function renderRekapKRTable(){
const cols = [
{ key: 'kelas', label: 'Kelas' },
{ key: 'rombel', label: 'Rombel' },
{ key: 'status', label: 'Status' },
{ key: 'jumlah', label: 'Jumlah' }
];
const head = document.getElementById('rekapKRHead');
head.innerHTML = cols.map(c => {
const icon = REKAP_KR_SORT_COL === c.key ? (REKAP_KR_SORT_DIR === 'asc' ? '↑' : '↓') : '↕';
const activeClass = REKAP_KR_SORT_COL === c.key ? ' active' : '';
return `<th class="sortable-th${activeClass}" onclick="sortRekapKR('${c.key}')">${c.label} <span class="sort-icon">${icon}</span></th>`;
}).join('');
const rows = sortRekap_(REKAP_KR_FILTERED, REKAP_KR_SORT_COL, REKAP_KR_SORT_DIR);
const totalJumlah = rows.reduce((sum, r) => sum + (Number(r.jumlah) || 0), 0);
document.getElementById('rekapKRCount').textContent = rows.length + ' baris • total ' + totalJumlah + ' siswa';
const body = document.getElementById('rekapKRBody');
const empty = document.getElementById('rekapKREmpty');
if (rows.length === 0){ body.innerHTML = ''; empty.style.display = 'block'; return; }
empty.style.display = 'none';
body.innerHTML = rows.map(r =>
`<tr><td>${escapeHtml(r.kelas)}</td><td>${escapeHtml(r.rombel)}</td><td>${escapeHtml(r.status)}</td><td>${r.jumlah}</td></tr>`
).join('') + `<tr class="total-row"><td colspan="3">TOTAL</td><td>${totalJumlah}</td></tr>`;
}
function renderRekapSJTable(){
const cols = [
{ key: 'status', label: 'Status' },
{ key: 'jk', label: 'JK' },
{ key: 'jumlah', label: 'Jumlah' }
];
const head = document.getElementById('rekapSJHead');
head.innerHTML = cols.map(c => {
const icon = REKAP_SJ_SORT_COL === c.key ? (REKAP_SJ_SORT_DIR === 'asc' ? '↑' : '↓') : '↕';
const activeClass = REKAP_SJ_SORT_COL === c.key ? ' active' : '';
return `<th class="sortable-th${activeClass}" onclick="sortRekapSJ('${c.key}')">${c.label} <span class="sort-icon">${icon}</span></th>`;
}).join('');
const rows = sortRekap_(REKAP_SJ_FILTERED, REKAP_SJ_SORT_COL, REKAP_SJ_SORT_DIR);
const totalJumlah = rows.reduce((sum, r) => sum + (Number(r.jumlah) || 0), 0);
document.getElementById('rekapSJCount').textContent = rows.length + ' baris • total ' + totalJumlah + ' siswa';
const body = document.getElementById('rekapSJBody');
const empty = document.getElementById('rekapSJEmpty');
if (rows.length === 0){ body.innerHTML = ''; empty.style.display = 'block'; return; }
empty.style.display = 'none';
body.innerHTML = rows.map(r =>
`<tr><td>${escapeHtml(r.status)}</td><td>${escapeHtml(r.jk)}</td><td>${r.jumlah}</td></tr>`
).join('') + `<tr class="total-row"><td colspan="2">TOTAL</td><td>${totalJumlah}</td></tr>`;
}
/* ================= SETTINGS (admin) ================= */
function openSettingsModal(){
if (ROLE !== 'admin'){ showToast('Cuma admin yang bisa buka Settings'); return; }
if (!ADMIN_SETTINGS){ showToast('Pengaturan belum siap, coba lagi sebentar'); return; }
document.getElementById('settingSiteTitle').value = ADMIN_SETTINGS.siteTitle || '';
document.getElementById('settingTagline').value = ADMIN_SETTINGS.tagline || '';
document.getElementById('settingLogo').value = ADMIN_SETTINGS.logo || '';
document.getElementById('settingAccentColor').value = ADMIN_SETTINGS.accentColor || '#4C7EF3';
document.getElementById('settingLoginHeading').value = ADMIN_SETTINGS.loginHeading || '';
document.getElementById('settingLoginSubtitle').value = ADMIN_SETTINGS.loginSubtitle || '';
document.getElementById('settingCopyrightText').value = ADMIN_SETTINGS.copyrightText || '';
const headers = ADMIN_SETTINGS.allHeaders || ALL_HEADERS;
renderColCheckList('colListUser', headers, ADMIN_SETTINGS.columnVisibility && ADMIN_SETTINGS.columnVisibility.user);
renderColCheckList('colListTamu', headers, ADMIN_SETTINGS.columnVisibility && ADMIN_SETTINGS.columnVisibility.tamu);
loadUserList();
document.getElementById('settingsOverlay').classList.add('show');
}
function closeSettingsModal(){ document.getElementById('settingsOverlay').classList.remove('show'); }
function renderColCheckList(containerId, headers, allowedList){
const allowAll = !allowedList || allowedList.length === 0;
document.getElementById(containerId).innerHTML = headers.map(h => {
const checked = (allowAll || allowedList.indexOf(h) !== -1) ? 'checked' : '';
const safeId = containerId + '_' + h.replace(/[^a-zA-Z0-9]/g, '_');
return `<label class="col-check-item" for="${safeId}">
<input type="checkbox" id="${safeId}" data-col-header="${h.replace(/"/g,'"')}" ${checked}>
${escapeHtml(h)}</label>`;
}).join('');
}
function readColCheckList(containerId, allHeaders){
const boxes = document.querySelectorAll('#' + containerId + ' input[type=checkbox]');
const selected = [];
boxes.forEach(b => { if (b.checked) selected.push(b.dataset.colHeader); });
return (selected.length === allHeaders.length) ? null : selected;
}
function saveSiteSettings(){
const headers = (ADMIN_SETTINGS && ADMIN_SETTINGS.allHeaders) || ALL_HEADERS;
const newSettings = {
siteTitle: document.getElementById('settingSiteTitle').value.trim() || 'Database Siswa',
tagline: document.getElementById('settingTagline').value.trim(),
logo: document.getElementById('settingLogo').value.trim() || '🎓',
accentColor: document.getElementById('settingAccentColor').value,
loginHeading: document.getElementById('settingLoginHeading').value.trim() || 'Database Siswa',
loginSubtitle: document.getElementById('settingLoginSubtitle').value.trim(),
copyrightText: document.getElementById('settingCopyrightText').value.trim() || 'Database Siswa',
columnVisibility: {
user: readColCheckList('colListUser', headers),
tamu: readColCheckList('colListTamu', headers)
}
};
google.script.run
.withSuccessHandler(()=>{
showToast('Pengaturan disimpan ✅');
ADMIN_SETTINGS = Object.assign(ADMIN_SETTINGS || {}, newSettings);
applyTheme(newSettings);
closeSettingsModal();
loadData();
})
.withFailureHandler(onError)
.saveSettings(TOKEN, newSettings);
}
function loadUserList(){
google.script.run
.withSuccessHandler(function(users){
document.getElementById('userList').innerHTML = users.map(u => `
<div class="user-row">
<span><span class="uname">${escapeHtml(u.username)}</span><span class="urole">${escapeHtml(u.role)}</span></span>
${u.username === USERNAME ? '' : `<button class="del-user-btn" onclick="deleteUserAccount('${u.username.replace(/'/g,"\\'")}')">✕</button>`}
</div>
`).join('');
})
.withFailureHandler(onError)
.listUsers(TOKEN);
}
function addNewUser(){
const username = document.getElementById('newUserUsername').value.trim();
const password = document.getElementById('newUserPassword').value;
const role = document.getElementById('newUserRole').value;
if (!username || !password){ showToast('Isi username & password akun baru dulu'); return; }
google.script.run
.withSuccessHandler(()=>{
showToast('Akun ' + username + ' berhasil dibuat ✅');
document.getElementById('newUserUsername').value = '';
document.getElementById('newUserPassword').value = '';
loadUserList();
})
.withFailureHandler(onError)
.addUser(TOKEN, username, password, role);
}
function deleteUserAccount(username){
if (!confirm('Hapus akun "' + username + '"?')) return;
google.script.run
.withSuccessHandler(()=>{ showToast('Akun dihapus'); loadUserList(); })
.withFailureHandler(onError)
.deleteUser(TOKEN, username);
}
/* ================= START ================= */
initFromSession();
</script>
</body>
</html>