KasirSheet — Kasir & Stok Toko (Google Apps Script)
Aplikasi kasir web di atas Google Apps Script dengan Google Spreadsheet sebagai database. Kasir mencatat pesanan dari HP, stok berkurang otomatis secara transaksional, dan owner memantau rekap harian — tanpa server, hosting, atau biaya langganan.
KasirSheet — Kasir & Stok Toko
Aplikasi kasir web di atas Google Apps Script dengan Google Spreadsheet sebagai database. Kasir mencatat pesanan dari HP, stok berkurang otomatis secara transaksional, dan owner memantau rekap harian — tanpa server, hosting, atau biaya langganan.
Problem
Toko kecil butuh pencatatan kasir dan stok yang murah dan bisa diakses banyak orang dari HP, tapi aplikasi kasir berbayar terlalu mahal dan spreadsheet manual rawan salah hitung serta bentrok saat dua kasir mengedit bersamaan.
Goals
- Kasir bisa membuat satu pesanan multi-item dalam < 30 detik dari HP.
- Stok produk selalu akurat: berkurang otomatis saat pesanan disimpan, tanpa selisih walau banyak kasir bersamaan.
- Owner melihat rekap omzet & jumlah transaksi hari ini tanpa membuka spreadsheet mentah.
- Berjalan penuh di satu akun Google (Apps Script + Spreadsheet), tanpa server atau biaya bulanan.
Non-Goals
- Integrasi pembayaran digital / QRIS (fase awal cukup tunai/manual).
- Cetak struk ke printer termal (ditunda; fokus catat & rekap dulu).
- Laporan multi-cabang / konsolidasi antar toko.
- Aplikasi mobile native (cukup Web App responsif).
Target Users
- Owner toko — Kelola produk & harga, pantau omzet harian, atur siapa yang boleh jadi kasir.
- Kasir — Catat pesanan cepat dari HP tanpa ribet, tanpa takut salah kurangi stok.
User Stories
- must-haveSebagai kasir, saya ingin memilih beberapa produk dan jumlahnya lalu menyimpan satu pesanan, agar transaksi tercatat dan stok otomatis berkurang.
- must-haveSebagai kasir, saya ingin melihat sisa stok saat memilih produk, agar tidak menjual barang yang habis.
- must-haveSebagai owner, saya ingin menambah/mengubah produk, harga, dan stok, agar katalog kasir selalu benar.
- must-haveSebagai owner, saya ingin melihat rekap total omzet dan jumlah pesanan hari ini, agar tahu performa toko tanpa membuka sheet mentah.
- should-haveSebagai owner, saya ingin menentukan email mana yang berperan owner atau kasir, agar hanya orang berwenang bisa mengubah produk.
- should-haveSebagai kasir, saya ingin membatalkan pesanan yang salah, agar stok dikembalikan dan rekap tetap akurat.
User Flow
- Kasir membuka URL Web App dan login otomatis dengan akun Google (identitas diambil server dari Session).
- Sistem mengecek email di tab Pengguna untuk menentukan peran (owner/kasir); non-terdaftar ditolak.
- Kasir memilih produk dari daftar (menampilkan harga & sisa stok), menambah ke keranjang, mengatur jumlah.
- Kasir menekan Simpan → server mengunci (LockService), memvalidasi stok, membuat nomor pesanan, menulis Pesanan + ItemPesanan, mengurangi stok Produk, lalu melepas kunci.
- Owner membuka menu Rekap → melihat total omzet dan jumlah transaksi hari ini.
- Owner membuka menu Produk → menambah/mengubah produk, harga, dan stok.
Features
Autentikasi & Peran dari Akun Google
Identitas diambil server dari Session.getActiveUser().getEmail(); peran (owner/kasir) dibaca dari tab Pengguna. Tidak ada password. Deployment 'Execute as: user accessing, Who has access: siapa saja di organisasi/anyone with Google' ditetapkan di awal dan dicatat di README.
- Email pengguna diambil dari Session di server, TIDAK pernah dari input klien.
- Email yang tidak ada di tab Pengguna (atau aktif=false) ditolak dengan pesan jelas.
- Setiap fungsi yang mengubah data mengecek peran server-side; kasir tidak bisa memanggil fungsi khusus owner walau memaksa lewat konsol.
Pencatatan Pesanan Multi-item + Pengurangan Stok Transaksional
Kasir menyusun keranjang (beberapa produk × qty) lalu menyimpan. Server membuat nomor pesanan, menulis header Pesanan dan baris ItemPesanan, serta mengurangi stok Produk — seluruhnya di dalam satu LockService agar tidak bentrok.
- Menyimpan pesanan menulis 1 baris Pesanan + N baris ItemPesanan dan mengurangi stok tiap produk sesuai qty.
- Bila stok salah satu item tidak cukup, seluruh pesanan dibatalkan (tidak ada penulisan sebagian) dan pesan menyebut produk yang kurang.
- Dua penyimpanan bersamaan tidak membuat stok minus atau nomor pesanan kembar (dilindungi LockService).
- Total pesanan = jumlah subtotal item; subtotal = qty × harga_satuan saat transaksi.
Manajemen Produk (khusus Owner)
Owner menambah/mengubah produk: sku, nama, harga, stok, status aktif. Produk nonaktif tidak muncul untuk kasir.
- Owner bisa menambah produk baru; sku wajib unik (ditolak bila kembar).
- Owner bisa mengubah harga & stok; perubahan langsung terlihat kasir pada refresh berikutnya.
- Hanya peran owner yang bisa memanggil fungsi produk; kasir mendapat penolakan.
Rekap Penjualan Harian
Rekap total omzet dan jumlah pesanan berstatus SELESAI untuk tanggal berjalan (zona waktu Asia/Jakarta), dibaca cepat dari tab Pesanan.
- Rekap menampilkan total omzet & jumlah transaksi untuk hari ini (Asia/Jakarta).
- Pesanan berstatus BATAL tidak dihitung dalam omzet.
- Perhitungan membaca range sekali (batch), bukan sel per sel.
Pembatalan Pesanan
Kasir/owner membatalkan pesanan; status jadi BATAL dan stok tiap item dikembalikan, di dalam LockService.
- Membatalkan pesanan SELESAI mengubah status jadi BATAL dan menambah kembali stok tiap item.
- Pesanan yang sudah BATAL tidak bisa dibatalkan dua kali (idempoten).
Tech Stack
Tanpa npm/server terpisah — semua di akun Google. Set timeZone Asia/Jakarta & runtime V8 di appsscript.json. Konfigurasi/rahasia (SPREADSHEET_ID, OWNER_EMAIL) di Script Properties, bukan hardcode. Hormati batas eksekusi 6 menit & kuota harian: baca/tulis batch (getValues/setValues), tanpa loop per-sel.
Architecture
Aplikasi single-spreadsheet: HTML Service menyajikan UI, google.script.run memanggil fungsi Apps Script yang membaca/menulis tab-tab Spreadsheet. Tidak ada database eksternal; konkurensi dijaga LockService, baca panas dipercepat CacheService.
index.html + js.html (klien)
Render UI kasir/rekap/produk; panggil server via google.script.run; tangani loading/sukses/gagal.
Code.gs (router & fungsi publik)
doGet() sajikan HTML; fungsi publik (getProduk, simpanPesanan, dst) sebagai boundary — validasi input & envelope { success, message, data }.
Sheets.gs (akses data)
Baca/tulis batch, resolve kolom by header, bungkus tulis dalam LockService, buat id via Utilities.getUuid().
Auth.gs (identitas & peran)
Ambil email dari Session, cek peran di tab Pengguna, sediakan requireOwner()/requireKasir().
Alur data
- Klien google.script.run.simpanPesanan(keranjang) → Code.gs validasi input & peran
- Code.gs → Sheets.gs: LockService.waitLock → cek stok → tulis Pesanan + ItemPesanan → kurangi Produk → releaseLock
- Sheets.gs → Code.gs kembalikan { success, message, data: {nomorPesanan} } → klien tampilkan hasil
Database Schema
Ini struktur data aplikasimu. Kamu tidak perlu paham semuanya — AI yang akan membuatnya. Kolom created_at/updated_at ditambahkan otomatis tiap tabel.
Relasi
- item_pesanan.pesanan_id → pesanan.id (many-to-one)
- item_pesanan.produk_id → produk.id (many-to-one)
Struktur Folder
Struktur ini dikunci agar AI agent membangun dengan susunan file yang sama.
- appsscript.json — manifest: timeZone Asia/Jakarta, runtime V8, webapp { access, executeAs }
- Code.gs — doGet() + semua fungsi publik yang dipanggil google.script.run
- Sheets.gs — akses data: baca/tulis batch, resolve kolom by header, LockService, getUuid
- Auth.gs — Session.getActiveUser + cek peran dari tab pengguna (requireOwner/requireKasir)
- index.html — shell UI, memuat css.html & js.html via include()
- css.html — gaya UI
- js.html — logika klien: google.script.run + render keranjang/rekap/produk
API Endpoints
Peran (RBAC)
Enum
Environment Variables
Library Inti
Tasks (siap kirim ke AI agent)
Siapkan proyek Apps Script, manifest, dan Script Properties.
Lihat isi perintah
Buat proyek Google Apps Script baru (runtime V8). Tulis appsscript.json dengan timeZone 'Asia/Jakarta', runtime V8, dan blok webapp { access: 'ANYONE'/'DOMAIN' (catat pilihan di README), executeAs: 'USER_ACCESSING' }. Buat file Code.gs, Sheets.gs, Auth.gs, index.html, css.html, js.html (boleh kosong dulu). Tambahkan fungsi include(filename) yang memakai HtmlService.createHtmlOutputFromFile(filename).getContent() untuk memuat css.html & js.html di dalam index.html. Dokumentasikan di README: cara set Script Properties SPREADSHEET_ID & OWNER_EMAIL, dan bahwa update live dilakukan lewat Manage deployments → New version. Definition of done: doGet() mengembalikan HtmlService dari index.html dan Web App bisa dibuka.Fungsi sekali-jalan yang membuat tab pengguna, produk, pesanan, item_pesanan beserta header & seed owner.
Lihat isi perintah
Di Sheets.gs, tulis fungsi setupSheets() yang idempoten: untuk tiap tab (pengguna, produk, pesanan, item_pesanan) buat sheet bila belum ada, tulis baris header sesuai skema PRD (bold + freeze baris 1), dan pasang data validation dropdown untuk kolom enum (pengguna.peran = OWNER/KASIR, pesanan.status = SELESAI/BATAL). Seed satu baris pengguna dari Script Property OWNER_EMAIL dengan peran OWNER, aktif true. Jangan hapus data yang sudah ada. (Catatan: Skematik menyediakan export 'setup-sheets.gs' yang men-generate kerangka ini dari skema — pakai sebagai titik awal lalu tambah seed owner.) Definition of done: menjalankan setupSheets() pada spreadsheet kosong menghasilkan 4 tab dengan header benar + 1 owner ter-seed.
Helper baca/tulis batch berbasis header, LockService, dan cek peran dari Session.
Lihat isi perintah
Di Sheets.gs buat helper generik: readRows(tab) yang mengembalikan array objek (map header→nilai) dengan SATU getValues(); appendRow(tab, obj) & updateRowById(tab, id, patch) yang menulis via setValues dan SELALU dibungkus LockService.getScriptLock().waitLock(30000) dengan release di finally; resolve kolom BERDASARKAN NAMA header (bukan indeks). Di Auth.gs buat getSesiPengguna() yang mengambil email via Session.getActiveUser().getEmail(), mencari di tab pengguna (aktif=true), dan mengembalikan {email, peran} atau melempar error 'akses ditolak'; plus requireOwner()/requireKasir(). Definition of done: unit manual — appendRow lalu readRows konsisten; email tak terdaftar ditolak.Fungsi publik daftar produk aktif + UI pemilihan produk.
Lihat isi perintah
Di Code.gs buat fungsi publik getProduk() yang mengembalikan { success, message, data } berisi daftar produk aktif (id, nama, harga, stok); cache hasil dengan CacheService (TTL 60 detik) dan validasi sesi via getSesiPengguna() di awal. Di js.html/index.html, render daftar produk sebagai kartu/daftar dengan harga & sisa stok, tombol tambah ke keranjang, dan pengatur qty. Panggil server dengan google.script.run.withSuccessHandler(...).withFailureHandler(...).getProduk(); tampilkan loading & tangani error. Definition of done: membuka Web App menampilkan produk aktif dengan stok terbaru.Fungsi simpanPesanan yang menulis pesanan+item dan mengurangi stok dalam satu lock.
Lihat isi perintah
Di Code.gs buat simpanPesanan(keranjang) — keranjang = array {produkId, qty}. Validasi: sesi & peran (kasir/owner), qty>0, produk ada & aktif. Bungkus SELURUH operasi dalam LockService.getScriptLock().waitLock(30000) (release di finally): baca stok tiap produk (batch), bila ada yang kurang → return { success:false, message } menyebut produk kurang TANPA menulis apa pun; bila cukup → buat nomor_pesanan unik, tulis 1 baris pesanan (status SELESAI, total = Σ subtotal, tanggal ISO Asia/Jakarta) + N baris item_pesanan (harga_satuan = harga saat ini, subtotal = qty×harga_satuan), lalu kurangi stok tiap produk. Invalidasi cache produk. Kembalikan { success, data:{ nomorPesanan } }. Di klien, kirim keranjang, tampilkan nomor pesanan, kosongkan keranjang, refresh stok. Definition of done: pesanan tersimpan, stok berkurang tepat, dan uji dua pemanggilan cepat tidak membuat stok minus/nomor kembar.Fungsi simpanProduk + UI kelola produk, digate peran owner.
Lihat isi perintah
Di Code.gs buat simpanProduk(produk) yang memanggil requireOwner() di awal (tolak kasir), memvalidasi sku unik (kecuali saat update produk yang sama), lalu appendRow/updateRowById ke tab produk via helper ber-Lock. Di UI, tambahkan menu 'Produk' yang hanya tampil untuk owner (peran dari getSesi), form tambah/ubah (sku, nama, harga, stok, aktif) dengan label di atas input & error di bawah field. Definition of done: owner bisa menambah/mengubah produk; kasir yang memaksa memanggil simpanProduk mendapat { success:false } 'akses ditolak'.getRekapHarian (owner) dan batalkanPesanan (kembalikan stok).
Lihat isi perintah
Di Code.gs buat getRekapHarian() (requireOwner): baca tab pesanan SEKALI (batch), filter tanggal = hari ini Asia/Jakarta & status SELESAI, kembalikan { totalOmzet, jumlahTransaksi }. Buat batalkanPesanan(pesananId) dalam LockService: bila status sudah BATAL → return sukses idempoten tanpa perubahan; jika SELESAI → set status BATAL dan tambahkan kembali stok tiap item_pesanan terkait. Di UI owner, tampilkan kartu rekap (omzet & jumlah transaksi hari ini) dan tombol batalkan pada daftar pesanan. Definition of done: rekap benar (pesanan BATAL tak dihitung); membatalkan mengembalikan stok dan tidak bisa dobel.Terbitkan sebagai Web App dan dokumentasikan cara update.
Lihat isi perintah
Terbitkan: Deploy → New deployment → Web app, set 'Execute as' & 'Who has access' sesuai keputusan T1, salin URL. Tulis README lengkap: (1) buat spreadsheet & isi Script Property SPREADSHEET_ID + OWNER_EMAIL, (2) jalankan setupSheets() sekali, (3) deploy Web App, (4) PENTING: setiap perubahan kode baru live HANYA setelah Manage deployments → Edit → New version (bukan otomatis saat Save). Sertakan catatan batas 6 menit & kuota, dan bahwa semua tulis pakai LockService. Definition of done: pengguna baru bisa mengikuti README dari nol sampai Web App jalan.
Risks
- Kelalaian LockService pada salah satu jalur tulis → stok bisa selisih saat kasir bersamaan. Semua tulis WAJIB lewat helper ber-Lock.
- Lupa 'New version' saat deploy → perubahan tidak live walau kode sudah disimpan (kebingungan umum GAS).
- Volume tinggi (ribuan pesanan) memperlambat baca batch → pertimbangkan arsip bulanan atau CacheService untuk rekap.
- Salah pilih mode 'Execute as/Who has access' → identitas/izin tidak sesuai; tetapkan & uji di awal.
Open Questions
- Nomor pesanan: format urut global atau per-tanggal (mis. 20260713-001)?
- Kasir boleh membatalkan pesanan siapa saja atau hanya miliknya sendiri?
- Perlukah arsip/rekap bulanan sejak awal, atau cukup harian dulu?