Katalog Acak

Daftar Arsip

ARSIP AKTIF
NO. 6803698112251769635

Database Siswa (Index)

 <!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 &amp; 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 &amp; 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

  }


  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,'&quot;')}" ${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>


← Kembali ke Daftar Arsip