Cara Membuat Website Pre-Order Katering dengan Google Apps Script: Batas H-1, Kuota Porsi, dan Rekap Dapur
Usaha katering rumahan hampir selalu dimulai dari WhatsApp: pelanggan mengetik pesanan, pemilik menyalin ke buku, lalu malam harinya menghitung manual berapa porsi yang harus dimasak besok. Begitu pelanggan bertambah, muncul masalah klasik: pesanan masuk setelah belanja ke pasar, menu favorit kelebihan pesanan, dan alamat antar terselip di antara ratusan chat.
Di tutorial ini kita membangun inti website pre-order katering dengan Google Apps Script dan Google Sheets: batas pesan H-1, kuota porsi yang tidak bisa terjual dua kali, keranjang untuk beberapa tanggal sekaligus, ongkir per hari antar, dan rekap dapur. Contohnya diambil dari aplikasi Katering Pre-Order yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Batas pesan H-1 dihitung di server
Aturannya sederhana: pesanan untuk besok ditutup jam 20.00 hari ini. Setelah jam itu, tanggal terdekat yang bisa dipesan adalah lusa. Logika ini harus ada di server, karena jam di HP pelanggan bisa saja salah.
function jamKini_() { return Utilities.formatDate(new Date(), 'Asia/Jakarta', 'HH:mm'); }
function tanggalBuka_(cfg) {
return jamKini_() < cfg.jam_tutup ? tambahHari_(hariIni_(), 1) : tambahHari_(hariIni_(), 2);
}
function bisaPesan_(tgl, cfg) {
return tgl >= tanggalBuka_(cfg) && tgl <= tambahHari_(hariIni_(), Number(cfg.maks_hari) || 7);
}
Jam disimpan sebagai teks HH:mm, jadi perbandingan string '19:45' < '20:00' sudah benar. Untuk hitung mundur di landing page, server mengirim dua angka: batas waktu dalam milidetik dan waktu server saat itu. Browser menghitung selisih jam lokal dengan jam server sekali, lalu memakainya untuk setiap detik:
if (!selisih) selisih = L.sekarangMs - Date.now();
var sisa = Math.max(0, L.batasMs - (Date.now() + selisih));
Dengan cara ini hitung mundur tetap akurat walaupun jam HP pelanggan terlambat lima menit.
2. Menu per tanggal dengan kuota
Setiap baris di sheet Menu adalah satu menu pada satu tanggal: 2026-10-21 | M1 | Nasi Garang Asem | 30000 | kuota 30. Kuota tersisa tidak disimpan, melainkan dihitung dari pesanan yang tidak dibatalkan:
function terpakai_(orders) {
var o = {};
orders.forEach(function (x) {
if (x.status === 'Dibatalkan') return;
x.item.forEach(function (i) { var k = i.tanggal + '|' + i.kode; o[k] = (o[k] || 0) + i.qty; });
});
return o;
}
// sisa = menu.kuota - (terpakai[tanggal + '|' + kode] || 0)
Karena tidak ada angka "sisa" yang disimpan, membatalkan pesanan otomatis mengembalikan kuota, dan menaikkan kuota di sheet langsung terlihat di landing page. Satu aturan tambahan di panel admin: kuota tidak boleh diturunkan di bawah jumlah porsi yang sudah dipesan.
3. Checkout: browser hanya mengirim kode dan jumlah
Keranjang di browser menyimpan item sebagai tanggal|kode -> qty. Saat checkout, yang dikirim hanya { tanggal, kode, qty }. Nama menu, harga, dan subtotal dibangun ulang dari sheet:
var gabung = {};
items.forEach(function (it) {
var key = it.tanggal + '|' + String(it.kode).toUpperCase(), m = menu[key], q = Math.floor(Number(it.qty));
if (!m) throw new Error('Menu tidak tersedia di tanggal ' + it.tanggal + '.');
if (!bisaPesan_(it.tanggal, cfg)) throw new Error('Pesanan untuk ' + it.tanggal + ' sudah ditutup.');
if (!(q >= 1 && q <= 50)) throw new Error('Jumlah harus 1 - 50 porsi.');
gabung[key] = (gabung[key] || 0) + q; // item yang sama digabung dulu
});
Object.keys(gabung).forEach(function (key) {
var m = menu[key], sisa = m.kuota - (pakai[key] || 0);
if (gabung[key] > sisa) throw new Error(m. + + sisa + );
out.({ : m., : m., : m., : m., : gabung[key], : m. * gabung[key] });
});
Menggabungkan item sebelum mengecek kuota penting. Tanpa langkah itu, seseorang bisa mengirim dua baris "2 porsi" untuk menu yang tinggal 3 porsi, dan masing-masing baris lolos pengecekan. Semua ini berjalan di dalam LockService.getScriptLock() supaya dua pelanggan yang menekan tombol bersamaan tidak sama-sama mendapat porsi terakhir.
4. Ongkir per hari antar, bukan per item
Pelanggan yang memesan untuk Senin sampai Jumat akan dikunjungi kurir lima kali, berapa pun porsinya. Jadi ongkir dikalikan jumlah tanggal unik:
var hariAntar = out.map(function (i) { return i.tanggal; }).filter(function (t, i, a) { return a.indexOf(t) === i; });
var ongkir = area.ongkir * hariAntar.length;
Aturan yang sama dipakai untuk minimal porsi: jika diatur minimal 2 porsi, yang dicek adalah jumlah porsi per tanggal antar, bukan per menu. Pelanggan bebas mengombinasikan satu nasi kotak dan satu minuman.
5. Rekap dapur: dari pesanan ke panci
Pertanyaan pertama pemilik katering setiap pagi adalah "masak berapa?". Rekap dapur menjawabnya dengan menjumlahkan item per kode menu untuk tanggal yang dipilih, lalu menyusun daftar antar yang diurutkan per area:
var orders = bacaOrder_().filter(function (o) {
return o.status !== 'Dibatalkan' && o.item.some(function (i) { return i.tanggal === tanggal; });
});
var porsi = {};
orders.forEach(function (o) { o.item.forEach(function (i) { if (i.tanggal === tanggal) porsi[i.kode] = (porsi[i.kode] || 0) + i.qty; }); });
var antar = orders.map(function (o) { /* nama, alamat, item hari itu, status bayar */ })
.sort(function (a, b) { return a.area === b.area ? (a.nama < b.nama ? -1 : 1) : (a.area < b.area ? -1 : 1); });
Satu pesanan multi-tanggal hanya menampilkan item untuk hari itu, sehingga label kotak dan rute kurir selalu benar. Label dicetak langsung dari browser dengan @media print yang menyembunyikan seluruh halaman kecuali kotak-kotak label dua kolom.
Ringkasan
- Batas H-1 dan tanggal buka dihitung di server; hitung mundur memakai selisih jam server.
- Simpan kuota, hitung sisa dari pesanan yang tidak dibatalkan.
- Checkout hanya mengirim tanggal, kode, dan jumlah; gabungkan item yang sama sebelum mengecek kuota, di dalam LockService.
- Ongkir dan minimal porsi dihitung per tanggal antar.
- Rekap dapur menjumlahkan porsi per menu dan mengelompokkan rute per area.
Download source code lengkapnya
Versi lengkapnya sudah termasuk landing page papan kapur dengan menu besok dan hitung mundur, menu seminggu dengan sisa porsi, keranjang multi-tanggal, cek pesanan, panel dapur dengan label siap cetak, pengaturan kuota cepat, salin menu, laporan omzet, dan ekspor CSV. Semua berjalan di Google Sheets tanpa hosting.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Sistem Tiket Travel dengan Pilih Kursi di Google Apps Script: Layout Armada, Anti Kursi Dobel, dan Kursi Kedaluwarsa
20 Oktober 2026
Cara Membuat Website Donasi Transparan dengan Google Apps Script: Kode Unik Transfer dan Laporan Penyaluran
20 Oktober 2026