Minggu, 12 April 2026

MPA - Aplikasi Monev SMA DT

 












Kesimpulan Spesifikasi Aplikasi: Monev SMA Double Track

Aplikasi ini merupakan sistem informasi berbasis web yang dirancang untuk memantau dan mengevaluasi (Monev) progres program SMA Double Track secara real-time.

1. Informasi Umum

  • Nama Aplikasi: Monitoring & Evaluasi (Monev) SMA Double Track.

  • Target Pengguna: Admin Pusat, Koordinator Sekolah, dan Instruktur/Trainer.

  • Tujuan Utama: Integrasi data pelatihan, sertifikasi siswa, manajemen sekolah, dan dokumentasi jurnal harian.

2. Arsitektur & Teknologi

  • Arsitektur: Multi-Page Application (MPA) yang disimulasikan melalui perutean sisi klien (client-side routing) di dalam satu lingkungan utama.

  • Frontend:

    • UI Framework: Tailwind CSS (Desain modern, responsif, dan berbasis utilitas).

    • Tipografi: Plus Jakarta Sans (Google Fonts).

    • Ikon: Font Awesome 6.0.0.

    • Visualisasi: Chart.js (Grafik batang dan donat).

  • Backend: Google Apps Script (GAS) menggunakan fungsi doGet dan google.script.run.

  • Database: Google Sheets (SpreadsheetApp).

  • Penyimpanan Media: Google Drive (menggunakan ID Folder spesifik untuk foto jurnal).

3. Fitur Utama

  • Landing Page: Halaman muka profesional dengan Hero Section untuk pengenalan sistem.

  • Dashboard Real-time:

    • KPI Cards: Menampilkan statistik total siswa, persentase sertifikasi, jumlah sekolah, dan jumlah trainer.

    • Data Visual: Grafik sebaran siswa per bidang keahlian dan status kelulusan.

    • Aktivitas Terbaru: Feed jurnal pelatihan yang menampilkan deskripsi singkat dan foto dokumentasi.

  • Manajemen Input Data:

    • Input Siswa: Registrasi peserta, kelas, bidang, dan jam pelatihan.

    • Input Sekolah: Pendataan NPSN, alamat, dan pimpinan sekolah.

    • Input Trainer: Database instruktur berdasarkan spesialisasi.

    • Input Jurnal Pelatihan: Laporan harian aktivitas yang dilengkapi fitur unggah foto.

  • Sistem Pelaporan:

    • Tabel detail data peserta sertifikasi.

    • Fitur Cetak Laporan yang dioptimalkan untuk printer/PDF (menyembunyikan elemen navigasi saat dicetak).

4. Fitur Teknis Khusus

  • Client-side Image Compression: Mengompresi foto secara otomatis menggunakan HTML5 Canvas sebelum dikirim ke server untuk menghemat kuota dan mempercepat proses upload.

  • Direct Image Linking: Mengonversi ID file Google Drive menjadi URL lh3.googleusercontent.com agar foto dapat dimuat langsung sebagai gambar di dashboard.

  • UX/UI Enhancements:

    • Sistem notifikasi Toast untuk umpan balik sukses/gagal.

    • Loading Overlay dengan pesan status yang dinamis.

    • Desain navigasi Glassmorphism yang tetap berada di atas (sticky).

5. Struktur Database (Sheet)

  1. Siswa: ID, Nama, Sekolah, Kelas, Bidang, Jam, Status, Timestamp.

  2. Sekolah: ID, Nama Sekolah, NPSN, Alamat, Kepala Sekolah, Timestamp.

  3. Trainer: ID, Nama Trainer, Spesialisasi, Sekolah Asal, Kontak, Timestamp.

  4. Jurnal: ID, Sekolah, Bidang, Kegiatan, Jam, Foto URL, Tanggal.


PROMPT

Master Prompt: Aplikasi Monev SMA Double Track

Gunakan prompt berikut untuk mereplikasi atau membangun ulang aplikasi sistem informasi Monitoring & Evaluasi (Monev) berbasis Google Apps Script dengan spesifikasi yang telah dioptimalkan.

Role: Bertindak sebagai Senior Full-stack Developer ahli Google Apps Script, Tailwind CSS, dan integrasi Google Workspace.

Tujuan: Bangun aplikasi web MPA (Multi-Page Application) untuk "Monitoring & Evaluasi SMA Double Track" dengan fitur utama input data terpadu, dashboard real-time, dan sistem unggah dokumentasi.

Spesifikasi Teknis:

  1. Frontend: HTML5, Tailwind CSS (Glassmorphism design), Chart.js untuk visualisasi, dan Font Awesome untuk ikon.

  2. Backend: Google Apps Script (GAS) sebagai server-side engine.

  3. Database: Google Sheets dengan 4 sheet utama: Siswa, Sekolah, Trainer, dan Jurnal.

  4. Penyimpanan: Google Drive untuk foto jurnal menggunakan ID Folder spesifik.

Struktur Halaman (MPA Simulation):

  • Landing Page: Hero section yang modern dengan narasi program Double Track.

  • Halaman Input (Terpisah): - Form Input Siswa (Personalia & Sertifikasi).

    • Form Input Sekolah (NPSN & Alamat).

    • Form Input Trainer (Spesialisasi & Kontak).

    • Form Input Jurnal (Kegiatan & Upload Foto).

  • Dashboard: Menampilkan KPI Cards (Total Siswa, % Sertifikasi, dsb), Grafik Bar (Bidang), dan Feed Jurnal terbaru.

  • Laporan: Tabel detail yang memiliki fitur Cetak Laporan (print-friendly CSS).

Fitur Khusus (Wajib Ada):

  • Client-side Image Compression: Sebelum diunggah, foto harus dikompresi menggunakan HTML5 Canvas untuk mempercepat proses upload dan menghemat penyimpanan.

  • Direct Image Link: Gunakan format URL lh3.googleusercontent.com/d/[FILE_ID] agar foto Drive dapat tampil langsung di tag <img> tanpa kendala izin pratinjau.

  • UI/UX: Gunakan font 'Plus Jakarta Sans', loading overlay dengan pesan status dinamis, dan sistem toast notification untuk feedback user.

  • Mocking System: Tambahkan logika agar aplikasi tetap bisa berjalan (display data simulasi) saat dijalankan di lingkungan preview/lokal di mana API google.script.run tidak tersedia.

Variabel Konfigurasi di code.gs:

  • SPREADSHEET_ID: Mengambil ID aktif secara otomatis.

  • FOLDER_ID: Variabel string untuk menyimpan ID folder Google Drive tujuan upload.

Output yang diharapkan: Berikan kode lengkap dalam satu file index.html (termasuk CSS & JS) dan satu file code.gs yang siap dideploy sebagai Web App.

Code.gs

/**
 * KONFIGURASI SISTEM MONEV SMA DOUBLE TRACK (MPA VERSION)
 */
const SPREADSHEET_ID = SpreadsheetApp.getActiveSpreadsheet().getId();

// PENTING: GANTI DENGAN ID FOLDER GOOGLE DRIVE ANDA
const FOLDER_ID = "151ZtS5n_iIeHAZW85MY8a5fa7MnqKIvc";

/**
 * Perutean Utama (MPA Logic)
 */
function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle("Monev SMA Double Track")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

/**
 * Inisialisasi Seluruh Database
 */
function setupDatabase() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheets = [
    { name: "Siswa", headers: ["id", "nama", "sekolah", "kelas", "bidang", "jam_pelatihan", "status_sertifikasi", "created_at"] },
    { name: "Sekolah", headers: ["id", "nama_sekolah", "npsn", "alamat", "kepala_sekolah", "created_at"] },
    { name: "Trainer", headers: ["id", "nama_trainer", "spesialisasi", "sekolah_asal", "kontak", "created_at"] },
    { name: "Jurnal", headers: ["id", "sekolah", "bidang", "kegiatan", "jam", "foto_url", "tanggal"] }
  ];

  sheets.forEach(s => {
    let sheet = ss.getSheetByName(s.name);
    if (!sheet) {
      sheet = ss.insertSheet(s.name);
      sheet.appendRow(s.headers);
      sheet.getRange(1, 1, 1, s.headers.length).setFontWeight("bold").setBackground("#f3f4f6");
    }
  });
}

/**
 * Fungsi Simpan Data Universal
 */
function saveFormData(type, data) {
  try {
    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    const sheetName = capitalizeFirstLetter(type);
    const sheet = ss.getSheetByName(sheetName);
    if (!sheet) throw new Error("Sheet '" + sheetName + "' tidak ditemukan.");

    let fileUrl = "";
   
    // Khusus untuk Jurnal Pelatihan yang ada upload fotonya
    if (type === 'jurnal' && data.fileData && data.fileName) {
      if (FOLDER_ID) {
        fileUrl = uploadToDriveAndGetLh3(data.fileData, data.fileName, data.fileMimeType);
      } else {
        fileUrl = "ID_FOLDER_KOSONG";
      }
    }

    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const row = headers.map(header => {
      if (header === 'id') return Utilities.getUuid();
      if (header === 'foto_url') return fileUrl;
      if (header === 'created_at' || header === 'tanggal') return new Date();
      return data[header] || "";
    });

    sheet.appendRow(row);
    return { success: true, message: "Data " + sheetName + " berhasil disimpan" };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

/**
 * Upload ke Drive dan ambil link lh3 (Direct Link)
 */
function uploadToDriveAndGetLh3(base64Data, fileName, mimeType) {
  try {
    const folder = DriveApp.getFolderById(FOLDER_ID);
    const decoded = Utilities.base64Decode(base64Data);
    const blob = Utilities.newBlob(decoded, mimeType || "image/jpeg", fileName);
    const file = folder.createFile(blob);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    return "https://lh3.googleusercontent.com/d/" + file.getId();
  } catch (e) {
    return "UPLOAD_ERROR: " + e.message;
  }
}

/**
 * Agregasi Data Dashboard (Gabungan)
 */
function getDashboardData() {
  try {
    const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
    const siswa = getSheetData(ss.getSheetByName("Siswa"));
    const jurnal = getSheetData(ss.getSheetByName("Jurnal"));
    const sekolah = getSheetData(ss.getSheetByName("Sekolah"));
    const trainer = getSheetData(ss.getSheetByName("Trainer"));
   
    // Hitung KPI
    const totalSiswa = siswa.length;
    const totalSekolah = sekolah.length;
    const totalTrainer = trainer.length;
    const lulus = siswa.filter(s => s.status_sertifikasi === 'Lulus').length;
    const sertifikasiRate = totalSiswa > 0 ? Math.round((lulus / totalSiswa) * 100) : 0;

    // Grafik Bidang
    const bidangCounts = {};
    siswa.forEach(s => bidangCounts[s.bidang] = (bidangCounts[s.bidang] || 0) + 1);

    return {
      stats: { totalSiswa, totalSekolah, totalTrainer, sertifikasiRate },
      bidangCounts,
      recentJurnal: jurnal.sort((a,b) => new Date(b.tanggal) - new Date(a.tanggal)).slice(0, 10),
      allSiswa: siswa // Digunakan untuk laporan detail
    };
  } catch (e) {
    return { error: e.message };
  }
}

function getSheetData(sheet) {
  if (!sheet) return [];
  const rows = sheet.getDataRange().getValues();
  if (rows.length < 2) return [];
  const headers = rows.shift();
  return rows.map(row => {
    let obj = {};
    headers.forEach((h, i) => {
      let val = row[i];
      if (val instanceof Date) val = val.toISOString();
      obj[h] = val;
    });
    return obj;
  });
}

function capitalizeFirstLetter(string) {
  if (!string) return "";
  return string.charAt(0).toUpperCase() + string.slice(1);
}

Index.html

<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Monev SMA Double Track</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600;700&display=swap');
        body { font-family: 'Plus Jakarta Sans', sans-serif; scroll-behavior: smooth; }
        .hero-gradient { background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); }
        .glass-nav { background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); }
        .loading-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.95); z-index: 9999; justify-content: center; align-items: center; }
        @media print { .no-print { display: none !important; } .print-only { display: block !important; } }
        .print-only { display: none; }
    </style>
</head>
<body class="bg-slate-50 min-h-screen">

    <!-- Loading Animation -->
    <div id="loading" class="loading-overlay">
        <div class="text-center">
            <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-blue-600 border-opacity-50 mx-auto"></div>
            <p id="loading-text" class="mt-4 font-bold text-slate-700">Sedang Memproses...</p>
        </div>
    </div>

    <!-- Navigation -->
    <nav class="glass-nav sticky top-0 z-50 border-b border-slate-200 no-print">
        <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
            <div class="flex justify-between h-16 items-center">
                <div class="flex items-center gap-2 cursor-pointer" onclick="navigate('landing')">
                    <div class="bg-blue-600 p-2 rounded-lg text-white"><i class="fas fa-graduation-cap"></i></div>
                    <span class="font-bold text-xl text-slate-800 tracking-tight">Monev SMA DT</span>
                </div>
                <div class="hidden md:flex space-x-6 text-sm font-semibold text-slate-600">
                    <button onclick="navigate('landing')" class="hover:text-blue-600 transition">Beranda</button>
                    <button onclick="navigate('dashboard')" class="hover:text-blue-600 transition">Dashboard</button>
                    <div class="relative group">
                        <button class="hover:text-blue-600 transition flex items-center gap-1">Input Data <i class="fas fa-chevron-down text-[10px]"></i></button>
                        <div class="absolute right-0 w-48 bg-white border rounded-xl shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all p-2 space-y-1">
                            <button onclick="navigate('input-siswa')" class="w-full text-left p-2 hover:bg-slate-50 rounded-lg">Data Siswa</button>
                            <button onclick="navigate('input-sekolah')" class="w-full text-left p-2 hover:bg-slate-50 rounded-lg">Data Sekolah</button>
                            <button onclick="navigate('input-trainer')" class="w-full text-left p-2 hover:bg-slate-50 rounded-lg">Data Trainer</button>
                            <button onclick="navigate('input-jurnal')" class="w-full text-left p-2 hover:bg-slate-50 rounded-lg">Jurnal Pelatihan</button>
                        </div>
                    </div>
                </div>
                <button onclick="navigate('dashboard')" class="bg-blue-600 text-white px-5 py-2 rounded-full text-sm font-bold shadow-lg hover:shadow-blue-200 transition">Lihat Laporan</button>
            </div>
        </div>
    </nav>

    <!-- CONTENT WRAPPER -->
    <div id="main-content">
       
        <!-- PAGE: LANDING -->
        <section id="page-landing" class="page-section">
            <div class="hero-gradient text-white py-24 px-6 relative overflow-hidden">
                <div class="max-w-7xl mx-auto flex flex-col md:flex-row items-center justify-between gap-12 relative z-10">
                    <div class="md:w-1/2">
                        <span class="bg-blue-500 bg-opacity-30 text-blue-100 px-4 py-1 rounded-full text-xs font-bold uppercase tracking-widest">Sistem Informasi SMA DT</span>
                        <h1 class="text-5xl md:text-6xl font-bold mt-4 leading-tight">Monitoring & Evaluasi <br><span class="text-blue-200">SMA Double Track</span></h1>
                        <p class="text-lg text-blue-100 mt-6 leading-relaxed">Kelola data pelatihan siswa, sertifikasi, dan jurnal harian dalam satu platform terintegrasi. Real-time, Akurat, dan Profesional.</p>
                        <div class="mt-10 flex gap-4">
                            <button onclick="navigate('dashboard')" class="bg-white text-blue-600 px-8 py-4 rounded-xl font-bold shadow-xl hover:scale-105 transition">Masuk Dashboard</button>
                            <button onclick="navigate('input-jurnal')" class="border-2 border-white border-opacity-30 px-8 py-4 rounded-xl font-bold hover:bg-white hover:text-blue-600 transition">Input Jurnal</button>
                        </div>
                    </div>
                    <div class="md:w-1/2 flex justify-center">
                        <div class="relative">
                            <div class="absolute -top-10 -left-10 w-40 h-40 bg-blue-400 rounded-full mix-blend-multiply filter blur-2xl opacity-30 animate-pulse"></div>
                            <div class="bg-white p-8 rounded-3xl shadow-2xl border-4 border-blue-400 border-opacity-20">
                                <i class="fas fa-chart-pie text-[100px] text-blue-500"></i>
                                <div class="mt-6 text-center text-slate-800">
                                    <p class="text-3xl font-bold">100%</p>
                                    <p class="text-sm font-semibold text-slate-500 uppercase">Akurasi Data</p>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </section>

        <!-- PAGE: INPUT DATA (Common Form Container) -->
        <section id="page-input-form" class="page-section hidden py-16 px-6">
            <div class="max-w-2xl mx-auto">
                <div class="bg-white rounded-3xl shadow-2xl border border-slate-100 overflow-hidden">
                    <div id="form-header" class="bg-slate-50 p-8 border-b">
                        <h2 id="form-title" class="text-2xl font-bold text-slate-800">Input Data</h2>
                        <p id="form-desc" class="text-slate-500 text-sm mt-1">Silakan isi formulir dengan lengkap.</p>
                    </div>
                   
                    <div class="p-8">
                        <!-- FORM SISWA -->
                        <form id="form-siswa" class="space-y-4 hidden" onsubmit="submitForm(event, 'siswa')">
                            <input type="text" name="nama" placeholder="Nama Lengkap Siswa" required class="w-full p-4 bg-slate-50 border rounded-2xl focus:ring-2 focus:ring-blue-500">
                            <input type="text" name="sekolah" placeholder="Nama Sekolah" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <div class="grid grid-cols-2 gap-4">
                                <input type="text" name="kelas" placeholder="Kelas" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                                <input type="text" name="bidang" placeholder="Bidang Keahlian" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            </div>
                            <input type="number" name="jam_pelatihan" placeholder="Total Jam Pelatihan" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <select name="status_sertifikasi" class="w-full p-4 bg-slate-50 border rounded-2xl">
                                <option>Terdaftar</option>
                                <option>Lulus</option>
                                <option>Tidak Lulus</option>
                            </select>
                            <button type="submit" class="w-full bg-blue-600 text-white font-bold py-4 rounded-2xl shadow-lg">Simpan Data Siswa</button>
                        </form>

                        <!-- FORM SEKOLAH -->
                        <form id="form-sekolah" class="space-y-4 hidden" onsubmit="submitForm(event, 'sekolah')">
                            <input type="text" name="nama_sekolah" placeholder="Nama Sekolah" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="npsn" placeholder="NPSN" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="alamat" placeholder="Alamat Sekolah" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="kepala_sekolah" placeholder="Nama Kepala Sekolah" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <button type="submit" class="w-full bg-slate-800 text-white font-bold py-4 rounded-2xl shadow-lg">Daftarkan Sekolah</button>
                        </form>

                        <!-- FORM TRAINER -->
                        <form id="form-trainer" class="space-y-4 hidden" onsubmit="submitForm(event, 'trainer')">
                            <input type="text" name="nama_trainer" placeholder="Nama Trainer" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="spesialisasi" placeholder="Spesialisasi" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="sekolah_asal" placeholder="Sekolah Asal" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="kontak" placeholder="Kontak (WA/Email)" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <button type="submit" class="w-full bg-indigo-600 text-white font-bold py-4 rounded-2xl shadow-lg">Simpan Data Trainer</button>
                        </form>

                        <!-- FORM JURNAL (DENGAN UPLOAD FOTO) -->
                        <form id="form-jurnal" class="space-y-4 hidden" onsubmit="submitForm(event, 'jurnal')">
                            <input type="text" name="sekolah" placeholder="Nama Sekolah" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <input type="text" name="bidang" placeholder="Bidang Keahlian" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                            <textarea name="kegiatan" placeholder="Deskripsi Jurnal Pelatihan Hari Ini" required class="w-full p-4 bg-slate-50 border rounded-2xl h-32"></textarea>
                            <input type="number" name="jam" placeholder="Jumlah Jam Pelatihan" required class="w-full p-4 bg-slate-50 border rounded-2xl">
                           
                            <div class="border-2 border-dashed border-slate-200 p-6 rounded-2xl text-center hover:bg-slate-50 transition cursor-pointer relative">
                                <input type="file" id="foto-jurnal" accept="image/*" class="absolute inset-0 opacity-0 cursor-pointer" onchange="previewImg(event)">
                                <div id="upload-placeholder">
                                    <i class="fas fa-camera text-3xl text-slate-300 mb-2"></i>
                                    <p class="text-sm text-slate-500">Ketuk untuk pilih foto pelatihan</p>
                                </div>
                                <img id="jurnal-preview" class="hidden mx-auto h-40 rounded-xl shadow-md border-4 border-white">
                            </div>
                           
                            <button type="submit" class="w-full bg-teal-600 text-white font-bold py-4 rounded-2xl shadow-lg">Unggah Jurnal & Foto</button>
                        </form>
                    </div>
                </div>
            </div>
        </section>

        <!-- PAGE: DASHBOARD -->
        <section id="page-dashboard" class="page-section hidden py-12 px-6">
            <div class="max-w-7xl mx-auto">
                <div class="flex justify-between items-end mb-8 no-print">
                    <div>
                        <h2 class="text-3xl font-bold text-slate-800">Dashboard Real-time</h2>
                        <p class="text-slate-500">Pantau progres Double Track seluruh sekolah.</p>
                    </div>
                    <button onclick="window.print()" class="bg-white border p-3 rounded-xl shadow-sm hover:bg-slate-50 transition font-bold text-slate-700 flex items-center gap-2">
                        <i class="fas fa-print"></i> Cetak Laporan
                    </button>
                </div>

                <!-- KPI CARDS -->
                <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
                    <div class="bg-white p-6 rounded-3xl border shadow-sm">
                        <p class="text-slate-500 text-sm font-semibold uppercase">Total Siswa</p>
                        <h3 id="dash-total-siswa" class="text-4xl font-bold mt-2">0</h3>
                    </div>
                    <div class="bg-white p-6 rounded-3xl border shadow-sm">
                        <p class="text-slate-500 text-sm font-semibold uppercase">% Sertifikasi</p>
                        <h3 id="dash-sertif-rate" class="text-4xl font-bold mt-2 text-green-600">0%</h3>
                    </div>
                    <div class="bg-white p-6 rounded-3xl border shadow-sm">
                        <p class="text-slate-500 text-sm font-semibold uppercase">Jml Sekolah</p>
                        <h3 id="dash-total-sekolah" class="text-4xl font-bold mt-2 text-blue-600">0</h3>
                    </div>
                    <div class="bg-white p-6 rounded-3xl border shadow-sm">
                        <p class="text-slate-500 text-sm font-semibold uppercase">Trainer</p>
                        <h3 id="dash-total-trainer" class="text-4xl font-bold mt-2 text-purple-600">0</h3>
                    </div>
                </div>

                <!-- CHARTS -->
                <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8 no-print">
                    <div class="bg-white p-8 rounded-3xl border shadow-sm h-[400px]">
                        <h4 class="font-bold mb-4">Siswa per Bidang Keahlian</h4>
                        <canvas id="chart-bidang"></canvas>
                    </div>
                    <div class="bg-white p-8 rounded-3xl border shadow-sm overflow-hidden h-[400px] flex flex-col">
                        <h4 class="font-bold mb-4">Aktivitas Jurnal Terbaru</h4>
                        <div id="list-jurnal" class="flex-1 overflow-y-auto space-y-4 pr-2">
                            <!-- Injected Jurnal -->
                        </div>
                    </div>
                </div>

                <!-- DETAILED TABLE FOR PRINT -->
                <div class="bg-white rounded-3xl border shadow-sm overflow-hidden">
                    <div class="p-6 border-b bg-slate-50 flex justify-between items-center no-print">
                        <h4 class="font-bold text-slate-700 uppercase text-sm tracking-widest">Detail Data Peserta Sertifikasi</h4>
                        <button onclick="refreshDashboard()" class="text-blue-600 font-bold text-xs">REFRESH DATA</button>
                    </div>
                    <div class="p-4 print-only text-center mb-6">
                        <h1 class="text-2xl font-bold">LAPORAN MONITORING SMA DOUBLE TRACK</h1>
                        <p>Tanggal Cetak: <span id="print-date"></span></p>
                    </div>
                    <div class="overflow-x-auto">
                        <table class="w-full text-left">
                            <thead class="bg-slate-100 text-slate-500 font-bold text-xs uppercase">
                                <tr>
                                    <th class="px-6 py-4">Nama</th>
                                    <th class="px-6 py-4">Sekolah</th>
                                    <th class="px-6 py-4">Bidang</th>
                                    <th class="px-6 py-4">Jam</th>
                                    <th class="px-6 py-4">Status</th>
                                </tr>
                            </thead>
                            <tbody id="table-laporan-detail" class="divide-y text-sm">
                                <!-- Injected Table -->
                            </tbody>
                        </table>
                    </div>
                </div>
            </div>
        </section>

    </div>

    <!-- Toast Notification -->
    <div id="toast" class="fixed bottom-10 left-1/2 -translate-x-1/2 bg-slate-800 text-white px-8 py-4 rounded-full shadow-2xl transition-all translate-y-24 opacity-0 z-[10000] font-bold flex items-center gap-3">
        <i id="toast-icon" class="fas fa-check-circle text-green-400"></i>
        <span id="toast-msg"></span>
    </div>

    <script>
        // --- ROUTING SYSTEM ---
        function navigate(page) {
            document.querySelectorAll('.page-section').forEach(p => p.classList.add('hidden'));
           
            if(page === 'landing') {
                document.getElementById('page-landing').classList.remove('hidden');
            } else if(page === 'dashboard') {
                document.getElementById('page-dashboard').classList.remove('hidden');
                refreshDashboard();
            } else if(page.startsWith('input-')) {
                const type = page.split('-')[1];
                document.getElementById('page-input-form').classList.remove('hidden');
                setupInputPage(type);
            }
            window.scrollTo(0,0);
        }

        function setupInputPage(type) {
            document.querySelectorAll('#page-input-form form').forEach(f => f.classList.add('hidden'));
            document.getElementById('form-' + type).classList.remove('hidden');
           
            const titles = {
                siswa: "Input Data Siswa",
                sekolah: "Registrasi Sekolah Baru",
                trainer: "Data Trainer & Mentor",
                jurnal: "Input Jurnal Harian Pelatihan"
            };
            const descs = {
                siswa: "Tambahkan peserta baru dalam program Double Track.",
                sekolah: "Daftarkan sekolah penyelenggara program.",
                trainer: "Kelola data tenaga pengajar dan instruktur.",
                jurnal: "Laporkan aktivitas pelatihan harian beserta dokumentasi foto."
            };
            document.getElementById('form-title').innerText = titles[type];
            document.getElementById('form-desc').innerText = descs[type];
        }

        // --- DASHBOARD SYSTEM ---
        let charts = {};
        function refreshDashboard() {
            setLoading(true, "Sinkronisasi Data...");
            google.script.run
                .withSuccessHandler(data => {
                    setLoading(false);
                    if(data.error) return showToast(data.error, 'error');
                   
                    // Stats
                    document.getElementById('dash-total-siswa').innerText = data.stats.totalSiswa;
                    document.getElementById('dash-sertif-rate').innerText = data.stats.sertifikasiRate + "%";
                    document.getElementById('dash-total-sekolah').innerText = data.stats.totalSekolah;
                    document.getElementById('dash-total-trainer').innerText = data.stats.totalTrainer;
                   
                    // Jurnal List
                    const listJurnal = document.getElementById('list-jurnal');
                    listJurnal.innerHTML = data.recentJurnal.length ? '' : '<p class="text-slate-400 text-center py-10">Belum ada jurnal.</p>';
                    data.recentJurnal.forEach(j => {
                        listJurnal.innerHTML += `
                            <div class="flex gap-4 p-4 bg-slate-50 rounded-2xl hover:bg-white border border-transparent hover:border-slate-100 transition shadow-sm">
                                <img src="${j.foto_url}" class="w-16 h-16 rounded-xl object-cover bg-slate-200" onerror="this.src='https://via.placeholder.com/64'">
                                <div>
                                    <p class="font-bold text-slate-800 text-sm leading-tight">${j.kegiatan}</p>
                                    <p class="text-xs text-slate-500 mt-1">${j.sekolah}${j.bidang}</p>
                                    <p class="text-[10px] text-blue-600 font-bold mt-1">${new Date(j.tanggal).toLocaleDateString('id-ID')}</p>
                                </div>
                            </div>
                        `;
                    });

                    // Detail Table for Print
                    const tableDetail = document.getElementById('table-laporan-detail');
                    tableDetail.innerHTML = '';
                    data.allSiswa.forEach(s => {
                        tableDetail.innerHTML += `
                            <tr>
                                <td class="px-6 py-4 font-semibold">${s.nama}</td>
                                <td class="px-6 py-4">${s.sekolah}</td>
                                <td class="px-6 py-4">${s.bidang}</td>
                                <td class="px-6 py-4 font-bold text-blue-600">${s.jam_pelatihan}h</td>
                                <td class="px-6 py-4 font-bold ${s.status_sertifikasi === 'Lulus' ? 'text-green-600' : 'text-orange-500'}">${s.status_sertifikasi}</td>
                            </tr>
                        `;
                    });
                    document.getElementById('print-date').innerText = new Date().toLocaleString('id-ID');

                    // Bar Chart
                    const ctx = document.getElementById('chart-bidang').getContext('2d');
                    if(charts.bidang) charts.bidang.destroy();
                    charts.bidang = new Chart(ctx, {
                        type: 'bar',
                        data: {
                            labels: Object.keys(data.bidangCounts),
                            datasets: [{ label: 'Jumlah Siswa', data: Object.values(data.bidangCounts), backgroundColor: '#3b82f6', borderRadius: 8 }]
                        },
                        options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, grid: { display: false } }, x: { grid: { display: false } } } }
                    });
                })
                .getDashboardData();
        }

        // --- FORM SUBMISSION ---
        async function submitForm(e, type) {
            e.preventDefault();
            const form = e.target;
            const formData = new FormData(form);
            const data = Object.fromEntries(formData.entries());

            setLoading(true, "Menyimpan Data...");

            if(type === 'jurnal') {
                const fileInput = document.getElementById('foto-jurnal');
                if(fileInput.files[0]) {
                    setLoading(true, "Mengompresi Gambar...");
                    const compressed = await compressImage(fileInput.files[0]);
                    data.fileData = compressed.base64;
                    data.fileName = fileInput.files[0].name;
                    data.fileMimeType = "image/jpeg";
                }
            }

            google.script.run
                .withSuccessHandler(res => {
                    setLoading(false);
                    if(res.success) {
                        showToast(res.message, 'success');
                        form.reset();
                        document.getElementById('jurnal-preview').classList.add('hidden');
                        document.getElementById('upload-placeholder').classList.remove('hidden');
                    } else {
                        showToast(res.message, 'error');
                    }
                })
                .saveFormData(type, data);
        }

        // --- IMAGE UTILS ---
        function previewImg(e) {
            const file = e.target.files[0];
            if(!file) return;
            const preview = document.getElementById('jurnal-preview');
            const placeholder = document.getElementById('upload-placeholder');
            preview.src = URL.createObjectURL(file);
            preview.classList.remove('hidden');
            placeholder.classList.add('hidden');
        }

        function compressImage(file) {
            return new Promise((resolve) => {
                const reader = new FileReader();
                reader.readAsDataURL(file);
                reader.onload = e => {
                    const img = new Image();
                    img.src = e.target.result;
                    img.onload = () => {
                        const canvas = document.createElement('canvas');
                        let w = img.width, h = img.height;
                        const max = 1000;
                        if(w > max) { h *= max/w; w = max; }
                        canvas.width = w; canvas.height = h;
                        const ctx = canvas.getContext('2d');
                        ctx.drawImage(img, 0, 0, w, h);
                        resolve({ base64: canvas.toDataURL('image/jpeg', 0.8).split(',')[1] });
                    };
                };
            });
        }

        // --- UI HELPERS ---
        function setLoading(s, text) {
            const l = document.getElementById('loading');
            l.style.display = s ? 'flex' : 'none';
            document.getElementById('loading-text').innerText = text || "Sedang Memproses...";
        }

        function showToast(msg, type) {
            const t = document.getElementById('toast');
            document.getElementById('toast-msg').innerText = msg;
            document.getElementById('toast-icon').className = type === 'success' ? 'fas fa-check-circle text-green-400' : 'fas fa-times-circle text-red-400';
            t.classList.remove('translate-y-24', 'opacity-0');
            setTimeout(() => t.classList.add('translate-y-24', 'opacity-0'), 3000);
        }

        // Mocking GAS for Canvas Preview
        if (typeof google === 'undefined') {
            window.google = { script: { run: {
                withSuccessHandler: function(cb){this.scb=cb; return this;},
                getDashboardData: function(){ setTimeout(()=>this.scb({stats:{totalSiswa:120,totalSekolah:15,totalTrainer:8,sertifikasiRate:75},bidangCounts:{"Boga":45,"Teknik":30},recentJurnal:[],allSiswa:[]}),500); },
                saveFormData: function(){ setTimeout(()=>this.scb({success:true,message:"Mock Simpan Berhasil"}),800); }
            }}};
        }
    </script>
</body>
</html>


MPA - Aplikasi Monev SMA DT

  https://script.google.com/macros/s/AKfycbyRItsyQ7jil6p41OvO6Ac1Dsg86HSOaznH0DaXDLGANUWFFxrBs3K2Nds1PEDS0oDp/exec https://script.google.com...