Senin, 13 April 2026

MPA - Ruang Karir

 





Ruang Karir Double Track

Ruang Karir Double Track adalah platform job market berbasis web yang dirancang khusus untuk menghubungkan alumni program vokasi (Pencari Kerja) dengan sektor industri secara efisien.

1. Arsitektur & Teknologi Utama

Aplikasi ini menggunakan pendekatan Lean Stack (ringan dan bebas biaya server) dengan memanfaatkan ekosistem Google:

  • Frontend: Multi-Page Application (MPA) menggunakan HTML5 dan Tailwind CSS untuk antarmuka yang modern dan responsif.

  • Backend: Google Apps Script (GAS) sebagai mesin API dan logika bisnis.

  • Database: Google Sheets sebagai penyimpanan data relasional yang mudah dipantau oleh admin secara langsung.

  • Media Storage: Google Drive untuk menyimpan foto profil dan logo industri.

2. Inovasi Fitur Visual

Salah satu keunggulan teknis aplikasi ini adalah penggunaan lh3.googleusercontent.com. Sistem secara otomatis mengubah ID file dari Google Drive menjadi link konten langsung. Hal ini menjamin:

  • Pemuatan gambar yang instan (tidak ada delay dari Google Drive).

  • Kompatibilitas penuh dengan elemen HTML <img> standar.

  • Performa web yang tetap cepat meskipun banyak memuat foto industri dan pelamar.

3. Kategori Pengguna & Fungsi

  • Pencari Kerja: Mengelola profil, mengunggah foto formal, dan melamar lowongan.

  • Industri: Posting lowongan kerja, verifikasi pelamar, dan manajemen branding perusahaan melalui logo.

  • Admin: Melakukan verifikasi akun industri dan memantau seluruh aktivitas melalui Google Sheets.

  • Guest: Menjelajahi daftar lowongan kerja secara terbuka.

4. Keunggulan Sistem

  • Zero Infrastructure Cost: Seluruh sistem berjalan di atas infrastruktur gratis Google Workspace.

  • High Scalability: Mudah dikembangkan lebih lanjut (misalnya ditambah fitur notifikasi WhatsApp atau email otomatis).

  • Maintenance Mudah: Admin tidak perlu keahlian coding untuk mengedit data; cukup melakukan perubahan pada baris di Google Sheets.


Status Implementasi

Saat ini, spesifikasi telah mencakup:

  1. Struktur Folder & File (MPA).

  2. Skema Database di Google Sheets.

  3. Logika Backend (Code.gs) untuk routing dan upload file Base64.

  4. Komponen Frontend (HTML/Tailwind) dengan sistem integrasi google.script.run.

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 - Ruang Karir

  Ruang Karir Double Track Ruang Karir Double Track adalah platform job market berbasis web yang dirancang khusus untuk menghubungkan alum...