Jumat, 06 Februari 2026

Absen - Admin Level UP

 



Panduan

PANDUAN INSTALASI APLIKASI ABSENSI FACE API (VERSI 2)
=====================================================

Ikuti langkah-langkah berikut untuk menginstall aplikasi ini dari awal di Google Spreadsheet Anda.

LANGKAH 1: PERSIAPAN SPREADSHEET
1. Buka Google Drive (drive.google.com).
2. Buat Spreadsheet Baru (Klik + Baru > Google Sheets).
3. Beri nama spreadsheet, misal "Database Absensi Sekolah".

LANGKAH 2: BUKA APPS SCRIPT
1. Di menu Spreadsheet, klik "Ekstensi" (Extensions) > "Apps Script".
2. Editor kode akan terbuka di tab baru.
3. Beri nama proyek, misal "App Absensi V2".

LANGKAH 3: COPY KODE PROGRAM
1. Hapus semua kode yang ada di file "Kode.gs" (atau "Code.gs").
2. Copy seluruh kode dari file `Code.gs` yang sudah dibuat (pastikan semua fungsi termuat).
   (Catatan: Aplikasi ini menggunakan Single File Structure, artinya HTML & CSS sudah ada di dalam variabel string di Code.gs, jadi tidak perlu buat file .html terpisah).
3. Klik Icon Save (Disket) atau tekan Ctrl+S.

LANGKAH 4: SETUP DATABASE OTOMATIS
1. Di toolbar atas editor Apps Script, cari menu dropdown nama fungsi (biasanya tertulis "myFunction").
2. Ubah/Pilih fungsi bernama `setupSpreadsheet`.
3. Klik tombol "Jalankan" (Run).
4. **PERIZINAN (PENTING)**:
   - Akan muncul popup "Authorization Required".
   - Klik "Review Permissions".
   - Pilih akun Google Anda.
   - Jika muncul peringatan "Google hasn’t verified this app", klik "Advanced" (Lanjutan) lalu klik "Go to ... (unsafe)".
   - Klik "Allow" (Izinkan).
5. Tunggu proses selesai. Cek Spreadsheet Anda, seharusnya sekarang sudah muncul sheet:
   - Data Siswa
   - Data Guru
   - Data Absensi
   - Data Kelas
   - Settings

LANGKAH 5: SETUP PERMISSION EMAIL (Khusus Fitur Laporan Wali Kelas)
1. Kembali ke editor Apps Script.
2. Pilih fungsi `testEmailPermissions` di dropdown.
3. Klik "Jalankan" (Run).
4. Jika diminta izin akses Email, klik Allow.

LANGKAH 6: DEPLOY APLIKASI (PUBLIKASI)
1. Klik tombol biru "Terapkan" (Deploy) di pojok kanan atas > "Deployment Baru" (New deployment).
2. Di sebelah kiri, klik icon Roda Gigi (Settings) > pilih "Aplikasi Web" (Web app).
3. Isi konfigurasi berikut:
   - **Description**: Versi 1 (bebas)
   - **Execute as**: "Me" (Saya / Akun Anda) -> *Penting agar akses database pakai akun admin*.
   - **Who has access**: "Anyone" (Siapa saja) -> *Penting agar siswa bisa akses tanpa login Google*.
4. Klik "Terapkan" (Deploy).
5. Salin **Web App URL** yang muncul (akhiran `/exec`).

LANGKAH 7: PENGGUNAAN
1. Buka URL Web App tersebut di browser (Chrome/Safari).
2. Tampilan Default adalah halaman Absensi Siswa.
3. Untuk masuk ke halaman Admin:
   - Tambahkan parameter `?page=admin` di belakang URL.
   - Contoh: `https://script.google.com/macros/s/.../exec?page=admin`
   - (Saat ini belum ada proteksi password login, jadi URL admin harap dijaga).

LANGKAH 8: PENGISIAN DATA AWAL
1. Buka halaman Admin.
2. Masuk ke menu "Populasi Sekolah".
3. Tambahkan Data Kelas terlebih dahulu (Klik Tab "Data Kelas" > Tambah).
4. Tambahkan Data Siswa/Guru.
5. Setup selesai! Aplikasi siap digunakan.

CATATAN TAMBAHAN:
- Jika ada update kode, ulangi Langkah 6 (Deploy) -> Pilih "Manage deployments" -> Klik icon pensil -> Pada "Version" pilih "New version" -> Klik Deploy. Jika tidak, perubahan kode tidak akan tampil di user.

Code.gs

var SPREADSHEET_ID = "";
var SHEET_SISWA = "Siswa";
var SHEET_GURU = "Guru";
var SHEET_ABSENSI = "Absensi"; 
var SHEET_SETTINGS = "Settings";
var SHEET_KELAS = "Data Kelas";

function setupSpreadsheet(){
  var ss = getSpreadsheet();
  var sheets = [
    {name: SHEET_SISWA, headers: ["ID","Barcode","Nama","Kelas","JK","Foto","Status","FaceDescriptor","Email"]},
    {name: SHEET_GURU, headers: ["ID","NIP","Nama","Jabatan","JK","Foto","Status","FaceDescriptor","Email"]},
    {name: SHEET_ABSENSI, headers: ["ID","PersonID","Nama","Kelas","Tanggal","Waktu","Status","FotoAbsen","FaceMatch","Lat","Lng","Alamat","Device","Type"]},
    {name: SHEET_KELAS, headers: ["Nama Kelas","Nama Wali Kelas","Email"]},
    {name: SHEET_SETTINGS, headers: ["Key","Value"]}
  ];

  var msg = "Setup Process:\n";
  sheets.forEach(function(d){
    var s = ss.getSheetByName(d.name);
    if(!s) {
      s = ss.insertSheet(d.name);
      msg += "- Created sheet: " + d.name + "\n";
    }
    if(s.getLastRow() === 0) {
      s.appendRow(d.headers);
      msg += "- Added headers to: " + d.name + "\n";
    } else {
        if(d.name === SHEET_SISWA || d.name === SHEET_GURU){
            var h = s.getRange(1,1,1,9).getValues()[0];
            if(h.length<9 || h[8]!=="Email"){
                s.getRange(1,9).setValue("Email");
                msg += "- Added Email column to " + d.name + "\n";
            }
        }
    }
  });

  // Default Settings
  var sSet = ss.getSheetByName(SHEET_SETTINGS);
  var data = sSet.getDataRange().getValues();
  if(data.length <= 1) {
     sSet.appendRow(["jamTerlambat","07:15"]);
     sSet.appendRow(["faceMatchThreshold","0.5"]);
     msg += "- Added default settings.\n";
  }
  
  return msg + "Done.";
}

function forceSetup(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var name = "Data Kelas";
  var s = ss.getSheetByName(name);
  if(!s){
    ss.insertSheet(name).appendRow(["Nama Kelas","Nama Wali Kelas","Email"]);
    Logger.log("Created sheet: " + name);
  } else {
    Logger.log("Sheet already exists: " + name);
  }
}

function testEmailPermissions(){
  MailApp.getRemainingDailyQuota();
  Logger.log("Email permission granted. Quota remaining: " + MailApp.getRemainingDailyQuota());
}


function doGet(e) {
  var page = e.parameter.page || "student";
  var html = buildHtml(page);
  return HtmlService.createHtmlOutput(html).setTitle("Admin LevelUp").setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function buildHtml(page) {
  var css = getCSS();
  var body = page === "admin" ? getAdminHTML() : getStudentHTML();
  var js = page === "admin" ? getAdminJS() + getAdminJS2() + getAdminJS3() + getAdminJS4() : getStudentJS();
  return "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><link href='https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap' rel='stylesheet'><script src='https://unpkg.com/lucide@latest'></script><script src='https://unpkg.com/html5-qrcode@2.3.4/html5-qrcode.min.js'></script><script src='https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.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/xlsx/0.18.5/xlsx.full.min.js'></script><style>" + css + "</style></head><body>" + body + "<script>" + js + "</script></body></html>";
}

function getCSS() {
  return "*{margin:0;padding:0;box-sizing:border-box}:root{--primary:#3b82f6;--success:#22c55e;--warning:#f59e0b;--danger:#ef4444;--info:#06b6d4;--purple:#8b5cf6;--bg:#f8fafc;--white:#fff;--border:#e5e7eb;--text:#1f2937;--gray:#6b7280}body{font-family:Poppins,sans-serif;background:var(--bg);color:var(--text)}.app{display:flex;min-height:100vh}.sidebar{width:240px;background:var(--white);border-right:1px solid var(--border);position:fixed;top:0;left:0;bottom:0;display:flex;flex-direction:column}.sidebar-header{padding:20px;display:flex;align-items:center;gap:12px}.logo-icon{width:40px;height:40px;background:var(--info);border-radius:10px;display:flex;align-items:center;justify-content:center;color:#fff}.logo-text h1{font-size:16px;font-weight:700}.logo-text p{font-size:11px;color:var(--gray)}.nav{flex:1;padding:10px 0}.nav-item{display:flex;align-items:center;gap:12px;padding:12px 20px;color:var(--gray);cursor:pointer;font-size:14px;font-weight:500;margin:2px 10px;border-radius:8px}.nav-item:hover{background:var(--bg);color:var(--text)}.nav-item.active{background:var(--primary);color:#fff}.nav-item i{width:20px;height:20px}.sidebar-footer{padding:15px 20px;border-top:1px solid var(--border)}.user{display:flex;align-items:center;gap:10px}.user-avatar{width:36px;height:36px;background:linear-gradient(135deg,var(--primary),var(--info));border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:12px}.user-info{font-size:13px;font-weight:600}.user-info p{color:var(--gray);font-size:11px;font-weight:400}.main{flex:1;margin-left:240px}.topbar{background:var(--white);padding:15px 25px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--border)}.topbar h2{font-size:15px;font-weight:600}.topbar-right{display:flex;align-items:center;gap:12px}.search-box{display:flex;align-items:center;gap:8px;background:var(--bg);padding:8px 14px;border-radius:8px;border:1px solid var(--border)}.search-box input{border:none;background:transparent;outline:none;font-size:13px;width:150px;font-family:Poppins}.topbar-icon{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:var(--gray)}.content{padding:25px}.page-header{margin-bottom:20px}.page-header h1{font-size:24px;font-weight:700;margin-bottom:5px}.page-header p{color:var(--gray);font-size:13px}.header-actions{display:flex;gap:10px;margin-top:15px}.btn{padding:10px 18px;border-radius:8px;font-size:13px;font-weight:500;border:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px;font-family:Poppins}.btn-primary{background:var(--primary);color:#fff}.btn-success{background:var(--success);color:#fff}.btn-danger{background:var(--danger);color:#fff}.btn-outline{background:var(--white);border:1px solid var(--border);color:var(--text)}.card{background:var(--white);border-radius:12px;border:1px solid var(--border);margin-bottom:20px}.card-body{padding:20px}.filters{display:flex;align-items:flex-end;gap:30px;margin-bottom:20px}.filter-group{display:flex;flex-direction:column;gap:8px}.filter-label{font-size:11px;color:var(--gray);text-transform:uppercase;font-weight:600;letter-spacing:.5px}.filter-tabs{display:flex}.filter-tab{padding:8px 16px;font-size:13px;border:1px solid var(--border);background:var(--white);cursor:pointer;font-family:Poppins;font-weight:500}.filter-tab:first-child{border-radius:8px 0 0 8px}.filter-tab:last-child{border-radius:0 8px 8px 0}.filter-tab:not(:first-child){margin-left:-1px}.filter-tab.active{background:var(--primary);color:#fff;border-color:var(--primary)}.date-picker{padding:8px 14px;border:1px solid var(--border);border-radius:8px;font-size:13px;display:flex;align-items:center;gap:10px;background:var(--white)}.table{width:100%;border-collapse:collapse}.table th{padding:12px 16px;text-align:left;font-size:11px;text-transform:uppercase;color:var(--gray);font-weight:600;letter-spacing:.5px;border-bottom:1px solid var(--border)}.table td{padding:14px 16px;border-bottom:1px solid var(--border);font-size:13px}.table tr:hover{background:var(--bg)}.student-cell{display:flex;align-items:center;gap:12px}.avatar{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:#fff}.avatar.green{background:var(--success)}.avatar.blue{background:var(--primary)}.avatar.orange{background:var(--warning)}.avatar.purple{background:var(--purple)}.avatar.cyan{background:var(--info)}.badge{display:inline-block;padding:5px 12px;border-radius:6px;font-size:12px;font-weight:500}.badge-success{background:#dcfce7;color:#16a34a}.badge-warning{background:#fef3c7;color:#d97706}.badge-danger{background:#fee2e2;color:#dc2626}.badge-info{background:#cffafe;color:#0891b2}.time-late{color:var(--danger);font-weight:500}.verify-btn{width:32px;height:32px;border-radius:8px;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center}.verify-btn.verified{background:#dbeafe;color:var(--primary)}.verify-btn.not-verified{background:var(--bg);color:#9ca3af}.action-btn{width:28px;height:28px;border:none;background:transparent;cursor:pointer;color:var(--gray)}.pagination{display:flex;justify-content:space-between;align-items:center;padding:15px 0}.pagination-info{font-size:13px;color:var(--gray)}.pagination-btns{display:flex;gap:5px}.page-btn{width:32px;height:32px;border:1px solid var(--border);background:var(--white);border-radius:8px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-family:Poppins}.page-btn.active{background:var(--primary);color:#fff;border-color:var(--primary)}.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:15px;margin-bottom:25px}.stat-card{background:var(--white);padding:20px;border-radius:12px;border:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}.stat-info h3{font-size:11px;color:var(--gray);text-transform:uppercase;font-weight:600;margin-bottom:5px}.stat-info .value{font-size:32px;font-weight:700}.stat-info .trend{font-size:11px;margin-top:5px}.stat-info .trend.up{color:var(--success)}.stat-info .trend.down{color:var(--danger)}.stat-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center}.stat-icon.green{background:#dcfce7;color:var(--success)}.stat-icon.orange{background:#fef3c7;color:var(--warning)}.stat-icon.red{background:#fee2e2;color:var(--danger)}.stat-icon.blue{background:#dbeafe;color:var(--primary)}.feed-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.feed-title{font-size:18px;font-weight:700;display:flex;align-items:center;gap:12px}.live-badge{display:inline-flex;align-items:center;gap:6px;background:#fee2e2;color:#dc2626;padding:4px 10px;border-radius:6px;font-size:11px;font-weight:600}.live-dot{width:8px;height:8px;background:#dc2626;border-radius:50%;animation:pulse 1.5s infinite}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}.feed-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}.feed-card{background:var(--white);border-radius:16px;overflow:hidden;border:1px solid var(--border);transition:all .2s}.feed-card:hover{transform:translateY(-4px);box-shadow:0 12px 30px rgba(0,0,0,.1)}.feed-img{position:relative}.feed-photo{width:100%;aspect-ratio:1;object-fit:cover;background:linear-gradient(135deg,#e0e7ff,#dbeafe)}.feed-badge{position:absolute;top:12px;left:12px;padding:5px 10px;border-radius:6px;font-size:10px;font-weight:600;text-transform:uppercase}.feed-badge.verified{background:var(--success);color:#fff}.feed-badge.late{background:var(--warning);color:#fff}.feed-check{position:absolute;bottom:12px;right:12px;width:24px;height:24px;background:var(--success);border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center}.feed-info{padding:14px}.feed-name{font-size:14px;font-weight:600;margin-bottom:3px}.feed-class{font-size:12px;color:var(--gray);margin-bottom:8px}.feed-time{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--gray)}.feed-time.late{color:var(--warning)}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);display:none;align-items:center;justify-content:center;z-index:1000}.modal-overlay.active{display:flex}.modal{background:var(--white);border-radius:16px;width:100%;max-width:450px}.modal-header{padding:20px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}.modal-title{font-size:16px;font-weight:600}.modal-close{border:none;background:none;font-size:24px;cursor:pointer;color:var(--gray)}.modal-body{padding:20px}.modal-footer{padding:15px 20px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:10px}.form-group{margin-bottom:15px}.form-label{display:block;margin-bottom:6px;font-size:13px;font-weight:500}.form-input,.form-select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:8px;font-size:13px;font-family:Poppins}.toast-container{position:fixed;top:20px;right:20px;z-index:9999}.toast{background:var(--white);padding:12px 20px;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,.1);margin-bottom:10px;border-left:4px solid var(--success);font-size:13px}.toast.error{border-left-color:var(--danger)}.student-page{min-height:100vh;background:linear-gradient(135deg,var(--primary),#1d4ed8);display:flex;align-items:center;justify-content:center;padding:20px}.att-box{background:var(--white);border-radius:20px;width:100%;max-width:420px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.2)}.att-header{background:linear-gradient(135deg,var(--primary),#1d4ed8);color:#fff;padding:35px;text-align:center}.att-header h1{font-size:24px;font-weight:700}.att-body{padding:25px}.opt-btn{display:flex;align-items:center;gap:15px;padding:18px;border:1px solid var(--border);border-radius:12px;background:var(--white);cursor:pointer;width:100%;margin-bottom:12px;text-align:left;transition:all .2s}.opt-btn:hover{border-color:var(--primary);background:#eff6ff;transform:translateY(-2px)}.opt-icon{width:48px;height:48px;border-radius:12px;display:flex;align-items:center;justify-content:center}.opt-icon.blue{background:#eff6ff;color:var(--primary)}.opt-icon.green{background:#dcfce7;color:var(--success)}.opt-icon.purple{background:#ede9fe;color:var(--purple)}.opt-text h3{font-size:14px;font-weight:600;margin-bottom:3px}.opt-text p{font-size:12px;color:var(--gray)}.camera-box{width:100%;border-radius:12px;overflow:hidden;background:#000;margin-bottom:15px}.camera-video{width:100%;height:280px;object-fit:cover}.face-status{padding:12px;border-radius:8px;text-align:center;font-size:13px;margin-bottom:15px;font-weight:500}.face-status.loading{background:#fef3c7;color:#b45309}.face-status.success{background:#dcfce7;color:#16a34a}.face-status.error{background:#fee2e2;color:#dc2626}.loc-info{background:var(--bg);padding:12px 14px;border-radius:8px;font-size:12px;margin-bottom:15px;display:flex;align-items:center;gap:8px}.success-screen{text-align:center;padding:20px}.success-icon{width:80px;height:80px;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;margin:0 auto 20px;font-size:40px}.rekap-table{font-size:11px}.rekap-table th,.rekap-table td{padding:8px 4px;text-align:center;min-width:28px}.rekap-table .day-h{background:#dcfce7}.rekap-table .day-t{background:#fef3c7}.rekap-table .day-s{background:#ede9fe}.rekap-table .day-i{background:#cffafe}.rekap-table .day-a{background:#fee2e2}@media(max-width:768px){.sidebar{display:none}.main{margin-left:0}.stats-grid,.feed-grid{grid-template-columns:1fr 1fr}}";
}

function getAdminHTML() {
  return "<div class='app'><aside class='sidebar'><div class='sidebar-header'><div class='logo-icon'><i data-lucide='graduation-cap' style='width:22px;height:22px'></i></div><div class='logo-text'><h1>Admin LevelUp</h1><p>Admin Portal</p></div></div><nav class='nav'><div class='nav-item active' data-page='dashboard' onclick='showPage(\"dashboard\")'><i data-lucide='home'></i><span>Dashboard</span></div><div class='nav-item' data-page='laporan' onclick='showPage(\"laporan\")'><i data-lucide='clipboard-check'></i><span>Laporan Kehadiran</span></div><div class='nav-item' data-page='rekap' onclick='showPage(\"rekap\")'><i data-lucide='calendar-range'></i><span>Rekap Bulanan</span></div><div class='nav-item' data-page='siswa' onclick='showPage(\"siswa\")'><i data-lucide='users'></i><span>Populasi Sekolah</span></div><div class='nav-item' data-page='pengaturan' onclick='showPage(\"pengaturan\")'><i data-lucide='settings'></i><span>Pengaturan</span></div></nav><div class='sidebar-footer'><div class='user'><div class='user-avatar'>BS</div><div class='user-info'>Budi Santoso<p>Administrator</p></div></div></div></aside><main class='main'><div class='topbar'><h2 id='page-title'>Dashboard</h2><div class='topbar-right'><div class='search-box'><i data-lucide='search' style='width:16px;color:var(--gray)'></i><input type='text' placeholder='Cari nama siswa...'></div><div class='topbar-icon'><i data-lucide='bell' style='width:20px'></i></div><div class='topbar-icon'><i data-lucide='message-square' style='width:20px'></i></div></div></div><div class='content' id='main-content'></div></main></div><div class='modal-overlay' id='modal'></div><div class='toast-container' id='toasts'></div>";
}

function getStudentHTML() {
  return "<div class='student-page'><div class='att-box'><div class='att-header'><h1>Admin LevelUp</h1><p>Sistem Absensi Digital</p></div><div class='att-body' id='student-content'></div></div></div><div class='toast-container' id='toasts'></div>";
}

function getAdminJS() {
  return "var currentPage='dashboard';var activeType='siswa';var currentFilterDate='';var students=[];var attendance=[];var stats={};var rekapData=null;var filterKelas='Semua';var colors=['blue','green','orange','purple','cyan'];document.addEventListener('DOMContentLoaded',function(){lucide.createIcons();loadData();});function loadData(dt){if(dt) currentFilterDate=dt; google.script.run.withSuccessHandler(function(r){if(r.success)stats=r.stats;renderPage();}).getStats(activeType);google.script.run.withSuccessHandler(function(r){if(r.success){attendance=r.attendance; if(r.date) currentFilterDate=r.date; renderPage();}}).getAttendanceToday(currentFilterDate);if(activeType === 'guru'){google.script.run.withSuccessHandler(function(r){if(r.success)students=r.teachers;renderPage();}).getTeacherList();}else if(activeType === 'kelas'){google.script.run.withSuccessHandler(function(r){if(r.success)students=r.classes;renderPage();}).getClassList();}else{google.script.run.withSuccessHandler(function(r){if(r.success)students=r.students;renderPage();}).getStudentList();}}function switchType(t){activeType=t;filterKelas='Semua';loadData();}function changeDate(d){currentFilterDate=d;loadData(d);}function getColor(i){return colors[i%colors.length];}function showPage(p){currentPage=p;document.querySelectorAll('.nav-item').forEach(function(n){n.classList.remove('active');});document.querySelector('[data-page=\"'+p+'\"]').classList.add('active');var t={'dashboard':'Dashboard','laporan':'Laporan Kehadiran','rekap':'Rekap Bulanan','siswa':'Populasi Sekolah','pengaturan':'Pengaturan'};document.getElementById('page-title').textContent=t[p];renderPage();}function renderPage(){if(currentPage==='dashboard')renderDashboard();else if(currentPage==='laporan')renderLaporan();else if(currentPage==='rekap')renderRekap();else if(currentPage==='siswa')renderSiswa();else if(currentPage==='pengaturan')renderPengaturan();}function renderDashboard(){var c=document.getElementById('main-content');var btnS=activeType=='siswa'?'btn-primary':'btn-outline';var btnG=activeType=='guru'?'btn-primary':'btn-outline';var targetType=activeType=='guru'?'Guru':'Siswa';var feedData=attendance.filter(function(a){return activeType=='guru'?a.type==='Guru':(!a.type||a.type==='Siswa');});var verified=0;var total=feedData.length;for(var i=0;i<feedData.length;i++){if(feedData[i].faceMatch>0.5)verified++;}var pct=total>0?((verified/total)*100).toFixed(1):'0';var feed='';if(feedData.length>0){for(var j=0;j<feedData.length;j++){var a=feedData[j];var badge=a.faceMatch>0.5?(a.status==='Terlambat'?'<span class=\"feed-badge late\">TERLAMBAT</span>':'<span class=\"feed-badge verified\">TERVERIFIKASI</span>'):'';var photoSrc=a.fotoAbsen?a.fotoAbsen:'';var photoHtml=photoSrc?'<img class=\"feed-photo\" src=\"'+photoSrc+'\">':'<div class=\"feed-photo\" style=\"display:flex;align-items:center;justify-content:center;font-size:48px;font-weight:700;color:var(--primary)\">'+getInit(a.nama)+'</div>';var timeClass=a.status==='Terlambat'?' late':'';var subText = a.kelas; if(a.kelas === 'Guru') subText = 'Guru';feed+='<div class=\"feed-card\"><div class=\"feed-img\">'+photoHtml+badge+'<div class=\"feed-check\"><i data-lucide=\"check\" style=\"width:14px;height:14px\"></i></div></div><div class=\"feed-info\"><div class=\"feed-name\">'+a.nama+'</div><div class=\"feed-class\">'+subText+'</div><div class=\"feed-time'+timeClass+'\"><i data-lucide=\"clock\" style=\"width:12px\"></i> '+a.waktu+' WIB</div></div></div>';}}else{feed='<div style=\"text-align:center;padding:80px 20px;color:var(--gray);grid-column:1/-1\"><i data-lucide=\"users\" style=\"width:64px;height:64px;margin-bottom:20px;opacity:.3\"></i><h3 style=\"margin-bottom:8px\">Belum Ada Absensi Hari Ini</h3><p style=\"font-size:13px\">'+targetType+' yang absen akan muncul di sini secara real-time</p></div>';}var lblHadir='Total '+targetType+' Hadir';var lblTelat=targetType+' Terlambat';c.innerHTML='<div class=\"page-header\"><h1>Dashboard '+targetType+'</h1><div class=\"header-actions\"><button class=\"btn '+btnS+'\" onclick=\"switchType(\\'siswa\\')\">Dashboard Siswa</button><button class=\"btn '+btnG+'\" onclick=\"switchType(\\'guru\\')\">Dashboard Guru</button></div></div><div class=\"stats-grid\"><div class=\"stat-card\"><div class=\"stat-info\"><h3>'+lblHadir+'</h3><div class=\"value\">'+(stats.totalHadir||0)+'</div><div class=\"trend up\">+5% vs Kemarin</div></div><div class=\"stat-icon green\"><i data-lucide=\"users\"></i></div></div><div class=\"stat-card\"><div class=\"stat-info\"><h3>'+lblTelat+'</h3><div class=\"value\">'+(stats.totalTerlambat||0)+'</div><div class=\"trend down\">-2% Membaik</div></div><div class=\"stat-icon orange\"><i data-lucide=\"clock\"></i></div></div><div class=\"stat-card\"><div class=\"stat-info\"><h3>Belum Absen</h3><div class=\"value\">'+(stats.belumAbsen||0)+'</div><div class=\"trend down\">Perlu Cek Manual</div></div><div class=\"stat-icon red\"><i data-lucide=\"user-x\"></i></div></div><div class=\"stat-card\"><div class=\"stat-info\"><h3>Akurasi Selfie</h3><div class=\"value\">'+pct+'%</div><div class=\"trend up\">AI Verification</div></div><div class=\"stat-icon blue\"><i data-lucide=\"scan-face\"></i></div></div></div><div class=\"feed-header\"><div class=\"feed-title\">Feed Absensi Real-time <span class=\"live-badge\"><span class=\"live-dot\"></span> LIVE</span></div><div style=\"display:flex;gap:10px\"><button class=\"btn btn-outline\" onclick=\"loadData()\"><i data-lucide=\"filter\" style=\"width:14px\"></i> Refresh</button><button class=\"btn btn-primary\" onclick=\"exportExcel()\"><i data-lucide=\"download\" style=\"width:14px\"></i> Export Log</button></div></div><div class=\"feed-grid\">'+feed+'</div><div class=\"pagination\"><div class=\"pagination-info\">Menampilkan 1 - '+feedData.length+' dari '+feedData.length+' '+targetType+'</div></div>';lucide.createIcons();}";
}

function getAdminJS2() {
  return "function renderLaporan(){var c=document.getElementById('main-content');var btnS=activeType=='siswa'?'btn-primary':'btn-outline';var btnG=activeType=='guru'?'btn-primary':'btn-outline';c.innerHTML='<div class=\"page-header\"><h1>Laporan Kehadiran '+(activeType=='guru'?'Guru':'Siswa')+'</h1><p>Kelola dan pantau data kehadiran harian '+(activeType=='guru'?'guru':'siswa')+' dengan verifikasi selfie.</p><div class=\"header-actions\"><button class=\"btn '+btnS+'\" onclick=\"switchType(\\'siswa\\')\">Laporan Siswa</button><button class=\"btn '+btnG+'\" onclick=\"switchType(\\'guru\\')\">Laporan Guru</button></div><div class=\"header-actions\" style=\"margin-top:10px\"><button class=\"btn btn-danger\" onclick=\"exportPDF()\"><i data-lucide=\"file-text\" style=\"width:16px\"></i> Export PDF</button><button class=\"btn btn-success\" onclick=\"exportExcel()\"><i data-lucide=\"file-spreadsheet\" style=\"width:16px\"></i> Export Excel</button><button class=\"btn btn-primary\" onclick=\"propagateEmail()\"><i data-lucide=\"mail\" style=\"width:16px\"></i> Kirim ke Wali Kelas</button></div></div><div class=\"card\"><div class=\"card-body\"><div class=\"filters\"><div class=\"filter-group\"><div class=\"filter-label\">Filter '+(activeType=='guru'?'Jabatan':'Kelas')+'</div><div class=\"filter-tabs\" id=\"filter-tabs\"></div></div><div class=\"filter-group\"><div class=\"filter-label\">Tanggal Laporan</div><input type=\"date\" class=\"form-input\" value=\"'+(currentFilterDate||'')+'\" onchange=\"changeDate(this.value)\" style=\"max-width:200px\"></div></div><table class=\"table\"><thead><tr><th>Nama</th><th>'+(activeType=='guru'?'Jabatan':'Kelas')+'</th><th>Tanggal</th><th>Jam Masuk</th><th>Status</th><th>Verifikasi</th><th>Aksi</th></tr></thead><tbody id=\"lap-table\"></tbody></table><div class=\"pagination\"><div class=\"pagination-info\" id=\"pg-info\"></div><div class=\"pagination-btns\"><button class=\"page-btn\">&lt;</button><button class=\"page-btn active\">1</button><button class=\"page-btn\">&gt;</button></div></div></div></div>';renderFilters();renderLapTable();lucide.createIcons();}function renderFilters(){var t=document.getElementById('filter-tabs');if(activeType=='siswa'){t.innerHTML='<button class=\"filter-tab '+(filterKelas=='Semua'?'active':'')+'\" onclick=\"setFilter(this,\\'Semua\\')\">Semua</button><button class=\"filter-tab '+(filterKelas=='X'?'active':'')+'\" onclick=\"setFilter(this,\\'X\\')\">Kelas X</button><button class=\"filter-tab '+(filterKelas=='XI'?'active':'')+'\" onclick=\"setFilter(this,\\'XI\\')\">Kelas XI</button><button class=\"filter-tab '+(filterKelas=='XII'?'active':'')+'\" onclick=\"setFilter(this,\\'XII\\')\">Kelas XII</button>';}else{var roles=['Semua'];if(students&&students.length>0){var u={};for(var i=0;i<students.length;i++){if(students[i].jabatan)u[students[i].jabatan]=1;}for(var k in u)roles.push(k);}var h='';for(var j=0;j<roles.length;j++)h+='<button class=\"filter-tab '+(filterKelas==roles[j]?'active':'')+'\" onclick=\"setFilter(this,\\''+roles[j]+'\\')\">'+roles[j]+'</button>';t.innerHTML=h;}}function setFilter(el,f){filterKelas=f;renderFilters();renderLapTable();}function renderLapTable(){var tb=document.getElementById('lap-table');if(!tb)return;var targetType=(activeType=='guru'?'Guru':'Siswa');var data=attendance.filter(function(a){var isType=(a.type===targetType)||(activeType=='siswa'&&!a.type);return isType&&(filterKelas==='Semua'||a.kelas.indexOf(filterKelas)>-1);});document.getElementById('pg-info').innerText='Menampilkan 1 - '+data.length+' dari '+data.length+' data';if(data.length===0){tb.innerHTML='<tr><td colspan=\"7\" style=\"text-align:center;padding:40px;color:var(--gray)\">Tidak ada data</td></tr>';return;}var h='';for(var i=0;i<data.length;i++){var a=data[i];var bc=a.status==='Hadir'?'success':a.status==='Terlambat'?'warning':a.status==='Izin'?'info':'danger';var tc=a.status==='Terlambat'?' class=\"time-late\"':'';var v=a.faceMatch>0.5?'<button class=\"verify-btn verified\"><i data-lucide=\"camera\" style=\"width:16px\"></i></button>':'<button class=\"verify-btn not-verified\"><i data-lucide=\"camera-off\" style=\"width:16px\"></i></button>';var thumb=a.fotoAbsen?'<img src=\"'+a.fotoAbsen+'\" style=\"width:36px;height:36px;border-radius:50%;object-fit:cover\">':'<div class=\"avatar '+getColor(i)+'\">'+getInit(a.nama)+'</div>';h+='<tr><td><div class=\"student-cell\">'+thumb+'<span style=\"font-weight:500\">'+a.nama+'</span></div></td><td>'+a.kelas+'</td><td>'+formatDate(a.tanggal)+'</td><td'+tc+'>'+(a.waktu||'--:--')+'</td><td><span class=\"badge badge-'+bc+'\">'+a.status+'</span></td><td>'+v+'</td><td><button class=\"action-btn\"><i data-lucide=\"more-vertical\" style=\"width:18px\"></i></button></td></tr>';}tb.innerHTML=h;lucide.createIcons();}function formatDate(d){if(!d)return'-';var p=String(d).split('-');var m=['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agt','Sep','Okt','Nov','Des'];return p.length===3?p[2]+' '+m[parseInt(p[1])-1]+' '+p[0]:d;}function renderRekap(){var c=document.getElementById('main-content'); var btnS = activeType=='siswa'?'btn-primary':'btn-outline'; var btnG = activeType=='guru'?'btn-primary':'btn-outline'; c.innerHTML='<div class=\"page-header\"><h1>Rekap Bulanan</h1><p>Rekap kehadiran '+activeType+' per bulan dengan status harian.</p> <div class=\"header-actions\"><button class=\"btn '+btnS+'\" onclick=\"switchType(\\'siswa\\')\">Rekap Siswa</button> <button class=\"btn '+btnG+'\" onclick=\"switchType(\\'guru\\')\">Rekap Guru</button></div></div><div class=\"card\"><div class=\"card-body\" id=\"rekap-content\"><p style=\"text-align:center;padding:30px;color:var(--gray)\">Memuat data '+activeType+'...</p></div></div>';google.script.run.withSuccessHandler(function(r){if(r.success){rekapData=r;showRekapTable();}}).getMonthlyRecap(new Date().getMonth()+1,new Date().getFullYear(), activeType);}function showRekapTable(){var rc=document.getElementById('rekap-content');if(!rekapData||!rekapData.students||rekapData.students.length===0){rc.innerHTML='<p style=\"text-align:center;padding:30px;color:var(--gray)\">Tidak ada data rekap</p>';return;}var h='<div style=\"overflow-x:auto\"><table class=\"table rekap-table\"><thead><tr><th style=\"text-align:left;min-width:150px\">Nama</th>';for(var d=1;d<=rekapData.daysInMonth;d++)h+='<th>'+d+'</th>';h+='<th style=\"background:#dcfce7\">H</th><th style=\"background:#fef3c7\">T</th><th style=\"background:#ede9fe\">S</th><th style=\"background:#cffafe\">I</th><th style=\"background:#fee2e2\">A</th></tr></thead><tbody>';for(var k=0;k<rekapData.students.length;k++){var s=rekapData.students[k];h+='<tr><td style=\"text-align:left\"><div class=\"student-cell\"><div class=\"avatar '+getColor(k)+'\" style=\"width:28px;height:28px;font-size:10px\">'+getInit(s.nama)+'</div><div><strong style=\"font-size:12px\">'+s.nama+'</strong><br><small style=\"color:var(--gray)\">'+s.kelas+'</small></div></div></td>';for(var d=1;d<=rekapData.daysInMonth;d++){var code=s.days[d]||'-';var cls=code==='H'?'day-h':code==='T'?'day-t':code==='S'?'day-s':code==='I'?'day-i':code==='A'?'day-a':'';h+='<td class=\"'+cls+'\"><strong>'+code+'</strong></td>';}h+='<td style=\"font-weight:700;color:var(--success)\">'+(s.totals.H||0)+'</td><td style=\"font-weight:700;color:var(--warning)\">'+(s.totals.T||0)+'</td><td style=\"font-weight:700;color:var(--purple)\">'+(s.totals.S||0)+'</td><td style=\"font-weight:700;color:var(--info)\">'+(s.totals.I||0)+'</td><td style=\"font-weight:700;color:var(--danger)\">'+(s.totals.A||0)+'</td></tr>';}h+='</tbody></table></div>';rc.innerHTML=h;}function propagateEmail(){if(confirm('Kirim laporan kehadiran hari ini ke semua Wali Kelas?')){google.script.run.withSuccessHandler(function(r){var msg='Total Terkirim: '+r.sent+'\\nGagal: '+r.failed;if(r.failed>0){alert(msg+'\\n\\nDetail:\\n'+r.log.join('\\n'));}else{showToast(msg,'success');}}).sendDailyReportToWaliKelas(currentFilterDate);}}";
}

function getAdminJS3() {
  return "function renderSiswa(){var c=document.getElementById('main-content'); " +
  "var btnS=activeType=='siswa'?'btn-primary':'btn-outline'; var btnG=activeType=='guru'?'btn-primary':'btn-outline'; var btnK=activeType=='kelas'?'btn-primary':'btn-outline'; " +
  "c.innerHTML='<div class=\"page-header\"><h1>Populasi Sekolah</h1><p>Kelola data Siswa, Guru, dan Kelas.</p>" +
  "<div class=\"header-actions\"><button class=\"btn '+btnS+'\" onclick=\"switchType(\\'siswa\\')\">Data Siswa</button><button class=\"btn '+btnG+'\" onclick=\"switchType(\\'guru\\')\">Data Guru</button><button class=\"btn '+btnK+'\" onclick=\"switchType(\\'kelas\\')\">Data Kelas</button></div>" +
  "<div class=\"header-actions\" style=\"margin-top:10px\"><button class=\"btn btn-primary\" onclick=\"showAddModal()\"><i data-lucide=\"plus\" style=\"width:16px\"></i> Tambah '+(activeType=='guru'?'Guru':activeType=='kelas'?'Kelas':'Siswa')+'</button><button class=\"btn btn-success\" onclick=\"runSetup()\"><i data-lucide=\"database\" style=\"width:16px\"></i> Setup Sheet</button></div></div>" +
  "<div class=\"card\"><div class=\"card-body\" style=\"padding:0\"><table class=\"table\"><thead><tr>'+(activeType=='kelas'?'<th>Nama Kelas</th><th>Wali Kelas</th><th>Email</th><th>Aksi</th>':'<th>'+(activeType=='guru'?'Guru':'Siswa')+'</th><th>'+(activeType=='guru'?'NIP':'Barcode')+'</th><th>'+(activeType=='guru'?'Jabatan':'Kelas')+'</th><th>Email</th><th>Verifikasi</th><th>Status</th><th>Aksi</th>')+'</tr></thead><tbody id=\"siswa-table\"></tbody></table></div></div>';lucide.createIcons();renderSiswaTable();} " +
  
  "function renderSiswaTable(){var tb=document.getElementById('siswa-table');if(!tb)return;if(students.length===0){tb.innerHTML='<tr><td colspan=\"7\" style=\"text-align:center;padding:40px;color:var(--gray)\">Belum ada data.</td></tr>';return;}var h='';for(var i=0;i<students.length;i++){var s=students[i]; if(activeType==='kelas'){h+='<tr><td>'+s.nama+'</td><td>'+s.wali+'</td><td>'+s.email+'</td><td><button class=\"action-btn\" onclick=\"deleteSiswa(\\''+s.nama+'\\')\" title=\"Hapus\"><i data-lucide=\"trash-2\" style=\"width:18px\"></i></button></td></tr>';}else{var fb=s.hasFace?'<button class=\"verify-btn verified\"><i data-lucide=\"check\" style=\"width:16px\"></i></button>':'<button class=\"btn btn-outline\" style=\"padding:6px 12px;font-size:12px\" onclick=\"regFace(\\''+s.id+'\\',\\''+s.nama+'\\')\" ><i data-lucide=\"camera\" style=\"width:14px\"></i> Register</button>';var stBadge=s.status==='Aktif'?'success':'danger'; var code = s.nip || s.barcode; var sub = s.jabatan || s.kelas; var em = s.email || '-'; h+='<tr><td><div class=\"student-cell\"><div class=\"avatar '+getColor(i)+'\">'+getInit(s.nama)+'</div><span style=\"font-weight:500\">'+s.nama+'</span></div></td><td>'+code+'</td><td>'+sub+'</td><td>'+em+'</td><td>'+fb+'</td><td><span class=\"badge badge-'+stBadge+'\">'+(s.status||'Aktif')+'</span></td><td><button class=\"action-btn\" onclick=\"deleteSiswa(\\''+s.id+'\\')\" title=\"Hapus\"><i data-lucide=\"trash-2\" style=\"width:18px\"></i></button></td></tr>';}}tb.innerHTML=h;lucide.createIcons();} " +
  
  "function renderPengaturan(){var c=document.getElementById('main-content');c.innerHTML='<div class=\"page-header\"><h1>Pengaturan</h1><p>Konfigurasi jam terlambat dan parameter sistem.</p></div><div class=\"card\"><div class=\"card-body\"><div style=\"max-width:400px\"><div class=\"form-group\"><label class=\"form-label\">Jam Batas Terlambat</label><input type=\"time\" class=\"form-input\" id=\"set-jam\" value=\"07:15\"></div><div class=\"form-group\"><label class=\"form-label\">Threshold Face Match (0-1)</label><input type=\"number\" class=\"form-input\" id=\"set-th\" value=\"0.5\" min=\"0\" max=\"1\" step=\"0.1\"></div><button class=\"btn btn-primary\" onclick=\"saveSettings()\"><i data-lucide=\"save\" style=\"width:16px\"></i> Simpan Pengaturan</button></div></div></div>';lucide.createIcons();} " +
  
  "function showAddModal(){var m=document.getElementById('modal');if(activeType==='kelas'){m.innerHTML='<div class=\"modal\"><div class=\"modal-header\"><span class=\"modal-title\">Tambah Kelas Baru</span><button class=\"modal-close\" onclick=\"closeModal()\">&times;</button></div><div class=\"modal-body\"><div class=\"form-group\"><label class=\"form-label\">Nama Kelas</label><input type=\"text\" class=\"form-input\" id=\"add-kelas-nama\" placeholder=\"Contoh: X-IPA 1\"></div><div class=\"form-group\"><label class=\"form-label\">Nama Wali Kelas</label><input type=\"text\" class=\"form-input\" id=\"add-kelas-wali\" placeholder=\"Nama Wali Kelas\"></div><div class=\"form-group\"><label class=\"form-label\">Email Wali Kelas</label><input type=\"email\" class=\"form-input\" id=\"add-kelas-email\" placeholder=\"email@sekolah.sch.id\"></div></div><div class=\"modal-footer\"><button class=\"btn btn-outline\" onclick=\"closeModal()\">Batal</button><button class=\"btn btn-primary\" onclick=\"submitAdd()\">Simpan</button></div></div>';}else{var lblCode=activeType=='guru'?'NIP':'Barcode / NIS'; var lblSub=activeType=='guru'?'Jabatan':'Kelas'; m.innerHTML='<div class=\"modal\"><div class=\"modal-header\"><span class=\"modal-title\">Tambah '+(activeType=='guru'?'Guru':'Siswa')+' Baru</span><button class=\"modal-close\" onclick=\"closeModal()\">&times;</button></div><div class=\"modal-body\"><div class=\"form-group\"><label class=\"form-label\">'+lblCode+'</label><input type=\"text\" class=\"form-input\" id=\"add-bc\" placeholder=\"Masukan '+lblCode+'\"></div><div class=\"form-group\"><label class=\"form-label\">Nama Lengkap</label><input type=\"text\" class=\"form-input\" id=\"add-nama\" placeholder=\"Nama Lengkap\"></div><div class=\"form-group\"><label class=\"form-label\">'+lblSub+'</label><input type=\"text\" class=\"form-input\" id=\"add-kelas\" placeholder=\"'+lblSub+'\"></div><div class=\"form-group\"><label class=\"form-label\">Email (Opsional)</label><input type=\"email\" class=\"form-input\" id=\"add-email\" placeholder=\"email@domain.com\"></div></div><div class=\"modal-footer\"><button class=\"btn btn-outline\" onclick=\"closeModal()\">Batal</button><button class=\"btn btn-primary\" onclick=\"submitAdd()\">Simpan</button></div></div>';}m.classList.add('active');} " +
  
  "function regFace(id,nm){var m=document.getElementById('modal');m.innerHTML='<div class=\"modal\"><div class=\"modal-header\"><span class=\"modal-title\">Register Wajah - '+nm+'</span><button class=\"modal-close\" onclick=\"closeModal()\">&times;</button></div><div class=\"modal-body\"><div style=\"text-align:center;padding:20px;border:2px dashed var(--border);border-radius:12px;margin-bottom:15px;cursor:pointer\" onclick=\"document.getElementById(\\'foto-input\\').click()\"><i data-lucide=\"upload\" style=\"width:48px;height:48px;color:var(--gray);margin-bottom:10px\"></i><p style=\"color:var(--gray);font-size:13px\">Klik untuk upload foto wajah</p><input type=\"file\" id=\"foto-input\" accept=\"image/*\" style=\"display:none\" onchange=\"previewFoto(this)\"></div><div id=\"preview-box\" style=\"display:none;margin-bottom:15px\"><img id=\"preview-img\" style=\"width:100%;max-height:300px;object-fit:contain;border-radius:8px\"></div><div class=\"face-status loading\" id=\"face-status\" style=\"display:none\">Memproses wajah...</div><button class=\"btn btn-primary\" style=\"width:100%\" id=\"save-btn\" disabled onclick=\"saveFace(\\''+id+'\\')\"><i data-lucide=\"save\" style=\"width:16px\"></i> Simpan Wajah</button></div></div>';m.classList.add('active');lucide.createIcons();loadFaceApi();}";
}

function getAdminJS4() {
  return "var uploadedImg=null;function previewFoto(input){if(input.files&&input.files[0]){var reader=new FileReader();reader.onload=function(e){uploadedImg=new Image();uploadedImg.onload=function(){document.getElementById('preview-box').style.display='block';document.getElementById('preview-img').src=e.target.result;document.getElementById('save-btn').disabled=false;};uploadedImg.src=e.target.result;};reader.readAsDataURL(input.files[0]);}}function loadFaceApi(){Promise.all([faceapi.nets.tinyFaceDetector.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model'),faceapi.nets.faceLandmark68Net.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model'),faceapi.nets.faceRecognitionNet.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model')]).then(function(){console.log('Face API loaded');}).catch(function(e){console.log('Face API error',e);});}function saveFace(id){if(!uploadedImg){showToast('Pilih foto dulu','error');return;}document.getElementById('face-status').style.display='block';document.getElementById('face-status').className='face-status loading';document.getElementById('face-status').textContent='Mendeteksi wajah...';document.getElementById('save-btn').disabled=true;var cv=document.createElement('canvas');cv.width=uploadedImg.width;cv.height=uploadedImg.height;cv.getContext('2d').drawImage(uploadedImg,0,0);faceapi.detectSingleFace(cv,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptor().then(function(d){if(d){var desc=Array.from(d.descriptor);var small=document.createElement('canvas');small.width=200;small.height=200;small.getContext('2d').drawImage(uploadedImg,0,0,200,200);var url=small.toDataURL('image/jpeg',0.3);document.getElementById('face-status').textContent='Menyimpan...';google.script.run.withSuccessHandler(function(r){closeModal();if(r.success){showToast('Wajah berhasil didaftarkan!','success');loadData();}else{showToast('Gagal menyimpan: '+(r.error||''),'error');}}).withFailureHandler(function(e){document.getElementById('face-status').className='face-status error';document.getElementById('face-status').textContent='Error: '+e.message;document.getElementById('save-btn').disabled=false;}).registerFace({studentId:id,faceDescriptor:desc,fotoURL:url});}else{document.getElementById('face-status').className='face-status error';document.getElementById('face-status').textContent='Wajah tidak terdeteksi dalam foto';document.getElementById('save-btn').disabled=false;}}).catch(function(e){document.getElementById('face-status').className='face-status error';document.getElementById('face-status').textContent='Error deteksi: '+e.message;document.getElementById('save-btn').disabled=false;});}function stopCam(){if(window.regStream){window.regStream.getTracks().forEach(function(t){t.stop();});}}function closeModal(){document.getElementById('modal').classList.remove('active');uploadedImg=null;} " +
  "function submitAdd(){if(activeType==='kelas'){var n=document.getElementById('add-kelas-nama').value;var w=document.getElementById('add-kelas-wali').value;var e=document.getElementById('add-kelas-email').value;if(!n||!w){showToast('Lengkapi field wajib','error');return;} google.script.run.withSuccessHandler(function(r){if(r.success){showToast('Kelas berhasil ditambahkan','success');closeModal();loadData();}}).addClass({nama:n,wali:w,email:e}); return;} var bc=document.getElementById('add-bc').value;var nm=document.getElementById('add-nama').value;var kl=document.getElementById('add-kelas').value;var em=document.getElementById('add-email').value;if(!bc||!nm||!kl){showToast('Lengkapi semua field','error');return;} if(activeType==='guru'){google.script.run.withSuccessHandler(function(r){if(r.success){showToast('Guru berhasil ditambahkan','success');closeModal();loadData();}}).addTeacher({nip:bc,nama:nm,jabatan:kl,email:em});}else{google.script.run.withSuccessHandler(function(r){if(r.success){showToast('Siswa berhasil ditambahkan','success');closeModal();loadData();}}).addStudent({barcode:bc,nama:nm,kelas:kl,email:em});}} " +
  "function deleteSiswa(id){if(confirm('Yakin ingin menghapus?')){if(activeType==='kelas'){google.script.run.withSuccessHandler(function(r){if(r.success){showToast('Kelas dihapus','success');loadData();}else{showToast(r.error,'error');}}).deleteClass(id);return;} if(activeType==='guru'){google.script.run.withSuccessHandler(function(r){if(r.success){showToast('Guru dihapus','success');loadData();}}).deleteTeacher(id);}else{google.script.run.withSuccessHandler(function(r){if(r.success){showToast('Siswa dihapus','success');loadData();}}).deleteStudent(id);} }} " +
  "function runSetup(){google.script.run.withSuccessHandler(function(r){showToast(r,'success');loadData();}).setupSpreadsheet();}function saveSettings(){var j=document.getElementById('set-jam').value;var t=document.getElementById('set-th').value;google.script.run.withSuccessHandler(function(r){if(r.success)showToast('Pengaturan disimpan','success');}).updateSettings({jamTerlambat:j,faceMatchThreshold:t});}function exportPDF(){var doc=new jspdf.jsPDF();doc.text('Laporan Kehadiran Siswa',20,20);var y=35;for(var i=0;i<attendance.length;i++){var a=attendance[i];doc.text((i+1)+'. '+a.nama+' - '+a.kelas+' - '+a.waktu+' - '+a.status,20,y);y+=8;}doc.save('laporan.pdf');showToast('PDF diunduh','success');}function exportExcel(){var wb=XLSX.utils.book_new();var data=[['No','Nama','Kelas','Tanggal','Waktu','Status','Verifikasi']];for(var i=0;i<attendance.length;i++){var a=attendance[i];data.push([i+1,a.nama,a.kelas,a.tanggal||'-',a.waktu,a.status,a.faceMatch>0.5?'Ya':'Tidak']);}var ws=XLSX.utils.aoa_to_sheet(data);XLSX.utils.book_append_sheet(wb,ws,'Kehadiran');XLSX.writeFile(wb,'laporan.xlsx');showToast('Excel diunduh','success');}function showToast(m,t){var tc=document.getElementById('toasts');var d=document.createElement('div');d.className='toast '+(t==='error'?'error':'');d.textContent=m;tc.appendChild(d);setTimeout(function(){d.remove();},3000);}function getInit(n){if(!n)return'?';var p=n.split(' ');return p.length>=2?(p[0][0]+p[1][0]).toUpperCase():n.substring(0,2).toUpperCase();}";
}

function getStudentJS() {
  return "var currentStudent=null;var videoStream=null;var locationData={lat:'',lng:'',alamat:''};var faceLoaded=false;document.addEventListener('DOMContentLoaded',function(){lucide.createIcons();showOptions();});function showOptions(){stopCam();var c=document.getElementById('student-content');c.innerHTML='<button class=\"opt-btn\" onclick=\"startScanner()\"><div class=\"opt-icon blue\"><i data-lucide=\"scan-barcode\"></i></div><div class=\"opt-text\"><h3>Scan Barcode</h3><p>Pindai kartu pelajar / NIP</p></div></button><button class=\"opt-btn\" onclick=\"showManual()\"><div class=\"opt-icon green\"><i data-lucide=\"keyboard\"></i></div><div class=\"opt-text\"><h3>Input Manual</h3><p>Ketik barcode / NIP</p></div></button><button class=\"opt-btn\" onclick=\"showSick()\"><div class=\"opt-icon purple\"><i data-lucide=\"thermometer\"></i></div><div class=\"opt-text\"><h3>Sakit / Izin</h3><p>Form tidak hadir</p></div></button>';lucide.createIcons();}function showManual(){var c=document.getElementById('student-content');c.innerHTML='<div class=\"form-group\"><label class=\"form-label\">Barcode / NIS / NIP</label><input type=\"text\" class=\"form-input\" id=\"barcode\"></div><button class=\"btn btn-primary\" style=\"width:100%\" onclick=\"findStudent()\">Cari Data</button><button class=\"btn btn-outline\" style=\"width:100%;margin-top:10px\" onclick=\"showOptions()\">Kembali</button>';}function startScanner(){var c=document.getElementById('student-content');c.innerHTML='<div id=\"scanner\" style=\"width:100%;height:260px;border-radius:12px;overflow:hidden;margin-bottom:15px\"></div><button class=\"btn btn-outline\" style=\"width:100%\" onclick=\"stopScan();showOptions()\">Batal</button>';try{window.scanner=new Html5Qrcode('scanner');window.scanner.start({facingMode:'environment'},{fps:10,qrbox:{width:250,height:100}},function(t){stopScan();findByCode(t);},function(){});}catch(e){showToast('Error','error');showOptions();}}function stopScan(){if(window.scanner){try{window.scanner.stop();}catch(e){}}}function findStudent(){var bc=document.getElementById('barcode').value.trim();if(!bc){showToast('Masukkan barcode','error');return;}findByCode(bc);}function findByCode(bc){google.script.run.withSuccessHandler(function(r){if(r.success){currentStudent=r.student;showSelfie();}else{showToast('Data tidak ditemukan','error');showOptions();}}).getPersonByBarcode(bc);}function showSelfie(){locationData={lat:'',lng:'',alamat:''};getLoc();var c=document.getElementById('student-content');c.innerHTML='<div style=\"text-align:center;margin-bottom:20px\"><div class=\"avatar blue\" style=\"width:60px;height:60px;font-size:20px;margin:0 auto 10px\">'+getInit(currentStudent.nama)+'</div><h3>'+currentStudent.nama+'</h3><p style=\"color:var(--gray)\">'+currentStudent.kelas+'</p></div><div class=\"camera-box\"><video id=\"selfie-video\" class=\"camera-video\" autoplay playsinline></video></div><div class=\"face-status loading\" id=\"fs\">Memuat...</div><div class=\"loc-info\" id=\"li\"><i data-lucide=\"map-pin\" style=\"width:14px\"></i>Mendeteksi lokasi...</div><button class=\"btn btn-success\" style=\"width:100%\" id=\"sbtn\" disabled onclick=\"captureSubmit()\"><i data-lucide=\"camera\" style=\"width:16px\"></i> Absen Sekarang</button><button class=\"btn btn-outline\" style=\"width:100%;margin-top:10px\" onclick=\"stopCam();showOptions()\">Batal</button>';lucide.createIcons();startSelfie();}function startSelfie(){navigator.mediaDevices.getUserMedia({video:{facingMode:'user',width:640,height:480}}).then(function(s){videoStream=s;document.getElementById('selfie-video').srcObject=s;loadFace();}).catch(function(){document.getElementById('fs').className='face-status error';document.getElementById('fs').textContent='Gagal kamera';});}function stopCam(){if(videoStream){videoStream.getTracks().forEach(function(t){t.stop();});videoStream=null;}}function loadFace(){if(faceLoaded){document.getElementById('fs').className='face-status success';document.getElementById('fs').textContent='Siap!';document.getElementById('sbtn').disabled=false;return;}Promise.all([faceapi.nets.tinyFaceDetector.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model'),faceapi.nets.faceLandmark68Net.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model'),faceapi.nets.faceRecognitionNet.loadFromUri('https://cdn.jsdelivr.net/npm/@vladmandic/face-api/model')]).then(function(){faceLoaded=true;document.getElementById('fs').className='face-status success';document.getElementById('fs').textContent='Siap!';document.getElementById('sbtn').disabled=false;}).catch(function(){document.getElementById('fs').className='face-status error';document.getElementById('fs').textContent='Gagal model';});}function getLoc(){if(navigator.geolocation){navigator.geolocation.getCurrentPosition(function(p){locationData.lat=p.coords.latitude;locationData.lng=p.coords.longitude;fetch('https://nominatim.openstreetmap.org/reverse?format=json&lat='+p.coords.latitude+'&lon='+p.coords.longitude).then(function(r){return r.json();}).then(function(d){locationData.alamat=d.display_name||'';document.getElementById('li').innerHTML='<i data-lucide=\"map-pin\" style=\"width:14px;color:var(--success)\"></i>'+locationData.alamat.substring(0,40)+'...';lucide.createIcons();});},function(){document.getElementById('li').innerHTML='<i data-lucide=\"map-pin\" style=\"width:14px;color:var(--danger)\"></i>Lokasi tidak tersedia';lucide.createIcons();});}}function captureSubmit(){var v=document.getElementById('selfie-video');var cv=document.createElement('canvas');cv.width=v.videoWidth;cv.height=v.videoHeight;cv.getContext('2d').drawImage(v,0,0);document.getElementById('fs').textContent='Memproses...';document.getElementById('sbtn').disabled=true;faceapi.detectSingleFace(cv,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceDescriptor().then(function(d){if(!d){document.getElementById('fs').className='face-status error';document.getElementById('fs').textContent='Wajah tidak terdeteksi';document.getElementById('sbtn').disabled=false;return;}var desc=d.descriptor;var fm=0;if(currentStudent.faceDescriptor){var dist=faceapi.euclideanDistance(desc,currentStudent.faceDescriptor);fm=Math.max(0,1-dist);}var photo=cv.toDataURL('image/jpeg',0.3);stopCam();google.script.run.withSuccessHandler(function(r){if(r.success){showSuccess(r.status,r.time,fm);}else{showToast('Gagal','error');showOptions();}}).recordAttendance({studentId:currentStudent.id,nama:currentStudent.nama,kelas:currentStudent.kelas,fotoAbsen:photo,faceMatch:fm,latitude:locationData.lat,longitude:locationData.lng,alamat:locationData.alamat});});}function showSick(){var c=document.getElementById('student-content');c.innerHTML='<div class=\"form-group\"><label class=\"form-label\">Barcode / NIS / NIP</label><input type=\"text\" class=\"form-input\" id=\"sbc\"></div><div class=\"form-group\"><label class=\"form-label\">Status</label><select class=\"form-select\" id=\"sst\"><option value=\"Sakit\">Sakit</option><option value=\"Izin\">Izin</option></select></div><div class=\"form-group\"><label class=\"form-label\">Keterangan</label><textarea class=\"form-input\" id=\"skt\" rows=\"3\"></textarea></div><button class=\"btn btn-primary\" style=\"width:100%\" onclick=\"submitSick()\">Kirim</button><button class=\"btn btn-outline\" style=\"width:100%;margin-top:10px\" onclick=\"showOptions()\">Batal</button>';}function submitSick(){var bc=document.getElementById('sbc').value.trim();var st=document.getElementById('sst').value;var kt=document.getElementById('skt').value;if(!bc){showToast('Isi barcode','error');return;}google.script.run.withSuccessHandler(function(r){if(r.success){currentStudent=r.student;google.script.run.withSuccessHandler(function(r2){if(r2.success)showSuccess(r2.status,r2.time,0);}).submitSickLeave({studentId:currentStudent.id,nama:currentStudent.nama,kelas:currentStudent.kelas,status:st,keterangan:kt});}else showToast('Tidak ditemukan','error');}).getPersonByBarcode(bc);}function showSuccess(st,tm,fm){var c=document.getElementById('student-content');var col=st==='Terlambat'?'var(--warning)':'var(--success)';c.innerHTML='<div class=\"success-screen\"><div class=\"success-icon\" style=\"background:'+col+'\"><i data-lucide=\"check\" style=\"width:40px;height:40px\"></i></div><h2 style=\"color:'+col+';margin-bottom:10px\">Berhasil!</h2><strong>'+currentStudent.nama+'</strong><p style=\"color:var(--gray)\">'+currentStudent.kelas+'</p><div style=\"background:var(--bg);padding:15px;border-radius:8px;margin:20px 0;text-align:left\"><p><strong>Status:</strong> '+st+'</p><p><strong>Waktu:</strong> '+tm+'</p><p><strong>Verifikasi:</strong> '+(fm>0.5?'Cocok':'Tidak')+'</p></div><button class=\"btn btn-primary\" style=\"width:100%\" onclick=\"showOptions()\">Selesai</button></div>';lucide.createIcons();}function showToast(m,t){var tc=document.getElementById('toasts');var d=document.createElement('div');d.className='toast '+(t==='error'?'error':'');d.textContent=m;tc.appendChild(d);setTimeout(function(){d.remove();},3000);}function getInit(n){if(!n)return'?';var p=n.split(' ');return p.length>=2?(p[0][0]+p[1][0]).toUpperCase():n.substring(0,2).toUpperCase();}";
}

function getSpreadsheet(){if(SPREADSHEET_ID)return SpreadsheetApp.openById(SPREADSHEET_ID);return SpreadsheetApp.getActiveSpreadsheet();}
function getSheet(n){var ss=getSpreadsheet();var s=ss.getSheetByName(n);if(!s)s=ss.insertSheet(n);return s;}

function setupSpreadsheet(){var ss=getSpreadsheet();var s1=ss.getSheetByName(SHEET_SISWA);if(!s1)s1=ss.insertSheet(SHEET_SISWA);if(s1.getLastRow()===0){s1.appendRow(["ID","Barcode","Nama","Kelas","JK","Foto","Status","FaceDescriptor","Email"]);}else{var h=s1.getRange(1,1,1,9).getValues()[0];if(h.length<9||h[8]!=="Email")s1.getRange(1,9).setValue("Email");}var sG=ss.getSheetByName(SHEET_GURU);if(!sG)sG=ss.insertSheet(SHEET_GURU);if(sG.getLastRow()===0){sG.appendRow(["ID","NIP","Nama","Jabatan","JK","Foto","Status","FaceDescriptor","Email"]);}else{var h=sG.getRange(1,1,1,9).getValues()[0];if(h.length<9||h[8]!=="Email")sG.getRange(1,9).setValue("Email");}var s2=ss.getSheetByName(SHEET_ABSENSI);if(!s2)s2=ss.insertSheet(SHEET_ABSENSI);if(s2.getLastRow()===0)s2.appendRow(["ID","StudentID","Nama","Kelas","Tanggal","Waktu","Status","FotoAbsen","FaceMatch","Lat","Lng","Alamat","Device","Type"]);var s3=ss.getSheetByName(SHEET_SETTINGS);if(!s3)s3=ss.insertSheet(SHEET_SETTINGS);if(s3.getLastRow()===0){s3.appendRow(["Key","Value"]);s3.appendRow(["jamTerlambat","07:15"]);s3.appendRow(["faceMatchThreshold","0.5"]);}return "Setup berhasil! Kolom Email ditambahkan.";}

function getSettings(){var s=getSheet(SHEET_SETTINGS);var d=s.getDataRange().getValues();var r={};for(var i=1;i<d.length;i++)r[d[i][0]]=d[i][1];return {success:true,settings:r};}

function updateSettings(ns){var s=getSheet(SHEET_SETTINGS);for(var k in ns){var found=false;var rows=s.getDataRange().getValues();for(var i=1;i<rows.length;i++){if(rows[i][0]===k){s.getRange(i+1,2).setValue(ns[k]);found=true;break;}}if(!found)s.appendRow([k,ns[k]]);}return {success:true};}

function getStudentList(){var s=getSheet(SHEET_SISWA);var d=s.getDataRange().getValues();var arr=[];for(var i=1;i<d.length;i++)arr.push({id:String(d[i][0]),barcode:String(d[i][1]),nama:String(d[i][2]),kelas:String(d[i][3]),foto:String(d[i][5]||""),status:String(d[i][6]||"Aktif"),hasFace:d[i][7]?true:false,email:String(d[i][8]||"")});return {success:true,students:arr};}

function getTeacherList(){
  var s = getSheet(SHEET_GURU);
  var d = s.getDataRange().getValues();
  var arr = [];
  for(var i=1;i<d.length;i++){
    arr.push({
      id:String(d[i][0]),
      nip:String(d[i][1]),
      nama:String(d[i][2]),
      jabatan:String(d[i][3]),
      foto:String(d[i][5]||""),
      status:String(d[i][6]||"Aktif"),
      hasFace:d[i][7]?true:false,
      email:String(d[i][8]||"")
    });
  }
  return {success:true, teachers:arr};
  return {success:true, teachers:arr};
}

function getClassList(){
  var s = getSheet(SHEET_KELAS);
  var d = s.getDataRange().getValues();
  var arr = [];
  for(var i=1;i<d.length;i++){
    arr.push({
      nama: String(d[i][0]||""),
      wali: String(d[i][1]||""),
      email: String(d[i][2]||"")
    });
  }
  return {success:true, classes:arr};
}

function addClass(d){
  var s = getSheet(SHEET_KELAS);
  s.appendRow([d.nama, d.wali, d.email]);
  return {success:true};
}

function deleteClass(nama){
  var s = getSheet(SHEET_KELAS);
  var rows = s.getDataRange().getValues();
  for(var i=1;i<rows.length;i++){
    if(String(rows[i][0]) === String(nama)){
      s.deleteRow(i+1);
      return {success:true};
    }
  }
  return {success:false, error:'Kelas tidak ditemukan'};
}

function getPersonByBarcode(bc){
  // Check Siswa
  var s = getSheet(SHEET_SISWA);
  var d = s.getDataRange().getValues();
  for(var i=1;i<d.length;i++){
    if(String(d[i][1]) === String(bc)){
      var desc = null;
      if(d[i][7]){ try{desc=JSON.parse(d[i][7]);}catch(e){} }
      return {success:true, type:'siswa', student:{id:String(d[i][0]), barcode:String(d[i][1]), nama:String(d[i][2]), kelas:String(d[i][3]), foto:String(d[i][5]||""), faceDescriptor:desc, email:String(d[i][8]||"")}};
    }
  }
  
  // Check Guru
  var sg = getSheet(SHEET_GURU);
  var dg = sg.getDataRange().getValues();
  for(var j=1;j<dg.length;j++){
    if(String(dg[j][1]) === String(bc)){
      var descG = null;
      if(dg[j][7]){ try{descG=JSON.parse(dg[j][7]);}catch(e){} }
      return {success:true, type:'guru', student:{id:String(dg[j][0]), barcode:String(dg[j][1]), nama:String(dg[j][2]), kelas:String(dg[j][3]), foto:String(dg[j][5]||""), faceDescriptor:descG, email:String(dg[j][8]||"")}}; // Mapping keys to 'student' structure for frontend compatibility
    }
  }
  
  return {success:false};
}

function addTeacher(d){
  var s = getSheet(SHEET_GURU);
  var id = "TCH"+Date.now();
  s.appendRow([id, d.nip, d.nama, d.jabatan, "", "", "Aktif", "", d.email||""]);
  return {success:true};
}

function deleteTeacher(id){
   var s = getSheet(SHEET_GURU);
   var rows = s.getDataRange().getValues();
   for(var i=1;i<rows.length;i++){
     if(rows[i][0]===id){
       s.deleteRow(i+1);
       return {success:true};
     }
   }
   return {success:false};
}

function addStudent(d){var s=getSheet(SHEET_SISWA);var id="STD"+Date.now();s.appendRow([id,d.barcode,d.nama,d.kelas,"","","Aktif","",d.email||""]);return {success:true,id:id};}

function deleteStudent(id){var s=getSheet(SHEET_SISWA);var rows=s.getDataRange().getValues();for(var i=1;i<rows.length;i++){if(rows[i][0]===id){s.deleteRow(i+1);return {success:true};}}return {success:false};}

function registerFace(d){
  // Try Siswa
  var s = getSheet(SHEET_SISWA);
  var rows = s.getDataRange().getValues();
  for(var i=1;i<rows.length;i++){
    if(rows[i][0]===d.studentId){
      if(d.fotoURL) s.getRange(i+1,6).setValue(d.fotoURL);
      s.getRange(i+1,8).setValue(JSON.stringify(d.faceDescriptor));
      return {success:true};
    }
  }
  // Try Guru
  var sg = getSheet(SHEET_GURU);
  var rowsG = sg.getDataRange().getValues();
  for(var j=1;j<rowsG.length;j++){
    if(rowsG[j][0]===d.studentId){
      if(d.fotoURL) sg.getRange(j+1,6).setValue(d.fotoURL);
      sg.getRange(j+1,8).setValue(JSON.stringify(d.faceDescriptor));
      return {success:true};
    }
  }
  return {success:false};
}

function recordAttendance(d){
  var s = getSheet(SHEET_ABSENSI);
  var set = getSettings().settings||{};
  var now = new Date();
  var today = Utilities.formatDate(now,"Asia/Jakarta","yyyy-MM-dd");
  var time = Utilities.formatDate(now,"Asia/Jakarta","HH:mm:ss");
  var jt = set.jamTerlambat||"07:15";
  var ct = Utilities.formatDate(now,"Asia/Jakarta","HH:mm");
  var st = ct > jt ? "Terlambat" : "Hadir";
  var id = "ATT"+Date.now();
  
  // Determine Type based on ID prefix or passed data (assuming ID starts with TCH for Teacher)
  var type = (d.studentId && d.studentId.indexOf("TCH") === 0) ? "Guru" : "Siswa";

  s.appendRow([id,d.studentId,d.nama,d.kelas,today,time,st,d.fotoAbsen||"",d.faceMatch||0,d.latitude||"",d.longitude||"",d.alamat||"","", type]);
  return {success:true, id:id, status:st, time:time};
}

function submitSickLeave(d){var s=getSheet(SHEET_ABSENSI);var now=new Date();var today=Utilities.formatDate(now,"Asia/Jakarta","yyyy-MM-dd");var time=Utilities.formatDate(now,"Asia/Jakarta","HH:mm:ss");var id="ATT"+Date.now();s.appendRow([id,d.studentId,d.nama,d.kelas,today,time,d.status,"",0,"","",d.keterangan||"","","Siswa"]);return {success:true,id:id,status:d.status,time:time};}

function getAttendanceToday(dateStr){var s=getSheet(SHEET_ABSENSI);var d=s.getDataRange().getDisplayValues();var targetDate=dateStr||Utilities.formatDate(new Date(),"Asia/Jakarta","yyyy-MM-dd");var map={};for(var i=1;i<d.length;i++){if(String(d[i][4]).trim()===targetDate){var sid=String(d[i][1]);map[sid]={id:d[i][0],nama:d[i][2],kelas:d[i][3],tanggal:d[i][4],waktu:d[i][5],status:d[i][6],fotoAbsen:d[i][7]||"",faceMatch:parseFloat(d[i][8])||0,alamat:d[i][11]||"",type:d[i][13]||"Siswa"};}}var arr=[];for(var k in map)arr.push(map[k]);return {success:true,attendance:arr,date:targetDate};}

function getStats(type){
  var isGuru = (String(type).toLowerCase() === "guru");
  var sheetName = isGuru ? SHEET_GURU : SHEET_SISWA;
  var today = Utilities.formatDate(new Date(),"Asia/Jakarta","yyyy-MM-dd");
  
  var personSheet = getSheet(sheetName).getDataRange().getValues();
  var absen = getSheet(SHEET_ABSENSI).getDataRange().getDisplayValues();
  
  var total = 0;
  for(var i=1;i<personSheet.length;i++) if(personSheet[i][6]==="Aktif") total++;
  
  var map = {};
  for(var j=1;j<absen.length;j++){
    if(String(absen[j][4]).trim()===today){
       // Filter by Type
       var valType = absen[j][13];
       if(isGuru && valType && valType !== "Guru") continue;
       if(!isGuru && valType && valType === "Guru") continue;
       
       map[absen[j][1]] = absen[j][6];
    }
  }
  
  var h=0,t=0,sk=0,iz=0;
  for(var k in map){
    if(map[k]==="Hadir") h++;
    if(map[k]==="Terlambat") t++;
    if(map[k]==="Sakit") sk++;
    if(map[k]==="Izin") iz++;
  }
  
  return {success:true, stats:{totalHadir:h, totalTerlambat:t, totalSakit:sk, totalIzin:iz, belumAbsen:total-(h+t+sk+iz), totalSiswa:total}};
}

function getMonthlyRecap(m, y, type){
  // Default to Siswa if type not specified
  var isGuru = (String(type).toLowerCase() === "guru");
  var sheetName = isGuru ? SHEET_GURU : SHEET_SISWA;
  
  var personSheet = getSheet(sheetName).getDataRange().getValues();
  var absen = getSheet(SHEET_ABSENSI).getDataRange().getDisplayValues();
  var arr = [];
  
  // Get active people list
  for(var i=1;i<personSheet.length;i++){
    if(personSheet[i][6]==="Aktif"){
       // For Guru, col 3 is Jabatan. For Siswa, col 3 is Kelas.
       var kls = isGuru ? String(personSheet[i][3]) : String(personSheet[i][3]); 
       arr.push({id:String(personSheet[i][0]), nama:String(personSheet[i][2]), kelas:kls});
    }
  }

  var days = new Date(y, m, 0).getDate();
  var map = {};
  for(var s=0;s<arr.length;s++) map[arr[s].id] = {};
  
  // Map attendance
  for(var j=1;j<absen.length;j++){
    var tgl = String(absen[j][4]).trim();
    var p = tgl.split("-");
    if(p.length === 3 && parseInt(p[0]) === y && parseInt(p[1]) === m){
      var sid = String(absen[j][1]);
      // Optional: Filter by Type if column exists, or just rely on ID matching
      // Since map only contains keys for the requested type (from personSheet), non-matching IDs will be ignored.
      var valType = absen[j][13]; // If Type column exists
      if(isGuru && valType && valType !== "Guru") continue; // Extra safety
      
      var day = parseInt(p[2]);
      var st = absen[j][6];
      var c = st==="Hadir"?"H":st==="Terlambat"?"T":st==="Sakit"?"S":st==="Izin"?"I":"A";
      if(map[sid]) map[sid][day] = c;
    }
  }
  
  var recap = [];
  for(var k=0;k<arr.length;k++){
    var p = arr[k];
    var r = {id:p.id, nama:p.nama, kelas:p.kelas, days:{}, totals:{H:0,T:0,S:0,I:0,A:0}};
    for(var d=1;d<=days;d++){
      var code = map[p.id][d] || "-";
      if(new Date(y,m-1,d) > new Date()) code = "-";
      r.days[d] = code;
      if(code!=="-") r.totals[code]++;
    }
    recap.push(r);
  }
  return {success:true, month:m, year:y, daysInMonth:days, students:recap, type:type};
}

function getClassMap(){
  var s = getSheet(SHEET_KELAS);
  var d = s.getDataRange().getValues();
  var m = {};
  for(var i=1;i<d.length;i++){
    if(d[i][0] && d[i][2]){
      m[String(d[i][0]).trim()] = String(d[i][2]).trim();
    }
  }
  return m;
}

function sendDailyReportToWaliKelas(dateStr){
  var targetDate = dateStr || Utilities.formatDate(new Date(),"Asia/Jakarta","yyyy-MM-dd");
  var attendanceData = getAttendanceToday(targetDate).attendance;
  var classMap = getClassMap();
  var grouped = {};
  for(var i=0;i<attendanceData.length;i++){
    var a = attendanceData[i];
    if(a.type === 'Guru') continue; 
    var kls = a.kelas;
    if(!grouped[kls]) grouped[kls] = [];
    grouped[kls].push(a);
  }
  var sentCount = 0;
  var failedCount = 0;
  var log = [];
  for(var kls in grouped){
    var email = classMap[kls];
    if(email){
      try {
        var subject = "Laporan Kehadiran Kelas " + kls + " - " + targetDate;
        var body = "<h3>Laporan Kehadiran Kelas " + kls + "</h3>";
        body += "<p>Tanggal: " + targetDate + "</p>";
        body += "<table border='1' cellpadding='5' style='border-collapse:collapse;width:100%'><thead><tr style='background:#f3f4f6'><th>Nama</th><th>Status</th><th>Waktu</th><th>Keterangan</th></tr></thead><tbody>";
        var list = grouped[kls];
        for(var j=0;j<list.length;j++){
             var s = list[j];
             var color = s.status==='Hadir'?'#dcfce7':s.status==='Terlambat'?'#fef3c7':'#fee2e2';
             body += "<tr><td>" + s.nama + "</td><td style='background:"+color+"'>" + s.status + "</td><td>" + (s.waktu||"-") + "</td><td>" + (s.alamat?"Lokasi Terdeteksi":"") + "</td></tr>";
        }
        body += "</tbody></table>";
        body += "<p><small>Dikirim otomatis oleh Sistem Absensi LevelUp</small></p>";
        MailApp.sendEmail({to: email, subject: subject, htmlBody: body});
        sentCount++;
        log.push("Sent to " + kls);
      } catch(e) {
        failedCount++;
        log.push("Failed " + kls + ": " + e.message);
      }
    } else {
        log.push("No email for " + kls);
    }
  }
  return {success:true, sent:sentCount, failed:failedCount, log:log};
}


Tidak ada komentar:

Posting Komentar

Google Apps Script Beginner

  https://www.youtube.com/watch?v=YVGZI6IEN3I&list=PL_xiAt6o4ZXwYTKr7G6R_ajM7C7UZ32fB&index=8