Rabu, 27 Mei 2026

MPA - Bukti Pendaftaran

 




PROMPT

Act as a senior Google Apps Script developer, UI/UX designer, dan system architect.

Bangun sebuah aplikasi web menggunakan Google Apps Script dengan metode Standalone Script dan arsitektur MPA (Multi Page Application) untuk sistem pendaftaran siswa SMA berbasis online. id spreadsheet : 1fvtolGMLP1c-p4j776tp4aNR5RT2Vz2NL8rHohr7HyQ

sheetname : Siswa

id Drive bukti pendaftaran : 1Q7QmXJzlGrdPrMcfYkx8isZgSie-ZFaj

id Drive foto :17rSN4bUIHjIDHVLaPRbDK5pxr-yGaxkL. Aplikasi digunakan untuk proses pendaftaran siswa baru SMA secara online dengan fitur:



Landing page modern

Form pendaftaran siswa

CRUD data pendaftaran

Upload dan penyimpanan foto ke Google Drive

Generate dan cetak PDF bukti pendaftaran

Dashboard admin

Responsive UI modern





Spesifikasi Teknologi

Platform

Google Apps Script (Standalone Script)

Google Spreadsheet sebagai database

Google Drive sebagai penyimpanan foto dan PDF

HTML Service untuk frontend

Arsitektur

Gunakan arsitektur:



MPA (Multi Page Application)

Setiap halaman dipisah menjadi file HTML berbeda 

Berikut adalah seluruh kod sumber yang telah dipecahkan kepada fail-fail berasingan mengikut struktur projek Google Apps Script (GAS) MPA yang profesional, lengkap dengan reka bentuk UI/UX yang premium dan panduan langkah demi langkah untuk proses deployment.

Kita akan mencipta 5 fail berikut:

  1. Code.gs (Logik Backend & Penjana PDF)

  2. Landing.html (Halaman Utama)

  3. Form.html (Borang Pendaftaran Multi-step & Muat Naik Foto)

  4. Dashboard.html (Halaman Pentadbir/Admin & Pengurusan Data)

  5. Panduan_Deploy.md (Langkah Deployment & Persediaan Google Drive/Spreadsheet)

Panduan Penyebaran Portal PPDB SMA Atas Talian

Dokumen panduan ini bertujuan untuk membantu proses deployment dan konfigurasi kod sumber di atas ke Google Apps Script (GAS).

Langkah 1: Persediaan Pangkalan Data (Google Spreadsheet)

  1. Cipta Google Spreadsheet baru atau gunakan fail sedia ada yang mempunyai ID: 1fvtolGMLP1c-p4j776tp4aNR5RT2Vz2NL8rHohr7HyQ.

  2. Tukar nama tab helaian pertama (Sheet Name) kepada: Siswa.

  3. Sediakan 10 kepala kolum (Headers) berikut pada baris pertama (Baris 1 dari Kolum A hingga J):

    • A1: ID

    • B1: NISN

    • C1: Nama

    • D1: Alamat

    • E1: Email

    • F1: Asal SMP

    • G1: URL Foto

    • H1: Sekolah Tujuan

    • I1: Timestamp

    • J1: URL PDF

Langkah 2: Persediaan Fail Folder Google Drive

Sistem memerlukan dua folder khusus di dalam Google Drive untuk penyimpanan dokumen digital. Cipta folder baharu dan salin ID foldernya:

  1. Folder Foto Siswa:

    • Cipta folder baharu dan namakannya Foto Siswa PPDB.

    • Kongsi folder ini secara terbuka: Klik Kanan -> Kongsi -> Pilih Sesiapa sahaja yang mempunyai pautan boleh melihat (Viewer).

    • Salin ID foldernya (cth: 17rSN4bUIHjIDHVLaPRbDK5pxr-yGaxkL) dan kemas kini pemalar photoFolderId dalam CONFIG fail Code.gs.

  2. Folder Bukti PDF:

    • Cipta folder kedua bernama Kad PDF Bukti Pendaftaran.

    • Setkan akses kongsi folder kepada: Sesiapa sahaja yang mempunyai pautan boleh melihat (Viewer).

    • Salin ID foldernya (cth: 1Q7QmXJzlGrdPrMcfYkx8isZgSie-ZFaj) dan kemas kini pemalar pdfFolderId dalam CONFIG fail Code.gs.

Langkah 3: Membuat Projek Google Apps Script (GAS)

  1. Layari script.google.com dan log masuk menggunakan akaun Google anda.

  2. Klik butang New Project (Projek Baharu).

  3. Namakan semula projek anda sebagai: Sistem Portal PPDB SMA.

  4. Cipta fail-fail baharu mengikut senarai di bawah (Gunakan butang + -> Pilih format HTML atau Script):

Nama Fail di GAS

Jenis Fail

Sumber Kandungan (Sila Salin dari Atas)

Code.gs

Script

Salin semua kod daripada bahagian Code.gs

Landing.html

HTML

Salin kandungan HTML dari fail Landing.html

Form.html

HTML

Salin kandungan HTML dari fail Form.html

Dashboard.html

HTML

Salin kandungan HTML dari fail Dashboard.html

Langkah 4: Proses Pelancaran Aplikasi Web (Deployment)

  1. Di dalam Editor GAS, klik butang Deploy di penjuru kanan atas -> Pilih New deployment (Penyebaran baharu).

  2. Klik pada ikon tetapan roda gerigi (Select type) -> Pilih Web app (Aplikasi Web).

  3. Konfigurasikan tetapan penyebaran seperti berikut:

    • Description: PPDB Versi Pelancaran Stabil

    • Execute as: Me (emel_anda@gmail.com) (Ini penting bagi membenarkan kod mengakses Drive & Spreadsheet bagi pihak aplikasi)

    • Who has access: Anyone (Pendaftar luar boleh membuka halaman borang pendaftaran)

  4. Klik butang Deploy.

  5. Sekiranya permohonan kebenaran keselamatan dipaparkan, klik Authorize Access, log masuk ke akaun Google anda, klik Advanced -> Pilih Go to Sistem Portal PPDB SMA (unsafe) untuk memberikan kebenaran.

  6. Selepas selesai, sistem akan memaparkan Web app URL. Salin pautan URL ini untuk dipautkan di laman sosial atau diakses secara rasmi oleh pelajar.

🎉 Selesai!

Portal PPDB MPA anda kini sedia menerima pendaftaran siswa baharu secara langsung. Semua data yang diisi, fail pas foto yang dimuat naik, serta fail PDF kad bukti pendaftaran akan disimpan secara automatik dalam Workspace Google anda.

MPA - Pendaftaran

 







Act as a senior Google Apps Script developer, UI/UX designer, dan system architect.

Bangun sebuah aplikasi web menggunakan Google Apps Script dengan metode Standalone Script dan arsitektur MPA (Multi Page Application) untuk sistem pendaftaran siswa SMA berbasis online.


Tujuan Aplikasi

Aplikasi digunakan untuk proses pendaftaran siswa baru SMA secara online dengan fitur:


Landing page modern

Form pendaftaran siswa

CRUD data pendaftaran

Upload dan penyimpanan foto ke Google Drive

Generate dan cetak PDF bukti pendaftaran

Dashboard admin

Responsive UI modern

Spesifikasi Teknologi

Platform

Google Apps Script (Standalone Script)

Google Spreadsheet sebagai database

Google Drive sebagai penyimpanan foto dan PDF

HTML Service untuk frontend

Arsitektur

Gunakan arsitektur:


MPA (Multi Page Application)

Setiap halaman dipisah menjadi file HTML berbeda

Struktur File

Gunakan struktur file berikut:


Code.gs

Config.gs

Auth.gs

Student.gs

Pdf.gs

Drive.gs

Index.html

Landing.html

Register.html

Dashboard.html

Detail.html

Navbar.html

Sidebar.html

Footer.html

CSS.html

JS.html

Gunakan fungsi include() untuk template reusable.

Contoh:


function include(filename){

  return HtmlService.createHtmlOutputFromFile(filename).getContent();

}

Landing Page

Buat landing page modern gaya startup education technology.


Hero Section

Hero section harus memiliki:


Headline besar:

“Sistem Pendaftaran Siswa SMA”

Subheadline:

“Platform digital untuk pendaftaran siswa baru secara cepat, aman, dan modern”

Tombol CTA:

Daftar Sekarang

Login Admin

Visual Style

Gunakan desain:


Modern

Clean

Edu-tech startup

Responsive

Minimalis

Gradient modern

Soft shadow

Rounded card

Warna

Gunakan kombinasi:


Biru

Putih

Navy

Abu modern

Gunakan

Bootstrap 5

Bootstrap Icons

Google Fonts (Poppins)

Database Spreadsheet

Nama Sheet

Sheet: DATA_SISWA


Struktur Kolom

| ID | NISN | Nama | Alamat | Email | Asal SMP | URL Foto | Sekolah Tujuan | Timestamp |

Gunakan auto increment ID.

Fitur CRUD

Create

Form input siswa:


NISN

Nama

Alamat

Email

Asal SMP

Sekolah Tujuan

Upload Foto

Read

Tampilkan data:


Table Bootstrap

Search realtime

Pagination

Sorting

Update

Admin dapat edit data siswa.


Delete

Admin dapat hapus data dengan konfirmasi modal.

Upload Foto

Ketentuan

Foto disimpan di Google Drive

Folder Drive dibuat otomatis

Simpan URL file ke spreadsheet

Foto preview tersedia

Validasi

JPG/PNG

Maksimal 2MB

Dashboard Admin

Fitur Dashboard

Total pendaftar

Jumlah sekolah asal

Statistik harian

Tabel data siswa

Export PDF

Cetak bukti pendaftaran

UI Dashboard

Gunakan:


Card statistik

Sidebar modern

Navbar sticky

DataTable Bootstrap

Generate PDF Bukti Pendaftaran

PDF berisi:

Logo sekolah

Judul bukti pendaftaran

Nomor pendaftaran

Identitas siswa

NISN

Nama lengkap

Alamat

Email

Asal SMP

Sekolah tujuan

Foto siswa

Tanggal pendaftaran

QR Code verifikasi

Format

Layout profesional

A4 Portrait

Bisa di-download

Bisa dicetak

Penyimpanan

PDF otomatis tersimpan di Google Drive

URL PDF disimpan ke spreadsheet

Routing MPA

Gunakan parameter:


?page=landing

?page=register

?page=dashboard

?page=detail&id=1

Contoh:


function doGet(e){

  const page = e.parameter.page || 'landing';


  if(page == 'register'){

    return HtmlService.createTemplateFromFile('Register')

      .evaluate();

  }


  if(page == 'dashboard'){

    return HtmlService.createTemplateFromFile('Dashboard')

      .evaluate();

  }


  return HtmlService.createTemplateFromFile('Landing')

    .evaluate();

}

Validasi Form

Validasi Client Side

Semua field wajib diisi

Email valid

NISN numerik

Upload foto wajib

Validasi Server Side

Cek duplicate NISN

Sanitasi data

Validasi ukuran file

UX Features

Tambahkan:


Loading spinner

Toast notification

SweetAlert2

Modal Bootstrap

Empty state

Responsive mobile

Smooth animation

Security

Gunakan:


Escape HTML

Sanitasi input

Validasi server-side

Hide spreadsheet ID di Config.gs

Coding Style

Gunakan:


Clean code

Modular architecture

Reusable component

Async google.script.run

Komentar jelas

Output yang Harus Dihasilkan

Generate source code lengkap:


Backend Google Apps Script

Frontend Bootstrap

CRUD functionality

Upload image to Drive

Generate PDF

Responsive UI

Routing MPA

Dashboard admin

Landing page modern

Berikan:


Struktur folder

Kode lengkap setiap file

Penjelasan fungsi

Cara deploy sebagai Web App

Cara setup Spreadsheet dan Drive

Cara publish aplikasi

Studi kasus penggunaan aplikasi

Pastikan seluruh kode production-ready dan dapat langsung dijalankan di Google Apps Script.


Prompt Pengembangan Web App Google Apps Script

Sistem Pendaftaran Siswa SMA (Standalone Script + Arsitektur MPA)

Act as a senior Google Apps Script developer, UI/UX designer, dan system architect.

Bangun sebuah aplikasi web menggunakan Google Apps Script dengan metode Standalone Script dan arsitektur MPA (Multi Page Application) untuk sistem pendaftaran siswa SMA berbasis online.

Tujuan Aplikasi

Aplikasi digunakan untuk proses pendaftaran siswa baru SMA secara online dengan fitur:

  • Landing page modern

  • Form pendaftaran siswa

  • CRUD data pendaftaran

  • Upload dan penyimpanan foto ke Google Drive

  • Generate dan cetak PDF bukti pendaftaran

  • Dashboard admin

  • Responsive UI modern


Spesifikasi Teknologi

Platform

  • Google Apps Script (Standalone Script)

  • Google Spreadsheet sebagai database

  • Google Drive sebagai penyimpanan foto dan PDF

  • HTML Service untuk frontend

Arsitektur

Gunakan arsitektur:

  • MPA (Multi Page Application)

  • Setiap halaman dipisah menjadi file HTML berbeda

Struktur File

Gunakan struktur file berikut:

  • Code.gs

  • Config.gs

  • Auth.gs

  • Student.gs

  • Pdf.gs

  • Drive.gs

  • Index.html

  • Landing.html

  • Register.html

  • Dashboard.html

  • Detail.html

  • Navbar.html

  • Sidebar.html

  • Footer.html

  • CSS.html

  • JS.html

Gunakan fungsi include() untuk template reusable.

Contoh:

function include(filename){
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

Landing Page

Buat landing page modern gaya startup education technology.

Hero Section

Hero section harus memiliki:

  • Headline besar:
    “Sistem Pendaftaran Siswa SMA”

  • Subheadline:
    “Platform digital untuk pendaftaran siswa baru secara cepat, aman, dan modern”

  • Tombol CTA:

    • Daftar Sekarang

    • Login Admin

Visual Style

Gunakan desain:

  • Modern

  • Clean

  • Edu-tech startup

  • Responsive

  • Minimalis

  • Gradient modern

  • Soft shadow

  • Rounded card

Warna

Gunakan kombinasi:

  • Biru

  • Putih

  • Navy

  • Abu modern

Gunakan

  • Bootstrap 5

  • Bootstrap Icons

  • Google Fonts (Poppins)


Database Spreadsheet

Nama Sheet

Sheet: DATA_SISWA

Struktur Kolom

| ID | NISN | Nama | Alamat | Email | Asal SMP | URL Foto | Sekolah Tujuan | Timestamp |

Gunakan auto increment ID.


Fitur CRUD

Create

Form input siswa:

  • NISN

  • Nama

  • Alamat

  • Email

  • Asal SMP

  • Sekolah Tujuan

  • Upload Foto

Read

Tampilkan data:

  • Table Bootstrap

  • Search realtime

  • Pagination

  • Sorting

Update

Admin dapat edit data siswa.

Delete

Admin dapat hapus data dengan konfirmasi modal.


Upload Foto

Ketentuan

  • Foto disimpan di Google Drive

  • Folder Drive dibuat otomatis

  • Simpan URL file ke spreadsheet

  • Foto preview tersedia

Validasi

  • JPG/PNG

  • Maksimal 2MB


Dashboard Admin

Fitur Dashboard

  • Total pendaftar

  • Jumlah sekolah asal

  • Statistik harian

  • Tabel data siswa

  • Export PDF

  • Cetak bukti pendaftaran

UI Dashboard

Gunakan:

  • Card statistik

  • Sidebar modern

  • Navbar sticky

  • DataTable Bootstrap


Generate PDF Bukti Pendaftaran

PDF berisi:

  • Logo sekolah

  • Judul bukti pendaftaran

  • Nomor pendaftaran

  • Identitas siswa

  • NISN

  • Nama lengkap

  • Alamat

  • Email

  • Asal SMP

  • Sekolah tujuan

  • Foto siswa

  • Tanggal pendaftaran

  • QR Code verifikasi

Format

  • Layout profesional

  • A4 Portrait

  • Bisa di-download

  • Bisa dicetak

Penyimpanan

  • PDF otomatis tersimpan di Google Drive

  • URL PDF disimpan ke spreadsheet


Routing MPA

Gunakan parameter:

?page=landing
?page=register
?page=dashboard
?page=detail&id=1

Contoh:

function doGet(e){
  const page = e.parameter.page || 'landing';

  if(page == 'register'){
    return HtmlService.createTemplateFromFile('Register')
      .evaluate();
  }

  if(page == 'dashboard'){
    return HtmlService.createTemplateFromFile('Dashboard')
      .evaluate();
  }

  return HtmlService.createTemplateFromFile('Landing')
    .evaluate();
}

Validasi Form

Validasi Client Side

  • Semua field wajib diisi

  • Email valid

  • NISN numerik

  • Upload foto wajib

Validasi Server Side

  • Cek duplicate NISN

  • Sanitasi data

  • Validasi ukuran file


UX Features

Tambahkan:

  • Loading spinner

  • Toast notification

  • SweetAlert2

  • Modal Bootstrap

  • Empty state

  • Responsive mobile

  • Smooth animation


Security

Gunakan:

  • Escape HTML

  • Sanitasi input

  • Validasi server-side

  • Hide spreadsheet ID di Config.gs


Coding Style

Gunakan:

  • Clean code

  • Modular architecture

  • Reusable component

  • Async google.script.run

  • Komentar jelas


Output yang Harus Dihasilkan

Generate source code lengkap:

  • Backend Google Apps Script

  • Frontend Bootstrap

  • CRUD functionality

  • Upload image to Drive

  • Generate PDF

  • Responsive UI

  • Routing MPA

  • Dashboard admin

  • Landing page modern

Berikan:

  1. Struktur folder

  2. Kode lengkap setiap file

  3. Penjelasan fungsi

  4. Cara deploy sebagai Web App

  5. Cara setup Spreadsheet dan Drive

  6. Cara publish aplikasi

  7. Studi kasus penggunaan aplikasi

Pastikan seluruh kode production-ready dan dapat langsung dijalankan di Google Apps Script.

MPA Inventaris Cetak

 






Buatkan contoh studi kasus lengkap dengan fitur landing page hero, CRUD dan cetak PDF

Berikut adalah kode Studi Kasus Lengkap Sistem Manajemen Inventaris (InvenTrack) dengan arsitektur Multi-Page Application (MPA) yang memisahkan Frontend dan Backend menggunakan model standalone script.

Sistem ini sudah mencakup semua fitur yang Anda minta:

  1. Landing Page Modern dengan Hero Section dan tombol Call-to-Action (CTA).

  2. Dashboard Manajemen Stok dengan operasional CRUD Lengkap (Create, Read, Update, Delete) menggunakan modal dialog.

  3. Fitur Cetak PDF Otomatis yang memanfaatkan template Google Docs di Drive, mengisi placeholder {{KODE}}, {{NAMA}}, dll, mengonversinya ke PDF secara real-time, dan langsung mengunduhnya.

🏛️ Struktur File Proyek di Editor Apps Script

Buat 5 file berikut di dalam editor script.google.com:

  • Code.gs (Semua Logika Backend, Router MPA, & Database CRUD + PDF)

  • index.html (Layout Induk Dashboard Internal & Navbar)

  • hero.html (Landing Page Utama / Publik)

  • dashboard.html (Komponen Antarmuka Form, Tabel Data, & Modal Edit)

  • javascript.html (Logika Frontend / API Controller Asinkronus)

🛠️ 1. Backend: Code.gs

Ganti seluruh kode di file Code.gs Anda. Jangan lupa untuk mengisi SPREADSHEET_ID, GOOGLE_DOC_TEMPLATE_ID, dan DRIVE_FOLDER_ID.

// =========================================================================

// CONFIG DATABASE & FILE SYSTEM (Ganti dengan ID milik Anda)

// =========================================================================

const SPREADSHEET_ID = '1yBsCdnY0ZvJiuWrdF_vAGh8EF3uON7TDr1DhO0ZYtv0'; 

const SHEET_NAME = 'Inventaris';

const GOOGLE_DOC_TEMPLATE_ID = '1Xe7z06o4mgU3hXQ7Wz6nwy2iZ0vbItAY5rFzkGRyc3U';

const DRIVE_FOLDER_ID = '1LgO3j5O7XIKnc00agk7U_abmlwqsx6Gf';




MPA Sistem Inventaris

 




Bertindaklah sebagai apps script expert. Buatkan study kasus sistem inventaris dengan frontend dan backend terpisah. Buatkan landing page dengan hero. Arsitektur MPA. Gunakan model standalone script. File kode terpisah .

PROJECT SUMMARY PROMPT: SISTEM INVENTARIS MP-GAS

1. Ringkasan Arsitektur & Teknologi

  • Nama Sistem: InvenTrack (Sistem Manajemen Inventaris Pintar)

  • Arsitektur: Multi-Page Application (MPA) dengan pemisahan Frontend dan Backend yang modular.

  • Model Script: Standalone Script (Google Apps Script).

  • Database: Google Spreadsheet (Tab: Inventaris).

  • Frontend Tech Stack: HTML5, Tailwind CSS (via CDN), JavaScript Vanilla (Asynchronous DOM Manipulation).

  • Communication Bridge: google.script.run (Asinkronus / AJAX bawaan Apps Script).

2. Struktur File Proyek (Flat Directory)

Sistem ini menggunakan 5 file utama yang diletakkan sejajar di dalam editor Google Apps Script:

  1. Code.gs (Backend): * Mengatur routing aplikasi berbasis parameter URL (?page=...).

    • Menyediakan fungsi inklusi file komponen (include()).

    • Berisi logikal API CRUD database (getInventoryData() dan addStockItem()) dengan mekanisme case-insensitive column mapping dan pembersihan data (anti-ghost rows filtering).

  2. index.html (Layout Induk Dashboard):

    • Berisi struktur core HTML, CDN Tailwind CSS, dan komponen global (Navbar & Status Admin).

    • Bertindak sebagai wrapper yang menyuntikkan file visual internal secara dinamis via scriptlet evaluasi Apps Script.

  3. hero.html (Landing Page):

    • Halaman publik terpisah dengan desain modern minimalis yang fokus pada pemasaran produk dan Call-to-Action (CTA) link dinamis menuju halaman dashboard internal.

  4. dashboard.html (View/Partial HTML Fitur):

    • Kerangka antarmuka fitur utama yang membagi halaman menjadi 2 kolom: Form Tambah Item Baru (Kiri) dan Tabel Real-Time Daftar Stok Barang (Kanan).

  5. javascript.html (Client-Side Controller):

    • Berisi logika frontend untuk menangani event submit form, memicu pemuatan data otomatis saat DOM siap, merender manipulasi tabel secara dinamis, dan mengubah state tombol simpan (loading animation).

3. Struktur Database (Google Spreadsheet)

  • Nama Lembar Kerja (Tab): Inventaris

  • Struktur Kolom (Baris 1): Timestamp | Kode Barang | Nama Barang | Stok | Kategori

  • Fitur Fleksibilitas Database: Kode backend didesain case-insensitive dan tahan terhadap pergeseran letak kolom menggunakan pemetaan indeks berbasis .toLowerCase().trim().

4. Alur Kerja Sistem (Data Flow)

  1. Routing: Pengguna mengakses URL Web App standar ➡️ doGet() merender hero.html. Jika mengakses URL dengan ?page=dashboard ➡️ doGet() merender index.html yang membungkus dashboard.html & javascript.html.

  2. Membaca Data (Read): Halaman termuat ➡️ JavaScript frontend memicu getInventoryData() di server ➡️ Server membaca baris Spreadsheet, mengubahnya menjadi Array of Objects yang bersih, lalu mengirimkannya kembali ➡️ Frontend menerimanya dan merender data ke dalam tabel HTML.

  3. Menulis Data (Write): Pengguna menekan tombol simpan ➡️ Tombol dinonaktifkan (state: Menyimpan...) ➡️ Frontend mengirimkan input data ke server via addStockItem() ➡️ Server melakukan .appendRow() ke Google Sheets dan mengembalikan respon sukses ➡️ Frontend mengosongkan form dan otomatis memicu fungsi Read Data ulang tanpa melakukan refresh browser.

💡 Cara Menggunakan Summary Ini untuk Pengembangan Selanjutnya:

Jika Anda ingin menambahkan fitur baru, Anda cukup menyalin teks di atas dan menambahkan perintah tambahan di bawahnya.

  • Contoh perintah tambahan: "Berdasarkan summary proyek di atas, buatkan modifikasi kode pada dashboard.html dan javascript.html untuk menambahkan tombol Hapus Barang di setiap baris tabel, serta buatkan fungsi backend deleteStockItem(kode) di Code.gs untuk menghapus baris barang yang sesuai di spreadsheet."

SPA dan MPA

 



MATERI GOOGLE APPS SCRIPT

SPA DAN MPA MENGGUNAKAN SPREADSHEET SEBAGAI BACKEND DATABASE

Pendahuluan

Dalam pengembangan aplikasi web modern menggunakan Google Apps Script, terdapat dua pendekatan utama yang sering digunakan:

  1. SPA (Single Page Application)

  2. MPA (Multi Page Application)

Kedua pendekatan ini dapat dikombinasikan dengan Google Spreadsheet sebagai backend database sehingga developer dapat membuat aplikasi berbasis cloud tanpa harus menggunakan server konvensional.

Google Spreadsheet sering digunakan sebagai database karena:

  • Gratis

  • Mudah digunakan

  • Real-time

  • Terintegrasi dengan Google Workspace

  • Cocok untuk prototyping

  • Mudah di-maintenance

Materi ini membahas:

  • Pengertian SPA dan MPA

  • Perbedaan SPA dan MPA

  • Cara kerja dengan Google Apps Script

  • Arsitektur aplikasi

  • Contoh implementasi

  • Studi kasus

  • Struktur project

  • Best practice


1. Apa Itu SPA?

SPA (Single Page Application) adalah aplikasi web yang hanya memuat satu halaman utama.

Konten berubah secara dinamis menggunakan JavaScript tanpa melakukan reload seluruh halaman.


Cara Kerja SPA

Browser
   │
   ▼
Load index.html sekali
   │
   ▼
JavaScript mengambil data dari Apps Script
   │
   ▼
Spreadsheet menjadi database
   │
   ▼
Konten berubah tanpa reload

Karakteristik SPA

1. Tidak Reload Halaman

Navigasi lebih cepat.

2. Menggunakan JavaScript Dinamis

Konten berubah menggunakan DOM.

3. Frontend dan Backend Dipisah

Frontend:

  • HTML

  • CSS

  • JavaScript

Backend:

  • Google Apps Script

  • Spreadsheet


2. Apa Itu MPA?

MPA (Multi Page Application) adalah aplikasi web yang terdiri dari banyak halaman.

Saat user berpindah halaman:

  • Browser melakukan reload

  • Server mengirim halaman baru


Cara Kerja MPA

User klik halaman
      │
      ▼
Request ke Apps Script
      │
      ▼
Apps Script generate halaman baru
      │
      ▼
Browser reload halaman

Karakteristik MPA

1. Banyak Halaman

Contoh:

  • Home

  • Dashboard

  • About

  • Contact

2. Render Server Side

Apps Script membuat halaman HTML baru.

3. Struktur Lebih Tradisional

Mirip website klasik.


3. Spreadsheet Sebagai Backend Database

Google Spreadsheet dapat digunakan sebagai database sederhana.


Contoh Struktur Spreadsheet

Sheet Users

IDNamaEmail
1Fajarfajar@gmail.com

Sheet Products

IDNama ProdukHarga
1Laptop7000000

Kelebihan Spreadsheet Sebagai Database

KelebihanPenjelasan
GratisTidak perlu hosting database
Mudah digunakanInterface spreadsheet
Real-timeData langsung update
Integrasi Apps ScriptSangat mudah
Cloud-basedOnline

Kekurangan Spreadsheet Sebagai Database

KekuranganPenjelasan
Tidak cocok big dataBatas performa
Relasi data terbatasTidak seperti SQL
Kurang amanJika salah konfigurasi

4. Arsitektur SPA dengan Apps Script

Browser
   │
   ▼
SPA Frontend
HTML + CSS + JavaScript
   │
   ▼
google.script.run
   │
   ▼
Google Apps Script
   │
   ▼
Google Spreadsheet

Struktur Project SPA

Apps Script
│
├── Code.gs
├── Index.html
├── JavaScript.html
├── css.html
└── Router.html

5. Contoh SPA Menggunakan Spreadsheet

Studi Kasus:

Dashboard Penjualan

Fitur:

  • Dashboard realtime

  • CRUD produk

  • Grafik penjualan

  • Filter data

  • Tanpa reload halaman


Spreadsheet Database

Sheet Products

IDNamaHarga
1Laptop7000000

Code.gs

function doGet(){

  return HtmlService
    .createTemplateFromFile('Index')
    .evaluate();
}

function getProducts(){

  const ss = SpreadsheetApp
    .openById('SPREADSHEET_ID');

  const sheet = ss
    .getSheetByName('Products');

  return sheet
    .getDataRange()
    .getValues();
}

Index.html

<!DOCTYPE html>
<html>
<head>
  <?!= include('css'); ?>
</head>
<body>

<div id="app"></div>

<?!= include('JavaScript'); ?>

</body>
</html>

JavaScript.html

<script>

function loadProducts(){

  google.script.run
    .withSuccessHandler(showProducts)
    .getProducts();
}

function showProducts(data){

  let html = '';

  data.forEach(row => {

    html += `
      <div class="card">
        <h3>${row[1]}</h3>
        <p>${row[2]}</p>
      </div>
    `;
  });

  document.getElementById('app')
    .innerHTML = html;
}

loadProducts();

</script>

Hasil SPA

  • Data tampil realtime

  • Tidak reload halaman

  • Navigasi cepat

  • User experience modern


6. Routing pada SPA Apps Script

SPA dapat menggunakan routing JavaScript.


Contoh Router

function navigate(page){

  if(page === 'dashboard'){
    loadDashboard();
  }

  if(page === 'products'){
    loadProducts();
  }
}

7. Arsitektur MPA dengan Apps Script

Browser
   │
   ▼
Apps Script Server
   │
   ▼
Generate HTML
   │
   ▼
Spreadsheet Database

Struktur Project MPA

Apps Script
│
├── Code.gs
├── Home.html
├── Products.html
├── About.html
├── Contact.html
└── css.html

8. Contoh MPA Menggunakan Spreadsheet

Studi Kasus:

Website Sekolah

Fitur:

  • Home

  • Profil

  • Guru

  • Berita

  • Kontak


Code.gs

function doGet(e){

  let page = e.parameter.page || 'Home';

  return HtmlService
    .createTemplateFromFile(page)
    .evaluate();
}

Home.html

<h1>Website Sekolah</h1>

<a href="?page=Products">Produk</a>
<a href="?page=About">About</a>

Products.html

<h1>Halaman Produk</h1>

Cara Kerja MPA

Saat klik menu:

  • Browser request halaman baru

  • Apps Script generate HTML

  • Halaman reload


9. CRUD Spreadsheet pada SPA

Create Data

function addProduct(name, price){

  const ss = SpreadsheetApp
    .openById('SPREADSHEET_ID');

  const sheet = ss
    .getSheetByName('Products');

  sheet.appendRow([
    new Date(),
    name,
    price
  ]);
}

Frontend

<input id="name">
<input id="price">

<button onclick="save()">
Save
</button>

JavaScript

function save(){

  const name = document
    .getElementById('name').value;

  const price = document
    .getElementById('price').value;

  google.script.run
    .addProduct(name, price);
}

10. Studi Kasus SPA

Dashboard Entrepreneur

Fitur:

  • Dashboard realtime

  • Grafik penjualan

  • Monitoring stok

  • CRUD produk

  • Notification


Mengapa SPA?

Karena:

  • Banyak interaksi realtime

  • Navigasi cepat

  • UX modern

  • Tidak reload halaman


Teknologi

Frontend:

  • HTML

  • Bootstrap

  • JavaScript

  • Chart.js

Backend:

  • Apps Script

Database:

  • Spreadsheet


11. Studi Kasus MPA

Sistem Informasi Sekolah

Fitur:

  • Profil sekolah

  • Data guru

  • Berita

  • Kontak

  • Artikel


Mengapa MPA?

Karena:

  • SEO lebih baik

  • Struktur sederhana

  • Banyak halaman informasi


Teknologi

Frontend:

  • HTML

  • CSS

  • Bootstrap

Backend:

  • Apps Script

Database:

  • Spreadsheet


12. Integrasi Google Charts

Contoh

<script>
google.charts.load('current', {
  packages:['corechart']
});
</script>

13. Perbandingan SPA dan MPA

AspekSPAMPA
HalamanSatu halamanBanyak halaman
ReloadTidakYa
UXModernTradisional
KecepatanCepatLebih lambat
SEOLebih sulitLebih mudah
KompleksitasTinggiRendah
Cocok untukDashboardWebsite informasi

14. Kelebihan SPA pada Apps Script

KelebihanPenjelasan
UX modernSmooth
CepatTanpa reload
Cocok dashboardReal-time
Hemat bandwidthHanya data berubah

15. Kekurangan SPA

KekuranganPenjelasan
JavaScript kompleksLebih sulit
SEO sulitRender client-side
Debugging lebih sulitBanyak state

16. Kelebihan MPA pada Apps Script

KelebihanPenjelasan
Mudah dibuatStruktur sederhana
SEO bagusHTML server-side
Cocok company profileBanyak halaman

17. Kekurangan MPA

KekuranganPenjelasan
Reload halamanLebih lambat
UX kurang modernTidak realtime

18. Hybrid SPA + MPA

Saat ini banyak aplikasi menggunakan hybrid.


Contoh

Landing page:

  • MPA

Dashboard:

  • SPA


Studi Kasus Hybrid

LMS Modern

Landing:

  • Informasi sekolah

  • SEO

Dashboard siswa:

  • Quiz realtime

  • Video learning

  • Live notification


19. Best Practice Apps Script SPA

1. Pisahkan File

Gunakan:

  • Code.gs

  • CSS

  • JavaScript


2. Gunakan include()

function include(filename){

  return HtmlService
    .createHtmlOutputFromFile(filename)
    .getContent();
}

3. Gunakan Modular Function

Contoh:

getProducts()
addProduct()
updateProduct()

4. Gunakan Loading Indicator

Agar UX lebih baik.


5. Gunakan Cache

Agar performa lebih cepat.


20. Best Practice Spreadsheet Database

1. Pisahkan Sheet

Contoh:

  • Users

  • Products

  • Orders


2. Gunakan Header

Baris pertama sebagai struktur data.


3. Hindari Data Terlalu Besar

Spreadsheet memiliki limit.


4. Gunakan Validasi

Untuk keamanan data.


21. Arsitektur Lengkap SPA Apps Script

Browser
   │
   ▼
Frontend SPA
HTML + Bootstrap + JS
   │
   ▼
google.script.run
   │
   ▼
Apps Script Backend
   │
   ▼
Spreadsheet Database

22. Arsitektur Lengkap MPA Apps Script

Browser
   │
   ▼
Apps Script Server
   │
   ▼
Generate HTML
   │
   ▼
Spreadsheet Database

23. Kapan Menggunakan SPA?

Gunakan SPA jika:

  • Dashboard realtime

  • Monitoring system

  • Marketplace

  • CRUD interaktif

  • LMS modern


24. Kapan Menggunakan MPA?

Gunakan MPA jika:

  • Website sekolah

  • Portal berita

  • Company profile

  • Website informasi


25. Kesimpulan

SPA dan MPA merupakan dua pendekatan penting dalam pengembangan aplikasi web menggunakan Google Apps Script.

SPA sangat cocok untuk:

  • Dashboard

  • Sistem realtime

  • Marketplace

  • CRUD modern

MPA sangat cocok untuk:

  • Website informasi

  • Portal sekolah

  • SEO

  • Company profile

Dengan menggunakan Spreadsheet sebagai backend database, developer dapat membangun aplikasi cloud dengan cepat, murah, dan mudah menggunakan Google Apps Script.

Standalone Script

 




Bertindaklah sebagai apps script expert. Buatkan study kasus sistem inventari dengan front end dan backend terpisah. Gunakan model standalone script. File kode terpisah . Tambahkan pula landing page dengan hero

Halo! Sebagai seorang Apps Script Expert, saya sangat merekomendasikan pendekatan Front-End dan Back-End terpisah (Decoupled Architecture) untuk Google Apps Script (GAS). Pendekatan ini membuat aplikasi jauh lebih cepat, rapi, mudah di-maintain, dan menghindari masalah klasik GAS seperti script timeout atau callback hell.

Kita akan menggunakan model Standalone Script (tidak menempel pada Sheet tertentu) dengan struktur file yang terpisah.

Berikut adalah studi kasus Sistem Inventaris Barang Rumah Sakit / Kantor beserta seluruh kode yang kamu butuhkan.

🏛️ Arsitektur Sistem

  • Database (Back-End): Google Sheets untuk menyimpan data barang.

  • Logika Bisnis (Back-End): Google Apps Script (Standalone) sebagai REST API mikro menggunakan fungsi doGet() dan doPost().

  • Front-End: HTML5, CSS (Tailwind CSS via CDN), dan JavaScript modern (Fetch API) untuk interaksi data.


MATERI GOOGLE APPS SCRIPT: STANDALONE SCRIPT

Pendahuluan

Dalam pengembangan aplikasi berbasis Google Workspace, Google Apps Script dapat digunakan untuk membuat otomatisasi, dashboard, integrasi sistem, hingga web application berbasis cloud.

Selain Bound Script, terdapat jenis script lain yang sangat penting yaitu Standalone Script.

Standalone Script sering digunakan untuk:

  • Sistem skala besar

  • Integrasi multi spreadsheet

  • Dashboard enterprise

  • Integrasi API

  • Sistem monitoring

  • Automation service

  • Multi-user web app

  • Integrasi database cloud

Materi ini membahas:

  • Pengertian Standalone Script

  • Cara kerja

  • Perbedaan dengan Bound Script

  • Struktur project

  • Contoh implementasi

  • Studi kasus

  • Integrasi web app

  • Best practice


1. Apa Itu Standalone Script?

Standalone Script adalah project Google Apps Script yang berdiri sendiri dan tidak terikat langsung pada file Google Workspace tertentu.

Artinya:

Script dibuat secara independen dan dapat mengakses banyak file Google Workspace.


2. Analogi Standalone Script

Bayangkan:

  • Spreadsheet = rumah

  • Apps Script = robot otomatis

Pada Standalone Script:

Robot tidak tinggal di satu rumah.
Robot dapat berpindah-pindah dan bekerja di banyak rumah.


3. Karakteristik Standalone Script

1. Berdiri Sendiri

Script tidak terikat pada spreadsheet tertentu.

2. Menggunakan ID File

Karena tidak terikat file aktif.

3. Lebih Fleksibel

Bisa mengakses:

  • Banyak spreadsheet

  • Banyak docs

  • Banyak forms

4. Cocok untuk Sistem Besar

Digunakan pada aplikasi enterprise.


4. Cara Membuat Standalone Script

Langkah-langkah

  1. Buka:

https://script.google.com

  1. Klik:

New Project

  1. Project Apps Script akan dibuat secara independen.


5. Struktur Standalone Script

Standalone Apps Script
│
├── Code.gs
├── Config.gs
├── API.gs
├── Database.gs
├── Index.html
├── css.html
└── JavaScript.html

6. Cara Kerja Standalone Script

Alur kerja:

  1. Script dijalankan

  2. Script membaca ID file

  3. Script membuka spreadsheet tertentu

  4. Data diproses

  5. Hasil dikirim ke user


7. Mengakses Spreadsheet pada Standalone Script

Karena tidak menggunakan file aktif, maka menggunakan:

SpreadsheetApp.openById()

Contoh

function getData(){

  const spreadsheet = SpreadsheetApp
    .openById('SPREADSHEET_ID');

  const sheet = spreadsheet
    .getSheetByName('Sales');

  const data = sheet
    .getDataRange()
    .getValues();

  Logger.log(data);
}

8. Penjelasan Kode

KodeFungsi
openById()Membuka spreadsheet tertentu
getSheetByName()Mengambil sheet
getValues()Mengambil data

9. Perbedaan Bound Script dan Standalone Script

Bound ScriptStandalone Script
Terikat fileBerdiri sendiri
Menggunakan file aktifMenggunakan ID file
Cocok project kecilCocok project besar
Mudah dibuatLebih fleksibel
Tidak reusableReusable

10. Kelebihan Standalone Script

KelebihanPenjelasan
FleksibelBisa akses banyak file
ReusableBisa digunakan ulang
Cocok enterpriseUntuk sistem besar
Integrasi mudahDengan API dan cloud
ModularMudah maintenance

11. Kekurangan Standalone Script

KekuranganPenjelasan
Lebih kompleksPerlu konfigurasi
Harus pakai ID fileTidak otomatis
Setup lebih lamaUntuk pemula lebih sulit

12. Mengambil ID Spreadsheet

Contoh URL spreadsheet:

https://docs.google.com/spreadsheets/d/1ABC123XYZ/edit

ID Spreadsheet:

1ABC123XYZ

13. Standalone Script untuk Web App

Standalone Script sangat populer untuk membangun web app.


Struktur Web App

Apps Script
│
├── Code.gs
├── Index.html
├── css.html
└── JavaScript.html

14. Fungsi doGet()

function doGet(){

  return HtmlService
    .createTemplateFromFile('Index')
    .evaluate();
}

15. Fungsi include()

function include(filename){

  return HtmlService
    .createHtmlOutputFromFile(filename)
    .getContent();
}

16. Contoh Index.html

<!DOCTYPE html>
<html>
<head>
  <?!= include('css'); ?>
</head>
<body>

<h1>Dashboard Monitoring</h1>

<?!= include('JavaScript'); ?>

</body>
</html>

17. Contoh css.html

<style>
body{
  font-family:Arial;
  background:#f5f5f5;
}

.card{
  background:white;
  padding:20px;
  border-radius:10px;
}
</style>

18. Contoh JavaScript.html

<script>

google.script.run
  .withSuccessHandler(showData)
  .getData();

function showData(data){
  console.log(data);
}

</script>

19. Studi Kasus 1: Dashboard Multi Sekolah

Kebutuhan

Sistem harus:

  • Mengambil data dari banyak spreadsheet sekolah

  • Menampilkan dashboard pusat

  • Menampilkan grafik

  • Monitoring real-time


Solusi

Menggunakan Standalone Script.


Arsitektur

Sekolah A Spreadsheet
Sekolah B Spreadsheet
Sekolah C Spreadsheet
        │
        ▼
Standalone Script
        │
        ▼
Dashboard Monitoring

Contoh Script

function getAllSchoolData(){

  const files = [
    'ID_FILE_1',
    'ID_FILE_2',
    'ID_FILE_3'
  ];

  let allData = [];

  files.forEach(id => {

    const ss = SpreadsheetApp.openById(id);

    const sheet = ss.getSheetByName('Data');

    const data = sheet
      .getDataRange()
      .getValues();

    allData.push(data);
  });

  return allData;
}

20. Studi Kasus 2: Dashboard UMKM

Kebutuhan

  • Data penjualan cabang

  • Grafik revenue

  • Monitoring stok

  • Dashboard pusat


Solusi

Standalone Script mengambil data dari berbagai spreadsheet cabang.


Teknologi

Frontend:

  • HTML

  • Bootstrap

  • Google Charts

Backend:

  • Apps Script

Database:

  • Google Sheets


21. Studi Kasus 3: Sistem Generate Sertifikat

Kebutuhan

  • Ambil data peserta

  • Generate PDF sertifikat

  • Kirim email otomatis


Contoh Script

function sendCertificate(){

  const ss = SpreadsheetApp
    .openById('SPREADSHEET_ID');

  const sheet = ss
    .getSheetByName('Peserta');

  const data = sheet
    .getDataRange()
    .getValues();

  data.forEach(row => {

    const email = row[1];

    MailApp.sendEmail(
      email,
      'Sertifikat',
      'Sertifikat Anda telah tersedia'
    );
  });
}

22. Studi Kasus 4: Integrasi API

Standalone Script dapat digunakan untuk mengambil data API.


Contoh API Request

function getAPI(){

  const response = UrlFetchApp.fetch(
    'https://api.example.com/data'
  );

  const data = JSON.parse(
    response.getContentText()
  );

  Logger.log(data);
}

23. Trigger pada Standalone Script

Trigger digunakan untuk otomatisasi.


Jenis Trigger

TriggerFungsi
Time-drivenBerdasarkan waktu
onOpenSaat file dibuka
onEditSaat data berubah
API triggerBerdasarkan request

Contoh Time Trigger

function autoBackup(){
  Logger.log('Backup berjalan');
}

Trigger:

  • Setiap jam

  • Setiap hari

  • Setiap minggu


24. Integrasi Database

Standalone Script dapat terhubung dengan:

  • Google Sheets

  • Firebase

  • MySQL

  • PostgreSQL

  • REST API


25. Integrasi Google Charts

Contoh

<script>
google.charts.load('current', {
  packages:['corechart']
});
</script>

26. Best Practice Standalone Script

1. Gunakan Modular Code

Pisahkan:

  • API

  • Database

  • Frontend

  • Config


2. Gunakan Config File

Contoh:

const CONFIG = {
  spreadsheetId:'ID_FILE'
}

3. Gunakan Error Handling

try{

}catch(error){
  Logger.log(error);
}

4. Gunakan include()

Agar code lebih rapi.


5. Hindari Hardcode

Gunakan variabel.


27. Arsitektur Standalone Script

User Browser
      │
      ▼
Frontend HTML/CSS/JS
      │
      ▼
Standalone Apps Script
      │
      ├── Spreadsheet A
      ├── Spreadsheet B
      ├── Spreadsheet C
      │
      ▼
Dashboard Monitoring

28. Kapan Menggunakan Standalone Script?

Gunakan jika:

  • Sistem multi spreadsheet

  • Dashboard enterprise

  • Integrasi API

  • Sistem besar

  • Monitoring pusat

  • Multi-user app

  • Integrasi cloud


29. Kapan Tidak Menggunakan Standalone Script?

Hindari jika:

  • Sistem sederhana

  • Satu spreadsheet saja

  • Dashboard kecil

  • Automation ringan


30. Perbandingan Bound dan Standalone

AspekBound ScriptStandalone Script
StrukturTerikat fileIndependen
Akses fileFile aktifBanyak file
KompleksitasSederhanaKompleks
ReusableTidakYa
Cocok untukDashboard kecilEnterprise

31. Kesimpulan

Standalone Script adalah Google Apps Script yang berdiri sendiri dan tidak terikat pada file tertentu.

Standalone Script sangat cocok digunakan untuk:

  • Dashboard enterprise

  • Sistem monitoring pusat

  • Integrasi API

  • Multi spreadsheet system

  • Automation skala besar

  • Web app profesional

Kelebihan utama Standalone Script:

  • Fleksibel

  • Reusable

  • Scalable

  • Cocok untuk aplikasi besar

Dengan memahami Standalone Script, developer dapat membangun sistem cloud modern berbasis Google Workspace yang lebih kompleks dan profesional.

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>







MPA - Bukti Pendaftaran

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