Kamis, 18 Desember 2025

Apps Script - Upload Foto

 








Buatkan aplikasi Studi Kasus (Final UI) Aplikasi Upload Foto 
 ✔ SPA 
✔ CRUD Lengkap 
✔ Bootstrap 5 
✔ Hero Section 
✔ Responsive

BackEnd - Code.gs


// ================== KONFIGURASI ==================
const SPREADSHEET_ID = "PASTE_SPREADSHEET_ID_DI_SINI";
const SHEET_NAME = "DataSiswa";
const FOLDER_ID = "PASTE_FOLDER_ID_DI_SINI";

// ================== WEB APP ENTRY ==================
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index")
    .setTitle("SISKO Photo CRUD")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag("viewport", "width=device-width, initial-scale=1.0");
}

// ================== UTIL ==================
function getSheet_() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  let sh = ss.getSheetByName(SHEET_NAME);
  if (!sh) {
    sh = ss.insertSheet(SHEET_NAME);
    sh.appendRow(["id", "timestamp", "nama", "nisn", "kelas", "jurusan", "alamat", "fotoUrl"]);
    sh.getRange(1, 1, 1, 8).setFontWeight("bold");
  }
  return sh;
}

function uuid_() {
  // ID sederhana & unik (cukup untuk sheet)
  return "ID-" + Date.now() + "-" + Math.floor(Math.random() * 1e6);
}

function normalizeUrl_(url) {
  if (!url) return "";
  const s = String(url);
  // format lama: drive.google.com/open?id=...
  if (s.includes("drive.google.com") && s.includes("id=")) {
    try {
      const id = s.split("id=")[1].split("&")[0];
      return "https://lh3.googleusercontent.com/d/" + id;
    } catch (e) {
      return s;
    }
  }
  return s;
}

// ================== UPLOAD FOTO ==================
function uploadImage(base64, filename, mimeType) {
  try {
    const base64Data = String(base64).split(",")[1]; // ambil data murni
    const bytes = Utilities.base64Decode(base64Data);

    const finalMime = mimeType || "image/jpeg";
    const safeName = filename || ("foto_" + Date.now());

    const blob = Utilities.newBlob(bytes, finalMime, safeName);

    const folder = DriveApp.getFolderById(FOLDER_ID);
    const file = folder.createFile(blob);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);

    // URL direct render (anti-block)
    const directUrl = "https://lh3.googleusercontent.com/d/" + file.getId();

    return { success: true, url: directUrl, fileId: file.getId() };
  } catch (error) {
    return { success: false, message: "Error upload: " + error.toString() };
  }
}

// ================== CRUD ==================
function createStudent(payload) {
  try {
    const sh = getSheet_();
    const id = uuid_();
    const now = new Date();

    const row = [
      id,
      now,
      payload.nama || "",
      payload.nisn || "",
      payload.kelas || "",
      payload.jurusan || "",
      payload.alamat || "",
      normalizeUrl_(payload.fotoUrl || "")
    ];

    sh.appendRow(row);
    return { success: true, message: "Data berhasil ditambahkan", id: id };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

function listStudents() {
  try {
    const sh = getSheet_();
    const values = sh.getDataRange().getValues();
    if (values.length <= 1) return { success: true, data: [] };

    const headers = values[0];
    const data = values.slice(1).map(r => {
      const obj = {};
      headers.forEach((h, i) => (obj[h] = r[i]));
      obj.timestamp = obj.timestamp ? new Date(obj.timestamp).toISOString() : "";
      obj.fotoUrl = normalizeUrl_(obj.fotoUrl);
      return obj;
    });

    // urut terbaru
    data.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));

    return { success: true, data };
  } catch (e) {
    return { success: false, message: e.toString(), data: [] };
  }
}

function getStudentById(id) {
  const sh = getSheet_();
  const values = sh.getDataRange().getValues();
  if (values.length <= 1) return { success: false, message: "Data kosong" };

  const headers = values[0];
  for (let i = 1; i < values.length; i++) {
    if (String(values[i][0]) === String(id)) {
      const obj = {};
      headers.forEach((h, idx) => (obj[h] = values[i][idx]));
      obj.timestamp = obj.timestamp ? new Date(obj.timestamp).toISOString() : "";
      obj.fotoUrl = normalizeUrl_(obj.fotoUrl);
      return { success: true, data: obj };
    }
  }
  return { success: false, message: "ID tidak ditemukan" };
}

function updateStudent(id, payload) {
  try {
    const sh = getSheet_();
    const values = sh.getDataRange().getValues();
    if (values.length <= 1) return { success: false, message: "Data kosong" };

    // kolom: id, timestamp, nama, nisn, kelas, jurusan, alamat, fotoUrl
    for (let i = 1; i < values.length; i++) {
      if (String(values[i][0]) === String(id)) {
        const rowIndex = i + 1; // 1-based
        const updated = [
          id,
          new Date(), // update timestamp
          payload.nama || "",
          payload.nisn || "",
          payload.kelas || "",
          payload.jurusan || "",
          payload.alamat || "",
          normalizeUrl_(payload.fotoUrl || "")
        ];
        sh.getRange(rowIndex, 1, 1, 8).setValues([updated]);
        return { success: true, message: "Data berhasil diupdate" };
      }
    }
    return { success: false, message: "ID tidak ditemukan" };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}

function deleteStudent(id) {
  try {
    const sh = getSheet_();
    const values = sh.getDataRange().getValues();
    if (values.length <= 1) return { success: false, message: "Data kosong" };

    for (let i = 1; i < values.length; i++) {
      if (String(values[i][0]) === String(id)) {
        sh.deleteRow(i + 1);
        return { success: true, message: "Data berhasil dihapus" };
      }
    }
    return { success: false, message: "ID tidak ditemukan" };
  } catch (e) {
    return { success: false, message: e.toString() };
  }
}


FrontEnd - Index.html

<!doctype html>
<html lang="id">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>SISKO Photo CRUD</title>

  <!-- Bootstrap 5 -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">

  <style>
    :root{
      --brand:#dc3545; /* merah */
      --ink:#0b1220;
      --muted:#6c757d;
      --card:#ffffff;
      --bg:#f6f7fb;
    }
    body{ background:var(--bg); color:var(--ink); }
    .navbar{ backdrop-filter:saturate(180%) blur(10px); }
    .hero{
      background: radial-gradient(1200px 600px at 10% 10%, rgba(220,53,69,.18), transparent 60%),
                  radial-gradient(900px 500px at 90% 20%, rgba(13,110,253,.12), transparent 55%),
                  linear-gradient(180deg, #fff, #f7f8ff);
      border-bottom: 1px solid rgba(0,0,0,.06);
    }
    .hero-badge{ background:rgba(220,53,69,.1); color:var(--brand); border:1px solid rgba(220,53,69,.25); }
    .card{ border:1px solid rgba(0,0,0,.08); box-shadow: 0 10px 25px rgba(0,0,0,.04); }
    .thumb{
      width: 52px; height: 52px; border-radius: 12px;
      object-fit: cover; border:1px solid rgba(0,0,0,.12);
      background:#fff;
    }
    .route{ display:none; }
    .route.active{ display:block; }
    .btn-brand{ background:var(--brand); border-color:var(--brand); }
    .btn-brand:hover{ filter:brightness(.95); }
    .skeleton{
      background: linear-gradient(90deg, #eee, #f5f5f5, #eee);
      background-size: 200% 100%;
      animation: sh 1.2s infinite;
      border-radius: 12px;
      height: 64px;
    }
    @keyframes sh { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
    .toast-container{ z-index: 9999; }
  </style>
</head>

<body>
  <!-- NAV -->
  <nav class="navbar navbar-expand-lg bg-white border-bottom sticky-top">
    <div class="container py-1">
      <a class="navbar-brand fw-bold" href="#/">
        <span class="badge rounded-pill me-2" style="background:var(--brand)">SISKO</span>
        Photo CRUD
      </a>

      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav">
        <span class="navbar-toggler-icon"></span>
      </button>

      <div id="nav" class="collapse navbar-collapse">
        <ul class="navbar-nav ms-auto gap-lg-2">
          <li class="nav-item"><a class="nav-link" href="#/">Home</a></li>
          <li class="nav-item"><a class="nav-link" href="#/form">Form</a></li>
          <li class="nav-item"><a class="nav-link" href="#/data">Data</a></li>
        </ul>
      </div>
    </div>
  </nav>

  <!-- TOAST -->
  <div class="toast-container position-fixed top-0 end-0 p-3"></div>

  <!-- HERO / HOME -->
  <section id="route-home" class="route active hero">
    <div class="container py-5">
      <div class="row align-items-center g-4">
        <div class="col-lg-6">
          <div class="d-inline-flex align-items-center gap-2 px-3 py-2 rounded-pill hero-badge mb-3">
            <span class="fw-semibold">✔ SPA</span>
            <span class="text-muted">•</span>
            <span class="fw-semibold">✔ CRUD</span>
            <span class="text-muted">•</span>
            <span class="fw-semibold">✔ Upload Foto</span>
          </div>

          <h1 class="display-6 fw-bold lh-sm">
            Aplikasi Upload Foto Siswa<br class="d-none d-md-block">
            dengan <span class="text-danger">UI Final</span> & Responsive
          </h1>
          <p class="mt-3 text-secondary">
            Studi kasus untuk sistem informasi sekolah: simpan data siswa ke Google Sheet
            dan upload foto ke Google Drive (langsung tampil di web).
          </p>

          <div class="d-flex gap-2 flex-wrap mt-4">
            <a class="btn btn-brand text-white px-4" href="#/form">Mulai Input</a>
            <a class="btn btn-outline-secondary px-4" href="#/data">Lihat Data</a>
          </div>

          <div class="mt-4 small text-secondary">
            Tips: gunakan foto ukuran kecil agar upload lebih cepat.
          </div>
        </div>

        <div class="col-lg-6">
          <div class="card p-4">
            <div class="d-flex align-items-start gap-3">
              <div class="rounded-4 d-flex align-items-center justify-content-center"
                   style="width:56px;height:56px;background:rgba(220,53,69,.1);">
                <span class="fw-bold text-danger">UI</span>
              </div>
              <div>
                <div class="fw-bold">Fitur Utama</div>
                <div class="text-secondary small">Upload → simpan → tampil → edit → hapus</div>
              </div>
            </div>

            <hr class="my-4">

            <div class="row g-3">
              <div class="col-12 col-md-6">
                <div class="p-3 border rounded-4 bg-light">
                  <div class="fw-semibold">Drive Upload</div>
                  <div class="small text-secondary">Foto jadi URL direct render</div>
                </div>
              </div>
              <div class="col-12 col-md-6">
                <div class="p-3 border rounded-4 bg-light">
                  <div class="fw-semibold">Google Sheet DB</div>
                  <div class="small text-secondary">CRUD lengkap</div>
                </div>
              </div>
              <div class="col-12">
                <div class="p-3 border rounded-4 bg-light">
                  <div class="fw-semibold">SPA Routing</div>
                  <div class="small text-secondary">Tanpa reload halaman (#/home, #/form, #/data)</div>
                </div>
              </div>
            </div>

          </div>
        </div>

      </div>
    </div>
  </section>

  <!-- FORM -->
  <section id="route-form" class="route">
    <div class="container py-4 py-lg-5">
      <div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
        <div>
          <h2 class="h4 fw-bold mb-1">Form Siswa</h2>
          <div class="text-secondary small">Tambah data / edit data + upload foto</div>
        </div>
        <div class="d-flex gap-2">
          <button class="btn btn-outline-secondary" id="btnReset">Reset</button>
          <a class="btn btn-outline-secondary" href="#/data">Ke Data</a>
        </div>
      </div>

      <div class="row g-4">
        <div class="col-lg-7">
          <div class="card p-4">
            <form id="studentForm" class="row g-3">
              <input type="hidden" id="id">

              <div class="col-md-6">
                <label class="form-label fw-semibold">Nama</label>
                <input class="form-control" id="nama" required placeholder="Contoh: Budi Santoso">
              </div>
              <div class="col-md-6">
                <label class="form-label fw-semibold">NISN</label>
                <input class="form-control" id="nisn" required placeholder="Contoh: 2024001">
              </div>

              <div class="col-md-4">
                <label class="form-label fw-semibold">Kelas</label>
                <input class="form-control" id="kelas" placeholder="X / XI / XII">
              </div>
              <div class="col-md-4">
                <label class="form-label fw-semibold">Jurusan</label>
                <input class="form-control" id="jurusan" placeholder="IPA / IPS / TKJ ...">
              </div>
              <div class="col-md-4">
                <label class="form-label fw-semibold">Foto</label>
                <input class="form-control" id="foto" type="file" accept="image/*">
              </div>

              <div class="col-12">
                <label class="form-label fw-semibold">Alamat</label>
                <textarea class="form-control" id="alamat" rows="2" placeholder="Alamat lengkap..."></textarea>
              </div>

              <div class="col-12">
                <div class="d-flex align-items-center gap-3 flex-wrap">
                  <button class="btn btn-brand text-white px-4" type="submit" id="btnSubmit">
                    Simpan
                  </button>
                  <div class="text-secondary small" id="uploadHint">
                    Jika foto dipilih, sistem akan upload dulu lalu simpan URL-nya.
                  </div>
                </div>
              </div>
            </form>
          </div>

          <div class="text-secondary small mt-3">
            Catatan: upload foto akan dibuat “public link” (anyone with link).
          </div>
        </div>

        <div class="col-lg-5">
          <div class="card p-4">
            <div class="d-flex align-items-center justify-content-between mb-3">
              <div class="fw-bold">Preview</div>
              <span class="badge text-bg-light border">Live</span>
            </div>

            <div class="d-flex align-items-center gap-3">
              <img id="previewImg" class="thumb" alt="preview"
                   src="https://via.placeholder.com/96x96.png?text=Foto">
              <div>
                <div class="fw-semibold" id="previewName">Nama Siswa</div>
                <div class="text-secondary small" id="previewMeta">NISN • Kelas • Jurusan</div>
              </div>
            </div>

            <hr class="my-4">

            <div class="small text-secondary">
              Tips kualitas UI: pakai foto square (1:1) agar rapi di tabel.
            </div>
          </div>
        </div>

      </div>
    </div>
  </section>

  <!-- DATA -->
  <section id="route-data" class="route">
    <div class="container py-4 py-lg-5">
      <div class="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-3">
        <div>
          <h2 class="h4 fw-bold mb-1">Data Siswa</h2>
          <div class="text-secondary small">Search, edit, delete (CRUD lengkap)</div>
        </div>

        <div class="d-flex gap-2 flex-wrap">
          <div class="input-group">
            <span class="input-group-text">🔎</span>
            <input id="q" class="form-control" placeholder="Cari nama / NISN / kelas...">
          </div>
          <button class="btn btn-outline-secondary" id="btnRefresh">Refresh</button>
          <a class="btn btn-brand text-white" href="#/form">Tambah</a>
        </div>
      </div>

      <div class="card p-3 p-lg-4">
        <div id="loadingList" class="d-grid gap-2">
          <div class="skeleton"></div>
          <div class="skeleton"></div>
          <div class="skeleton"></div>
        </div>

        <div class="table-responsive" id="tableWrap" style="display:none">
          <table class="table align-middle mb-0">
            <thead>
              <tr class="text-secondary small">
                <th>Foto</th>
                <th>Nama</th>
                <th>NISN</th>
                <th>Kelas</th>
                <th>Jurusan</th>
                <th>Alamat</th>
                <th class="text-end">Aksi</th>
              </tr>
            </thead>
            <tbody id="tbody"></tbody>
          </table>
        </div>

        <div id="emptyState" class="text-center text-secondary py-5" style="display:none">
          Belum ada data. Klik <b>Tambah</b> untuk input siswa.
        </div>
      </div>
    </div>
  </section>

  <!-- Bootstrap -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

  <script>
    // =============== STATE ===============
    let ALL = [];
    let CURRENT_PHOTO_URL = ""; // url foto yang sudah diupload / existing

    // =============== ROUTER ===============
    const routes = {
      "/": "route-home",
      "/form": "route-form",
      "/data": "route-data"
    };

    function setActiveRoute(path){
      const id = routes[path] || routes["/"];
      document.querySelectorAll(".route").forEach(el => el.classList.remove("active"));
      document.getElementById(id).classList.add("active");

      // page hooks
      if (path === "/data") loadList();
      if (path === "/form") syncPreview();
    }

    function parseHash(){
      const raw = location.hash.replace("#", "") || "/";
      // format: /form?id=...
      const [path, qs] = raw.split("?");
      const params = new URLSearchParams(qs || "");
      return { path, params };
    }

    window.addEventListener("hashchange", () => {
      const { path, params } = parseHash();
      setActiveRoute(path);
      // handle edit from query string
      const id = params.get("id");
      if (path === "/form" && id) loadToForm(id);
    });

    // =============== TOAST ===============
    function toast(msg, type="success"){
      const wrap = document.querySelector(".toast-container");
      const el = document.createElement("div");
      el.className = `toast align-items-center text-bg-${type} border-0`;
      el.role = "alert";
      el.innerHTML = `
        <div class="d-flex">
          <div class="toast-body">${msg}</div>
          <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
        </div>`;
      wrap.appendChild(el);
      const t = new bootstrap.Toast(el, { delay: 2500 });
      t.show();
      el.addEventListener("hidden.bs.toast", () => el.remove());
    }

    // =============== HELPERS ===============
    function byId(id){ return document.getElementById(id); }

    function setLoading(isLoading){
      byId("loadingList").style.display = isLoading ? "grid" : "none";
      byId("tableWrap").style.display = isLoading ? "none" : "block";
    }

    function escapeHtml(s){
      return String(s ?? "")
        .replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;")
        .replaceAll('"',"&quot;").replaceAll("'","&#039;");
    }

    // =============== PREVIEW ===============
    function syncPreview(){
      const nama = byId("nama").value || "Nama Siswa";
      const nisn = byId("nisn").value || "NISN";
      const kelas = byId("kelas").value || "Kelas";
      const jurusan = byId("jurusan").value || "Jurusan";
      byId("previewName").textContent = nama;
      byId("previewMeta").textContent = `${nisn} • ${kelas} • ${jurusan}`;

      const file = byId("foto").files[0];
      if (file){
        const url = URL.createObjectURL(file);
        byId("previewImg").src = url;
      } else if (CURRENT_PHOTO_URL){
        byId("previewImg").src = CURRENT_PHOTO_URL;
      } else {
        byId("previewImg").src = "https://via.placeholder.com/96x96.png?text=Foto";
      }
    }

    ["nama","nisn","kelas","jurusan","alamat"].forEach(id => {
      document.addEventListener("input", (e) => {
        if (e.target && e.target.id === id) syncPreview();
      });
    });
    byId("foto").addEventListener("change", syncPreview);

    // =============== CRUD CALLS ===============
    function loadList(){
      setLoading(true);
      byId("emptyState").style.display = "none";

      google.script.run
        .withSuccessHandler(res => {
          setLoading(false);
          if (!res.success){ toast(res.message || "Gagal load data", "danger"); return; }
          ALL = res.data || [];
          renderTable();
        })
        .withFailureHandler(err => {
          setLoading(false);
          toast("Error: " + err.message, "danger");
        })
        .listStudents();
    }

    function renderTable(){
      const q = (byId("q").value || "").toLowerCase().trim();
      const filtered = ALL.filter(x => {
        const hay = `${x.nama} ${x.nisn} ${x.kelas} ${x.jurusan} ${x.alamat}`.toLowerCase();
        return !q || hay.includes(q);
      });

      const tb = byId("tbody");
      tb.innerHTML = "";

      if (!filtered.length){
        byId("tableWrap").style.display = "none";
        byId("emptyState").style.display = "block";
        return;
      } else {
        byId("tableWrap").style.display = "block";
        byId("emptyState").style.display = "none";
      }

      filtered.forEach(item => {
        const tr = document.createElement("tr");
        const foto = item.fotoUrl ? escapeHtml(item.fotoUrl) : "https://via.placeholder.com/96x96.png?text=Foto";

        tr.innerHTML = `
          <td><img class="thumb" src="${foto}" alt="foto"></td>
          <td class="fw-semibold">${escapeHtml(item.nama)}</td>
          <td>${escapeHtml(item.nisn)}</td>
          <td>${escapeHtml(item.kelas)}</td>
          <td>${escapeHtml(item.jurusan)}</td>
          <td class="text-secondary small" style="max-width:260px">${escapeHtml(item.alamat)}</td>
          <td class="text-end">
            <div class="btn-group">
              <a class="btn btn-sm btn-outline-primary" href="#/form?id=${encodeURIComponent(item.id)}">Edit</a>
              <button class="btn btn-sm btn-outline-danger" data-del="${escapeHtml(item.id)}">Hapus</button>
            </div>
          </td>
        `;
        tb.appendChild(tr);
      });

      // bind delete
      tb.querySelectorAll("[data-del]").forEach(btn => {
        btn.addEventListener("click", () => {
          const id = btn.getAttribute("data-del");
          if (!confirm("Hapus data ini?")) return;

          google.script.run
            .withSuccessHandler(res => {
              if (!res.success){ toast(res.message || "Gagal hapus", "danger"); return; }
              toast("Berhasil dihapus");
              loadList();
            })
            .withFailureHandler(err => toast("Error: " + err.message, "danger"))
            .deleteStudent(id);
        });
      });
    }

    byId("q").addEventListener("input", renderTable);
    byId("btnRefresh").addEventListener("click", loadList);

    // =============== FORM: LOAD EDIT ===============
    function loadToForm(id){
      resetForm(false);

      google.script.run
        .withSuccessHandler(res => {
          if (!res.success){ toast(res.message || "ID tidak ditemukan", "danger"); return; }
          const d = res.data;

          byId("id").value = d.id || "";
          byId("nama").value = d.nama || "";
          byId("nisn").value = d.nisn || "";
          byId("kelas").value = d.kelas || "";
          byId("jurusan").value = d.jurusan || "";
          byId("alamat").value = d.alamat || "";
          CURRENT_PHOTO_URL = d.fotoUrl || "";

          byId("btnSubmit").textContent = "Update";
          toast("Mode edit aktif", "primary");
          syncPreview();
        })
        .withFailureHandler(err => toast("Error: " + err.message, "danger"))
        .getStudentById(id);
    }

    // =============== FORM: SUBMIT ===============
    function fileToBase64(file){
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
        reader.readAsDataURL(file);
      });
    }

    async function handleSubmit(e){
      e.preventDefault();

      const payload = {
        nama: byId("nama").value.trim(),
        nisn: byId("nisn").value.trim(),
        kelas: byId("kelas").value.trim(),
        jurusan: byId("jurusan").value.trim(),
        alamat: byId("alamat").value.trim(),
        fotoUrl: CURRENT_PHOTO_URL || ""
      };

      const id = byId("id").value;
      const file = byId("foto").files[0];

      byId("btnSubmit").disabled = true;
      byId("btnSubmit").textContent = "Memproses...";

      try {
        // 1) jika ada foto baru → upload dulu
        if (file){
          const base64 = await fileToBase64(file);
          const up = await new Promise((resolve, reject) => {
            google.script.run
              .withSuccessHandler(resolve)
              .withFailureHandler(reject)
              .uploadImage(base64, file.name, file.type);
          });

          if (!up.success) throw new Error(up.message || "Upload gagal");
          payload.fotoUrl = up.url;
          CURRENT_PHOTO_URL = up.url;
        }

        // 2) create / update
        const res = await new Promise((resolve, reject) => {
          const runner = google.script.run.withSuccessHandler(resolve).withFailureHandler(reject);
          if (id) runner.updateStudent(id, payload);
          else runner.createStudent(payload);
        });

        if (!res.success) throw new Error(res.message || "Gagal simpan");
        toast(id ? "Berhasil diupdate" : "Berhasil ditambahkan");

        resetForm(true);
        location.hash = "#/data";
      } catch (err) {
        toast(err.message || "Terjadi error", "danger");
      } finally {
        byId("btnSubmit").disabled = false;
        byId("btnSubmit").textContent = (byId("id").value ? "Update" : "Simpan");
      }
    }

    byId("studentForm").addEventListener("submit", handleSubmit);

    // =============== RESET FORM ===============
    function resetForm(showToast=true){
      byId("id").value = "";
      byId("nama").value = "";
      byId("nisn").value = "";
      byId("kelas").value = "";
      byId("jurusan").value = "";
      byId("alamat").value = "";
      byId("foto").value = "";
      CURRENT_PHOTO_URL = "";
      byId("btnSubmit").textContent = "Simpan";
      if (showToast) toast("Form direset", "secondary");
      syncPreview();
    }
    byId("btnReset").addEventListener("click", () => resetForm(true));

    // =============== INIT ===============
    (function init(){
      const { path, params } = parseHash();
      setActiveRoute(path);
      const id = params.get("id");
      if (path === "/form" && id) loadToForm(id);
      if (!location.hash) location.hash = "#/";
    })();
  </script>
</body>
</html>


Tidak ada komentar:

Posting Komentar

Laporan KUS - Sertifikat

  https://www.youtube.com/watch?v=1xegpb4fLk4 =arrayformula(if(row(A:A)=1;"Image";substitute(F:F;"open?";"uc?export...