Butuh aplikasi pencatatan keuangan yang lengkap, simpel, gratis dan pastinya tanpa berlangganan?
Selain itu kamu juga butuh aplikasi pencatatan keuangan tanpa hosting dan domain yang bisa dibuka dengan berbagai device, tanpa melakukan export atau import manual, karena secanggih apapun pencatatan keuangan, tapi hanya bisa di akses di satu device pastinya bikin pusing bolak balik HP ke komputer atau sebaliknya.
Modal kamu cuma tiga.
Akun google aktif, laptop dan NIAT.
Bahkan untuk yang awam sekalipun, tutorial google apps script pemula terkait pencatatan keuangan ini dapat dengan mudah diikuti, asalkan step by stepnya nanti diperhatikan dengan benar, serta lakukan dengan sesuai instruksi.
Sebelum lanjut, ini Demo Aplikasinya: Finansaku Pro by JasaDigi – guidofamula
File Tutorial: Spreadsheet Database Finansaku Pro
Jasa saya membuat web app ini, hubungi saya di: Fastwork Indonesia
Table of Contents
Baiklah…
Perkenalkan Finansaku, merupakan sebuah nama yang saya berikan untuk Aplikasi Pencatatan Keuangan Berjenis SPA (Single Page Application).
Apa itu SPA?
SPA merupakan jenis website yang mengusung satu halaman saja tanpa perlu reload atau dinamis, otak SPA yang digunakan nanti adalah Javascript dengan acuan GAS – Google Apps Script tutorial.
Adapun tech stack (Kumpulan Teknologi) yang digunakan pada tutorial ini adalah
- Google Sheets database atau spreadsheets diperuntukan sebagai penyimpanan data.
- Google Apps Script (GAS) digunakan sebagai server.
- HTML, Tailwind CSS, Vanilla JS sebagai antar muka (UI) Frontendnya.
Hubungan dari tiga tech tersebut sebagai arsitektur yang nantinya membuat website berkomunikasi dengan database (sheets) menggunakan google.script.run.
Merancang Database Dengan Google Sheets
Untuk bagian ini fokus kita hanya merancang database, terkait pembuatan sheets, perancangan database dengan membuat header-headernya terlebih dahulu, sebagai awal dari cara membuat aplikasi pencatatan keuangan dari nol gratis.
Pastikan kamu sudah standby di halam google drive, serta sudah menyiapkan folder tersendiri, lalu di dalam folder tersebut buat satu file spreadsheet kosong yang akan kita gunakan sebagai google sheets database, seperti dibawah ini.

Pada sheet1 ubah namanya menjadi Data_Transaksi sebagai awal dari membuat aplikasi keuangan SPA dengan Google Sheets.
Saran: Demi kemudahan, untuk penamaan sheet mengikuti apa yang saya tuliskan, jika nanti kamu sudah menyelesaikan tutorial hingga selesai, barulah kamu bisa menyesuaikan nama-nama sheet tersebut sesuai selera dan kebutuhan.
Lalu di sheet Data_Transaksi tersebut kamu arahkan kursor didalam sel A1, lalu tulis seperti gambar dibawah ini.

Berlaku juga untuk sel B1, C1, D1, E1, F1, G1, isi sesuai persis dengan gambar diatas.
Jika sudah menuliskan header secara lengkap, lanjutkan dengan membuat sheet baru, kemudian beri nama Buku_Besar.
Pada header Buku_Besar yakni: A1:F1 (dibaca A1 sampai dengan F1), isi masing-masing sel tersebut seperti gambar dibawah ini.

Jika sudah menyelesaikan sheet Buku_Besar, kamu bisa lanjut membuat sheet baru dengan nama Data_HutangPiutang
Buat header pada sel A1:I1, sama persis dengan gambar dibawah ini.

Setelah itu lanjut dengan membuat sheet baru bernama Data_PindahDana.
Sheet ini nantinya dipakai sebagai Google Sheets database mutasi atau perpindahan saldo antar rekening/dompet.
Untuk bagian header buat di sel A1:F1, dengan penamaan seperti gambar dibawah ini.

Buat sheet baru lagi dengan nama Data_Penyesuaian.
Sheet Data_Penyesuaian ini berguna untuk menampung data koreksi nilai saldo, misalnya kedepannya terdapat kesalahan input nominal atau salah hitung, jadi fitur penyesuaian dana ini bisa menyelesaikan masalah dengan memberikan keterangan yang jelas, sehingga arus kas masih saling terhubung.
Sehingga dengan adanya Data_Penyesuaian ini, Aplikasi keuangan Google Sheets menjadi lebih baik penerapannya, jadi buat sheet baru sesuai dengan gambar dibawah ini

Dilanjutkan dengan membuat Sheet Rekap_Saldo yang bertugas untuk menampung database rekapan masing-masing pergerakan saldo dari rekening atau dompet (tempat penyimpanan uang).
Buat dan sesuaikan headernya seperti dibawah ini

Selain Rekap_Saldo, kita juga butuh sheet baru untuk menampung pergerakan hutang (kita yang meminjam uang), agar untuk laporan kedepannya lebih mudah di track terkait jumlah uang yang kita punya itu sumbernya dari mana saja dan tentunya dapat dipertanggung-jawabkan.
Oke, buat sheet baru dengan nama Rekap_Hutang, untuk headernya samakan dengan gambar yang dibawah ini.

Fungsi dari Rekap_Hutang untuk menyimpan hasil olah transaksi-transaksi hutang, jadi nantinya direkap semuanya disini, baik yang sudah lunas maupun yang masih tahap cicilan.
Setelah Rekap_Hutang, lanjut membuat sheet Rekap_Piutang.
Rekap_Piutang ini berfungsi untuk menampung hasil rekapan piutang (orang atau instansi yang berhutang dengan kita), akan terdata secara akurat, terkait siapa-siapa saja yang sudah lunas dan yang masih berhutang.
Untuk headernya A1:H1 ikuti seperti gambar dibawah ini

Selain rekapan hutang dan piutang, tentunya di era yang serba digital dan kemudahan finansial teknologi, tak elok kalau kita melawatkan rekapan Paylater, dimana berguna untuk memisahkan mana yang transaksi menggunakan uang kita sendiri mana yang berhutang dengan istilah paylater.
Oke, buat satu sheet baru lagi, bernama Rekap_Paylater, dengan header A1:D1 seperti gambar dibawah ini

Setelah semua sheet transaksi dan rekap dibuat, lanjut membuat 1 sheet untuk menambahkan keterangan, seperti kategori keluar, masuk, platform hutang, paylater, piutang dan Dompet/Rek
Jadi buat satu sheet baru lagi, bernama Setup, dengan header A1:G1 seperti gambar dibawah ini.

Jika kamu sebagai pengajar atau Guru yang membutuhkan aplikasi kuis modern tanpa coding dan modal spreadsheet doang, wajib baca ini: Bikin Kuis Interaktif Google Sheets ala Quizizz, Tinggal Ketik Soal!
Otomatisasi Sheets Setup
ID Piutang (G1 sheet Setup)

=IF(E2="";""; LOWER(SUBSTITUTE(E2;" ";"")) & COUNTIFS(E$1:E2; E2))Tugas dari rumus ini adalah untuk mendapatkan id atau dengan kata lain mengenerate username yang unik di tiap nama orang yang ber-piutang (kolom E sheet Setup).
ID Piutang (I1 Sheet Data_HutangPiutang)

=ARRAYFORMULA(IF(A2:A=""; ""; IFERROR(VLOOKUP(E2:E; Setup!E2:G; 3; FALSE); "")))Tugas dari function dirumus ini untuk mencocokkan nama piutang yang ada di G2:G sheet Setup menggunakan VLOOKUP terhadap kolom E di sheet Data_HutangPiutang, jika ditemukan nama seorang piutang yang sesuai, maka I2:I di sheet Data_HutangPiutang akan otomatis terisi yang dikarenakan rumus ini menggunakan pembungkus ARRAYFORMULA.
Oke untuk dua poin diatas bisa kamu kerjakan terlebih dahulu, karena selanjutnya akan dibahas untuk otomatisasi bagian Rekap, yang lumayan panjang untuk penggunaan rumus dan functionnya.
Merancang Otomatisasi Rekap_Saldo
Pada bagian Rekap_Saldo ini kita akan menggunakan rumus function disetiap baris kolomnya.
Nama Kas A3 (A4:A)

Untuk header Nama Kas, kita cukup menggunakan rumus dibawah ini.
=INDEX(Setup!F2:F)Kita cukup menggunakan function =INDEX, karena hanya memerlukan penugasan pemanggilan semua yang ada di kolom Setup F2:F yaitu nama-nama kas dan rekening yang kita daftarkan.
Transaksi Masuk B3 (B4:B)

Untuk header Masuk, gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(SUMIFS(Data_Transaksi!F2:F;Data_TransaksiE2:E;A4;Data_Transaksi!C2:C;B$3)))Rumus ini bertugas untuk menghitung uang positif atau pemasukan yang hanya dari kategori masuk saja.
Transaksi Keluar C3 (C4:C)

Untuk header Keluar ini, maka gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(-SUMIFS(Data_Transaksi!$F$2:$F;Data_Transaksi!$E$2:$E;A4;Data_Transaksi!$C$2:$C;$C$3)))Perhatikan, rumus ini mirip dengan rumus untuk Transaksi Masuk, yang membedakan hanya acuannya di sel C3, karena rumus ini hanya menghitung kategori Keluar yang ada di Data_Transaksi dan harus sesuai dengan Nama Kas yang ada di A4.
Pinjam Uang D3 (D4:D)

Untuk header Pinjam Uang, gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$F$2:$F;A4;Data_HutangPiutang!$D$2:$D;$D$3)))Rumus diatas berfungsi untuk menghitung total jumlah pinjaman kita yang sesuai dengan Nama Kas yang kita pakai untuk menampung uang pinjaman tersebut.
Bayar Hutang E3 (E4:E)

Untuk header Bayar Hutang, gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(-SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$F$2:$F;A4;Data_HutangPiutang!$D$2:$D;$E$3)))Untuk perihal rumus ini diperuntukkan merekap progress pembayaran cicilan/hutang yang kita pinjam sebelumnya, sesuai dengan Nama Kas yang digunakan untuk membayar hutang tersebut, integrasinya tetap di Data_HutangPiutang.
Beri Pinjaman F3 (F4:F)

Pada bagian header Beri Pinjaman, gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(-SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$F$2:$F;A4;Data_HutangPiutang!$D$2:$D;$F$3)))Rumus ini digunakan untuk tracking uang yang kita gunakan untuk memberikan pinjaman, sesuai dengan Nama Kas yang kita gunakan untuk memberikan pinjaman tersebut.
Hutang Dibayar G3 (G4:G)

Sedangkan untuk header Hutang Dibayar ini, kita menggunakan rumus dibawah ini
=IF(A4="";"";IFERROR(SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$F$2:$F;A4;Data_HutangPiutang!$D$2:$D;$G$3)))Rumus spreadsheets ini digunakan untuk melakukan tracking progress terkait jumlah total hutang yang dibayar berdasarkan Nama Kas yang kita pakai untuk menampung uang pembayaran hutang tersebut.
Bayar Paylater H3 (H4:H)

Untuk bagian Bayar Paylater ini gunakan rumus google sheets dibawah ini
=IF(A4="";"";IFERROR(-SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$F$2:$F;A4;Data_HutangPiutang!$D$2:$D;$H$3)))Khusus untuk Bayar Paylater cukup gunakan satu metode saja, yaitu Bayar, karena untuk belanja menggunakan cicilan paylater pastinya ditalangkan dari pihak platform, jadi rumus ini untuk melakukan track progress terhadap Nama Kas yang kita gunakan untuk membayar cicilan Paylater tersebut.
Masuk (Terima) H3 (H4:H)

Sekarang kita masuk pada bagian rekapan mutasi, untuk header Masuk (Terima) gunakan rumus dibawah ini.
=IF(A4="";"";IFERROR(SUMIFS(Data_PindahDana!$E$2:$E;Data_PindahDana!$D$2:$D;A4)))Penggunaan rumus ini untuk melakukan track perpindahan dana, dimana khusus untuk yang Masuk (Terima) saja, sehingga nilai yang dihasilkan akan positif yang membuat jumlah saldo Nama Kas sebagai tujuan perpindahan tersebut menjadi bertambah.
Keluar (Pindah) I3 (I4:I)

Untuk bagian rekap mutasi Keluar (Pindah) ini gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(-SUMIFS(Data_PindahDana!$E$2:$E;Data_PindahDana!$C$2:$C;A4)))Rumus ini kurang lebih dengan Masuk (Terima) tetapi ini bernilai negatif, karena tujuannya Keluar (Pindah) ke Nama Kas yang lain, acuannya tetap di sheet Data_PindahDana.
Total Sebelum K3 (K4:K)

Untuk bagian Total Sebelum, ini cukup melakukan =SUM biasa, seperti dibawah ini
=IF(A4="";""; SUM(B4:J4))Jadi rumus ini mengkalkulasi otomatis total dari B4:J4
Pengurangan L3 (L4:L)

Untuk bagian Pengurangan ini digunakan sebagai Penyesuaian, semisal nanti ada salah hitung / koreksi, pengurangan dana pada Nama Kas bersangkutan bisa digunakan untuk mensiasati itu.
Jadi gunakan rumus di bawah ini.
=IF(A4="";"";IFERROR(-SUMIFS(Data_Penyesuaian!$E$2:$E;Data_Penyesuaian!$D$2:$D;A4;Data_Penyesuaian!$C$2:$C;$L$3)))Rumus ini mengacu pada sheet Data_Penyesuaian untuk mengambil total yang digunakan sebagai Pengurangan saldo dari Nama Kas bersangkutan tersebut.
Penambahan M3 (M4:M)

Sama seperti pada Pengurangan diatas, yang membedakan ini penyesuaian untuk bagian Penambahan, jadi gunakan rumus dibawah ini
=IF(A4="";"";IFERROR(SUMIFS(Data_Penyesuaian!$E$2:$E;Data_Penyesuaian!$D$2:$D;A4;Data_Penyesuaian!$C$2:$C;$M$3)))Sisa Uang (Bersih)

Ini adalah bagian pen-totalan diambil dari Total Sebelum, Pengurangan dan Penambahan, dengan menggunakan rumus dibawah ini
=IF(A4="";"";SUM(K4:M4))Masih di Rekap_Saldo, setelah menyelesaikan memasukan rumus-rumus tersebut, copy semua rumus tersebut hingga ke baris paling bawah sheet, caranya blok dari A4:N4, lalu tekan pada keyboard CTRL+SHIFT Panah Bawah dan tekan CTRL+D atau CTRL+V.
Merancang Otomatisasi Rekap_Hutang
Sekarang buka kembali sheet Rekap_Hutang.
Nama Platform A3 (A3:A)

Pada bagian sell A3 masukkan rumus INDEX dibawah ini
=INDEX(Setup!C2:C)Rumus ini digunakan untuk memanggil semua daftar Nama Platform yang ada di sheet Setup.
Hutang B3 (B3:B)

Untuk bagian sel B3 masukan rumus dibawah ini untuk mengkalkulasi total jumlah Hutang disetiap Nama Platform.
=IF(A3="";"";IFERROR(SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$E$2:$E;A3;Data_HutangPiutang!$C$2:$C;$B$2)))Bayar Hutang C3 (C3:C)

Lanjut ke sel berikutnya yaitu C3 untuk Bayar Hutang, dengan rumus dibawah ini
=IF(A3="";"";IFERROR(SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$E$2:$E;A3;Data_HutangPiutang!$C$2:$C;$C$2)))Rumus tersebut bertugas untuk menghitung total hutang yang sudah dibayar di setiap Nama Platform tersebut.
Bayar Hutang D3 (D3:D)

Untuk D3 pada Sisa Hutang, gunakan rumus dibawah ini
=IF(A3="";"";IF(AND(B3=0;C3=0);0;B3-C3))Rumus standar ini digunakan untuk mengetahui Sisa Hutang yang harus dibayar, jika hasilnya 0, asumsinya ada dua, antara kita sudah melunaskan atau belum memiliki Hutang pada platform tersebut.
Setelah keempat rumus otomatisasi Rekap_Hutang kamu masukkan, copykan sampai ke bawah lembar sheet (1000++), caranya dengan blok panjang dari A3:D3, tekan CTRL + C pada keyboard, lalu tekan CTRL + Shift + Panah Bawah, dan tekan CTRL + D atau CTRL + V.
Maka semua kolom dan baris data akan terotomatis terisi, sesuai di inputan web nya nanti.
Pastikan kamu sudah berhasil sampai tahap ini, jika belum berhasil ulangi lagi step by stepnya sampai berhasil.
Merancang Otomatisasi Rekap_Piutang
Sekarang kita lanjut ke sheet selanjutnya, yaitu Rekap_Piutang.
ID Piutang A3 (A3:A)

Pada bagian sel A3 masukan rumus dibawah ini, untuk mengambil ID Piutang dengan function Filter
=IFERROR(FILTER(Data_HutangPiutang!$A$2:$A; Data_HutangPiutang!$C$2:$C = "Piutang"); "")ID Orang B3 (B3:B)

Untuk sel B3 gunakan rumus ini
=IF(A3="";"";INDEX(Data_HutangPiutang!$I$2:$I;MATCH(A3;Data_HutangPiutang!$A$2:$A;0)))Sel B3 ini khusus untuk menangkap ID Orang yang sudah terdaftar di sheet Setup melalui Data_HutangPiutang.
Nama C3 (C3:C)

Lanjut ke sel C3, gunakan rumus ini
=IF(A3="";"";INDEX(Data_HutangPiutang!$E$2:$E;MATCH(A3;Data_HutangPiutang!$A$2:$A;0)))Fungsi rumus pada sel C3 ini mirip dengan sel B3, hanya saja C3 ini fokusnya untuk menangkap Nama Orang yang meminjam uang berdasarkan kesesuaian ID Piutang di A3 dengan yang ada di Data_HutangPiutang.
Tanggal Piutang D3 (D3:D)

Lanjut untuk sel D3, gunakan rumus dibawah ini
=IF(A3="";"";INDEX(Data_HutangPiutang!$B$2:$B;MATCH(A3;Data_HutangPiutang!$A$2:$A;0)))Digunakan untuk mengambil Tanggal berdasarkan kecocokan dengan ID Piutang di A3 terhadap Data_HutangPiutang.
Piutang E3 (E3:E)

Untuk sel E3, menggunakan rumus INDEX juga, seperti dibawah ini
=IF(A3="";"";-INDEX(Data_HutangPiutang!$G$2:$G;MATCH(A3;Data_HutangPiutang!$A$2:$A;0)))Berfungsi untuk mengambil jumlah piutang orang tersebut terhadap kita berdasarkan sheet Data_HutangPiutang.
Tanggal Bayar F3 (F3:F)

Lanjut ke sel F3, gunakan rumus ini.
=IF(A3=""; ""; IFERROR(FILTER(Data_HutangPiutang!$B$2:$B; Data_HutangPiutang!$I$2:$I=B3; Data_HutangPiutang!$C$2:$C="Piutang Dibayar"); ""))Rumus ini berfungsi untuk mengambil data Tanggal Bayar, semisal orang tersebut sudah membayar atau proses mencicil piutangnya terhadap kita.
Jumlah Bayar Piutang G3 (G3:G)

Untuk sel G3, masukkan rumus dibawah ini.
=IF(A3=""; ""; IFERROR(FILTER(Data_HutangPiutang!$G$2:$G; Data_HutangPiutang!$I$2:$I=B3; Data_HutangPiutang!$C$2:$C="Piutang Dibayar"); ""))Tujuannya untuk mengetahui jumlah nominal yang dibayarkannya terhadap Piutangnya.
Sisa Piutang H3 (H3:H)

Barulah untuk sel H3, gunakan rumus dibawah ini, untuk mengkalkulasi Jumlah/Sisa Piutang yang terbayar.
=IF(A3="";"";SUM(E3;G3))Setelah sel
A3:H3pada sheetRekap_Piutangkamu isi dengan rumus-rumus diatas, seperti pada langkah-langkah sebelumnya, lakukan copy rumus sampai ke baris paling bawah sheetRekap_Piutang.
Merancang Otomatisasi Rekap_Paylater
Sekarang kita lanjut untuk optimalisai sheet Rekap_Paylater, tenang saja untuk sheet ini tidak terlalu banyak kolom yang mesti di otomatisasi.
Platform A3 (A3:A)

Langsung saja kita beranjak ke sel A3, masukkan rumus seperti dibawah ini.
=IFERROR(INDEX(Setup!$D$2:$D);"")Rumus ini bertugas untuk mengambil data menggunakan =INDEX dari sheet Setup, sehingga nanti akan berisikan nama-nama platform paylater yang sudah kita daftarkan melalui WEB UI nya nanti.
Paylater B3 (B3:B)

Lanjut untuk sel B3, gunakan rumus dibawah ini untuk mendapatkan total jumlah paylater yang kita pakai untuk transaksi.
=IF(A3="";"";IFERROR(SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$E$2:$E;A3;Data_HutangPiutang!$C$2:$C;$B$2)))Bayar Paylater C3 (C3:C)

Sedangkan untuk sel C3 gunakan rumus dibawah ini.
=IF(A3="";"";IFERROR(SUMIFS(Data_HutangPiutang!$G$2:$G;Data_HutangPiutang!$E$2:$E;A3;Data_HutangPiutang!$C$2:$C;$C$2)))Untuk rumus yang di C3 bertugas untuk mengumpulkan berapa total jumlah uang yang sudah kita gunakan untuk membayar cicilan paylater tersebut.
Sisa Cicilan / Lebih Bunga D3 (D3:D)

Terakhir dari Rekap_Paylater ini pada sel D3, gunakan rumus dibawah ini untuk mengkalkulasi sisa paylater yang harus dibayar atau lunas.
=IF(A3="";"";IF(AND(B3=0;C3=0);0;B3-C3))Setelah sel A3:D3 dimasukkan rumus-rumus otomatisasi tersebut, seperti biasa lakukan copy dari A3:D3 lalu pastekan sampai ke seluruh baris hingga di baris 1000++.
Jika kamu mau sistem laporan keuangan tanpa script/web alias hanya spreadsheet, kamu wajib baca ini: Template Keuangan Pribadi Gratis Tanpa Coding
Tahap Backend Logic
Proses penyesuaian database di spreadsheet sudah rampung, saatnya kita mengakses backend dari aplikasi ini, melalui Apps Script.
Perhatikan ribon pada spreadsheet, pilih Extensions, lalu kilik Apps Script

Maka kamu akan dibawa ke halaman penulisan kode.
Perihal penamaan file backend kamu bisa menuliskan BE Finansaku sama seperti tutorial ini.
Jika terdapat pop up / pemberitahuan otorisasi akun google, terima/konfirmasi yes saja pilihan tersebut.

Pada bagian Code.gs hapus semua kode default tersebut.

Lalu ganti dengan kode-kode berikut
Function doGet

Kode pertama dan yang paling wajib ada saat kita membuat aplikasi SPA (Single Page Application) di GAS adalah fungsi doGet().
Bagi kamu yang baru pertama kali melihat kode, tenang saja, jangan buru-buru pusing. Bayangkan fungsi doGet() ini sebagai seorang resepsionis.
Ketika ada pengunjung yang mengklik link web aplikasi kamu, resepsionis inilah yang pertama kali menyambut mereka. Tugasnya adalah membukakan pintu dan menyajikan tampilan halaman utama ke layar pengunjung.
Mari kita bedah baris demi baris:
const html = HtmlService...('Index').evaluate();Di baris ini, si resepsionis mengambil sebuah dokumen bernama Index.html (yang nanti akan kita buat sebagai kanvas antarmuka/UI). Perintah .evaluate() memastikan semua kode yang ada di dalamnya diproses terlebih dahulu sebelum disajikan.
html.setTitle('Finansaku Pro');Ini berfungsi untuk memberikan judul pada tab browser pengunjung. Jadi ketika aplikasi dibuka, tab di atasnya akan bertuliskan “Finansaku Pro”, sehingga terlihat jauh lebih profesional dan rapi. Nantinya bisa kamu ubah sendiri sesuai nama yang kamu mau.
html.addMetaTag('viewport', ...);Baris ini sangat krusial, karena memiliki tugas agar antarmuka web kamu mobile-friendly (cocok buka di smartphone). Tanpa kode ini, saat aplikasi dibuka di HP, tampilannya akan berantakan atau terlihat super kecil seperti mode desktop yang dipaksakan masuk ke layar mungil.
return html;Terakhir, setelah semuanya disiapkan (dokumennya diambil, diberi judul, dan disesuaikan untuk layar HP), resepsionis akhirnya “menyerahkan/mengembalikan/memberikan” (Me-return) halaman tersebut ke layar perangkat pengunjung.
Function include

Pernah bingung bagaimana cara menghubungkan Google Sheets dengan Tailwind CSS atau logika JavaScript khusus jika kita tidak menggunakan hosting konvensional.
Nah, di sinilah fungsi include() berperan sebagai penyambung/jembatannya.
Biasanya, kalau kita membuat website pada umumnya, kita bisa dengan mudah memanggil file CSS atau JS eksternal menggunakan tag pendukung seperti <link href="..."> atau <script src="...">.
Sayangnya, di dalam ekosistem Google Apps Script, aturan mainnya sedikit berbeda. Semua kode HTML, CSS, dan JavaScript harus disatukan secara utuh saat disajikan ke layar pengguna.
Agar kode kita tidak menumpuk ribuan baris dan membuat pusing di dalam satu file Index.html (nanti akan kita buat file Index.html nya), kita harus memecahnya menjadi beberapa file terpisah. Nah, fungsi include(filename) inilah yang bertugas merakitnya kembali.
Meskipun cuma satu baris, tugasnya sangat vital. Mari kita bedah:
HtmlService.createHtmlOutputFromFile(filename)Perintah ini menyuruh sistem untuk mencari file HTML lain yang akan kita buat nantinya (seperti file js_main.html atau view_bukubesar.html).
.getContent()Daripada membuka file tersebut sebagai halaman baru, perintah ini akan mengambil teks murni dari kode-kode di dalamnya.
Isi teks kode yang sudah diambil tersebut kemudian akan disuntikkan/diteruskan secara dinamis ke halaman utama kita menggunakan tag khusus <?!= include('nama_file'); ?> (yang nanti akan kita bahas di bagian Frontend).
Berkat fungsi sederhana ini, cara membuat aplikasi SPA (Single Page Application) yang kompleks sekalipun tetap bisa ditulis dengan struktur direktori yang rapi.
Function generateID

Pernahkah kamu memperhatikan struk belanja dari minimarket? Di sana pasti selalu ada nomor struk atau nomor referensi yang berbeda-beda untuk setiap pembeli, kan?
Nah, di dalam aplikasi pencatatan keuangan yang sedang kita buat, kita juga sangat membutuhkan sistem penomoran unik seperti itu untuk setiap transaksi yang masuk. Tujuannya agar data tidak tertukar dan mudah dilacak.
Untuk itulah fungsi generateID(prefix) ini kita buat.
Dalam tutorial Google Apps Script pemula ini, membuat ID unik ternyata tidak perlu repot-repot menggunakan rumus matematika rumit atau generator acak.
Mari kita bedah cara kerjanya:
const ss = ... dan const tz = ...Dua baris ini ibarat kita mencocokkan jam tangan. Kodenya bertugas mengecek pengaturan Zona Waktu (Time Zone) yang ada di dalam Google Sheets database kamu.
Kenapa ini sangat penting? Agar jam transaksi yang tercatat akurat dengan waktu lokalmu (misalnya WIB/WITA/WIT), bukan malah mengikuti zona waktu Amerika Serikat tempat server Google berada.
return prefix + Utilities.formatDate(...)Di sinilah keajaibannya terjadi. Fungsi ini akan menggabungkan sebuah awalan kata (kita sebut prefix) dengan waktu saat ini yang diformat super presisi hingga hitungan detik: yyyyMMddHHmmss (Tahun-Bulan-Tanggal-Jam-Menit-Detik).
Sebagai contoh…
Misalkan kamu menyimpan data transaksi reguler, lalu memberikan prefix “TRX” pada tanggal 28 Oktober 2026 tepat pada jam 14:30 dan 05 detik.
Maka, mesin pencetak ini akan langsung merakit kodenya menjadi: TRX20261028143005.
Hasilnya? Kamu akan mendapatkan kode ID yang super unik, sangat rapi, dan dijamin 100% tidak akan pernah ada data transaksi yang memiliki ID kembar.
Function insertDataCerdas

Selanjutnya, kita masuk ke fungsi yang menurut saya vital dalam membuat aplikasi keuangan Google Sheets.
Dalam tutorial Google Apps Script pemula, biasanya penulis/tutor akan menyuruh kamu menggunakan kode bawaan sheet.appendRow() untuk menyimpan data baru ke baris paling bawah.
Lalu, kenapa di sini kita malah repot-repot membuat fungsi insertDataCerdas?
Masalahnya begini, di dalam Google Sheets database yang sudah kita rancang tadi, kita banyak menggunakan rumus ARRAYFORMULA untuk otomatisasi. Sayangnya, rumus ini membuat sistem Google Sheets mengira bahwa sel-sel di bawahnya sudah terisi data, padahal wujudnya kosong.
Jika kita memaksakan pakai appendRow(), data transaksimu akan melompat jauh dan tersimpan di baris ke-1001. Hasilnya? Database kamu akan dipenuhi dengan ribuan baris kosong yang berantakan.
Nah, fungsi insertDataCerdas hadir sebagai “Tukang Parkir Pintar” untuk mengatasi masalah tersebut. Mari kita bedah rincian scriptnya:
const kolomA = sheet.getRange("A:A").getValues();Daripada menebak-nebak di mana baris yang kosong, fungsi ini akan “memindai” seluruh isi Kolom A (kolom tempat ID Transaksi berada) dari atas sampai bawah.
Kodenya akan mengecek satu per satu sel di Kolom A. Begitu ia menemukan sel yang benar-benar kosong atau berisi teks kosong (""), proses pencarian akan langsung dihentikan (break). Skrip sekarang sudah mengunci nomor baris kosong yang tepat!
sheet.getRange(...).setValues([dataBaris]);Setelah spot parkir yang kosong ditemukan, barulah data transaksi baru milikmu disuntikkan (setValues) tepat di baris tersebut. Tidak akan ada lagi cerita data lompat ke baris ribuan!
updateBukuBesar();Setelah data sukses masuk dengan rapi, ia akan langsung memanggil fungsi updateBukuBesar() agar rekapitulasi riwayat aplikasi kamu langsung di- refresh secara otomatis detik itu juga.
Dengan fungsi sederhana ini, kita berhasil memodifikasi Google Sheets yang awalnya cuma aplikasi spreadsheet biasa, menjadi mesin database yang tangguh!
Function simpanTransaksWeb

Jika sebelumnya kita sudah membuat “Mesin Pencetak ID” (generateID) dan “Tukang Parkir Pintar” (insertDataCerdas), sekarang kita butuh seseorang untuk mengeksekusi semuanya secara berurutan.
Nah, di dalam tutorial Google Apps Script pemula ini, fungsi simpanTransaksiWeb bertugas layaknya seorang Kasir Bank.
Saat pengguna menekan tombol “Simpan Transaksi” di halaman depan web, data-data yang diketik (tanggal, jenis, kategori, dompet, dll) akan dikirim ke “kasir” ini.
Dalam hitungan milidetik, fungsi ini melakukan 3 langkah penting:
const data = [generateID("TRX"), tgl, jenis...];Pertama, si kasir akan mengambil semua formulir data tersebut, lalu menempelkan stiker ID otomatis (dengan kode awalan “TRX”) menggunakan fungsi generateID yang sudah kita bahas tadi. Semua data ini kemudian dibungkus rapi ke dalam satu paket (yang di dalam bahasa pemrograman disebut Array atau ditandai dengan kurung siku [...]).
insertDataCerdas("Data_Transaksi", data);Setelah paketnya rapi, kasir langsung menyerahkannya kepada si tukang parkir pintar (insertDataCerdas). Ia memberi instruksi: “Tolong simpan paket data ini ke dalam Google Sheets database khusus di lembar/sheet bernama Data_Transaksi, pastikan di baris yang kosong ya”
return "Data Transaksi berhasil disimpan!";Terakhir, setelah data sukses masuk, kasir akan memberikan “struk bukti” berupa pesan teks sukses. Pesan ini akan dikirim balik ke layar depan web-mu, yang nantinya akan kita ubah menjadi Toast Notification (notifikasi pop-up hijau) yang keren di dalam aplikasi pencatatan keuangan ini.
Karena aplikasi kita berkonsep SPA, proses bolak-balik pengiriman data ini terjadi di latar belakang tanpa membuat halaman website memuat ulang (reload) sama sekali. Keren, kan?
Function simpanPenyesuaianWeb

Function simpanPindahDanaWeb

Function simpanHutangWeb

Pola yang sama persis juga kita gunakan untuk fungsi penyimpanan lainnya, seperti simpanPenyesuaianWeb, simpanPindahDanaWeb, dan simpanHutangWeb. Yang membedakan hanyalah kode awalannya (seperti awalan “PE”, “PD”, atau “HP”) dan nama sheet tujuannya!
Function simpanSetupWeb

Pernahkah di tengah bulan kamu membuka dompet atau rekening baru, lalu bingung bagaimana menambahkannya ke dalam aplikasi pencatatan keuangan? Jangan khawatir, karena kita punya fungsi simpanSetupWeb!
Jika fungsi sebelumnya bertugas mencatat arus uang harian, fungsi yang satu ini bertugas sebagai “Pustakawan” yang mengurus rak lembar Setup atau Master Data (seperti mendaftarkan dompet baru, kategori pengeluaran baru, atau nama platform Paylater).
Mari kita lihat bagaimana tutorial Google Apps Script pemula ini menyulap Google Sheets database milikmu menjadi sangat cerdas dan terstruktur:
const mapCol = {...} (Peta Lokasi Rak)Di lembar Setup, kita memiliki banyak kolom yang saling mandiri. Kolom 1 (A) khusus kategori Keluar, Kolom 2 (B) untuk Masuk, Kolom 6 (F) untuk Dompet, dan seterusnya. Objek mapCol ini bertindak layaknya “peta” rahasia si Pustakawan agar ia tahu persis di kolom (rak) mana item barumu harus diletakkan sesuai kategori yang kamu pilih dari dropdown web.
Mencari Baris Kosong dengan Cara Mundur (Backward Loop)
Perhatikan blok kode for (let i = data.length - 1; i >= 0; i--). Berbeda dengan fungsi transaksi sebelumnya yang mengecek dari atas ke bawah, di sini skrip kita memindai sel dari bawah ke atas (i--).
Kenapa harus mundur?
Karena di lembar Setup, panjang isi setiap kolom pasti berbeda-beda (misalnya kategori makanan sudah panjang sampai 20 baris, tapi daftar dompet baru ada 2 baris). Dengan mengecek mundur dari ujung paling bawah tabel, si Pustakawan bisa menemukan “dasar tumpukan” di kolom spesifik tersebut tanpa tertipu oleh kolom-kolom panjang di sebelahnya!
sheet.getRange(...).setValue(nilaiBaru);Begitu baris kosong paling pertama di bawah tumpukan tersebut ditemukan, nama item barumu (misalnya “Rekening Jago” atau kategori “Uang Cash”) akan langsung disisipkan di sana dengan rapi.
Luar biasa, bukan? Hanya dengan belasan baris kode backend ini, kamu sudah beproses membuat aplikasi keuangan SPA yang sangat dinamis. Kamu bisa menambahkan kategori atau rekening baru kapan saja langsung dari antarmuka web di HP kamu, tanpa perlu repot membuka spreadsheet di laptop!
Function updateBukuBesar
function updateBukuBesar() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheetBB = ss.getSheetByName('Buku_Besar');
if (!sheetBB) sheetBB = ss.insertSheet('Buku_Besar');
let allData = [];
function getSheetData(namaSheet) {
const s = ss.getSheetByName(namaSheet);
if (!s) return [];
const data = s.getDataRange().getValues();
return data.length > 1 ? data.slice(1) : [];
}
...
...
...
...Karena function
updateBukuBesarlumayan panjang, kamu bisa akses di laman kode ini tersendiri melalui link: trakteer
Kalau fungsi-fungsi sebelumnya bertugas mencatat dan menyimpan data ke sheet tujuannya masing-masing, sekarang kita masuk ke fungsi yang menjadi “otak” pengolah seluruh riwayat transaksi kamu: updateBukuBesar().
Di dalam aplikasi pencatatan keuangan modern, pengguna pasti ingin melihat semua riwayat transaksinya dalam satu lini masa (timeline) yang urut berdasarkan tanggal, baik itu transaksi harian, catatan hutang, pindah dana antar rekening, maupun penyesuaian saldo.
Nah, daripada kita menyuruh pengguna membuka sheet satu per satu, fungsi updateBukuBesar() inilah yang bekerja di balik layar untuk menyatukan semuanya secara otomatis!
Mari kita bedah cara kerja raksasa ini langkah demi langkah:
Menarik Data dari 4 Sumber Database Berbeda
Fungsi ini menyisir 4 tabel utama: Data_Transaksi, Data_Penyesuaian, Data_PindahDana, dan Data_HutangPiutang.
Kodenya mengekstrak setiap baris lalu mengategorikannya secara tepat: mana yang termasuk arus Uang Masuk dan mana yang Uang Keluar. Bahkan untuk data Pindah Dana (mutasi saldo antar dompet), keterangannya dirakit otomatis agar kamu tahu dari mana uang tersebut dipindahkan.
Mengurutkan Transaksi (Sorting)
Setelah semua data dari 4 tabel terkumpul dalam satu wadah (Array allData), kodenya melakukan pengurutan otomatis dengan allData.sort(...). Hasilnya, transaksi yang baru saja pengguna catat akan selalu berada di baris paling atas (urut dari Terbaru ke Terlama).
Merapikan Format Tanggal & Jam
Melalui fungsi Utilities.formatDate, format waktu yang awalnya berantakan diubah menjadi standar teks yang sangat rapi: YYYY-MM-DD HH:mm. Jadi kamu bisa tahu pasti transaksi tersebut terjadi di tanggal berapa dan jam berapa.
Menulis Ulang Sheet & Trik Speed Optimization
Terakhir, sheet Buku_Besar dibersihkan dulu (clear()), lalu diisi ulang dengan data yang sudah urut dan rapi.
Satu rahasia optimasi di baris terakhir: Dengan menambahkan perintah return allData;, data yang baru dirakit langsung diteruskan ke layar HP pengguna dalam hitungan milidetik dari memori server (cache), tanpa perlu menunggu sistem membaca ulang spreadsheet yang bikin loading terasa lama.
Function getBukuBesar

Setelah fungsi raksasa updateBukuBesar() kita ciptakan untuk merakit dan mengurutkan seluruh data transaksi, lalu bagaimana cara halaman antarmuka web (UI) meminta data tersebut dari server?
Nah, di sinilah fungsi getBukuBesarData() berperan sebagai Pintu Jalur Cepat (Gateway) di backend.
Fungsi ini sengaja dibuat sangat ringkas dan efisien.
Meskipun terlihat cuma satu baris, fungsi ini memiliki alasan arsitektur yang sangat penting dalam membuat aplikasi keuangan SPA:
- Efisiensi Panggilan (Single Call): Saat tab Riwayat/Buku Besar dibuka di layar HP pengguna, JavaScript di frontend cukup memanggil satu fungsi ini lewat
google.script.run.getBukuBesarData().
- Auto-Fresh Data: Dengan mengeksekusi
updateBukuBesar()di dalamnya, fungsi ini menjamin bahwa data yang terkirim ke layar penggunamu adalah data paling aktual yang sudah terkonsolidasi dari 4 sheet berbeda, lengkap dengan pengurutan tanggal terbaru dan format jam yang rapi.
- Mendukung Fitur Cache: Karena
updateBukuBesar()mengembalikan hasil rakitannya secara langsung dari memori server (return allData), data riwayat transaksi tersebut bisa langsung disimpan di memori HP pengguna. Alhasil, saat pengguna bolak-balik berpindah tab, data riwayat tidak perlu ditarik ulang dari server, tampilannya langsung muncul instan dalam 0 detik.
Function getAllLaporanData

Pernahkah kamu membuka sebuah aplikasi finansial di HP, lalu saat berpindah dari halaman Saldo ke halaman Hutang atau Paylater, layarnya selalu loading berputar-putar 2 hingga 3 detik? Masalah seperti ini sangat sering terjadi jika setiap sub-menu dipaksa melakukan panggilan terpisah ke server Google.
Untuk mengatasi kelambatan tersebut, kita menciptakan fungsi getAllLaporanData().
Fungsi ini bertindak seperti “kurir” yang mengirimkan paket hemat sekaligus dalam sekali jalan. Daripada menyuruh kurir bolak-balik membawa paket satu per satu, fungsi ini mengemas seluruh data Laporan Kas Saldo, Hutang, Paylater, hingga Piutang ke dalam satu kontainer utuh.
Alur kerjanya sangat efisien dan krusial untuk kecepatan aplikasi:
Penyatuan Empat Laporan Utama
Fungsi ini memanggil empat fungsi laporan sekaligus dari file Model.gs (yang akan kita buat nanti):
kas: Mengambil rekapan Sisa Uang Bersih di setiap dompet/rekening.hutang: Mengambil total sisa tagihan pinjaman di setiap platform.paylater: Mengambil rekapan tagihan cicilan digital.piutang: Mengambil rekapan sisa piutang di setiap nama orang/pihak terkait.
Pengemasan ke Format Objek (JSON)
Keempat kelompok data tersebut dibungkus menjadi satu struktur objek yang ringkas.
Fondasi Client-Side Caching
Saat pengguna membuka tab Laporan di antarmuka web, JavaScript di HP akan memanggil fungsi ini cukup satu kali. Seluruh data laporan langsung tersimpan di memori HP (masterLaporanCache).
Dampaknya? Ketika pengguna berpindah-pindah sub-tab dari Kas Saldo ke Hutang, Paylater, maupun Piutang, tampilannya akan terbuka secara instan (0 detik tanpa loading). Trik inilah yang membuat aplikasi pencatatan keuangan berbahan Google Sheets ini memiliki performa cepat.
Function getAllSetupMaster

Setiap kali kita hendak mencatat transaksi baru atau menyaring daftar kategori di menu Setup, aplikasi tentu perlu tahu item apa saja yang sudah terdaftar di lembar Setup (seperti nama-nama dompet, daftar kategori keluar/masuk, hingga daftar platform hutang/piutang).
Daripada aplikasi memanggil server berkali-kali hanya untuk mengambil nama satu per satu kategori, fungsi getAllSetupMaster() diciptakan untuk menarik seluruh daftar master data sekaligus dari spreadsheet.
Mari kita bedah alur kerjanya yang sangat terorganisir:
Pemeriksaan Keberadaan Sheet
Pertama, fungsi ini mengecek apakah sheet Setup dan Isinya ada. Jika sheet belum dibuat atau masih kosong (hanya ada header), fungsi akan langsung mengembalikan nilai kosong {} agar aplikasi tidak error.
Proses Pemilahan (Looping & Filtering)
Skrip membaca seluruh baris data di lembar Setup, lalu memilah-milah nilainya berdasarkan indeks kolom:
- Kolom A: Ditaruh ke dalam kelompok
keluar - Kolom B: Ditaruh ke dalam kelompok
masuk - Kolom C: Ditaruh ke dalam kelompok
hutang - Kolom D: Ditaruh ke dalam kelompok
paylater - Kolom E: Ditaruh ke dalam kelompok
piutang - Kolom F: Ditaruh ke dalam kelompok
dompet
Mendukung Fitur Kartu Interaktif (Setup Cards)
Paket data yang sudah dipilah ini kemudian dikirim ke antarmuka web dan disimpan di cache memori HP (masterSetupCache).
Hasilnya, saat pengguna mengklik kartu navigasi Setup untuk berpindah dari melihat Daftar Dompet ke Kategori Keluar atau Platform Hutang, daftarnya akan langsung berganti secara instan tanpa jeda loading server.
Setelah menyelesaikan kodingan diatas, jangan lupa lakukan Save terlebih dahulu.
Lalu lanjut membuat satu file .gs yang baru.

Beri nama Model, maka otomatis nama filenya menjadi Model.gs

Function getSetupData

Setelah kita menyelesaikan dapur utama di file Code.gs, sekarang kita masuk ke file pembantu kedua di backend, yaitu Model.gs. Di dalam arsitektur pemrograman, file Model biasanya dikhususkan untuk urusan membaca dan mengambil struktur data spesifik dari database.
Fungsi pertama yang wajib ada di file ini adalah getSetupData().
Jika fungsi getAllSetupMaster() yang kita bahas sebelumnya digunakan untuk menampilkan kartu daftar di menu Setup, maka fungsi getSetupData() ini bertugas sebagai pemasok pilihan opsi (dropdown) di seluruh form input transaksi (seperti Form Transaksi Keluar/Masuk, Hutang/Piutang, Pindah Dana, dan Penyesuaian Saldo).
Mari kita bedah alur kerjanya yang fleksibel ini:
Pembacaan Dinamis Berdasarkan Nama Header (headers.indexOf)
Alih-alih menggunakan nomor kolom statis (seperti angka 0, 1, atau 2), fungsi ini mencari urutan kolom berdasarkan kata header-nya (headers.indexOf('Nama Header')).
Keuntungannya?
Jika di kemudian hari kamu mengubah atau menggeser posisi kolom di dalam Google Sheets database milikmu, kodenya tidak akan patah atau error, karena ia secara otomatis mencari nama header yang sesuai.
Pembersihan Baris Kosong (rows.forEach)
Setiap baris di dalam sheet dibaca, lalu hanya sel yang benar-benar ada isinya (row[col]) yang dimasukkan ke dalam objek setupData. Data kosong akibat panjang kolom yang tidak sama akan diabaikan secara otomatis.
Penyiapan Opsi Pilihan Saat Aplikasi Pertama Kali Dibuka
Saat pengguna membuka aplikasi pencatatan keuangan ini di browser, fungsi inilah yang pertama kali dipanggil lewat fungsi inisialisasiData() di frontend.
Hasilnya, pilihan nama dompet (BCA, Mandiri, Cash, dll) dan kategori pengeluaran/pemasukan akan langsung terisi rapi di dalam elemen <select> (dropdown form) detik itu juga.
Function getDashboardData

Setiap kali kamu membuka halaman Dashboard / Laporan Keuangan, hal utama yang pasti ingin pengguna lihat adalah: “Berapa sisa uang bersih yang saya punya di masing-masing rekening saat ini?”
Nah, di dalam Google Sheets database yang sudah kita rancang sebelumnya, kalkulasi angka-angka ini sebenarnya sudah dihitung secara otomatis oleh rumus-rumus di sheet Rekap_Saldo.
Tugas fungsi getDashboardData() di file Model.gs ini sangat sederhana namun krusial: mengambil hasil akhir angka tersebut dari spreadsheet, lalu mengubahnya menjadi format rapi agar bisa dibaca oleh antarmuka web/UI.
Mari kita bedah alur kerjanya yang efisien ini:
Pengecekan Baris Minimum (lastRow < 4)
Di sheet Rekap_Saldo, data daftar dompet dan rumusnya baru dimulai dari baris ke-4 (karena baris 1-3 digunakan untuk judul dan header). Jika jumlah baris kurang dari 4, fungsi akan langsung mengembalikan array kosong [] untuk mencegah terjadinya error.
Mengunci Kolom Hasil Akhir (row[13])
Fungsi ini menarik rentang data dari Kolom A sampai Kolom N. Kolom A (indeks 0) berisi Nama Dompet/Kas, sedangkan Kolom N (indeks 13) berisi hasil kalkulasi rumus Sisa Uang (Bersih).
Skrip akan memfilter dan hanya mengambil dompet yang memiliki nama, lalu memasangkan Nama Dompet dan Jumlah Saldonya ke dalam format objek yang rapi.
Siap Disajikan ke Layar HP
Data dalam bentuk array objek ini ([{dompet: "BCA", saldo: 5000000}, ...]) dikembalikan ke frontend untuk langsung diolah oleh komponen UI kartu laporan.
Dengan fungsi ini, aplikasi pencatatan keuangan kamu tidak perlu melakukan kalkulasi penjumlahan ulang yang berat di JavaScript, karena semua beban komputasinya sudah ditangani oleh Google Sheets secara real-time.
Function getLaporanHutangPiutang
function getLaporanHutangPiutang(jenis) {
const ss = SpreadsheetApp.getActiveSpreadsheet();
let sheetName = "";
let idxNama = 0; // Default Kolom A untuk Nama
let idxSaldo = 3; // Default Kolom D untuk Saldo Sisa
if(jenis === 'hutang') {
sheetName = 'Rekap_Hutang';
} else if(jenis === 'paylater') {
sheetName = 'Rekap_Paylater';
} else if(jenis === 'piutang') {
sheetName = 'Rekap_Piutang';
idxNama = 2; // Khusus Piutang, namanya ada di Kolom C
idxSaldo = 7; // Khusus Piutang, sisanya ada di Kolom H
}
...
...
...Karena kodenya terlalu panjang, Untuk mengakses function
getLaporanHutangPiutangini, kamu bisa akses melalui laman link trakteer, sehingga kodenya bisa langsung di copy paste.
Fungsi terakhir di file Model.gs ini dibuat dengan konsep reusable code (satu fungsi yang bisa dipakai berulang kali untuk kebutuhan berbeda). Tugas utamanya adalah menarik saldo sisa dari tiga jenis laporan kewajiban/hak keuangan kamu: Hutang, Paylater, dan Piutang.
Daripada kita membuat tiga fungsi terpisah yang isi kodenya mirip-mirip, kita cukup menggunakan satu fungsi cerdas bernama getLaporanHutangPiutang(jenis).
Cara kerjanya sangat taktis dan rapi:
Parameter Dinamis (jenis)
Fungsi ini menerima masukan parameter berupa teks 'hutang', 'paylater', atau 'piutang'. Berdasarkan kata kunci tersebut, skrip secara otomatis menentukan nama sheet mana yang harus dibuka (Rekap_Hutang, Rekap_Paylater, atau Rekap_Piutang).
Penyesuaian Posisi Indeks Kolom (idxNama & idxSaldo)
Setiap sheet rekap memiliki tata letak tabel yang sedikit berbeda. Misalnya, di Rekap_Hutang nama platform ada di Kolom A (indeks 0), sedangkan di Rekap_Piutang nama penagih ada di Kolom C (indeks 2).
Logika if-else di atas secara fleksibel menyesuaikan patokan kolom penarikan data tanpa membuat kodenya berantakan!
Pembersihan Data Otomatis (Data Sanitization)
Saat membaca baris demi baris, fungsi ini menyaring data secara ketat. Baris yang kosong atau baris yang hanya berisi teks judul/header (seperti tulisan “Nama” atau “Platform”) akan diabaikan. Hanya nama pihak/platform nyata beserta nominal angkanya yang dibungkus dan dikirim ke frontend.
Nah, seluruh penjelasan logika Backend baik di file Code.gs maupun Model.gs sudah rampung kita bedah dengan detail dan ramah pemula.
Sekarang seluruh amunisi backend kamu sudah siap dipublikasikan di artikel blog. Langkah berikutnya tinggal masuk ke penulisan pembahasan bagian Tahap Frontend UI & Client-Side Logic (Index.html, view_bukubesar.html, dan js_main.html).
Tahap FrontEnd UI dan Komponen
Oke untuk tahap backend cukup menggunakan dua file .gs diatas, sekarang kita lanjut untuk membuat kode pada tampilan web UI nya.
Seperti biasa tambah file baru, kali ini pilih HTML.

Lalu beri nama Index (I nya huruf kapital), maka filenya akan bernama Index.html

Setelah pondasi backend di Apps Script rampung, saatnya kita membangun wujud fisik dari aplikasi pencatatan keuangan ini.
Hapus semua isi yang ada di Index.html, lalu masukan kode-kode berikut
<!DOCTYPE html>
<html lang="id">
<head>
<base target="_top">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 text-slate-800 pb-20 font-sans">
<!-- Header dengan Akses Cepat Setup -->
<header class="bg-indigo-600 text-white p-4 sticky top-0 z-20 flex justify-between items-center shadow-md">
<h1 class="text-xl font-bold">Finansaku Pro</h1>
<button onclick="pindahTab('setup')" class="text-sm bg-indigo-700 hover:bg-indigo-800 px-3 py-1 rounded">⚙️ Setup</button>
</header>
<main class="p-4 max-w-md mx-auto" id="app-container">
<div id="loading" class="text-center py-10 text-slate-500 animate-pulse">Memuat Database...</div>
<!-- 1. TAB TRANSAKSI -->
<div id="view-transaksi" class="hidden bg-white p-5 rounded-xl shadow border border-slate-200">
<h2 class="text-lg font-bold mb-4 border-b pb-2">Transaksi Reguler</h2>
<div class="space-y-4">
<div><label class="block text-sm font-semibold mb-1">Tanggal</label><input type="datetime-local" id="trx_tgl" class="w-full border p-2 rounded" required></div>
<div>
<label class="block text-sm font-semibold mb-1">Jenis</label>
<select id="trx_jenis" onchange="updateUITrx()" class="w-full border p-2 rounded">
<option value="Keluar">Keluar</option><option value="Masuk">Masuk</option>
</select>
</div>
...
...
...Oh iya, untuk styling disini saya menggunakan tailwind css ya, agar lebih mudah untuk maintenance stylingnya, bagi yang terbiasa menggunakan tailwind css pastinya sudah tidak asing dengan class-class tersebut.
Di dalam ekosistem web modern, konsep Single Page Application (SPA) memungkinkan pengguna berpindah menu tanpa perlu reload halaman. Semua kontainer tampilan (view) kita tumpuk dalam satu file utama, yaitu Index.html, lalu kita atur visibilitasnya menggunakan kelas CSS .hidden.
Mari kita bedah bagian-bagian krusial yang ada di dalam Index.html:
<!DOCTYPE html>
<html lang="id">
<head>
<base target="_top">
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50 text-slate-800 pb-20 font-sans">1. Integrasi Tailwind CSS Tanpa Install (CDN)
Pada bagian <head>, kita memanggil CDN Tailwind CSS:
<script src="[https://cdn.tailwindcss.com](https://cdn.tailwindcss.com)"></script>Mengapa Tailwind CSS?
Dengan Tailwind, kita tidak perlu repot menulis ribuan baris kode CSS secara manual di file terpisah. Cukup selipkan nama-nama kelas utility langsung di dalam elemen HTML (seperti bg-slate-50, text-slate-800, rounded-xl, dll). Ini sangat mempercepat proses styling dan memastikan tampilan aplikasi langsung responsif serta estetik di berbagai ukuran layar HP maupun laptop.
2. Header & Akses Cepat Setup
<header class="bg-indigo-600 text-white p-4 sticky top-0 z-20 flex justify-between items-center shadow-md">
<h1 class="text-xl font-bold">Finansaku Pro</h1>
<button onclick="pindahTab('setup')" class="text-sm bg-indigo-700 hover:bg-indigo-800 px-3 py-1 rounded">⚙️ Setup</button>
</header>sticky top-0 z-20: Menjaga barisan header tetap melayang di bagian paling atas layar saat halaman di-scroll.
- Tombol Setup: Tombol pintas dengan fungsi
onclick="pindahTab('setup')"agar pengguna bisa langsung masuk ke pengaturan kategori master kapan saja dengan satu sentuhan.
3. Kontainer Tab Berbasis SPA (Single Page Application)
Di dalam elemen <main>, kita menyediakan beberapa wadah tampilan (<div>) yang secara default disembunyikan menggunakan kelas hidden.
Contohnya pada wadah Form Transaksi Reguler:
<div id="view-transaksi" class="hidden bg-white p-5 rounded-xl shadow border border-slate-200">
<h2 class="text-lg font-bold mb-4 border-b pb-2">Transaksi Reguler</h2>
<!-- Form Input -->
</div>Ketika pengguna menekan tombol menu di bagian bawah, JavaScript cukup melepas atau menambahkan kelas .hidden pada ID kontainer yang dituju. Perpindahan antarlayar pun terjadi secara seketika (0 detik tanpa refresh).
4. Bottom Bar Navigation (Navigasi Bawah ala Aplikasi Mobile)
<nav class="fixed bottom-0 w-full bg-white border-t flex justify-between px-2 py-2 text-[10px] font-semibold text-slate-500 shadow-[-0_-5px_15px_rgba(0,0,0,0.05)] z-20 overflow-x-auto">
<button onclick="pindahTab('transaksi')" class="flex-1 flex flex-col items-center hover:text-indigo-600 px-1"><span class="text-lg block mb-1">💸</span>Transaksi</button>
<button onclick="pindahTab('hutang')" class="flex-1 flex flex-col items-center hover:text-indigo-600 px-1"><span class="text-lg block mb-1">💳</span>Hutang</button>
<button onclick="pindahTab('mutasi')" class="flex-1 flex flex-col items-center hover:text-indigo-600 px-1"><span class="text-lg block mb-1">🔄</span>Mutasi</button>
<button onclick="pindahTab('dashboard')" class="flex-1 flex flex-col items-center hover:text-indigo-600 px-1"><span class="text-lg block mb-1">📊</span>Laporan</button>
<button onclick="pindahTab('bukubesar')" class="flex-1 flex flex-col items-center hover:text-indigo-600 px-1"><span class="text-lg block mb-1">📖</span>Riwayat</button>
</nav>fixed bottom-0: Mengunci posisi bilah navigasi di bagian bawah layar seperti aplikasi Android/iOS native.
- 5 Menu Utama: Menyediakan akses cepat ke menu Transaksi, Hutang, Mutasi, Laporan, dan Riwayat.
5. Penyuntikan Komponen Modular
Di baris-baris akhir Index.html, kita memanggil komponen modular dan skrip JavaScript menggunakan tag templat bawaan GAS:
<?!= include('view_bukubesar'); ?>
<?!= include('js_main'); ?>Inilah hasil perakitan dari fungsi include() yang sudah kita bahas sebelumnya di backend. Tampilan tabel Buku Besar dan logika JavaScript utama akan langsung disuntikkan di sini sehingga file HTML utama kita tetap bersih dan rapi.
Sekarang dilanjutkan membuat file baru HTML, beri nama view_bukubesar
Maka nanti namanya otomatis menjadi view_bukubesar.html
Hapus semua kode default yang ada di view_bukubesar.html
Lalu ketik / masukkan codingan dibawah ini:
Berikut potongan kode ringkasnya
<div class="bg-white p-5 rounded-xl shadow border border-slate-200">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center border-b pb-4 mb-4 gap-3">
<div>
<h2 class="text-lg font-bold text-slate-800">Buku Besar</h2>
<p class="text-xs text-slate-500">Rekapitulasi seluruh riwayat transaksi</p>
</div>
<div class="flex items-center gap-2 w-full md:w-auto">
<!-- Dropdown Filter Waktu -->
<select id="filter_waktu_bb" onchange="terapkanFilterBB()" class="flex-1 md:w-auto border border-slate-300 p-2 rounded-lg text-sm bg-slate-50 focus:ring-2 focus:ring-indigo-500 outline-none cursor-pointer">
<option value="1">1 Hari Terakhir</option>
<option value="7">7 Hari Terakhir</option>
<option value="30">1 Bulan Terakhir</option>
<option value="90">3 Bulan Terakhir</option>
<option value="180">6 Bulan Terakhir</option>
<option value="365">1 Tahun Terakhir</option>
<option value="all" selected>Semua Transaksi</option>
<option value="custom">Pilih Tanggal Kustom...</option>
</select>
<!-- Tombol Refresh Manual -->
<button onclick="paksaRefreshBukuBesar()" class="bg-slate-100 hover:bg-slate-200 text-slate-700 p-2 rounded-lg text-sm font-bold transition flex items-center gap-1 shrink-0" title="Muat Ulang Data">
Refresh ↻
</button>
</div>
</div>
...
...
...Sekarang untuk UI nya sudah dibuat, tinggal codingan untuk optimasi otak UI yang perlu kita buat lagi.
Buat satu file lagi masih dalam berbentuk .HTML
Beri nama js_main , nanti akan menjadi js_main.html, lalu masukkan kode-kode dibawah ini
Berikut potongan kode dari
js_main.html
<script>
let master = {};
let dataBukuBesar = []; // Menyimpan data mentah buku besar
let masterLaporanCache = null; // Menyimpan semua data laporan di memori HP
document.addEventListener('DOMContentLoaded', () => {
// Mengambil waktu saat ini (Lokal)
const now = new Date();
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
const waktuLokal = now.toISOString().slice(0, 16); // Format YYYY-MM-DDTHH:mm
// Set tanggal dan jam hari ini ke semua input form
['trx_tgl', 'hp_tgl', 'pd_tgl', 'pe_tgl'].forEach(id => {
document.getElementById(id).value = waktuLokal;
});
inisialisasiData();
});
function inisialisasiData() {
google.script.run
.withSuccessHandler(data => {
master = data;
document.getElementById('loading').classList.add('hidden');
// Pemicu awal pengisian dropdown
updateUITrx();
updateUIHutang();
// Isi form Pindah Dana & Penyesuaian
isi(document.getElementById('pd_sumber'), master.DompetRek);
isi(document.getElementById('pd_tujuan'), master.DompetRek);
isi(document.getElementById('pe_kas'), master.DompetRek);
pindahTab('transaksi'); // Tab default
})
.getSetupData();
}
...
...
...Kode-kode diatas saya hanya melampirkan potongan-nya saja, untuk kode lengkapnya kamu bisa akses di laman trakteer saya

Oke, sekarang saya akan menjelaskan masing-masing dari function js_main.html
Function inisialisasiData

Setelah seluruh komponen tampilan HTML siap, sekarang kita masuk ke file yang menjadi “otak penggerak” dari antarmuka web kita, yaitu js_main.html.
Di dalam file ini, fungsi pertama yang dieksekusi saat aplikasi pencatatan keuangan kamu dibuka di browser adalah inisialisasiData().
Bayangkan fungsi ini seperti sakelar lampu utama di sebuah gedung. Begitu halaman web selesai dimuat (DOM Content Loaded), fungsi ini bekerja di latar belakang untuk menghubungi server Google, mengambil semua data master dari spreadsheet, lalu membagikannya ke opsi pilihan (dropdown) form input.
Mari kita bedah cara kerja fungsi sakelar ini langkah demi langkah:
google.script.run.getSetupData()Inilah jembatan utama antara frontend (JavaScript di HP) dan backend (Apps Script di server). Di baris ini, JavaScript memanggil fungsi getSetupData() dari file Model.gs untuk menyedot semua daftar master data (nama dompet, kategori keluar, masuk, platform hutang, paylater, dan piutang).
withSuccessHandler(data => { ... })Begitu server selesai mengirimkan paket datanya, fungsi balikan (callback) ini akan langsung mengeksekusi beberapa tugas otomatis:
master = data;: Menyimpan seluruh data master ke dalam variabel globalmasterdi HP agar bisa dipakai berulang kali tanpa perlu menelpon server lagi.
classList.add('hidden'): Menyembunyikan layar penanda loading (Memuat Database…) karena data utama sudah siap.
Mengisi Opsi Dropdown Form secara Dinamis
Fungsi ini memanggil helper isi() untuk menyuntikkan daftar nama rekening/dompet ke dalam pilihan Pindah Dana (Sumber & Tujuan) dan Penyesuaian Saldo (Kas). Selain itu, fungsi updateUITrx() dan updateUIHutang() juga ikut dipicu agar pilihan kategori transaksi reguler dan hutang/piutang langsung siap digunakan.
pindahTab('transaksi');Terakhir, setelah semua opsi form terisi rapi di balik layar, aplikasi secara otomatis menampilkan halaman Transaksi Reguler sebagai tampilan depan (default).
Sangat sistematis, bukan? Dengan adanya fungsi inisialisasiData(), aplikasi kamu terjamin bebas dari masalah dropdown kosong atau tombol yang tidak merespon saat pertama kali dibuka!
Function isi

Di dalam antarmuka web yang memiliki banyak form input, menyuntikkan daftar pilihan () ke dalam elemen dropdown () secara manual satu per satu tentu akan sangat tidak efisien dan membuat kode berantakan.
Untuk menjaga kode kita tetap bersih dan berprinsip DRY (Don’t Repeat Yourself), kita membuat fungsi helper mungil namun sangat serbaguna bernama isi().
Tugas utama fungsi ini adalah menerima daftar teks dari server, lalu menyusunnya menjadi pilihan dropdown yang siap diklik oleh pengguna.
Mari kita bedah alur kerjanya yang simpel dan fleksibel ini:
Pengaman Element (if (!el) return;)
Sebelum melakukan apa pun, fungsi ini memastikan apakah elemen HTML target (misalnya dropdown dompet) benar-benar ada di dalam dokumen. Jika elemen tidak ditemukan, fungsi langsung berhenti agar tidak memicu pesan error di konsol browser.
Reset Pilihan (el.innerHTML = '<option value="">-- Pilih --</option>')
Setiap kali dipanggil, fungsi ini akan “kosongkan” dulu isi dropdown tersebut dan menyisipkan satu pilihan bawaan berlabel -- Pilih --. Ini penting agar pengguna diwajibkan memilih opsi nyata sebelum mengirim formulir transaksi.
Penyuntikan Opsi Dinamis (list.forEach)
Fungsi ini kemudian menyisir array daftar data (list) yang dikirim dari server, seperti daftar nama dompet ['BCA', 'Mandiri', 'Cash']. Untuk setiap item, fungsi ini merakit tag HTML <option value="BCA">BCA</option> dan menempelkannya ke dalam elemen dropdown.
Contoh Penggunaannya di Kode Lain:
Cukup dengan menulis satu baris perintah seperti ini:
isi(document.getElementById('pd_sumber'), master.DompetRek);Maka elemen dropdown dengan ID pd_sumber akan langsung terisi penuh oleh daftar nama rekening/dompet secara otomatis.
Function updateUITrx

Salah satu ciri utama antarmuka aplikasi pencatatan keuangan yang ramah pengguna (user-friendly) adalah form input yang dapat beradaptasi secara otomatis. Pengguna tidak boleh dibingungkan oleh pilihan kategori yang salah.
Misalnya, saat pengguna memilih jenis transaksi “Masuk”, opsi kategori yang muncul di dropdown seharusnnya hanya kategori pemasukan (seperti Gaji, Freelance, atau Bonus). Sebaliknya, jika pengguna memilih “Keluar”, pilihan yang tampil harus berganti menjadi kategori pengeluaran (seperti Makanan, Internet, atau Listrik).
Nah, tugas mengatur pertukaran data secara otomatis itulah yang dipegang oleh fungsi updateUITrx().
Mari kita bedah alur kerjanya yang sangat presisi:
Mengecek Pilihan Jenis Transaksi (document.getElementById('trx_jenis').value)
Setiap kali pengguna mengubah pilihan pada elemen dropdown jenis transaksi (apakah memilih Masuk atau Keluar), fungsi ini dipanggil lewat perintah event onchange="updateUITrx()".
Kondisional Kategori Otomatis (if - else if)
- Jika Jenis = “Masuk”: Fungsi langsung memanggil helper
isi()untuk menyuntikkan daftar kategori pemasukan (master.Masuk) ke dalam elemen dropdown kategori (trx_kategori).
- Jika Jenis = “Keluar”: Elemen kategori otomatis dikosongkan dan diisi ulang menggunakan daftar kategori pengeluaran (
master.Keluar).
- Jika Belum Dipilih: Pilihan kategori di-reset menjadi kosong agar pengguna tidak salah memilih data.
Penyediaan Opsi Dompet/Rekening (isi(..., master.DompetRek))
Di saat yang sama, fungsi ini memastikan bahwa seluruh daftar dompet atau rekening yang kamu miliki (BCA, Mandiri, Cash, dll.) selalu tersedia dan siap dipilih di dropdown akun tujuan.
Dengan menerapkan teknik penanganan UI seperti ini di dalam tutorial Google Apps Script untuk pemula, kita berhasil membuat aplikasi keuangan SPA yang tidak hanya cepat, tetapi juga minim potensi human error saat mencatat keuangan harian.
Function updateUIHutang
function updateUIHutang() {
const jenis = document.getElementById('hp_jenis').value;
const desc = document.getElementById('hp_desc');
const lblSumber = document.getElementById('lbl_hp_sumber');
const selSumber = document.getElementById('hp_sumber');
const lblDompet = document.getElementById('lbl_hp_dompet');
const selDompet = document.getElementById('hp_dompet');
const divDompet = document.getElementById('div_hp_dompet');
divDompet.classList.remove('hidden'); // Tampilkan Dompet secara default
isi(selDompet, master.DompetRek); // Default isi Dompet
if (jenis === 'Hutang') {
desc.value = 'Pinjam Uang';
lblSumber.innerText = 'Sumber Platform'; isi(selSumber, master.Hutang);
lblDompet.innerText = 'Masuk Ke';
}
...
...
...Jika fungsi updateUITrx() bertugas mengatur transaksi reguler, maka fungsi updateUIHutang() ini bertanggung jawab mengelola kerumitan form khusus pencatatan tagihan dan kewajiban keuangan.
Di dalam aplikasi pencatatan keuangan ini, kita menggabungkan tiga tipe transaksi utang-piutang ke dalam satu form yang adaptif:
- Hutang (Pinjaman dari pihak/platform lain)
- Paylater (Cicilan platform belanja/layanan digital)
- Piutang (Uang kita yang dipinjam oleh orang lain)
Tugas utama dari updateUIHutang() adalah mengubah isi dropdown kategori dan pilihan dompet secara real-time menyesuaikan jenis aksi yang sedang dipilih pengguna.
Mari kita bedah alur kerja dan kecerdasan kodenya:
Memantau Perubahan Opsi (hp_jenis)
Saat pengguna memilih menu di dropdown jenis (misal memilih “Bayar Paylater” atau “Piutang Dibayar”), event onchange="updateUIHutang()" akan langsung memicu fungsi ini bekerja.
Penyesuaian Sumber Data Master yang Tepat
- Jika aksi terkait Hutang (Tambah Hutang / Bayar Hutang): Dropdown nama platform (
hp_nama) hanya akan menampilkan daftar darimaster.Hutang(misalnya: Bank, Akulaku, Kredifazz).
- Jika aksi terkait Paylater (Pakai Paylater / Bayar Paylater): Dropdown nama platform otomatis beralih menyedot data dari
master.Paylater(misalnya: Spaylater, GoPayLater).
- Jika aksi terkait Piutang (Beri Piutang / Piutang Dibayar): Dropdown nama beralih menampilkan daftar nama orang dari
master.Piutang(misalnya: Roni, Budi).
Mencegah Kesalahan Input Data
Dengan penyaringan otomatis seperti ini, pengguna tidak akan pernah keliru memasukkan pembayaran Spaylater ke dalam kategori Hutang Bank, atau sebaliknya.
Teknik penanganan antarmuka yang presisi ini menjadi salah satu keunggulan utama dalam tutorial Google Apps Script untuk pemula, karena mampu menghadirkan pengalaman pengguna (user experience) yang solid “ya mirip-mirip” aplikasi finansial komersial hehe.
Function pindahTab

Di dalam tutorial Google Apps Script pemula ini, salah satu daya tarik utama dari aplikasi yang kita bangun adalah performanya yang serba instan. Pengguna bisa berpindah dari layar Form Transaksi ke Laporan Keuangan atau Riwayat Buku Besar tanpa perlu menunggu browser memuat ulang (reload) halaman.
Kunci rahasia dari mekanisme aplikasi pencatatan keuangan berkonsep SPA ini terletak pada satu fungsi sederhana: pindahTab(id).
Tugas utama dari fungsi ini adalah mengatur visibilitas kontainer antarmuka (UI) di layar HP pengguna, sekaligus memicu penarikan data secara otomatis jika menu yang dibuka membutuhkan data terbaru dari server.
Mari kita bedah alur kerjanya yang sangat efisien ini:
Pembersihan Layar (views.forEach)
Fungsi ini menyimpan seluruh daftar nama ID view yang ada di file Index.html. Saat tombol navigasi diklik, fungsi akan menyisir semua kontainer tersebut dan menyuntikkan kelas Tailwind CSS .hidden untuk menyembunyikan tampilan layar yang sedang aktif sebelumnya.
Menampilkan Target (classList.remove('hidden'))
Setelah semua layar tersembunyi, skrip mencari elemen dengan ID view-[id] yang sesuai dengan tombol yang diklik pengguna (misalnya view-bukubesar atau view-dashboard). Kelas .hidden pada elemen target tersebut kemudian dilepas sehingga layarnya langsung muncul di depan pengguna dalam hitungan 0 detik.
Pemicu Penarikan Data Otomatis (Lazy Loading)
Guna menghemat penggunaan memori dan kuota server, aplikasi kita tidak menarik seluruh data sekaligus di awal. Penarikan data laporan baru dieksekusi saat tab tersebut benar-benar dibuka oleh pengguna:
- Membuka Tab ‘bukubesar’: Otomatis memicu fungsi
muatBukuBesar().
- Membuka Tab ‘dashboard’: Otomatis memicu fungsi
muatDashboard().
- Membuka Tab ‘setup’: Otomatis memicu fungsi
muatSetupMaster().
Teknik navigasi yang ringkas namun tangguh ini membuktikan bahwa cara membuat aplikasi SPA menggunakan Google Apps Script dan Tailwind CSS sebenarnya sangat mudah dipelajari, bahkan bagi pemula yang baru terjun ke dunia web development.
Function formatRp

Saat mengolah data keuangan di backend, komputer hanya mengenali angka mentah tanpa titik atau simbol mata uang (contoh: 1500000). Namun, bagi pengguna yang membaca laporan di layar HP, melihat deretan angka nol yang panjang tentu sangat membingungkan dan rawan salah baca.
Untuk itulah kita membuat fungsi helper formatRp(angka).
Tugas utama dari fungsi ini adalah menyulap angka nominal mentah menjadi format standar mata uang Indonesia yang elegan, lengkap dengan simbol Rp dan pemisah ribuan menggunakan titik.
Mari kita bedah alur kerja dan proteksi kodenya:
Proteksi Nilai Kosong / Invalid (if (!angka || isNaN))
Sebelum melakukan konversi, fungsi ini mengecek apakah data yang masuk bernilai null, undefined, atau bukan angka (isNaN). Jika ada data yang tidak valid, fungsi akan dengan aman mengembalikan nilai default 'Rp 0' sehingga antarmuka aplikasi kamu tidak akan menampilkan pesan error seperti NaN atau undefined.
Standardisasi Lokalisasi Indonesia (toLocaleString('id-ID'))
Alih-alih menggunakan rumus manipulasi teks (string manipulation) yang panjang dan rumit, kita memanfaatkan fitur bawaan JavaScript modern yaitu .toLocaleString('id-ID').
Fitur ini secara otomatis menyisipkan tanda titik sebagai pemisah ribuan sesuai aturan standar penulisan Bahasa Indonesia.
Contoh Hasil: Angka 2500000 akan langsung diubah secara presisi menjadi Rp 2.500.000.
Fungsi mungil ini dipakai di hampir seluruh komponen tampilan aplikasi pencatatan keuangan ini—mulai dari ringkasan saldo kas di Dashboard, daftar tagihan Paylater, hingga rincian kolom uang masuk dan keluar pada tabel Buku Besar.
Function gantiLaporan

Di dalam menu Laporan / Dashboard, kita memiliki empat kategori informasi penting yang perlu dipantau secara berkala: Kas Saldo, Hutang, Paylater, dan Piutang.
Agar pengguna tidak perlu keluar masuk halaman, kita merancang antarmuka sub-tab berbasis Pill Buttons (tombol berbentuk kapsul). Saat pengguna mengklik salah satu tombol kapsul tersebut, tampilan daftar laporan di bawahnya akan langsung berganti secara instan (0 detik) tanpa loading ke server sama sekali!
Rahasia kecepatan kilat ini ada pada kombinasi variabel laporanAktif dan fungsi gantiLaporan(jenis).
Mari kita bedah cara kerja dan keunggulan kodenya:
Variabel Penanda (let laporanAktif = 'kas';)
Variabel ini bertindak sebagai penanda status (state management) untuk mencatat sub-tab mana yang sedang dibuka oleh pengguna. Nilai bawaannya adalah 'kas', sehingga saat menu Laporan pertama kali dibuka, rekapan saldo dompet/rekening yang akan tampil duluan.
Efek Visual Tombol Kapsul (['kas', 'hutang', ...].forEach)
Saat fungsi ini dipanggil (misalnya pengguna menekan tombol Paylater), skrip akan menyisir keempat tombol kapsul tersebut:
- Tombol yang Dipilih: Diberi gaya CSS utama Tailwind (
bg-indigo-600 text-white shadow-sm) agar tampil menonjol sebagai indikator bahwa sub-tab tersebut sedang aktif.
- Tombol Lainnya: Dikembalikan ke warna abu-abu netral (
bg-slate-100 text-slate-500) yang lembut di mata.
Eksekusi Render Lokal (renderSubTabAktif())
Setelah status tombol diperbarui, fungsi ini langsung memanggil renderSubTabAktif(). Karena data seluruh laporan sudah diisi dan disimpan di memori HP saat awal membuka tab (Client-Side Caching), proses penggantian daftar tabel di bawahnya terjadi secara lokal tanpa memanggil ulang Google Apps Script backend.
Penerapan teknik state management sederhana seperti ini di dalam tutorial Google Apps Script pemula membuktikan bahwa aplikasi berbasis web bisa terasa sangat responsif dan mulus layaknya aplikasi Android native.
Function renderSubTabAktif

Setelah fungsi gantiLaporan(jenis) mengubah status tab dan warna tombol kapsul, pertanyaan berikutnya adalah: Bagaimana data tabel di bawahnya ditampilkan ke layar?
Di sinilah fungsi renderSubTabAktif() mengambil alih tugas.
Fungsi ini bertindak sebagai “Penyaji Makanan” yang mengambil data laporan yang sudah tersimpan rapi di memori HP/browser (masterLaporanCache), lalu merakitnya menjadi elemen kartu (card list) yang cantik menggunakan Tailwind CSS.
Mari kita bedah alur kerja dan kecerdasan kodenya:
Mengambil Data dari Memori (Cache)
Skrip mengecek objek masterLaporanCache berdasarkan nilai variabel laporanAktif yang sedang terbuka (apakah kas, hutang, paylater, atau piutang). Karena data sudah ditarik di awal, proses ini berjalan dalam hitungan fraksi milidetik.
Penanganan Data Kosong (Empty State)
Jika belum ada data sama sekali pada kategori tersebut, fungsi ini secara elegan menampilkan pesan “Tidak ada data [kategori] untuk ditampilkan” agar tampilan web tetap rapi dan tidak kosong membingungkan.
Penyelarasan Nama Properti (const label = item.dompet || item.nama)
Tiap jenis laporan memiliki nama properti yang sedikit berbeda di backend (di laporan Kas menggunakan .dompet, sedangkan di laporan Hutang/Piutang menggunakan .nama). Baris kode ini secara pintar mendeteksi properti mana yang tersedia tanpa perlu membuat fungsi render terpisah.
Pewarnaan Saldo Dinamis (Conditional Styling)
Menggunakan template literals JavaScript, nilai saldo langsung dibungkus oleh fungsi formatRp(). Jika nominalnya bernilai minus, warna teks otomatis berubah menjadi merah (text-red-500), dan jika positif/nol akan berwarna ungu indigo (text-indigo-600).
Dengan gabungan fungsi gantiLaporan() dan renderSubTabAktif(), aplikasi pencatatan keuangan berbahan Google Sheets ini mampu menyajikan pengalaman pengguna (User Experience) yang sangat mulus, responsif, dan bebas hambatan loading.
Function refreshLaporanAktif

Meskipun sistem Client-Side Caching yang kita terapkan membuat navigasi antar-laporan terasa sangat cepat, terkadang pengguna tetap butuh kepastian bahwa data yang tampil di layar adalah data paling fresh yang baru saja diinput atau diperbarui di server.
Untuk itulah kita menyediakan tombol pembaru manual yang ditenagai oleh fungsi refreshLaporanAktif().
Fungsi ini bertugas mengabaikan data lama yang tersimpan di memori HP, lalu melakukan pemanggilan ulang ke backend Google Apps Script untuk menyedot data laporan terbaru dari spreadsheet.
Mari kita bedah alur kerja dan respon visual dari fungsi ini:
Umpan Balik Visual Instant (Loading Indicator)
Saat pengguna menekan tombol Refresh di menu Laporan, fungsi ini langsung mengubah isi kontainer #wadah_laporan_dinamis menjadi pesan “Memperbarui data laporan terbaru…” yang bergerak redup-terang lembut menggunakan kelas Tailwind CSS animate-pulse. Ini memberi rasa pasti kepada pengguna bahwa aplikasi sedang bekerja.
Penarikan Paket Komplit (getAllLaporanData())
Melalui baris google.script.run.getAllLaporanData(), JavaScript kembali memanggil fungsi “kurir paket hemat” di backend. Server akan mengirimkan kembali empat kelompok data sekaligus (Kas Saldo, Hutang, Paylater, dan Piutang) yang sudah diperhitungkan ulang oleh rumus-rumus spreadsheet.
Pembaruan Memori Klien & Re-render (withSuccessHandler)
Begitu data baru dari server tiba:
- Data lama di variabel
masterLaporanCachelangsung diganti (overwrite) secara utuh.
- Fungsi
renderSubTabAktif()dipanggil seketika untuk merender ulang tampilan sub-tab yang sedang dibuka pengguna.
Dengan hadirnya fungsi refreshLaporanAktif() ini, aplikasi pencatatan keuangan kamu memiliki keseimbangan sempurna: sangat cepat dalam penggunaan harian berkat fitur cache, tetapi tetap fleksibel dan akurat saat pengguna ingin memperbarui data secara real-time.
Function terapkanFilterDash

Di dalam antarmuka Dashboard, selain memantau saldo bersih di tiap rekening, pengguna biasanya ingin melihat ringkasan Arus Kas (Masuk vs Keluar) dalam rentang waktu tertentu—misalnya rentang 7 hari terakhir, bulan ini, atau tanggal kustom pilihan sendiri.
Untuk mengatur pergantian opsi saringan waktu tersebut secara fleksibel, kita membuat fungsi terapkanFilterDash().
Fungsi ini bertugas memantau pilihan pengguna di dropdown rentang waktu, mengatur visibilitas form tanggal kustom (Custom Date), dan langsung memicu kalkulasi ulang grafik/ringkasan arus kas.
Mari kita bedah alur kerjanya yang responsif ini:
Memantau Pilihan Dropdown (filter_waktu_dash)
Setiap kali pengguna mengubah opsi pada dropdown periode waktu, event onchange="terapkanFilterDash()" akan langsung memicu fungsi ini.
Kondisional Tampilan Tanggal Kustom (if (mode === 'custom'))
- Jika memilih “Custom”: Opsi ini menandakan pengguna ingin memasukkan tanggal mulai dan tanggal akhir secara manual. Fungsi akan melepas kelas
.hiddenpadawadahTanggalsehingga input DatePicker otomatis muncul di layar. Eksekusi fungsi ditahan sementara (return) sampai pengguna menekan tombol “Cari”.
- Jika memilih preset lain (seperti 7 Hari, 30 Hari, dll.): Input tanggal kustom otomatis disembunyikan kembali dengan menyuntikkan kelas
.hidden.
Eksekusi Render Otomatis (renderArusKas)
Jika opsi yang dipilih adalah preset bawaan, fungsi langsung memanggil renderArusKas(mode, null, null) untuk menghitung total uang masuk dan keluar pada periode tersebut secara instan dari memori cache.
Dengan adanya logika ini, antarmuka aplikasi pencatatan keuangan kamu tampil sangat bersih karena elemen input tanggal kustom hanya dimunculkan saat benar-benar dibutuhkan oleh pengguna.
Function terapkanFilterTanggalDash

Ketika pengguna memilih opsi rentang waktu kustom (Custom Date) di Dashboard, aplikasi akan menampilkan dua kotak input tanggal (tanggal mulai dan tanggal akhir) beserta satu tombol aksi “Cari Data”.
Fungsi terapkanFilterTanggalDash() inilah yang bertugas mengeksekusi perintah dari tombol tersebut.
Fungsi ini tidak sekadar meneruskan input tanggal ke pemrosesan data, melainkan juga bertindak sebagai gardu validasi untuk memastikan rentang tanggal yang dimasukkan oleh pengguna benar-benar valid dan logis sebelum dihitung oleh sistem.
Mari kita bedah alur kerja dan lapis proteksi pada fungsi ini:
Penarikan Nilai Input (dash_tgl_mulai & dash_tgl_akhir)
Skrip mengambil nilai string tanggal dari elemen <input type="date"> yang sudah diisi oleh pengguna di halaman Dashboard.
Validasi Input Kosong (if (!tglMulai || !tglAkhir))
Jika salah satu atau kedua kolom tanggal masih kosong saat tombol diklik, eksekusi kode langsung dihentikan (return) dan fungsi akan memicu showToast() untuk menampilkan notifikasi pop-up peringatan merah: “Pilih rentang tanggal terlebih dahulu!”.
Validasi Logika Rentang Waktu (new Date(tglMulai) > new Date(tglAkhir))
Komputer mengonversi teks tanggal menjadi objek waktu JavaScript (new Date()) untuk membandingkan nilainya. Jika pengguna secara tidak sengaja memasukkan tanggal mulai yang lebih muda/setelah tanggal akhir (misalnya: mulai 15 Oktober, akhir 01 Oktober), fungsi akan memblokirnya dan memunculkan notifikasi error: “Tanggal awal melewati tanggal akhir!”.
Pengiriman Parameter ke Pemrosesan Data (renderArusKas)
Setelah lulus dari kedua tahap validasi di atas, barulah fungsi memanggil renderArusKas('custom', tglMulai, tglAkhir) untuk menghitung total arus kas masuk dan keluar secara akurat berdasarkan rentang tanggal yang sudah dipastikan aman tersebut.
Adanya lapisan validasi seperti ini dalam tutorial Google Apps Script untuk pemula membuktikan pentingnya menangani potensi user error di sisi frontend agar antarmuka aplikasi terasa makin solid dan ramah pengguna.
Function renderArusKas
function renderArusKas(mode, tglMulai, tglAkhir) {
if (!dataBukuBesar || dataBukuBesar.length === 0) {
document.getElementById('dash_pemasukan').innerText = 'Rp 0';
document.getElementById('dash_pengeluaran').innerText = 'Rp 0';
return;
}
let dataTersaring = dataBukuBesar;
if (mode === 'custom') {
const start = new Date(tglMulai); start.setHours(0,0,0,0);
const end = new Date(tglAkhir); end.setHours(23,59,59,999);
dataTersaring = dataBukuBesar.filter(row => {
const tglRow = new Date(String(row[0]).replace(' ', 'T'));
return tglRow >= start && tglRow <= end;
});
} else if (mode !== 'all') {
const batasWaktu = new Date();
batasWaktu.setDate(batasWaktu.getDate() - parseInt(mode));
batasWaktu.setHours(0,0,0,0);
dataTersaring = dataBukuBesar.filter(row => {
const tglRow = new Date(String(row[0]).replace(' ', 'T'));
return tglRow >= batasWaktu;
});
}
let totalMasuk = 0;
let totalKeluar = 0;
dataTersaring.forEach(row => {
totalMasuk += Number(row[4]) || 0;
totalKeluar += Number(row[5]) || 0;
});
// Langsung tampilkan angka tanpa jeda server
document.getElementById('dash_pemasukan').innerText = formatRp(totalMasuk);
document.getElementById('dash_pengeluaran').innerText = formatRp(totalKeluar);
}Setelah pilihan periode waktu ditentukan lewat fungsi filter sebelumnya, fungsi renderArusKas() inilah yang mengeksekusi kalkulasi angkanya.
Fungsi ini bertugas memilah (filter) riwayat transaksi yang ada di memori HP, menghitung total uang masuk dan keluar secara otomatis, lalu menampilkan hasilnya ke kartu ringkasan Dashboard.
Karena seluruh proses perhitungan dilakukan langsung oleh engine JavaScript di peramban (browser) kamu, hasilnya bisa langsung muncul di layar secara instan tanpa menunggu waktu pemrosesan dari server Google.
Mari kita bedah alur kerja dan logika komputasinya:
Pengaman Data Kosong (if (!dataBukuBesar))
Jika variabel memori dataBukuBesar belum terisi atau memang belum ada transaksi sama sekali di spreadsheet, fungsi akan langsung mereset tampilan angka Pemasukan dan Pengeluaran menjadi 'Rp 0' lalu berhenti.
Penyaringan Waktu Berbasis Fleksibilitas Tinggi
- Mode Custom (
mode === 'custom'): Jam pada tanggal mulai diset ke paling awal (00:00:00), sedangkan tanggal akhir diset ke detik paling akhir (23:59:59). Hal ini dilakukan agar seluruh transaksi di tanggal tersebut tercakup secara penuh.
- Mode Preset Hari (
mode !== 'all'): Jam acuan dihitung mundur berdasarkan nilai hari (misal 7 atau 30 hari terakhir) dari tanggal hari ini.
- Parsing Format Waktu (
String(row[0]).replace(' ', 'T')): Trik konversi string tanggal ini digunakan agar format tanggal dari Google Sheets bisa dibaca dengan konsisten oleh seluruh jenis browser (Android, iOS, maupun Desktop).
Kalkulasi Otomatis dan Format Rupiah
Seluruh baris transaksi yang lolos saringan diisi ke dalam perulangan forEach. Indeks row[4] (kolom Uang Masuk) dan row[5] (kolom Uang Keluar) dijumlahkan secara beruntun. Hasil akhirnya kemudian dibungkus oleh fungsi formatRp() dan disuntikkan langsung ke elemen UI #dash_pemasukan serta #dash_pengeluaran.
Teknik pengolahan data berbasis Client-Side Filtering seperti ini sangat direkomendasikan saat kamu membuat aplikasi pencatatan keuangan berbasis Google Apps Script, karena menghemat kuota fetch server sekaligus optimasi performa aplikasi agar tetap gesit.
Function renderLaporanUI
function renderLaporanUI(data, labelUtama, labelRincian, warnaMode) {
const div = document.getElementById('dash-content');
let total = 0; let rincianHtml = '';
// CSS Engine untuk mewarnai tema secara dinamis
let themeBg = 'bg-indigo-100'; let themeBorder = 'border-indigo-200'; let themeText = 'text-indigo-600'; let themeAmount = 'text-indigo-900'; let valColor = 'text-indigo-700';
if(warnaMode === 'red') { themeBg = 'bg-red-50'; themeBorder = 'border-red-200'; themeText = 'text-red-600'; themeAmount = 'text-red-900'; valColor = 'text-red-600'; }
if(warnaMode === 'orange') { themeBg = 'bg-orange-50'; themeBorder = 'border-orange-200'; themeText = 'text-orange-600'; themeAmount = 'text-orange-900'; valColor = 'text-orange-600'; }
if(warnaMode === 'emerald') { themeBg = 'bg-emerald-50'; themeBorder = 'border-emerald-200'; themeText = 'text-emerald-600'; themeAmount = 'text-emerald-900'; valColor = 'text-emerald-600'; }
// Membangun baris rincian dompet/orang/platform
data.forEach(item => {
total += Number(item.saldo);
rincianHtml += `
<div class="flex justify-between items-center p-3 mb-2 bg-slate-50 border border-slate-100 rounded-lg shadow-sm">
<span class="font-semibold text-slate-700">${item.nama || item.dompet}</span>
<span class="font-bold ${valColor}">${formatRp(item.saldo)}</span>
</div>
`;
});
if(data.length === 0) {
rincianHtml = `<div class="text-center py-6 text-slate-400 text-sm">Tidak ada catatan bersisa.</div>`;
}
// Suntikkan ke HTML
div.innerHTML = `
<div class="mb-6 p-6 ${themeBg} rounded-xl text-center shadow-sm border ${themeBorder} transition-all duration-300">
<p class="text-xs font-bold ${themeText} mb-1 uppercase">${labelUtama}</p>
<h3 class="text-3xl font-black ${themeAmount}">${formatRp(total)}</h3>
</div>
<div class="text-left">
<h4 class="text-xs font-bold text-slate-400 mb-3 uppercase tracking-wider">${labelRincian}</h4>
${rincianHtml}
</div>
`;
}Di dalam antarmuka menu Laporan, kita menyajikan data dalam bentuk kartu-kartu ringkas (card UI). Daripada kita menulis kode perulangan HTML yang berulang-ulang untuk setiap jenis laporan (Kas Saldo, Hutang, Paylater, dan Piutang), kita menggunakan teknik reusable template melalui fungsi renderLaporanUI().
Fungsi ini bertugas menerima kiriman data mentah beserta parameter gaya visual, lalu mengubahnya menjadi deretan kartu komponen HTML yang rapi dengan sentuhan Tailwind CSS.
Mari kita bedah alur kerja dan Fleksibilitas Parameter dari fungsi ini:
Parameter Warna & Label Dinamis (labelUtama, labelRincian, warnaMode)
Fungsi ini fleksibel karena dapat disesuaikan penampilannya:
labelRincian: Menentukan judul kecil di atas nama item (misal: “Nama Dompet”, “Platform”, atau “Nama Peminjam”).
warnaMode: Menentukan skema warna teks angka saldo (misal warna hijau/indigo untuk saldo kas, atau warna oranye/merah untuk tagihan hutang).
Proteksi Tampilan Kosong (Empty State)
Jika parameter data kosong atau tidak memiliki baris, fungsi langsung mengembalikan elemen pembungkus berisi pesan informatif “Belum ada data [label] tercatat” agar tata letak halaman tidak bolong.
Sistem Kisi Responsif (Responsive Grid)
Output HTML dibungkus menggunakan kelas Tailwind grid grid-cols-1 sm:grid-cols-2 gap-3. Di layar HP (ukuran kecil), kartu akan tertumpuk 1 kolom ke bawah, sedangkan di layar laptop/tablet otomatis berjajar rapi menjadi 2 kolom.
Sentuhan Interaktif (Hover & Transition)
Setiap kartu dibekali efek hover:border-slate-200 transition yang memberikan sedikit respons visual halus saat kursor atau sentuhan jari pengguna berada di atas kartu tersebut.
Dengan mengisolasi logika pembuatan komponen UI ke dalam fungsi renderLaporanUI(), kode di file js_main.html menjadi jauh lebih ringkas, mudah dirawat (maintainable), dan sesuai dengan standar pengembangan web modern.
Function simpan
function simpan(tipe) {
let args = [];
let func = '';
let btnId = '';
if(tipe === 'transaksi') {
args = [
document.getElementById('trx_tgl').value,
document.getElementById('trx_jenis').value,
document.getElementById('trx_kategori').value,
document.getElementById('trx_dompet').value,
document.getElementById('trx_jumlah').value,
document.getElementById('trx_ket').value
];
// Validasi ketat lapis demi lapis
if(!args[0]) return showToast('Peringatan: Tanggal wajib diisi!', 'error');
if(!args[2]) return showToast('Peringatan: Kategori wajib dipilih!', 'error');
if(!args[3]) return showToast('Peringatan: Dompet / Rekening wajib dipilih!', 'error');
if(!args[4]) return showToast('Peringatan: Jumlah (Rp) wajib diisi!', 'error');
func = 'simpanTransaksiWeb'; btnId = 'btn_trx';
}
else if(tipe === 'hutang') {
const jenisHtg = document.getElementById('hp_jenis').value;
const isPaylater = jenisHtg === 'Paylater';
args = [
document.getElementById('hp_tgl').value,
jenisHtg,
document.getElementById('hp_desc').value,
document.getElementById('hp_sumber').value,
isPaylater ? '' : document.getElementById('hp_dompet').value,
document.getElementById('hp_jumlah').value,
document.getElementById('hp_ket').value
];
// VALIDASI KETAT MANAJEMEN HUTANG
if(!args[0]) return showToast('Peringatan: Tanggal wajib diisi!', 'error');
if(!args[3]) return showToast('Peringatan: Sumber Platform / Nama pihak terkait wajib dipilih!', 'error');
// Jika BUKAN Paylater, maka kolom dompet (Masuk Ke / Sumber Dana) WAJIB diisi
if(!isPaylater && !args[4]) {
return showToast('Peringatan: Dompet / Rekening tujuan/sumber wajib dipilih!', 'error');
}
if(!args[5]) return showToast('Peringatan: Jumlah (Rp) wajib diisi!', 'error');
func = 'simpanHutangWeb'; btnId = 'btn_hp';
}
else if(tipe === 'pindah') {
args = [document.getElementById('pd_tgl').value, document.getElementById('pd_sumber').value, document.getElementById('pd_tujuan').value, document.getElementById('pd_jumlah').value, document.getElementById('pd_ket').value];
if(!args[0] || !args[3]) return showToast('Tanggal dan Jumlah wajib diisi!');
if(args[1] === args[2]) return showToast('Sumber dan Tujuan tidak boleh sama!');
func = 'simpanPindahDanaWeb'; btnId = 'btn_pd';
}
else if(tipe === 'penyesuaian') {
args = [
document.getElementById('pe_tgl').value,
document.getElementById('pe_jenis').value,
document.getElementById('pe_kas').value,
document.getElementById('pe_jumlah').value,
document.getElementById('pe_ket').value
];
// VALIDASI KETAT PENYESUAIAN SALDO
if(!args[0]) return showToast('Peringatan: Tanggal wajib diisi!', 'error');
if(!args[2]) return showToast('Peringatan: Kas / Rekening wajib dipilih!', 'error'); // Pengecekan Kas Kosong
if(!args[3]) return showToast('Peringatan: Jumlah (Rp) wajib diisi!', 'error');
func = 'simpanPenyesuaianWeb'; btnId = 'btn_pe';
}
else if(tipe === 'setup') {
args = [document.getElementById('set_kategori').value, document.getElementById('set_nilai').value];
if(!args[1]) return showToast('Nama item baru wajib diisi!');
func = 'simpanSetupWeb'; btnId = 'btn_set';
masterSetupCache = null;
}
const btn = document.getElementById(btnId);
const teksAsli = btn.innerText;
btn.disabled = true; btn.innerText = 'Memproses...';
google.script.run
.withSuccessHandler(msg => {
// MENGGUNAKAN TOAST KUSTOM
showToast(msg, 'sukses');
btn.disabled = false; btn.innerText = teksAsli;
// ========================================================
// RESET CACHE MEMORI AGAR DATA TERBARU DITARIK ULANG
// ========================================================
dataBukuBesar = null; // Reset memori Buku Besar
masterLaporanCache = null; // Reset memori Laporan / Dashboard
masterSetupCache = null; // Reset memori Setup (jika mengubah data master)
// ========================================================
// Membersihkan input form
['trx_jumlah', 'trx_ket', 'hp_jumlah', 'hp_ket', 'pd_jumlah', 'pd_ket', 'pe_jumlah', 'pe_ket', 'set_nilai'].forEach(id => {
const el = document.getElementById(id);
if(el) el.value = '';
});
if(tipe === 'setup') inisialisasiData();
})
.withFailureHandler(err => {
// MENGGUNAKAN TOAST KUSTOM UNTUK ERROR
showToast("Error: " + err.message, 'error');
btn.disabled = false; btn.innerText = teksAsli;
})
[func](...args);
}Setelah membahas berbagai fungsi penyaji laporan dan tampilan, sekarang kita masuk ke bagian krusial di frontend: bagaimana data dari form diproses dan dikirim ke server.
Di dalam aplikasi ini, pengguna bisa menyimpan berbagai jenis transaksi, mulai dari transaksi reguler, hutang-piutang, pindah dana, hingga penyesuaian saldo. Daripada membuat event listener terpisah yang rumit untuk setiap tombol Submit, kita merancangnya lewat satu fungsi terpusat bernama simpan(tipe).
Fungsi ini bertugas mengidentifikasi form mana yang sedang dikirim pengguna, mengumpulkan nilai inputnya, melakukan validasi kelengkapan data, lalu mengirimkannya ke fungsi backend (Code.gs) yang sesuai.
Mari kita bedah keunggulan dan alur kerja dari arsitektur sakelar ini:
Prinsip Single Responsibility
Fungsi simpan(tipe) bertindak sebagai pembagi jalur (router). Fungsi ini tidak dibebani oleh tugas membaca nilai elemen HTML atau menghitung angka. Tugas spesifik tersebut diserahkan ke fungsi anaknya masing-masing (simpanTransaksi, simpanHutang, dsb.).
Kemudahan Pemanggilan di Event HTML
Di file Index.html, kamu cukup memanggil fungsi ini pada tombol penyerahan form dengan menyebutkan jenis tipenya:
- Form Transaksi:
onclick="simpan('transaksi')"
- Form Hutang/Piutang:
onclick="simpan('hutang')"
- Form Master Setup:
onclick="simpan('setup')"
Navigasi Kode yang Rapi & Terorganisir
Dengan memisahkan gerbang eksekusi ke dalam fungsi simpan(tipe), struktur kode di file js_main.html menjadi sangat bersih. Ketika kamu ingin menambah fitur form baru di kemudian hari, kamu cukup menambahkan satu cabang else if baru tanpa mengganggu logika form lainnya.
Function muatBukuBesar

Salah satu bagian yang paling rawan membuat aplikasi pencatatan keuangan terasa lambat adalah saat membuka tabel riwayat transaksi (Buku Besar). Jika setiap kali pengguna mengklik tab Riwayat, aplikasi harus menarik ribuan baris data dari Google Sheets, layar HP akan sering mengalami lag atau loading yang lama.
Untuk mengatasi masalah tersebut, kita menerapkan teknik Lazy Loading + Client-Side Caching yang dibungkus di dalam fungsi muatBukuBesar().
Fungsi ini bertugas memeriksa status memori di HP/browser pengguna sebelum memutuskan apakah perlu memanggil server Google atau cukup menampilkan data yang sudah ada.
Mari kita bedah alur kerja dan efisiensi kodenya:
Pemeriksaan Memori Lokal (if (dataBukuBesar && dataBukuBesar.length > 0))
Saat tab Buku Besar dibuka, fungsi ini terlebih dahulu mengecek variabel global dataBukuBesar. Jika variabel ini sudah berisi array data transaksi (hasil pemanggilan sebelumnya), fungsi langsung menghentikan proses pemanggilan server (return) dan mengeksekusi terapkanFilterBB().
Hasilnya? Tabel riwayat transaksi akan terbuka seketika (0 detik tanpa loading) meskipun pengguna berpindah-pindah tab berulang kali!
Penarikan Sekali di Awal (Fetch on Demand)
Jika variabel dataBukuBesar masih kosong (misalnya saat aplikasi baru pertama kali dibuka), barulah fungsi ini memicu tarikDataBukuBesarDariServer().
Dengan kombinasi strategi ini, aplikasi pencatatan keuangan kamu tidak akan membuang-buang kuota eksekusi Google Apps Script (quota limits), sekaligus memberikan impresi aplikasi yang sangat cepat dan responsif layaknya aplikasi pada umumnya.
Function paksaRefreshBukuBesar

Meskipun sistem penyimpan data sementara (Client-Side Caching) pada fungsi muatBukuBesar() sangat ampuh membuat aplikasi berjalan super cepat, ada kalanya pengguna ingin memastikan data riwayat yang tampil di tabel benar-benar data terbaru, misalnya setelah menambah transaksi baru atau mengedit data langsung di spreadsheet.
Di sinilah fungsi paksaRefreshBukuBesar() memainkan perannya sebagai sakelar pembaru data manual.
Fungsi ini bertugas menghapus data riwayat lama yang tersimpan di memori HP, menyedot ulang data paling fresh dari server Google Apps Script, dan memberikan notifikasi langsung ke pengguna.
Mari kita bedah 3 langkah sederhana di dalam alur kerjanya:
Pengosongan Memori HP (dataBukuBesar = null;)
Langkah pertama dan paling penting adalah mengubah variabel dataBukuBesar menjadi null. Dengan mengosongkan cache ini, sistem “dilepaskan” dari data lama sehingga pengecekan kondisi pada muatBukuBesar() tidak akan lagi membaca data yang tersimpan sebelumnya.
Penarikan Ulang Data dari Backend (tarikDataBukuBesarDariServer())
Setelah memori dibersihkan, fungsi langsung memanggil tarikDataBukuBesarDariServer(). Aplikasi akan menghubungi Google Apps Script backend untuk mengambil seluruh baris transaksi terbaru dari sheet Buku_Besar.
Umpan Balik Visual Instan (showToast(...))
Untuk memberikan kepastian kepada pengguna bahwa tombol yang mereka tekan sudah bekerja, fungsi memicu toast notification berwarna hijau dengan pesan: “Riwayat berhasil diperbarui!”.
Dengan menyertakan fungsi ini pada tombol Refresh ↻ di komponen view_bukubesar.html, pengguna diberi kendali penuh atas akurasi data tanpa mengorbankan kecepatan navigasi harian aplikasi.
Function tarikDataBukuBesarDariServer

Jika fungsi muatBukuBesar() bertindak sebagai penentu keputusan dan paksaRefreshBukuBesar() sebagai penekan tombol reset, maka tarikDataBukuBesarDariServer() adalah fungsi pekerja keras yang melakukan komunikasi jarak jauh ke server Google.
Fungsi ini bertugas mengirimkan permintaan data riwayat ke backend, menangani animasi indikator loading, menyimpan data balikan ke memori lokal, hingga menangani kondisi jika terjadi koneksi terputus (error handling).
Mari kita bedah alur kerja dan proteksi ganda pada fungsi ini:
Umpan Balik Indikator Loading (tbody.innerHTML)
Sebelum permintaan dikirim ke server, tabel riwayat diisi terlebih dahulu dengan baris animasi kedip animate-pulse berlabel “Mengambil data dari server…”. Trik UI kecil ini sangat penting agar pengguna tahu bahwa sistem sedang bekerja dan aplikasi tidak dianggap freeze.
Penanganan Sukses (withSuccessHandler)
Ketika server Google selesai memproses dan mengembalikan array data transaksi:
dataBukuBesar = data: Data mentah tersebut langsung disimpan ke variabel memori lokal HP (cache).
terapkanFilterBB(): Fungsi saringan langsung dipicu untuk merender data tersebut ke dalam tabel HTML sesuai filter waktu yang sedang aktif.
Penanganan Gagal (withFailureHandler)
Inilah lapis pertahanan penting saat membuat aplikasi pencatatan keuangan. Jika koneksi internet terputus atau server Apps Script mengalami masalah (timeout):
- Aplikasi tidak akan crash atau blank.
- Peringatan berupa notifikasi toast merah akan muncul menyampaikan detail pesan kesalahan (
err.message).
- Tabel otomatis menampilkan pesan komunikatif berwarna merah: “Gagal memuat data.”.
Pemanggilan Backend (getBukuBesarData())
Di akhir baris, fungsi google.script.run memanggil fungsi getBukuBesarData() yang ada di file Code.gs / Model.gs untuk menarik baris transaksi dari spreadsheet.
Dengan menerapkan skema komunikasi asynchronous yang dibekali success dan failure handler seperti ini, aplikasi buatanmu terjamin memiliki ketahanan tinggi (robustness) serta memberikan kenyamanan maksimal bagi para pengguna.
Function terapkanFilterBB

Setelah data riwayat transaksi berhasil ditarik dari server dan disimpan di memori HP (dataBukuBesar), langkah selanjutnya adalah menyajikannya ke dalam tabel HTML. Namun, menampilkan ratusan transaksi sekaligus tentu akan membuat tabel sangat panjang dan sulit dibaca.
Di sinilah fungsi terapkanFilterBB() berperan.
Fungsi ini bertugas memilah (filter) array transaksi yang ada di memori berdasarkan opsi rentang waktu yang dipilih pengguna pada dropdown Buku Besar, lalu meneruskan data hasil saringan tersebut ke fungsi render tabel.
Mari kita bedah alur kerja dan kecerdasan kodenya:
Pengendali Wadah Tanggal Kustom (mode === 'custom')
Jika pengguna memilih opsi “Pilih Tanggal Kustom…”, fungsi akan menampilkan form input rentang tanggal dengan melepas kelas .hidden pada wadahTanggal. Eksekusi fungsi langsung ditahan (return) karena pencarian baru akan dijalankan saat tombol “Cari Data” ditekan.
Pengaman Data Kosong
Jika dataBukuBesar di memori bernilai kosong atau null, fungsi langsung mengirimkan array kosong [] ke renderTabelBukuBesar() agar tabel menampilkan pesan bahwa data belum ada.
Logika Filter Preset Hari (mode !== 'all')
- Saat opsi seperti 1 Hari, 7 Hari, atau 1 Bulan Terakhir dipilih, fungsi menghitung batas tanggal acuan dengan mundur beberapa hari dari hari ini menggunakan
.setDate().
- Metode
.filter()pada array JavaScript kemudian menyisir setiap baris transaksi. Hanya baris dengan tanggal (row[0]) yang lebih baru atau sama denganbatasWaktuyang akan dimasukkan ke dalamdataTersaring.
Penyusunan Tampilan Tabel (renderTabelBukuBesar(dataTersaring))
Setelah proses penyaringan selesai di memori (dalam hitungan milidetik), data hasil pangkasan tersebut langsung dikirim ke fungsi renderTabelBukuBesar() untuk dirakit menjadi baris-baris tabel HTML.
Dengan memisahkan logika penyaringan (terapkanFilterBB) dari logika pembuatan HTML (renderTabelBukuBesar), kode aplikasi kita menjadi lebih rapi, modular, dan mudah dikembangkan di masa mendatang.
Function terapkanFilterTanggal

Setelah kita membahas bagaimana tabel Buku Besar menarik data dari server dan menangani memori lokal, kita juga perlu menyediakan kontrol saringan (filter) waktu yang presisi untuk pengguna.
Ketika pengguna memilih opsi “Pilih Tanggal Kustom…” pada dropdown Buku Besar, form input rentang tanggal akan muncul di layar. Fungsi terapkanFilterTanggal() inilah yang bertugas membaca input rentang tanggal tersebut, melakukan validasi, lalu mengeksekusi penyaringan data tabel secara lokal.
Mari kita bedah alur kerja dan lapis proteksinya:
Penarikan Input Rentang Tanggal (bb_tgl_mulai & bb_tgl_akhir)
Skrip mengambil nilai string tanggal dari elemen <input type="date"> yang ada di dalam kontainer #wadah_filter_tanggal komponen Buku Besar.
Validasi Kelengkapan Data (if (!tglMulai || !tglAkhir))
Sama seperti pada saringan Dashboard, fungsi ini memastikan pengguna tidak menekan tombol cari dalam keadaan kolom tanggal yang masih kosong. Notifikasi toast merah akan langsung memperingatkan pengguna jika ada kolom yang terlewat.
Validasi Urutan Logika Waktu (new Date(tglMulai) > new Date(tglAkhir))
Sistem membandingkan objek nilai waktu untuk memastikan tanggal awal tidak lebih baru daripada tanggal akhir. Proteksi ini penting agar perulangan filter tidak menghasilkan data kosong akibat logika rentang yang terbalik.
Penerus Parameter ke Engine Penyaring (terapkanFilterBB)
Setelah dipastikan aman, fungsi ini memanggil terapkanFilterBB('custom', tglMulai, tglAkhir). Mode diset ke 'custom' dan parameter tanggal mulai serta akhir diteruskan untuk menyaring baris-baris transaksi secara instan di memori HP.
Dengan memisahkan fungsi pembaca input tanggal ini dari engine utama penyaring tabel (terapkanFilterBB), struktur logika di file js_main.html kamu menjadi sangat rapi dan mudah di-maintain saat ada penambahan fitur saringan baru di kemudian hari.
Function renderTabelBukuBesar
function renderTabelBukuBesar(mode, tglMulai, tglAkhir) {
const tbody = document.getElementById('body_bukubesar');
if(!dataBukuBesar || dataBukuBesar.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="text-center p-4 text-slate-400">Belum ada riwayat transaksi.</td></tr>';
return;
}
let dataTersaring = dataBukuBesar;
if (mode === 'custom') {
const start = new Date(tglMulai); start.setHours(0,0,0,0);
const end = new Date(tglAkhir); end.setHours(23,59,59,999);
dataTersaring = dataBukuBesar.filter(row => {
const tglRow = new Date(String(row[0]).replace(' ', 'T'));
return tglRow >= start && tglRow <= end;
});
} else if (mode !== 'all') {
const batasWaktu = new Date();
batasWaktu.setDate(batasWaktu.getDate() - parseInt(mode));
batasWaktu.setHours(0,0,0,0);
dataTersaring = dataBukuBesar.filter(row => {
const tglRow = new Date(String(row[0]).replace(' ', 'T'));
return tglRow >= batasWaktu;
});
}
if(dataTersaring.length === 0) {
tbody.innerHTML = `<tr><td colspan="4" class="text-center p-4 text-slate-400">Tidak ada transaksi pada rentang waktu tersebut.</td></tr>`;
return;
}
let htmlHtml = dataTersaring.map(row => {
const txtMasuk = row[4] > 0 ? `<span class="text-emerald-600 font-bold">+${formatRp(row[4])}</span>` : '-';
const txtKeluar = row[5] > 0 ? `<span class="text-red-500 font-bold">-${formatRp(row[5])}</span>` : '-';
const tglArr = String(row[0]).split(' ');
const strTgl = tglArr[0];
const strJam = tglArr[1] ? `<span class="block text-[10px] text-slate-400 mt-1">⏰ ${tglArr[1]}</span>` : '';
return `
<tr class="hover:bg-slate-50 transition border-b border-slate-50 last:border-0">
<td class="p-3 text-xs align-top whitespace-nowrap">
<span class="font-bold text-slate-700">${strTgl}</span>
${strJam}
</td>
<td class="p-3 align-top">
<span class="text-[10px] font-bold bg-indigo-100 text-indigo-700 px-2 py-0.5 rounded">${row[2]}</span>
<p class="text-xs mt-1 text-slate-500 truncate max-w-[120px] md:max-w-[200px]" title="${row[3]}">${row[3]}</p>
</td>
<td class="p-3 text-right text-xs align-top bg-emerald-50/30">${txtMasuk}</td>
<td class="p-3 text-right text-xs align-top bg-red-50/30">${txtKeluar}</td>
</tr>
`;
}).join('');
tbody.innerHTML = htmlHtml;
}Setelah fungsi penyaring menentukan opsi waktu yang diinginkan pengguna, fungsi renderTabelBukuBesar() inilah yang mengeksekusi penyaringan baris data di memori HP dan merakit tabel HTML-nya.
Fungsi ini menyaring data transaksi berdasarkan rentang tanggal yang dipilih, menghitung total uang masuk dan keluar pada periode tersebut, lalu merender baris-baris data ke dalam elemen <tbody> tabel Buku Besar secara instan.
Mari kita bedah alur kerja dan detail teknis dari fungsi ini:
Pemeriksaan Memori & Penanganan Data Kosong
Jika variabel dataBukuBesar di memori HP masih kosong, fungsi langsung menyuntikkan pesan informatif “Belum ada riwayat transaksi” ke dalam tabel dan menghentikan proses eksekusi.
Fleksibilitas Logika Filter (modeFilter)
Fungsi ini dapat dipanggil secara fleksibel, baik dengan melempar parameter langsung maupun dengan membaca nilai dari elemen <select id="filter_waktu_bb">. Penyaringan berbasis preset hari (1, 7, 30 hari, dsb.) maupun rentang tanggal kustom dijalankan secara instan di sisi klien.
Pemberian Gaya Tampilan yang Rapi (Clean UI Styling)
- Informasi Transaksi Bertingkat: Kolom keterangan menampilkan deskripsi utama di atas, sedangkan kategori dan akun dompet ditampilkan dengan ukuran teks lebih kecil berwarna abu-abu/indigo di bawahnya agar hemat ruang layar HP.
- Pewarnaan Nilai Finansial: Uang masuk diberi warna hijau (emerald-600) dan uang keluar diberi warna merah/merah muda (rose-500). Nilai
0diubah menjadi tanda strip (-) agar tabel terlihat bersih dan mudah dibaca secara visual.
whitespace-nowrap: Memastikan teks tanggal dan nominal angka tidak patah atau terpotong ke bawah saat dibuka di layar HP yang sempit.
Dengan menerapkan penyaringan di memori lokal (Client-Side Filtering), tabel riwayat Buku Besar di aplikasi pencatatan keuangan kamu dapat merespons perubahan filter waktu dalam hitungan fraksi milidetik tanpa perlu membebankan quoter fetch server Google Apps Script.
Function showToast

Dalam pembuatan antarmuka web modern, memberikan umpan balik (feedback) visual yang jelas saat pengguna melakukan aksi, seperti menyimpan transaksi, menghapus data, atau ketika terjadi kesalahan input adalah hal yang sangat krusial.
Memakai fungsi bawaan browser seperti alert() tentu terasa kuno dan mengganggu alur penggunaan (user experience). Sebagai gantinya, kita membuat komponen notifikasi melayang sendiri yang elegan bernama showToast().
Fungsi ini bertugas memunculkan pesan singkat melayang (toast notification) di sudut layar HP/laptop pengguna yang akan hilang secara otomatis setelah beberapa detik.
Mari kita bedah alur kerja dan kecerdasan visual dari fungsi ini:
Parameter bawaan fleksibel (tipe = 'sukses')
Fungsi ini memiliki parameter tipe dengan nilai default 'sukses'. Kamu bisa memanggilnya secara ringkas untuk notifikasi sukses, atau menambahkan argumen kedua untuk jenis notifikasi lain:
- showToast(“Data disimpan!”): Warna Hijau (emerald) dengan ikon ✓.
- showToast(“Kolom wajib diisi!”, “error”): Warna Merah (rose) dengan ikon ✕.
- showToast(“Memperbarui data…”, “info”): Warna Ungu (indigo) dengan ikon ℹ.
Pembuatan Elemen Dinamis (document.createElement)
Setiap kali dipanggil, fungsi ini membuat elemen <div> baru di dalam DOM peramban dan menempelkannya ke dalam wadah #toast_container. Jika ada beberapa notifikasi dipanggil berurutan, notifikasi akan menumpuk secara rapi dari atas ke bawah.
Efek Animasi Halus dengan Tailwind CSS
- Saat Muncul: Elemen diawali dengan kelas
.opacity-0dan.translate-y-2. Lewat jeda mikrosetTimeout(..., 10), kelas tersebut dilepas sehingga notifikasi terlihat meluncur naik dengan halus (fade-in slide-up).
- Saat Menghilang: Setelah 3 detik (
3000ms), kelas transparansi dan pergeseran ditempatkan kembali. Begitu animasi selesai dalam 300 milidetik, elementoastsecara bersih dihapus dari memori halaman (toast.remove()).
Sistem notifikasi showToast() ini melengkapi seluruh arsitektur aplikasi pencatatan keuangan berbasis Google Apps Script, menjadikan interaksi aplikasi terasa sangat hidup, intuitif, dan profesional.
Function muatMasterSetup

Di dalam tab Setup Master Data (tempat kamu mengelola daftar Dompet, Kategori Masuk/Keluar, Platform Hutang, Paylater, dan Piutang), kita juga menerapkan strategi pemanggilan data pintar (Client-Side Caching).
Agar tidak membebankan panggilan server setiap kali kamu berpindah sub-tab di menu Setup, kita menggunakan dua variabel penanda (state) serta fungsi pembuka utama bernama muatMasterSetup().
Mari kita bedah alur kerja dan kecerdasan kodenya:
Variabel Penampung & Penanda State
let masterSetupCache = null: Bertindak sebagai “brankas mini” di HP pengguna. Saat pertama kali aplikasi dibuka, nilainya masihnull. Begitu data master berhasil diambil dari server, seluruh objek master disimpan di sini.
let setupTabAktif = 'dompet': Menentukan kategori master mana yang pertama kali ditampilkan saat tab Setup dibuka (secara default membuka daftar Dompet/Rekening).
Pengecekan Cache Cepat (if (masterSetupCache))
Saat fungsi muatMasterSetup() dipanggil, sistem mengecek dulu variabel masterSetupCache. Jika datanya sudah ada di memori HP, fungsi langsung menghentikan proses pemanggilan server dan mendebik fungsi renderListSetup(). Daftar master data langsung tampil seketika dalam beberapa detik.
Indikator Loading & Penanganan Error (Loading State & Error Handling)
Jika memori masih kosong, kontainer #wadah_list_setup akan menampilkan efek kedip animate-pulse berlabel “Memuat data master…”. Jika pemanggilan server berhasil, data disimpan ke masterSetupCache lalu dirender. Jika koneksi terputus, notifikasi toast merah akan muncul memberi tahu pesan kesalahannya.
Pemanggilan Backend (getAllSetupMaster())
Fungsi ini memanggil getAllSetupMaster() di backend Apps Script untuk menarik seluruh kolom master (termasuk kolom Paylater) sekaligus dalam satu kali paket pengiriman.
Strategi pengelolaan data master berbasis memori lokal ini membuat manajemen Setup Data Master pada aplikasi kamu terasa sangat responsif, enteng, dan hemat kuota eksekusi Google Apps Script!
Function gantiTabSetup

Di dalam menu Setup Master Data, terdapat berbagai kategori akun dan parameter yang bisa dikelola pengguna: Dompet, Kategori Masuk, Kategori Keluar, Hutang, Paylater, hingga Piutang.
Agar tampilan tetap ringkas dan tidak memenuhi layar HP, pilihan kategori tersebut disajikan dalam bentuk sub-tab (Pill Buttons). Fungsi gantiTabSetup(kategori) inilah yang bertugas mengatur perpindahan antar-kategori tersebut secara instan.
Fungsi ini memperbarui variabel status (state), mengubah warna tombol sub-tab yang sedang aktif, lalu memicu pembaruan daftar data di layar tanpa memanggil ulang server Google Apps Script.
Mari kita bedah alur kerja dan kecerdasan visual kodenya:
Pembaruan Status Aktif (setupTabAktif = kategori)
Nilai parameter kategori yang dikirim saat tombol diklik (misal: 'paylater' atau 'keluar') disimpan ke variabel global setupTabAktif. Variabel ini menjadi acuan utama bagi fungsi penampil data untuk menentukan daftar mana yang harus dirender ke layar.
Perubahan Visual Tombol Kapsul (forEach)
Skrip menyisir seluruh tombol sub-tab yang terdaftar di antarmuka Setup:
- Tombol Terpilih: Mendapatkan kombinasi kelas Tailwind CSS
bg-indigo-600 text-white shadow-smsehingga tampil mencolok sebagai penanda sub-tab aktif.
- Tombol Lainnya: Dikembalikan ke warna abu-abu netral (
bg-slate-100 text-slate-500 hover:bg-slate-200) agar tampilan antarmuka tetap konsisten dan rapi.
Eksekusi Render Lokal (renderListSetup())
Setelah tampilan tombol diperbarui, fungsi ini langsung memanggil renderListSetup(). Karena seluruh data master sudah tersimpan di variabel masterSetupCache, penggantian daftar item di bawahnya berlangsung dalam hitungan fraksi milidetik (0 detik tanpa jeda loading).
Penggunaan fungsi gantiTabSetup() ini melengkapi konsep Single Page Application (SPA) pada aplikasi pencatatan keuangan berbasis Google Apps Script, memastikan seluruh navigasi di tingkat sub-menu terasa responsif dan nyaman digunakan.
Function renderListSetup

Setelah fungsi gantiTabSetup(kategori) menentukan sub-tab mana yang sedang diakses (seperti Dompet, Kategori Keluar, atau Paylater), fungsi renderListSetup() inilah yang bertugas merakit daftar itemnya ke layar HP pengguna.
Fungsi ini membaca data dari variabel memori masterSetupCache, mengambil array item yang sesuai dengan setupTabAktif, lalu menyusunnya menjadi deretan kartu komponen HTML lengkap dengan tombol aksi hapus/kelola.
Mari kita bedah alur kerja dan fleksibilitas kodenya:
Pemetaaan Data Dinamis (masterSetupCache[setupTabAktif])
Alih-alih membuat fungsi render terpisah untuk masing-masing jenis master (misal renderDompet(), renderPaylater()), fungsi ini secara cerdas menggunakan dynamic key lookup. Cukup dengan mengakses masterSetupCache[setupTabAktif], data array yang ditarik otomatis sesuai dengan sub-tab yang sedang dibuka pengguna.
Penanganan Tampilan Kosong (Empty State)
Jika kategori master tersebut belum memiliki isi (misalnya belum pernah menambah akun Paylater), fungsi ini menampilkan pesan kondisional yang rapi: “Belum ada item master untuk [kategori]”.
Pemasangan Action Handler Dinamis (hapusMasterData)
Di setiap baris item yang dirender, tombol Hapus langsung dibekali perintah onclick="hapusMasterData('${setupTabAktif}', ${index})". Ini memastikan saat tombol hapus diklik, sistem tahu pasti kategori master mana dan baris indeks ke berapa yang ingin dihapus pengguna.
Desain Compact untuk Antarmuka Mobile
Item disusun menggunakan pembungkus Tailwind space-y-2 dan kartu rounded-xl yang bersih, memberikan jarak tekan (touch target) yang nyaman bagi jemari pengguna di layar HP.
Fungsi renderListSetup() ini menjadi penutup alur penyajian data pada menu Setup Master Data, membuktikan bahwa pengelolaan state sederhana pada aplikasi pencatatan keuangan berbasis Google Apps Script sanggup menghasilkan performa antarmuka yang sangat lincah dan instan.
Oke, jika semua kode diatas sudah ditulis dengan tepat dan sesuai dengan penamaannya, kita lakukan Test Deployment terlebih dahulu, unutk melihat apakah web UInya berjalan dengan baik atau masih ada yang perlu di perbaiki, baik dari sisi tampilan atau backendnya.



Jika pada saat Test Deployment sudah berhasil alias tidak ada kendala, lakukan New Deployment, seperti gambar dibawah ini.

Pada kolom “Description” tuliskan deskripsi dari aplikasi ini, misalnya “Aplikasi Finansaku Pro SPA versi 1.0”.
Untuk kolom “Execute as” pilih Me atau Saya, maksudnya sebagai autority yang punya data.
Pada kolom “Who has access” atau “Siapa dapat mengakses” pilih “Anyone” atau “Only myself” jika kamu hanya ingin kamu sendiri yg boleh mengkases, tapi kalau untuk ramai pengguna, pakai “Anyone”.

Sampai disini apakah aplikasinya sudah berhasil?
Jika terdapat kendala dan kesulitan untuk membuat kode dan sheetnya dari awal, tenang saja, saya sudah menyiapkan link full access dan source code, spesial bagi kamu yang tidak ada waktu untuk menyelesaikannya secara manual.
Jadi kamu bisa copypaste secara langsung antar sheet dengan lebih mudah
-> LINK AKSES TEMPLATE & SOURCE CODE FINANSAKU PRO <-
Semoga tutorial ini bermanfaat, jangan lupa bookmark blog web ini, agar tidak ketinggalan tutorial-tuorial yang lain.
Sampai jumpa dilain waktu.
