Source Code Laporan Peminjaman Buku Perpustakaan Digital
Di era digital saat ini, manajemen perpustakaan sekolah menuntut efisiensi dan kecepatan. Sistem manual seringkali merepotkan, mulai dari pencatatan peminjaman hingga pelacakan buku yang hilang. Oleh karena itu, saya membagikan Source Code Aplikasi Perpustakaan Digital Berbasis Web yang saya kembangkan khusus untuk lingkungan sekolah (studi kasus SMP Negeri 3 Kerinci).
Aplikasi ini bersifat open-source, gratis, responsif (mobile-friendly), dan menggunakan teknologi Cloud dari Google.
Apa itu Perpustakaan Digital?
Perpustakaan digital bukan sekadar tempat menyimpan buku elektronik. Ini adalah sistem manajemen informasi yang memungkinkan akses, penyimpanan, dan temu kembali informasi secara instan. Dalam konteks sekolah, ini berarti sistem sirkulasi (peminjaman & pengembalian) yang terkomputerisasi, menggantikan buku besar manual menjadi database yang rapi dan mudah diolah.
Mengenal Google Apps Script
Aplikasi ini dibangun menggunakan Google Apps Script (GAS). GAS adalah platform scripting berbasis cloud yang memungkinkan kita untuk mengintegrasikan dan mengotomatisasi produk Google. Dalam aplikasi ini, GAS berfungsi sebagai "Otak" (Server) yang menghubungkan antarmuka web dengan Google Sheets sebagai databasenya. Keunggulannya: Gratis, Serverless (tidak perlu sewa hosting), dan Aman.
- ✅ Anti-Stuck: Perbaikan pada pengiriman data tanggal (ISO String).
- ✅ Responsif: Menggunakan Bootstrap 5.3, tampilan bagus di HP & Laptop.
- ✅ Database Otomatis: Membuat file Google Sheet sendiri jika belum ada.
- ✅ Siap Cetak: Fitur laporan yang bersih saat diprint.
Download Source Code
Silakan salin kode di bawah ini ke dalam project Google Apps Script Anda.
/**
* SISTEM PERPUSTAKAAN SMPN 3 KERINCI - FINAL V3 (ANTI STUCK)
*/
const DB_NAME = "DB_Perpustakaan_SMPN3";
const SHEET_NAME = "Data_Sirkulasi";
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('Sistem Perpustakaan SMPN 3 Kerinci')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
// === FUNGSI DATABASE ===
function getDatabase() {
const props = PropertiesService.getScriptProperties();
let fileId = props.getProperty('db_id');
let ss;
if (fileId) {
try { ss = SpreadsheetApp.openById(fileId); } catch (e) { fileId = null; }
}
if (!fileId) {
const files = DriveApp.getFilesByName(DB_NAME);
if (files.hasNext()) {
ss = SpreadsheetApp.open(files.next());
} else {
ss = SpreadsheetApp.create(DB_NAME);
}
props.setProperty('db_id', ss.getId());
}
let sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
const defaultSheet = ss.getSheetByName('Sheet1');
if (defaultSheet) ss.deleteSheet(defaultSheet);
// Header
const headers = [["ID", "Nama Siswa", "Kelas", "Judul Buku", "Tgl Pinjam", "Batas Kembali", "Durasi", "Status", "Timestamp"]];
sheet.getRange(1, 1, 1, headers[0].length).setValues(headers).setFontWeight("bold").setBackground("#0d6efd").setFontColor("white");
sheet.setFrozenRows(1);
}
return sheet;
}
// === API UTAMA (DIPERBAIKI) ===
function getDataSirkulasi() {
try {
const sheet = getDatabase();
const lastRow = sheet.getLastRow();
// Jika data kosong, kembalikan array kosong langsung
if (lastRow < 2) return [];
const data = sheet.getRange(2, 1, lastRow - 1, 9).getValues();
// PERBAIKAN FATAL: Memaksa semua Date menjadi String ISO
// Agar Google Script tidak macet saat mengirim ke HTML
return data.map(row => {
return {
id: String(row[0]),
nama: String(row[1]),
kelas: String(row[2]),
judul: String(row[3]),
// Cek apakah tanggal valid, jika ya ubah ke ISO String, jika tidak kosongkan
pinjam: (row[4] instanceof Date) ? row[4].toISOString() : String(row[4]),
batas: (row[5] instanceof Date) ? row[5].toISOString() : String(row[5]),
durasi: row[6],
status: String(row[7]),
// Timestamp sering bikin error jika dikirim mentah, kita ubah string
timestamp: String(row[8])
};
}).reverse();
} catch (e) {
// Log error agar terbaca di dashboard Apps Script
Logger.log("ERROR GET DATA: " + e.toString());
throw new Error("Gagal mengambil data: " + e.message);
}
}
function tambahPeminjaman(form) {
try {
const sheet = getDatabase();
const id = new Date().getTime().toString();
const tglPinjam = new Date();
const durasi = parseInt(form.durasi) || 7;
const tglKembali = new Date(tglPinjam);
tglKembali.setDate(tglPinjam.getDate() + durasi);
const rowData = [
"'" + id, // Tambah kutip agar ID tetap string di Excel
form.nama,
form.kelas,
form.judul,
tglPinjam,
tglKembali,
durasi,
'Dipinjam',
new Date()
];
sheet.appendRow(rowData);
return { success: true, message: "Peminjaman berhasil dicatat!" };
} catch (e) {
return { success: false, message: e.message };
}
}
function kembalikanBuku(idTarget) {
try {
const sheet = getDatabase();
const data = sheet.getDataRange().getValues();
// Loop cari ID (pastikan cleaning string ID)
for (let i = 1; i < data.length; i++) {
// Bersihkan ID dari tanda kutip jika ada
let dbId = String(data[i][0]).replace("'", "");
if (dbId === String(idTarget)) {
sheet.getRange(i + 1, 8).setValue('Kembali');
return { success: true, message: "Buku berhasil dikembalikan!" };
}
}
return { success: false, message: "Data ID tidak ditemukan." };
} catch (e) {
return { success: false, message: e.message };
}
}
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sistem Perpustakaan SMPN 3 Kerinci</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body { font-family: 'Inter', sans-serif; background-color: #f3f4f6; color: #333; }
.navbar { background: linear-gradient(135deg, #0d47a1, #1565c0); box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); }
.card-stat { border: none; border-radius: 12px; transition: transform 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.card-stat:hover { transform: translateY(-3px); }
.table-container { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.02); }
.status-badge { font-size: 0.75rem; padding: 0.4em 0.8em; border-radius: 20px; font-weight: 600; letter-spacing: 0.5px; }
.btn-floating { position: fixed; bottom: 30px; right: 30px; border-radius: 50%; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(13, 110, 253, 0.4); z-index: 100; }
#loader { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.9); z-index: 9999; display: flex; flex-direction: column; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
/* PRINT STYLING - HANYA MUNCUL SAAT DICETAK */
@media print {
body { background-color: white; color: black; }
.navbar, .btn-floating, .no-print, #dashboard, #sirkulasi, .modal { display: none !important; }
#laporan { display: block !important; }
.card { border: none !important; shadow: none !important; }
.table-container { box-shadow: none; padding: 0; }
#print-header { display: block !important; margin-bottom: 20px; text-align: center; border-bottom: 2px solid black; padding-bottom: 10px; }
table { width: 100% !important; border-collapse: collapse; }
th, td { border: 1px solid #000 !important; padding: 8px !important; color: #000 !important; }
.badge { border: 1px solid #000; color: #000 !important; background: none !important; }
}
#print-header { display: none; }
</style>
</head>
<body>
<div id="loader">
<div class="spinner-border text-primary mb-3" style="width: 3rem; height: 3rem;" role="status"></div>
<h6 class="text-primary fw-bold">Memuat Data Perpustakaan...</h6>
</div>
<div id="print-header">
<h2>LAPORAN PERPUSTAKAAN</h2>
<h3>SMP NEGERI 3 KERINCI</h3>
<p>Data Peminjaman dan Pengembalian Buku</p>
</div>
<nav class="navbar navbar-expand-lg navbar-dark mb-4 sticky-top no-print">
<div class="container">
<a class="navbar-brand d-flex align-items-center fw-bold" href="#">
<i class="bi bi-book-half fs-4 me-2"></i>
<div>
<div class="lh-1">Perpustakaan Digital</div>
<div style="font-size: 0.7rem; opacity: 0.8; font-weight: 400;">SMPN 3 KERINCI</div>
</div>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto small fw-bold text-uppercase">
<li class="nav-item"><a class="nav-link active" href="#" onclick="switchTab('dashboard')">Dashboard</a></li>
<li class="nav-item"><a class="nav-link" href="#" onclick="switchTab('sirkulasi')">Data Sirkulasi</a></li>
<li class="nav-item"><a class="nav-link" href="#" onclick="switchTab('laporan')">Cetak Laporan</a></li>
</ul>
</div>
</div>
</nav>
<div class="container pb-5">
<section id="dashboard" class="tab-content">
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="card card-stat h-100 p-3 bg-white border-start border-4 border-primary">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted text-uppercase mb-1 small fw-bold">Aktif</h6><h2 class="mb-0 fw-bold text-primary" id="stat-aktif">0</h2></div>
<div class="bg-primary bg-opacity-10 p-3 rounded-circle text-primary"><i class="bi bi-journal-bookmark-fill fs-4"></i></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card card-stat h-100 p-3 bg-white border-start border-4 border-danger">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted text-uppercase mb-1 small fw-bold">Terlambat</h6><h2 class="mb-0 fw-bold text-danger" id="stat-terlambat">0</h2></div>
<div class="bg-danger bg-opacity-10 p-3 rounded-circle text-danger"><i class="bi bi-exclamation-triangle-fill fs-4"></i></div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card card-stat h-100 p-3 bg-white border-start border-4 border-success">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted text-uppercase mb-1 small fw-bold">Selesai</h6><h2 class="mb-0 fw-bold text-success" id="stat-selesai">0</h2></div>
<div class="bg-success bg-opacity-10 p-3 rounded-circle text-success"><i class="bi bi-check-circle-fill fs-4"></i></div>
</div>
</div>
</div>
</div>
<div class="table-container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="fw-bold mb-0 text-secondary">Peminjaman Terbaru</h5>
<button class="btn btn-primary btn-sm rounded-pill px-3" onclick="openModal()"><i class="bi bi-plus-lg me-1"></i> Baru</button>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead class="table-light small text-uppercase"><tr><th>Siswa</th><th>Buku</th><th>Tgl Pinjam</th><th>Batas</th><th>Status</th><th class="text-end">Aksi</th></tr></thead>
<tbody id="table-body" class="small"></tbody>
</table>
</div>
</div>
</section>
<section id="sirkulasi" class="tab-content d-none">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h4 class="fw-bold mb-3">Data Sirkulasi</h4>
<input type="text" id="searchBox" class="form-control mb-3" placeholder="Cari nama siswa atau buku..." onkeyup="filterTable('fullTable')">
<div class="table-responsive">
<table class="table table-striped align-middle" id="fullTable">
<thead class="table-dark"><tr><th>Siswa</th><th>Kelas</th><th>Buku</th><th>Tgl Pinjam</th><th>Status</th></tr></thead>
<tbody id="full-table-body"></tbody>
</table>
</div>
</div>
</div>
</section>
<section id="laporan" class="tab-content d-none">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4 no-print">
<div>
<h4 class="fw-bold">Laporan Peminjaman</h4>
<p class="text-muted mb-0">Halaman ini siap dicetak.</p>
</div>
<button onclick="window.print()" class="btn btn-dark"><i class="bi bi-printer-fill me-2"></i>Cetak Laporan</button>
</div>
<div class="row g-2 mb-4 no-print">
<div class="col-md-4">
<select id="filterStatus" class="form-select" onchange="renderLaporan()">
<option value="all">Semua Status</option>
<option value="Dipinjam">Sedang Dipinjam</option>
<option value="Kembali">Sudah Kembali</option>
</select>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered border-dark" id="laporanTable">
<thead>
<tr class="table-secondary">
<th>No</th>
<th>Nama Siswa</th>
<th>Kelas</th>
<th>Judul Buku</th>
<th>Tgl Pinjam</th>
<th>Status</th>
</tr>
</thead>
<tbody id="laporan-body">
</tbody>
</table>
</div>
<div class="mt-5 d-none d-print-block text-end">
<p>Kerinci, <span id="tgl-cetak"></span></p>
<br><br><br>
<p class="fw-bold">Petugas Perpustakaan</p>
</div>
</div>
</div>
</section>
</div>
<button class="btn btn-primary btn-floating d-md-none shadow no-print" onclick="openModal()"><i class="bi bi-plus-lg fs-2"></i></button>
<div class="modal fade" id="modalPinjam" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title fw-bold">Form Peminjaman</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<form id="formPinjam">
<div class="mb-3"><label class="form-label small fw-bold">Nama Siswa</label><input type="text" class="form-control" name="nama" required></div>
<div class="mb-3"><label class="form-label small fw-bold">Kelas</label>
<select class="form-select" name="kelas"><option>VII A</option><option>VII B</option><option>VIII A</option><option>VIII B</option><option>IX A</option><option>IX B</option></select>
</div>
<div class="mb-3"><label class="form-label small fw-bold">Judul Buku</label><input type="text" class="form-control" name="judul" required></div>
<div class="mb-3"><label class="form-label small fw-bold">Durasi (Hari)</label><input type="number" class="form-control" name="durasi" value="7" min="1"></div>
<div class="d-grid"><button type="submit" class="btn btn-primary fw-bold">SIMPAN DATA</button></div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let globalData = [];
const myModal = new bootstrap.Modal(document.getElementById('modalPinjam'));
document.addEventListener('DOMContentLoaded', loadData);
function switchTab(tabId) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('d-none'));
document.getElementById(tabId).classList.remove('d-none');
// Jika masuk tab laporan, render ulang laporan
if(tabId === 'laporan') renderLaporan();
}
function openModal() { document.getElementById('formPinjam').reset(); myModal.show(); }
function loadData() {
document.getElementById('loader').style.display = 'flex';
google.script.run.withSuccessHandler(renderApp).withFailureHandler(handleError).getDataSirkulasi();
}
function renderApp(data) {
globalData = data;
renderTable(data);
renderStats(data);
renderFullTable(data);
document.getElementById('tgl-cetak').innerText = new Date().toLocaleDateString('id-ID', {day: 'numeric', month: 'long', year: 'numeric'});
document.getElementById('loader').style.display = 'none';
}
function handleError(err) {
document.getElementById('loader').style.display = 'none';
Swal.fire('Error', 'Gagal memuat: ' + err.message, 'error');
}
document.getElementById('formPinjam').addEventListener('submit', function(e) {
e.preventDefault();
const btn = this.querySelector('button[type="submit"]');
const originalText = btn.innerHTML;
btn.innerHTML = 'Menyimpan...'; btn.disabled = true;
const formData = {
nama: this.nama.value, kelas: this.kelas.value, judul: this.judul.value, durasi: this.durasi.value
};
google.script.run.withSuccessHandler((res) => {
btn.innerHTML = originalText; btn.disabled = false;
if (res.success) {
myModal.hide();
Swal.fire({ icon: 'success', title: 'Berhasil', text: res.message, timer: 1500, showConfirmButton: false });
loadData();
} else Swal.fire('Gagal', res.message, 'error');
}).tambahPeminjaman(formData);
});
function kembalikanBuku(id) {
Swal.fire({
title: 'Konfirmasi', text: "Terima pengembalian buku ini?", icon: 'question', showCancelButton: true, confirmButtonText: 'Ya', cancelButtonText: 'Batal'
}).then((res) => {
if (res.isConfirmed) {
document.getElementById('loader').style.display = 'flex';
google.script.run.withSuccessHandler((r) => {
if (r.success) { Swal.fire('Oke!', r.message, 'success'); loadData(); }
}).kembalikanBuku(id);
}
});
}
function renderTable(data) {
const tbody = document.getElementById('table-body'); tbody.innerHTML = '';
data.slice(0, 10).forEach(item => {
const isLate = new Date() > new Date(item.batas) && item.status === 'Dipinjam';
let badge = item.status === 'Kembali' ? '<span class="badge bg-success bg-opacity-10 text-success">KEMBALI</span>' : (isLate ? '<span class="badge bg-danger bg-opacity-10 text-danger">TELAT</span>' : '<span class="badge bg-primary bg-opacity-10 text-primary">DIPINJAM</span>');
let act = item.status === 'Dipinjam' ? `<button onclick="kembalikanBuku('${item.id}')" class="btn btn-link btn-sm fw-bold p-0">Kembalikan</button>` : `<i class="bi bi-check-all text-success"></i>`;
tbody.innerHTML += `<tr><td><div class="fw-bold text-dark">${item.nama}</div><div class="text-muted" style="font-size:0.8em">${item.kelas}</div></td><td>${item.judul}</td><td>${formatDate(item.pinjam)}</td><td class="${isLate?'text-danger fw-bold':''}">${formatDate(item.batas)}</td><td>${badge}</td><td class="text-end">${act}</td></tr>`;
});
if(data.length===0) tbody.innerHTML = '<tr><td colspan="6" class="text-center">Kosong</td></tr>';
}
function renderFullTable(data) {
const tbody = document.getElementById('full-table-body'); tbody.innerHTML = '';
data.forEach(item => {
tbody.innerHTML += `<tr><td>${item.nama}</td><td>${item.kelas}</td><td>${item.judul}</td><td>${formatDate(item.pinjam)}</td><td>${item.status}</td></tr>`;
});
}
function renderLaporan() {
const filter = document.getElementById('filterStatus').value;
const tbody = document.getElementById('laporan-body'); tbody.innerHTML = '';
let filteredData = globalData;
if(filter !== 'all') filteredData = globalData.filter(d => d.status === filter);
filteredData.forEach((item, index) => {
tbody.innerHTML += `<tr><td class="text-center">${index+1}</td><td>${item.nama}</td><td>${item.kelas}</td><td>${item.judul}</td><td>${formatDate(item.pinjam)}</td><td>${item.status}</td></tr>`;
});
}
function filterTable(tableId) {
const val = document.getElementById("searchBox").value.toUpperCase();
const tr = document.getElementById(tableId).getElementsByTagName("tr");
for (let i = 1; i < tr.length; i++) {
let td = tr[i].textContent || tr[i].innerText;
tr[i].style.display = td.toUpperCase().indexOf(val) > -1 ? "" : "none";
}
}
function formatDate(dStr) {
if(!dStr) return "-";
return new Date(dStr).toLocaleDateString('id-ID', {day: 'numeric', month: 'short'});
}
function renderStats(data) {
document.getElementById("stat-aktif").innerText = data.filter(d => d.status === 'Dipinjam').length;
document.getElementById("stat-selesai").innerText = data.filter(d => d.status === 'Kembali').length;
document.getElementById("stat-terlambat").innerText = data.filter(d => d.status === 'Dipinjam' && new Date() > new Date(d.batas)).length;
}
</script>
</body>
</html>
Selamat mencoba! Jika ada kendala saat penerapan, jangan ragu untuk meninggalkan komentar di bawah.
