Lewati ke konten
Skematik
Semua template
Web GAS (Apps Script)

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.

Pakai template

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 tokoKelola produk & harga, pantau omzet harian, atur siapa yang boleh jadi kasir.
  • KasirCatat pesanan cepat dari HP tanpa ribet, tanpa takut salah kurangi stok.

User Stories

  • must-have
    Sebagai kasir, saya ingin memilih beberapa produk dan jumlahnya lalu menyimpan satu pesanan, agar transaksi tercatat dan stok otomatis berkurang.
  • must-have
    Sebagai kasir, saya ingin melihat sisa stok saat memilih produk, agar tidak menjual barang yang habis.
  • must-have
    Sebagai owner, saya ingin menambah/mengubah produk, harga, dan stok, agar katalog kasir selalu benar.
  • must-have
    Sebagai owner, saya ingin melihat rekap total omzet dan jumlah pesanan hari ini, agar tahu performa toko tanpa membuka sheet mentah.
  • should-have
    Sebagai owner, saya ingin menentukan email mana yang berperan owner atau kasir, agar hanya orang berwenang bisa mengubah produk.
  • should-have
    Sebagai kasir, saya ingin membatalkan pesanan yang salah, agar stok dikembalikan dan rekap tetap akurat.

User Flow

  1. Kasir membuka URL Web App dan login otomatis dengan akun Google (identitas diambil server dari Session).
  2. Sistem mengecek email di tab Pengguna untuk menentukan peran (owner/kasir); non-terdaftar ditolak.
  3. Kasir memilih produk dari daftar (menampilkan harga & sisa stok), menambah ke keranjang, mengatur jumlah.
  4. Kasir menekan Simpan → server mengunci (LockService), memvalidasi stok, membuat nomor pesanan, menulis Pesanan + ItemPesanan, mengurangi stok Produk, lalu melepas kunci.
  5. Owner membuka menu Rekap → melihat total omzet dan jumlah transaksi hari ini.
  6. Owner membuka menu Produk → menambah/mengubah produk, harga, dan stok.
Memuat diagram…

Features

Autentikasi & Peran dari Akun Google

must-have

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

must-have

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)

must-have

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

should-have

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

should-have

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

Frontend: HTML Service — index.html sebagai shell, css.html & js.html dimuat lewat helper include(). UI vanilla HTML/CSS/JS, mobile-first. Semua panggilan server via google.script.run (withSuccessHandler + withFailureHandler).
Backend: Google Apps Script (runtime V8). Fungsi publik di Code.gs, akses data di Sheets.gs, cek identitas & peran di Auth.gs. LockService membungkus setiap penulisan; CacheService untuk daftar produk yang sering dibaca.
Database: Google Sheets — satu spreadsheet, satu tab per entitas (Pengguna, Produk, Pesanan, ItemPesanan). Baris 1 = header (skema), kolom diakses BERDASARKAN NAMA header, bukan indeks.
Hosting: Google Apps Script Web App. Deploy: New deployment → Web app. Perubahan baru live HANYA lewat Manage deployments → Edit → New version (bukan otomatis saat Save).

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

  1. Klien google.script.run.simpanPesanan(keranjang) → Code.gs validasi input & peran
  2. Code.gs → Sheets.gs: LockService.waitLock → cek stok → tulis Pesanan + ItemPesanan → kurangi Produk → releaseLock
  3. Sheets.gs → Code.gs kembalikan { success, message, data: {nomorPesanan} } → klien tampilkan hasil
Memuat diagram…

Database Schema

Ini struktur data aplikasimu. Kamu tidak perlu paham semuanya — AI yang akan membuatnya. Kolom created_at/updated_at ditambahkan otomatis tiap tabel.

penggunaDaftar email yang boleh mengakses + perannya.
idstringUtilities.getUuid()
emailstringemail Google, cocokkan dengan Session
namastring
peranenum:PeranPenggunaOWNER atau KASIR
aktifbooleanfalse = diblokir akses
produkKatalog produk toko.
idstringUtilities.getUuid()
skustringkode unik produk
namastring
harganumberharga jual satuan (Rupiah)
stoknumbersisa stok; dikurangi saat pesanan disimpan
aktifbooleanfalse = disembunyikan dari kasir
pesananHeader transaksi.
idstringUtilities.getUuid()
nomor_pesananstringnomor urut/tanggal, dibuat server
tanggaldatewaktu simpan, Asia/Jakarta (ISO string)
kasir_emailstringemail kasir dari Session
totalnumberjumlah seluruh subtotal item
statusenum:StatusPesananSELESAI/BATAL
catatanstringopsional
item_pesananBaris item per pesanan.
idstringUtilities.getUuid()
pesanan_idstringFK ke pesanan
produk_idstringFK ke produk
qtynumberjumlah dibeli
harga_satuannumberharga saat transaksi (snapshot)
subtotalnumberqty × harga_satuan

Relasi

  • item_pesanan.pesanan_id → pesanan.id (many-to-one)
  • item_pesanan.produk_id → produk.id (many-to-one)
Memuat diagram…

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

GET
getProdukDaftar produk aktif (nama, harga, stok) untuk kasir.paginatedauth
POST
simpanPesananBuat pesanan multi-item + kurangi stok transaksional (LockService).auth
POST
batalkanPesananSet status BATAL + kembalikan stok item (LockService, idempoten).auth
GET
getRekapHarianTotal omzet & jumlah pesanan SELESAI hari ini (Asia/Jakarta).auth: OWNER
POST
simpanProdukTambah/ubah produk (sku unik, harga, stok, aktif).auth: OWNER
GET
getSesiKembalikan email & peran pengguna aktif dari Session.auth

Peran (RBAC)

OWNER
Pemilik toko.· kelola produk, lihat rekap, kelola pengguna, catat & batalkan pesanan
KASIR
Petugas kasir.· lihat produk, catat pesanan, batalkan pesanan sendiri

Enum

PeranPenggunaOWNER · KASIR
StatusPesananSELESAI · BATAL

Environment Variables

SPREADSHEET_IDbackendwajibScript Property: ID spreadsheet data
OWNER_EMAILbackendwajibScript Property: email owner awal (di-seed ke tab pengguna)

Library Inti

SpreadsheetApp · backendLockService · backendCacheService · backendPropertiesService · backendHtmlService · backendUtilities · backendSession · backend

Tasks (siap kirim ke AI agent)

T1
Inisialisasi proyek Apps Script + manifest· S

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.
T2
setupSheets(): bangun struktur Spreadsheet· S· Butuh dulu: T1

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.
T3
Lapisan akses data + identitas· M· Butuh dulu: T2

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.
T4
Katalog produk untuk kasir (getProduk)· M· Butuh dulu: T3

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.
T5
Simpan pesanan + kurangi stok (transaksional)· L· Butuh dulu: T4

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.
T6
Manajemen produk (owner)· M· Butuh dulu: T3

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'.
T7
Rekap harian + pembatalan pesanan· M· Butuh dulu: T5

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.
T8
Deploy Web App + README· S· Butuh dulu: T5, T6, T7

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?