Cara Membuat Sistem Peminjaman Buku Perpustakaan dengan Google Apps Script: Stok, Denda Otomatis, dan Antrean Pesanan
Perpustakaan sekolah sering punya masalah yang sama dari tahun ke tahun: buku favorit "hilang" karena tidak tercatat siapa yang meminjam, denda dihitung manual dengan kalender, dan siswa harus datang ke perpustakaan hanya untuk tahu buku yang dicari sedang ada atau tidak.
Di tutorial ini kita membangun inti sistem sirkulasi perpustakaan dengan Google Apps Script dan Google Sheets: menghitung stok tersedia, memvalidasi peminjaman, menghitung denda otomatis, membuat antrean pesanan yang adil, dan login anggota memakai PIN yang aman. Contohnya diambil dari aplikasi Perpustakaan Sekolah yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Stok tersedia tidak disimpan, tetapi dihitung
Godaan pertama adalah membuat kolom "Stok" di sheet Buku lalu mengurangi atau menambahnya setiap pinjam dan kembali. Masalahnya, satu kali error di tengah proses membuat angka itu selamanya salah. Lebih aman menyimpan jumlah eksemplar saja, lalu menghitung yang tersedia dari peminjaman aktif:
function stok_(bukuList, pinjam, pesan) {
var dipinjam = {}, antre = {};
pinjam.forEach(function (p) { if (p.status === 'Dipinjam') dipinjam[p.kode] = (dipinjam[p.kode] || 0) + 1; });
pesan.forEach(function (p) { if (p.status === 'Menunggu') antre[p.kode] = (antre[p.kode] || 0) + 1; });
var out = {};
bukuList.forEach(function (b) {
out[b.kode] = { tersedia: Math.max(0, b.eks - (dipinjam[b.kode] || 0)), antre: antre[b.kode] || 0 };
});
return out;
}
Dengan cara ini, kalau petugas menambah eksemplar dari 2 menjadi 3, katalog langsung benar tanpa perlu menyesuaikan angka lain. Sumber kebenarannya hanya satu: tabel peminjaman.
2. Validasi peminjaman di satu tempat
Sebelum mencatat peminjaman, ada beberapa aturan yang harus dicek. Kita kumpulkan dalam satu fungsi yang mengembalikan alasan penolakan dalam bahasa manusia, supaya petugas tahu harus bilang apa ke siswa:
var aktif = milik.filter(function (p) { return p.status === 'Dipinjam'; });
var belum = milik.filter(function (p) { return p.status === 'Kembali' && p.denda > 0 && p.lunas !== 'YA'; });
var blok = !a.aktif ? 'Anggota nonaktif.'
: aktif.some(function (p) { return p.tempo < hari; }) ? 'Masih ada buku yang terlambat dikembalikan.'
: belum.length ? 'Masih ada denda ' + rp_(totalDenda(belum)) + ' yang belum dibayar.'
: aktif.length >= at.maks ? 'Sudah meminjam ' + at.maks + ' buku (batas maksimal).'
: '';
Fungsi yang sama dipakai dua kali: untuk menampilkan kartu anggota di meja sirkulasi (stempel hijau "Boleh pinjam" atau merah "Tidak bisa pinjam"), dan sekali lagi di dalam pinjamBuku sebelum menulis. Tampilan dan aturan server tidak akan pernah berbeda karena kodenya satu.
Perhatikan juga perbandingan tanggal p.tempo < hari. Karena tanggal disimpan sebagai teks yyyy-MM-dd, perbandingan string sudah benar secara kronologis. Jangan lupa setNumberFormat('@') pada kolom tanggal agar Google Sheets tidak mengubahnya menjadi objek Date.
3. Denda berjalan dengan batas maksimal
Denda dihitung dari selisih hari antara jatuh tempo dan hari ini (untuk buku yang belum kembali), atau tanggal kembali (untuk yang sudah):
function selisihHari_(a, b) {
return Math.round((new Date(b + 'T12:00:00+07:00') - new Date(a + 'T12:00:00+07:00')) / 86400000);
}
function dendaPinjam_(p, at) {
if (p.status === 'Kembali') return p.denda; // sudah dibekukan saat kembali
var telat = Math.max(0, selisihHari_(p.tempo, hariIni_()));
return at.dendaMaks ? Math.min(at.dendaMaks, telat * at.denda) : telat * at.denda;
}
Trik jam 12:00 dan zona +07:00 mencegah selisih meleset satu hari akibat pergantian zona waktu. Batas maksimal (misalnya Rp20.000 per buku) penting di sekolah: tanpa batas, buku yang terlupa selama libur semester bisa berdenda lebih mahal dari harga bukunya.
Saat buku dikembalikan, nilai denda dibekukan ke kolom Denda, lalu petugas memilih apakah denda dibayar saat itu atau dicatat sebagai tunggakan.
4. Antrean pesanan yang adil
Fitur yang paling disukai siswa adalah memesan buku populer yang sedang dipinjam semua. Tantangannya ada di saat buku kembali: buku itu harus ditahan untuk pemesan pertama, bukan diambil siapa pun yang kebetulan datang duluan ke meja.
var antri = pesan.filter(function (p) { return p.kode === k && p.status === 'Menunggu'; })
.sort(function (x, y) { return x.dibuat < y.dibuat ? -1 : 1; });
var milikSendiri = antri.filter(function (p) { return p.nis === info.nis; })[0];
var didepan = milikSendiri ? antri.indexOf(milikSendiri) : antri.length;
if (didepan >= st[k].tersedia) throw new Error('Buku sudah dipesan anggota lain (' + antri.length + ' antrean).');
if (milikSendiri) tandaiSelesai(milikSendiri);
Logikanya: hitung berapa orang yang ada di depan peminjam ini dalam antrean. Kalau jumlahnya lebih besar atau sama dengan eksemplar yang tersedia, berarti semua eksemplar sudah menjadi hak orang lain. Anggota yang tidak memesan dianggap berada di paling belakang. Saat pemesan meminjam, pesanannya otomatis ditandai selesai dan antrean maju. Dengan aturan yang sama, perpanjangan ditolak jika ada orang yang memesan buku tersebut.
5. PIN anggota yang tidak disimpan mentah
Siswa masuk ke kartu anggota dengan NIS dan PIN. PIN tidak pernah disimpan dalam bentuk asli di sheet, melainkan sebagai HMAC dengan kunci rahasia di Script Properties:
function hashPin_(nis, pin) { return 'h:' + tanda_('pin', nis + ':' + pin); }
function masukAnggota(nis, pin) {
var kc = 'gagal_ang_' + nis, n = Number(cache.get(kc) || 0);
if (n >= 5) throw new Error('Terlalu banyak percobaan. Coba lagi 15 menit lagi.');
var a = cariAnggota(nis);
if (!a || a.pin !== hashPin_(nis, pin)) { cache.put(kc, String(n + 1), 900); throw new Error('NIS atau PIN salah.'); }
var isi = 'ang.' + nis + '.' + (Date.now() + 4 * 3600000);
return isi + '.' + tanda_(isi, a.pin); // token ikut ditandatangani hash PIN
}
NIS ikut dimasukkan ke hash sehingga dua siswa dengan PIN sama tetap punya hash berbeda. Token sesi ditandatangani memakai hash PIN, jadi saat petugas me-reset PIN anggota yang lupa atau curiga akunnya dipakai teman, semua sesi lama otomatis tidak berlaku. Batas lima percobaan per 15 menit membuat PIN empat digit tetap sulit ditebak.
Ringkasan
- Simpan jumlah eksemplar, hitung stok tersedia dari peminjaman aktif.
- Satukan aturan boleh-pinjam dalam satu fungsi yang dipakai tampilan dan server.
- Hitung denda dengan selisih tanggal yang aman zona waktu, beri batas maksimal, bekukan saat kembali.
- Antrean pesanan: hitung orang di depan dan bandingkan dengan eksemplar tersedia.
- PIN disimpan sebagai HMAC, token ikut ditandatangani hash PIN, percobaan dibatasi.
Download source code lengkapnya
Versi lengkapnya sudah termasuk katalog dengan filter dan urutan, kartu anggota dengan slip tanggal kembali, meja sirkulasi multi-buku, perpanjangan, pengingat WhatsApp untuk buku terlambat, manajemen anggota, laporan 12 bulan, buku terpopuler, kelas paling rajin, 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