Rabu, 27 Mei 2026

Contoh Bound Script

 





PROMPT


Bertindaklah sebagai apps script expert. Buatkan aplikasi CRUD  Sistem Manajemen Inventaris Sederhana. Gunakan bound script. Gunakan file terpisah Code.gs, Index, css, JavaScript.

Struktur Proyek: Sistem Manajemen Inventaris Sederhana

Dalam pengembangan aplikasi web berbasis Google Apps Script (GAS), memisahkan kode berdasarkan fungsinya (modularisasi) sangat membantu dalam hal pemeliharaan (maintenance), pembacaan kode, dan kolaborasi.

Proyek ini menggunakan metode Bound Script (skrip yang menempel langsung pada Google Sheets) dengan pembagian struktur file sebagai berikut:

1. Pohon Struktur File (Directory Tree)

Di dalam Google Apps Script Editor, Anda akan melihat susunan file seperti ini:

Sistem-Manajemen-Inventaris/
│
├── Code.gs             <-- [Backend] Logika server, CRUD, & konfigurasi database
├── Index.html          <-- [Frontend] Kerangka utama halaman antarmuka (UI)
├── CSS.html            <-- [Frontend] Desain visual & layout (Styling)
└── JavaScript.html     <-- [Frontend] Logika interaksi & jembatan komunikasi (API Client)

2. Penjelasan Detail Setiap Modul

A. Code.gs (Sisi Server / Backend)

File ini ditulis menggunakan sintaks JavaScript Google Apps Script dan berjalan langsung di server Google.

  • Fungsi Utama:

    • Routing (doGet): Menangkap permintaan HTTP GET ketika Web App diakses dan menyajikan tampilan Index.html.

    • Helper include(filename): Mengimpor modul CSS.html dan JavaScript.html untuk disisipkan ke dalam Index.html sebelum dikirim ke browser pengguna.

    • Database Handler (initSheet): Memastikan sheet "Inventaris" tersedia di Google Sheets aktif dengan format kolom yang rapi.

    • API CRUD (createRow, readAll, updateRow, deleteRow): Melakukan manipulasi baris data secara langsung pada Google Sheets berdasarkan instruksi dari frontend.

B. Index.html (Sisi Klien / UI Shell)

File HTML utama yang merender struktur halaman web yang dilihat oleh pengguna di browser mereka.

  • Fungsi Utama:

    • Menyusun kerangka visual seperti Navbar, Dashboard Statistik, Input Form (Modal), dan Tabel Inventaris menggunakan utility class dari Tailwind CSS.

    • Berfungsi sebagai wadah penggabungan komponen menggunakan sintaks scriptlet Apps Script:

      • <?!= include('CSS'); ?> untuk menyisipkan gaya visual.

      • <?!= include('JavaScript'); ?> untuk menyisipkan logika interaksi di akhir dokumen.

C. CSS.html (Sisi Klien / Styling)

Meskipun berakhiran ekstensi .html, file ini murni berisi kode CSS yang dibungkus di dalam tag <style>.

  • Fungsi Utama:

    • Menyediakan kustomisasi desain yang tidak dicakup oleh Tailwind CSS (seperti animasi transisi modal, desain scrollbar kustom, dan animasi masuk/keluar untuk kotak notifikasi Toast).

D. JavaScript.html (Sisi Klien / Logic & Connection)

Sama seperti CSS, file ini berakhiran .html namun murni berisi kode JavaScript sisi klien yang dibungkus di dalam tag <script>.

  • Fungsi Utama:

    • State Management: Menyimpan data inventaris sementara di browser (dbItems) agar pencarian dan penyaringan kategori dapat berjalan instan tanpa perlu membebani server.

    • Event Listeners & DOM Manipulation: Mengatur pembukaan/penutupan modal, validasi formulir input sebelum dikirim, dan pengisian ulang form saat mode edit diaktifkan.

    • Jembatan Komunikasi (google.script.run): Mengirimkan data inventaris secara asinkronus (async) ke server Code.gs dan menangani respons balik sukses (withSuccessHandler) atau gagal (withFailureHandler).

3. Diagram Alur Komunikasi Data (Data Flow)

Bagaimana komponen-komponen ini saling bekerja sama? Berikut adalah visualisasi alur kerjanya saat pengguna melakukan aksi pada aplikasi:

[ Browser Pengguna ]                                      [ Server Google ]
   (Frontend)                                                 (Backend)
       │                                                          │
       │─── 1. Akses URL Web App ────────────────────────────────>│ (doGet)
       │<── 2. Kirim Index.html + Hasil Evaluasi include() ───────│ (Menggabungkan CSS & JS)
       │                                                          │
       │─── 3. Muat Data Awal (google.script.run.readAll()) ─────>│ (Membaca Google Sheets)
       │<── 4. Kembalikan Array Data JSON Barang ─────────────────│ (Format Array Object)
       │                                                          │
       │─── 5. Simpan/Tambah Barang Baru (createRow) ────────────>│ (Menulis Baris ke Sheets)
       │<── 6. Kembalikan Status Kejayaan (Toast Alert) ──────────│

4. Cara Membuat Struktur Ini di Editor Apps Script

  1. Masuk ke Google Sheets Anda, klik Extensions > Apps Script.

  2. Secara default akan ada file bernama Code.gs. Tempelkan kode backend Anda di sana.

  3. Di sebelah kiri tulisan Files, klik tombol + (Tambah file).

  4. Pilih HTML, lalu beri nama Index (Editor secara otomatis menambahkan ekstensi .html).

  5. Ulangi langkah nomor 3 dan 4 untuk membuat file CSS dan JavaScript.

  6. Simpan semua file dengan menekan tombol ikon Disket atau pintasan keyboard Ctrl + S (Cmd + S di Mac).

Code

/**
 * SISTEM PENGURUSAN INVENTARIS - BACKEND SCRIPT (Code.gs)
 * Ditulis oleh: Apps Script Expert
 * Deskripsi: Menguruskan rute Web App, inisialisasi helaian (sheet), dan logik CRUD di pelayan.
 */

/**
 * Trigger onOpen untuk membina menu tersuai dalam Google Spreadsheet
 * Ini membolehkan pengguna menjalankan inisialisasi terus dari helaian.
 */
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('Sistem Inventaris')
    .addItem('Inisialisasi Database', 'initDatabase')
    .addToUi();
}

/**
 * Fungsi utama untuk menginisialisasi pangkalan data (database) dari Spreadsheet
 * Boleh dijalankan secara manual dari menu atau semasa aplikasi bermula.
 */
function initDatabase() {
  var ui = SpreadsheetApp.getUi();
  try {
    var sheet = initSheet();
   
    // Memaparkan mesej kejayaan kepada pengguna di Spreadsheet
    ui.alert(
      'Inisialisasi Berjaya',
      'Helaian "Inventaris" telah berjaya disediakan dengan format premium. Anda kini boleh menggunakan Web App.',
      ui.ButtonSet.OK
    );
  } catch (error) {
    ui.alert('Ralat Inisialisasi', 'Gagal membina database: ' + error.toString(), ui.ButtonSet.OK);
  }
}

/**
 * Endpoint utama Web App untuk memaparkan halaman HTML
 */
function doGet() {
  return HtmlService.createTemplateFromFile('Index')
      .evaluate()
      .setTitle('Sistem Pengurusan Inventaris')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
      .addMetaTag('viewport', 'width=device-width, initial-scale=1.0');
}

/**
 * Fungsi pembantu (helper) untuk mengimport fail HTML lain secara modular
 * @param {string} filename Nama fail HTML yang ingin dimasukkan
 * @return {string} Kandungan HTML dalam bentuk teks
 */
function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

/**
 * Memastikan Helaian (Sheet) 'Inventaris' sedia ada dan mempunyai pengepala (header) kolum yang betul
 * @return {Sheet} Objek Sheet Google
 */
function initSheet() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName("Inventaris");
 
  if (!sheet) {
    sheet = ss.insertSheet("Inventaris");
    var headers = ["ID", "Nama Barang", "Kategori", "Jumlah", "Harga", "Lokasi", "Tanggal Update"];
    sheet.appendRow(headers);
   
    // Memberikan format visual premium untuk pengepala (header)
    var headerRange = sheet.getRange(1, 1, 1, headers.length);
    headerRange.setFontWeight("bold")
               .setBackground("#1e293b") // Warna kelabu gelap (Slate 800)
               .setFontColor("#ffffff")
               .setHorizontalAlignment("center");
   
    // Melaraskan lebar kolum secara automatik
    sheet.autoResizeColumns(1, headers.length);
  }
  return sheet;
}

/**
 * READ - Mengambil semua data barang daripada Google Sheets
 * @return {Array<Object>} Senarai objek barang inventaris
 */
function readAll() {
  try {
    var sheet = initSheet();
    var data = sheet.getDataRange().getValues();
   
    if (data.length <= 1) {
      return []; // Hanya mengandungi baris pengepala
    }
   
    var headers = data[0];
    var formattedData = [];
   
    for (var i = 1; i < data.length; i++) {
      var row = data[i];
      var item = {};
     
      for (var j = 0; j < headers.length; j++) {
        // Menormalkan nama sifat (contoh: "Nama Barang" menjadi "namabarang")
        var propName = headers[j].toLowerCase().replace(/\s+/g, '');
        var value = row[j];
       
        // Memformat objek tarikh agar sesuai untuk JavaScript sisi klien
        if (value instanceof Date) {
          value = Utilities.formatDate(value, Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss");
        }
        item[propName] = value;
      }
      formattedData.push(item);
    }
   
    return formattedData;
  } catch (error) {
    throw new Error("Gagal mengambil data: " + error.message);
  }
}

/**
 * CREATE - Menambah barang baharu ke dalam inventaris
 * @param {Object} item Data barang dari frontend
 * @return {string} Mesej kejayaan dan ID barang
 */
function createRow(item) {
  try {
    var sheet = initSheet();
   
    // Menghasilkan ID Unik (Format: INV-123456)
    var timestampId = "INV-" + Date.now().toString().slice(-6);
    var now = new Date();
   
    sheet.appendRow([
      timestampId,
      item.namabarang,
      item.kategori,
      Number(item.jumlah),
      Number(item.harga),
      item.lokasi,
      now
    ]);
   
    return "Barang berjaya didaftarkan dengan ID: " + timestampId;
  } catch (error) {
    throw new Error("Gagal menyimpan data: " + error.message);
  }
}

/**
 * UPDATE - Mengemas kini data barang berdasarkan ID
 * @param {Object} item Objek data barang yang dikemas kini
 * @return {string} Mesej pengesahan kejayaan
 */
function updateRow(item) {
  try {
    var sheet = initSheet();
    var data = sheet.getDataRange().getValues();
    var idColIndex = 0; // Kolom ID berada pada indeks 0
   
    for (var i = 1; i < data.length; i++) {
      if (data[i][idColIndex] === item.id) {
        var rowNum = i + 1; // Baris helaian bermula dari indeks 1
       
        sheet.getRange(rowNum, 2).setValue(item.namabarang);
        sheet.getRange(rowNum, 3).setValue(item.kategori);
        sheet.getRange(rowNum, 4).setValue(Number(item.jumlah));
        sheet.getRange(rowNum, 5).setValue(Number(item.harga));
        sheet.getRange(rowNum, 6).setValue(item.lokasi);
        sheet.getRange(rowNum, 7).setValue(new Date());
       
        return "Barang dengan ID " + item.id + " berjaya dikemas kini!";
      }
    }
    throw new Error("ID Barang tidak ditemui.");
  } catch (error) {
    throw new Error("Gagal mengubah data: " + error.message);
  }
}

/**
 * DELETE - Memadam baris barang daripada database helaian Google Sheets
 * @param {string} id ID barang unik untuk dipadam
 * @return {string} Mesej pengesahan pemadaman barang
 */
function deleteRow(id) {
  try {
    var sheet = initSheet();
    var data = sheet.getDataRange().getValues();
    var idColIndex = 0;
   
    for (var i = 1; i < data.length; i++) {
      if (data[i][idColIndex] === id) {
        var rowNum = i + 1;
        sheet.deleteRow(rowNum);
        return "Barang dengan ID " + id + " berjaya dipadam daripada inventaris.";
      }
    }
    throw new Error("ID Barang tidak ditemui.");
  } catch (error) {
    throw new Error("Gagal memadam data: " + error.message);
  }
}

Index

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- Framework Tailwind CSS untuk Desain Responsif & Premium -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- FontAwesome CDN untuk Ikon Dashboard -->
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
   
    <!-- Memanggil Modul CSS Terpisah -->
    <?!= include('CSS'); ?>
  </head>
  <body class="bg-slate-50 text-slate-800 font-sans min-h-screen">

    <!-- Top Navigation Bar -->
    <nav class="bg-slate-900 text-white shadow-md sticky top-0 z-10">
      <div class="max-w-7xl mx-auto px-6 py-4 flex justify-between items-center">
        <div class="flex items-center space-x-3">
          <div class="bg-emerald-500 text-white p-2.5 rounded-lg shadow-inner">
            <i class="fa-solid fa-boxes-stacked text-xl"></i>
          </div>
          <div>
            <h1 class="text-xl font-bold tracking-tight">Sistem Inventaris</h1>
            <p class="text-xs text-slate-400">Google Apps Script Enterprise</p>
          </div>
        </div>
        <div class="text-sm bg-slate-800 px-4 py-2 rounded-full border border-slate-700 flex items-center gap-2">
          <span class="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-pulse"></span>
          <span>Database Terkoneksi</span>
        </div>
      </div>
    </nav>

    <!-- Konten Utama Dashboard -->
    <main class="max-w-7xl mx-auto px-6 py-8">
     
      <!-- STATISTIK DASHBOARD CARDS -->
      <section class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
        <!-- Card 1: Total Jenis -->
        <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1">
          <div>
            <p class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Total Jenis Barang</p>
            <h3 id="statTotalJenis" class="text-3xl font-extrabold text-slate-800 mt-1">0</h3>
          </div>
          <div class="bg-blue-50 text-blue-500 p-4 rounded-xl">
            <i class="fa-solid fa-cube text-2xl"></i>
          </div>
        </div>

        <!-- Card 2: Total Stok -->
        <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1">
          <div>
            <p class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Total Kuantitas Stok</p>
            <h3 id="statTotalStok" class="text-3xl font-extrabold text-slate-800 mt-1">0</h3>
          </div>
          <div class="bg-indigo-50 text-indigo-500 p-4 rounded-xl">
            <i class="fa-solid fa-warehouse text-2xl"></i>
          </div>
        </div>

        <!-- Card 3: Total Nilai Aset -->
        <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1">
          <div>
            <p class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Total Nilai Aset</p>
            <h3 id="statNilaiAset" class="text-2xl font-extrabold text-slate-800 mt-1">Rp 0</h3>
          </div>
          <div class="bg-emerald-50 text-emerald-500 p-4 rounded-xl">
            <i class="fa-solid fa-rupiah-sign text-2xl"></i>
          </div>
        </div>

        <!-- Card 4: Stok Menipis -->
        <div id="cardStokMenipis" class="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1">
          <div>
            <p class="text-xs font-semibold text-slate-400 uppercase tracking-wider">Stok Menipis (&lt; 5)</p>
            <h3 id="statStokMenipis" class="text-3xl font-extrabold text-slate-800 mt-1">0</h3>
          </div>
          <div id="iconStokMenipis" class="bg-amber-50 text-amber-500 p-4 rounded-xl">
            <i class="fa-solid fa-triangle-exclamation text-2xl"></i>
          </div>
        </div>
      </section>

      <!-- BAGIAN TABEL DATA & TOOLBAR -->
      <section class="bg-white rounded-3xl shadow-sm border border-slate-100 overflow-hidden">
       
        <!-- Toolbar Atas Tabel -->
        <div class="p-6 border-b border-slate-100 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
          <div class="flex flex-1 flex-col sm:flex-row gap-3">
            <!-- Search Input -->
            <div class="relative flex-1">
              <span class="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
                <i class="fa-solid fa-magnifying-glass"></i>
              </span>
              <input type="text" id="searchQuery" onkeyup="filterData()" class="w-full pl-10 pr-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 focus:outline-none transition" placeholder="Cari ID, nama barang, atau lokasi...">
            </div>
            <!-- Category Filter Dropdown -->
            <select id="categoryFilter" onchange="filterData()" class="px-4 py-2.5 bg-slate-50 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:outline-none transition min-w-[160px]">
              <option value="">Semua Kategori</option>
              <!-- Opsi Kategori Dinamis dari JavaScript -->
            </select>
          </div>
          <!-- Tambah Barang Button -->
          <button onclick="bukaModal()" class="bg-emerald-600 hover:bg-emerald-700 text-white px-5 py-2.5 rounded-xl font-semibold shadow-sm flex items-center gap-2 transition duration-150">
            <i class="fa-solid fa-plus"></i>
            <span>Tambah Barang</span>
          </button>
        </div>

        <!-- Tabel Data Barang -->
        <div class="overflow-x-auto">
          <table class="w-full text-left border-collapse">
            <thead>
              <tr class="bg-slate-50 text-slate-500 text-xs font-semibold tracking-wider uppercase border-b border-slate-100">
                <th class="py-4 px-6">ID Barang</th>
                <th class="py-4 px-6">Nama Barang</th>
                <th class="py-4 px-6">Kategori</th>
                <th class="py-4 px-6 text-center">Stok</th>
                <th class="py-4 px-6 text-right">Harga Satuan</th>
                <th class="py-4 px-6">Lokasi Rak</th>
                <th class="py-4 px-6">Terakhir Update</th>
                <th class="py-4 px-6 text-center">Aksi</th>
              </tr>
            </thead>
            <tbody id="tableBody" class="divide-y divide-slate-100">
              <!-- Loading State Default -->
              <tr>
                <td colspan="8" class="text-center py-20">
                  <div class="flex flex-col items-center justify-center space-y-3">
                    <div class="w-10 h-10 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
                    <span class="text-slate-400 font-medium">Sedang memuat data inventaris...</span>
                  </div>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
      </section>
    </main>

    <!-- MODAL FORM (TAMBAH / EDIT BARANG) -->
    <div id="formModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
      <!-- Backdrop Blurry -->
      <div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity"></div>
     
      <!-- Kontainer Modal -->
      <div class="flex min-h-full items-center justify-center p-4">
        <div class="relative bg-white rounded-3xl shadow-xl border border-slate-100 max-w-md w-full overflow-hidden transform transition-all duration-300 scale-95 opacity-0 modal-container">
          <!-- Modal Header -->
          <div class="bg-slate-900 text-white p-6 flex justify-between items-center">
            <h3 id="modalTitle" class="text-lg font-bold">Tambah Barang Baru</h3>
            <button onclick="tutupModal()" class="text-slate-400 hover:text-white transition">
              <i class="fa-solid fa-xmark text-lg"></i>
            </button>
          </div>
         
          <!-- Modal Body Form -->
          <form id="itemForm" onsubmit="simpanData(event)" class="p-6 space-y-4">
            <input type="hidden" id="itemId"> <!-- Menyimpan ID saat Mode Edit -->
           
            <div>
              <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">Nama Barang</label>
              <input type="text" id="itemName" required class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:outline-none transition" placeholder="Contoh: Monitor Xiaomi 27 inch">
            </div>

            <div class="grid grid-cols-2 gap-4">
              <div>
                <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">Kategori</label>
                <select id="itemCategory" required class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:outline-none transition">
                  <!-- Diisi Dinamis oleh JS -->
                </select>
              </div>
              <div>
                <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">Jumlah Stok</label>
                <input type="number" id="itemQuantity" min="0" required class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:outline-none transition" placeholder="0">
              </div>
            </div>

            <div class="grid grid-cols-2 gap-4">
              <div>
                <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">Harga Satuan (Rp)</label>
                <input type="number" id="itemPrice" min="0" required class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:outline-none transition" placeholder="Rp 0">
              </div>
              <div>
                <label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-1.5">Lokasi Rak Penyimpanan</label>
                <input type="text" id="itemLocation" required class="w-full px-4 py-2.5 border border-slate-200 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:outline-none transition" placeholder="Contoh: Rak A-3">
              </div>
            </div>

            <!-- Modal Footer Button -->
            <div class="flex justify-end space-x-3 pt-4 border-t border-slate-100 mt-6">
              <button type="button" onclick="tutupModal()" class="px-5 py-2.5 border border-slate-200 text-slate-600 font-semibold rounded-xl hover:bg-slate-50 transition">Batal</button>
              <button type="submit" id="btnSubmitForm" class="px-5 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold rounded-xl shadow-sm transition">Simpan Barang</button>
            </div>
          </form>
        </div>
      </div>
    </div>

    <!-- MODAL KONFIRMASI HAPUS -->
    <div id="deleteModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
      <div class="fixed inset-0 bg-black/40 backdrop-blur-sm transition-opacity"></div>
      <div class="flex min-h-full items-center justify-center p-4">
        <div class="relative bg-white rounded-3xl shadow-xl border border-slate-100 max-w-sm w-full overflow-hidden transform transition-all duration-300 scale-95 opacity-0 delete-modal-container p-6 text-center">
          <div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100 text-red-600 mb-4">
            <i class="fa-solid fa-trash-can text-lg"></i>
          </div>
          <h3 class="text-lg font-bold text-slate-800 mb-1">Hapus Barang?</h3>
          <p class="text-sm text-slate-500 mb-6">Apakah Anda yakin ingin menghapus barang <strong id="deleteTargetName">...</strong>? Tindakan ini tidak dapat dibatalkan.</p>
         
          <div class="flex justify-center space-x-3">
            <button onclick="tutupDeleteModal()" class="px-4 py-2 border border-slate-200 text-slate-600 font-semibold rounded-xl hover:bg-slate-50 transition">Kembali</button>
            <button id="btnConfirmDelete" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-xl shadow-sm transition">Hapus Permanen</button>
          </div>
        </div>
      </div>
    </div>

    <!-- NOTIFIKASI TOAST CONTAINER (BOTTOM-RIGHT) -->
    <div id="toastContainer" class="fixed bottom-6 right-6 z-50 space-y-3 pointer-events-none"></div>

    <!-- Memanggil Modul JavaScript Sisi Klien -->
    <?!= include('JavaScript'); ?>
  </body>
</html>

CSS

<style>
  /* Kustomisasi gaya scrollbar agar lebih estetis */
  ::-webkit-scrollbar {
    width: 6px;
    height: 6px;
  }
  ::-webkit-scrollbar-track {
    background: transparent;
  }
  ::-webkit-scrollbar-thumb {
    background: #cbd5e1;
    border-radius: 9999px;
  }
  ::-webkit-scrollbar-thumb:hover {
    background: #94a3b8;
  }

  /* Animasi Transisi Halus Masuk untuk Modal */
  .modal-open-anim {
    transform: scale(1) !important;
    opacity: 1 !important;
  }

  /* Transisi Kustom untuk Element Toast */
  .toast-enter {
    animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
  }

  .toast-leave {
    animation: fadeOutDown 0.3s ease-in forwards;
  }

  @keyframes slideInRight {
    from {
      transform: translateX(120%);
      opacity: 0;
    }
    to {
      transform: translateX(0);
      opacity: 1;
    }
  }

  @keyframes fadeOutDown {
    from {
      transform: translateY(0);
      opacity: 1;
    }
    to {
      transform: translateY(20px);
      opacity: 0;
    }
  }
</style>

JavaScript

<script>
  /**
   * SISTEM MANAJEMEN INVENTARIS - FRONTEND JAVASCRIPT (JavaScript.html)
   * Ditulis oleh: Apps Script Expert
   * Deskripsi: State management, filtering, render table, dan integrasi dengan Backend GAS.
   */

  // State Utama Aplikasi Sisi Klien
  let dbItems = [];
  const KATEGORI_LIST = ["Elektronik", "Alat Tulis Kantor", "Furnitur", "Konsumsi", "Lainnya"];
  let selectedDeleteId = null;

  // Mulai Proses Saat Window Selesai Memuat DOM
  window.onload = function() {
    initKategoriSelect();
    loadInventarisData();
  };

  /**
   * Mengisi elemen dropdown kategori secara dinamis
   */
  function initKategoriSelect() {
    const filterSelect = document.getElementById('categoryFilter');
    const formSelect = document.getElementById('itemCategory');
   
    // Reset konten
    filterSelect.innerHTML = '<option value="">Semua Kategori</option>';
    formSelect.innerHTML = '<option value="" disabled selected>Pilih Kategori...</option>';

    KATEGORI_LIST.forEach(cat => {
      // Tambah ke filter dropdown
      const optFilter = document.createElement('option');
      optFilter.value = cat;
      optFilter.textContent = cat;
      filterSelect.appendChild(optFilter);

      // Tambah ke form input dropdown
      const optForm = document.createElement('option');
      optForm.value = cat;
      optForm.textContent = cat;
      formSelect.appendChild(optForm);
    });
  }

  /**
   * Ambil data dari backend (Kode.gs -> readAll)
   */
  function loadInventarisData() {
    showTableLoading();
    google.script.run
      .withSuccessHandler(function(response) {
        dbItems = response;
        renderDashboardStats(dbItems);
        renderTable(dbItems);
      })
      .withFailureHandler(function(error) {
        showToast("Terjadi kesalahan sistem: " + error.message, "error");
        renderTable([]); // Tampilkan placeholder kosong
      })
      .readAll();
  }

  /**
   * Merender Kalkulasi Angka Dashboard Statistik
   */
  function renderDashboardStats(items) {
    const totalJenis = items.length;
    let totalStok = 0;
    let totalNilaiAset = 0;
    let stokMenipisCount = 0;

    items.forEach(item => {
      const qty = Number(item.jumlah) || 0;
      const price = Number(item.harga) || 0;
     
      totalStok += qty;
      totalNilaiAset += (qty * price);
     
      if (qty < 5) {
        stokMenipisCount++;
      }
    });

    document.getElementById('statTotalJenis').textContent = totalJenis;
    document.getElementById('statTotalStok').textContent = totalStok;
    document.getElementById('statNilaiAset').textContent = formatRupiah(totalNilaiAset);
    document.getElementById('statStokMenipis').textContent = stokMenipisCount;

    // Ubah visualisasi visual card peringatan stok menipis jika > 0
    const cardStok = document.getElementById('cardStokMenipis');
    const iconStok = document.getElementById('iconStokMenipis');
    if (stokMenipisCount > 0) {
      cardStok.className = "bg-amber-50 p-6 rounded-2xl shadow-sm border border-amber-200 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1";
      iconStok.className = "bg-amber-500 text-white p-4 rounded-xl";
    } else {
      cardStok.className = "bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1";
      iconStok.className = "bg-slate-100 text-slate-400 p-4 rounded-xl";
    }
  }

  /**
   * Merender Baris Tabel Data dari Array
   */
  function renderTable(itemsToRender) {
    const tbody = document.getElementById('tableBody');
    tbody.innerHTML = '';

    if (itemsToRender.length === 0) {
      tbody.innerHTML = `
        <tr>
          <td colspan="8" class="text-center py-20">
            <div class="flex flex-col items-center justify-center space-y-2">
              <i class="fa-solid fa-box-open text-slate-300 text-5xl"></i>
              <span class="text-slate-400 font-medium">Tidak ada data inventaris ditemukan</span>
            </div>
          </td>
        </tr>
      `;
      return;
    }

    itemsToRender.forEach(item => {
      const tr = document.createElement('tr');
      tr.className = "hover:bg-slate-50/50 transition duration-150";

      // Badge Kategori Kustomisasi Visual
      let catBadgeClass = "bg-slate-100 text-slate-800";
      if (item.kategori === "Elektronik") catBadgeClass = "bg-blue-50 text-blue-700 border border-blue-100";
      else if (item.kategori === "Alat Tulis Kantor") catBadgeClass = "bg-indigo-50 text-indigo-700 border border-indigo-100";
      else if (item.kategori === "Furnitur") catBadgeClass = "bg-purple-50 text-purple-700 border border-purple-100";
      else if (item.kategori === "Konsumsi") catBadgeClass = "bg-amber-50 text-amber-700 border border-amber-100";

      // Stok Text Styling jika menipis
      const isLowStock = Number(item.jumlah) < 5;
      const stokCell = isLowStock
        ? `<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-rose-50 text-rose-700 border border-rose-100"><span class="w-1.5 h-1.5 rounded-full bg-rose-600 animate-ping"></span>${item.jumlah} Unit</span>`
        : `<span class="text-sm font-semibold text-slate-700">${item.jumlah} Unit</span>`;

      tr.innerHTML = `
        <td class="py-4 px-6 text-sm font-semibold text-slate-500">${item.id}</td>
        <td class="py-4 px-6 text-sm font-bold text-slate-800">${item.namabarang}</td>
        <td class="py-4 px-6"><span class="px-2.5 py-1 rounded-lg text-xs font-semibold ${catBadgeClass}">${item.kategori}</span></td>
        <td class="py-4 px-6 text-center">${stokCell}</td>
        <td class="py-4 px-6 text-sm font-bold text-slate-700 text-right">${formatRupiah(item.harga)}</td>
        <td class="py-4 px-6 text-sm font-medium text-slate-600">${item.lokasi}</td>
        <td class="py-4 px-6 text-xs text-slate-400 font-mono">${item.tanggalupdate}</td>
        <td class="py-4 px-6 text-center">
          <div class="inline-flex items-center space-x-2">
            <button onclick="editBarang('${item.id}')" class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition" title="Edit Barang">
              <i class="fa-solid fa-pen-to-square"></i>
            </button>
            <button onclick="bukaDeleteModal('${item.id}', '${item.namabarang}')" class="p-2 text-rose-600 hover:text-rose-800 hover:bg-rose-50 rounded-lg transition" title="Hapus Barang">
              <i class="fa-solid fa-trash-can"></i>
            </button>
          </div>
        </td>
      `;
      tbody.appendChild(tr);
    });
  }

  /**
   * Filter & Search Real-Time
   */
  function filterData() {
    const q = document.getElementById('searchQuery').value.toLowerCase();
    const cat = document.getElementById('categoryFilter').value;

    const filtered = dbItems.filter(item => {
      const matchSearch = item.id.toLowerCase().includes(q) ||
                          item.namabarang.toLowerCase().includes(q) ||
                          item.lokasi.toLowerCase().includes(q);
      const matchCat = cat === "" || item.kategori === cat;
      return matchSearch && matchCat;
    });

    renderTable(filtered);
  }

  /**
   * Handler untuk Menyimpan Data (Create / Update)
   */
  function simpanData(e) {
    e.preventDefault();

    const id = document.getElementById('itemId').value;
    const name = document.getElementById('itemName').value;
    const cat = document.getElementById('itemCategory').value;
    const qty = document.getElementById('itemQuantity').value;
    const price = document.getElementById('itemPrice').value;
    const loc = document.getElementById('itemLocation').value;

    // Tombol Loading State
    const btnSubmit = document.getElementById('btnSubmitForm');
    const originalText = btnSubmit.innerText;
    btnSubmit.disabled = true;
    btnSubmit.innerText = "Memproses...";

    const itemData = {
      namabarang: name,
      kategori: cat,
      jumlah: qty,
      harga: price,
      lokasi: loc
    };

    if (id) {
      // MODE UPDATE BARANG
      itemData.id = id;
      google.script.run
        .withSuccessHandler(function(msg) {
          showToast(msg, "success");
          tutupModal();
          loadInventarisData();
        })
        .withFailureHandler(function(error) {
          showToast("Update gagal: " + error.message, "error");
          btnSubmit.disabled = false;
          btnSubmit.innerText = originalText;
        })
        .updateRow(itemData);
    } else {
      // MODE CREATE BARANG BARU
      google.script.run
        .withSuccessHandler(function(msg) {
          showToast(msg, "success");
          tutupModal();
          loadInventarisData();
        })
        .withFailureHandler(function(error) {
          showToast("Tambah gagal: " + error.message, "error");
          btnSubmit.disabled = false;
          btnSubmit.innerText = originalText;
        })
        .createRow(itemData);
    }
  }

  /**
   * MODE EDIT: Mengisi Form modal dengan data lama
   */
  function editBarang(id) {
    const matchedItem = dbItems.find(item => item.id === id);
    if (!matchedItem) return;

    bukaModal(true); // Buka modal dalam edit mode

    document.getElementById('itemId').value = matchedItem.id;
    document.getElementById('itemName').value = matchedItem.namabarang;
    document.getElementById('itemCategory').value = matchedItem.kategori;
    document.getElementById('itemQuantity').value = matchedItem.jumlah;
    document.getElementById('itemPrice').value = matchedItem.harga;
    document.getElementById('itemLocation').value = matchedItem.lokasi;
  }

  /**
   * DELETE - Pemicu aksi hapus baris barang
   */
  function eksekusiHapus() {
    if (!selectedDeleteId) return;

    const btn = document.getElementById('btnConfirmDelete');
    btn.disabled = true;
    btn.innerText = "Menghapus...";

    google.script.run
      .withSuccessHandler(function(msg) {
        showToast(msg, "success");
        tutupDeleteModal();
        loadInventarisData();
      })
      .withFailureHandler(function(error) {
        showToast("Gagal menghapus: " + error.message, "error");
        tutupDeleteModal();
      })
      .deleteRow(selectedDeleteId);
  }

  /* --- MANAJEMEN MODAL DAN POPUP UI --- */

  function bukaModal(isEdit = false) {
    const modal = document.getElementById('formModal');
    const container = modal.querySelector('.modal-container');
    document.getElementById('modalTitle').textContent = isEdit ? "Edit Data Barang" : "Tambah Barang Baru";
    document.getElementById('btnSubmitForm').textContent = isEdit ? "Simpan Perubahan" : "Simpan Barang";
   
    // Pastikan Form Bersih dari Data Sebelumnya jika form tambah
    if (!isEdit) {
      document.getElementById('itemForm').reset();
      document.getElementById('itemId').value = '';
    }

    modal.classList.remove('hidden');
    setTimeout(() => {
      container.classList.add('modal-open-anim');
    }, 10);
  }

  function tutupModal() {
    const modal = document.getElementById('formModal');
    const container = modal.querySelector('.modal-container');
    container.classList.remove('modal-open-anim');
    setTimeout(() => {
      modal.classList.add('hidden');
    }, 200);
  }

  function bukaDeleteModal(id, nama) {
    selectedDeleteId = id;
    document.getElementById('deleteTargetName').textContent = id + " (" + nama + ")";
   
    const modal = document.getElementById('deleteModal');
    const container = modal.querySelector('.delete-modal-container');
   
    const btn = document.getElementById('btnConfirmDelete');
    btn.disabled = false;
    btn.innerText = "Hapus Permanen";
    btn.onclick = eksekusiHapus;

    modal.classList.remove('hidden');
    setTimeout(() => {
      container.classList.add('modal-open-anim');
    }, 10);
  }

  function tutupDeleteModal() {
    const modal = document.getElementById('deleteModal');
    const container = modal.querySelector('.delete-modal-container');
    container.classList.remove('modal-open-anim');
    setTimeout(() => {
      modal.classList.add('hidden');
      selectedDeleteId = null;
    }, 200);
  }

  function showTableLoading() {
    document.getElementById('tableBody').innerHTML = `
      <tr>
        <td colspan="8" class="text-center py-20">
          <div class="flex flex-col items-center justify-center space-y-3">
            <div class="w-10 h-10 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
            <span class="text-slate-400 font-medium">Sedang memproses data...</span>
          </div>
        </td>
      </tr>
    `;
  }

  /* --- UTILITY HELPER FUNCTIONS --- */

  /**
   * Memformat angka standar menjadi format rupiah lokal Indonesia
   */
  function formatRupiah(num) {
    return new Intl.NumberFormat('id-ID', {
      style: 'currency',
      currency: 'IDR',
      minimumFractionDigits: 0
    }).format(num);
  }

  /**
   * Membuat dan memunculkan toast notifikasi kustom
   */
  function showToast(message, type = "success") {
    const container = document.getElementById('toastContainer');
    const toast = document.createElement('div');
   
    const isSuccess = type === "success";
    const bgClass = isSuccess ? "bg-emerald-900 border-emerald-800 text-emerald-100" : "bg-red-900 border-red-800 text-red-100";
    const icon = isSuccess ? "fa-circle-check text-emerald-400" : "fa-triangle-exclamation text-red-400";

    toast.className = `flex items-center gap-3 p-4 rounded-2xl shadow-xl border ${bgClass} toast-enter pointer-events-auto min-w-[300px] max-w-sm`;
    toast.innerHTML = `
      <i class="fa-solid ${icon} text-lg"></i>
      <span class="text-sm font-semibold flex-1">${message}</span>
    `;

    container.appendChild(toast);

    // Otomatis hapus toast dalam waktu 4 detik
    setTimeout(() => {
      toast.classList.add('toast-leave');
      setTimeout(() => {
        toast.remove();
      }, 300);
    }, 4000);
  }
</script>







Tidak ada komentar:

Posting Komentar

MPA - Bukti Pendaftaran

  https://script.google.com/macros/s/AKfycbw1NyiOCuf1kvPEHau3viii_943iUX5CTQJ3hQRM97ng_BMNLsAITQzjUKmp25TYBvx/exec https://drive.google.com/...