Aplikasi Perpustakaan Sekolah Gratis Berbasis Google Apps Script
Aplikasi Perpustakaan Sekolah Gratis Berbasis Google Apps Script
Mengelola perpustakaan sekolah tidak harus selalu menggunakan aplikasi berbayar. Dengan memanfaatkan Google Sheets dan Google Apps Script, kita dapat membuat aplikasi perpustakaan sederhana yang dapat diakses melalui browser.
Aplikasi ini dibuat untuk membantu pengelolaan perpustakaan SMK Dwija Praja dan memiliki beberapa fitur utama, yaitu:
Data buku
Data anggota
Peminjaman buku
Pengembalian buku
Pencarian buku
Stok buku
Statistik peminjaman
Log aktivitas
Tampilan responsive untuk komputer dan smartphone
Backend aplikasi menggunakan Google Apps Script dan terhubung dengan Google Sheets sebagai database.
Fitur Aplikasi Perpustakaan
1. Dashboard
Dashboard menampilkan informasi penting secara ringkas, seperti:
Total buku
Total anggota
Buku yang sedang dipinjam
Total pengembalian
Statistik peminjaman per bulan
Data statistik peminjaman diolah dari database dan ditampilkan dalam bentuk grafik.
2. Manajemen Data Buku
Petugas dapat menambahkan, mengedit, menghapus, dan mencari data buku.
Informasi buku terdiri dari:
| Kolom | Keterangan |
|---|---|
| ID Buku | ID otomatis |
| Judul | Judul buku |
| Pengarang | Nama pengarang |
| Penerbit | Nama penerbit |
| Tahun | Tahun terbit |
| Kategori | Kategori buku |
| Rak | Lokasi rak |
| Stok | Jumlah buku |
| Status | Ketersediaan |
ID buku dibuat otomatis menggunakan prefix BK, misalnya:
BK0001BK0002BK0003
Fungsi getNextId() digunakan untuk mencari nomor terbesar kemudian menghasilkan ID berikutnya secara otomatis.
3. Manajemen Data Anggota
Data anggota dapat ditambahkan dan diedit melalui aplikasi.
Informasi yang tersedia meliputi:
ID anggota
NISN
Nama
Kelas
Jenis kelamin
Nomor WhatsApp
Email
Status
Pada form aplikasi, nama dan NISN wajib diisi sebelum data disimpan.
4. Peminjaman Buku
Proses peminjaman dilakukan dengan memilih anggota dan buku.
Sistem terlebih dahulu memeriksa stok buku. Jika stok habis, peminjaman tidak dapat dilakukan.
Setelah transaksi berhasil:
ID peminjaman dibuat otomatis.
Tanggal peminjaman dicatat.
Tanggal pengembalian ditentukan 7 hari kemudian.
Status menjadi
DIPINJAM.Stok buku berkurang satu.
Data dicatat ke database.
Log aktivitas dibuat.
5. Email Konfirmasi Peminjaman
Aplikasi juga dapat mengirimkan email kepada anggota setelah peminjaman berhasil.
Email berisi:
Nama peminjam
Judul buku
ID peminjaman
Tanggal peminjaman
Tanggal pengembalian
Pengingat untuk mengembalikan buku
Fitur ini menggunakan MailApp.sendEmail() dari Google Apps Script.
6. Pengembalian Buku
Ketika buku dikembalikan, sistem akan:
Mencari transaksi peminjaman.
Menambahkan data ke sheet
KEMBALI.Menghapus transaksi dari
PINJAM.Menambahkan kembali stok buku.
Mencatat aktivitas pengembalian.
7. Log Aktivitas
Setiap aktivitas penting dicatat ke sheet LOG.
Contohnya:
Tambah buku
Edit buku
Hapus buku
Tambah anggota
Edit anggota
Hapus anggota
Peminjaman
Pengembalian
Data log juga menyimpan waktu, email pengguna, detail aktivitas, dan status.
Struktur Google Spreadsheet
Sebelum menjalankan aplikasi, buat satu Google Spreadsheet dengan sheet berikut:
BUKUANGGOTAPINJAMKEMBALILOG
Sheet BUKU
Gunakan struktur:
ID Buku | Judul | Pengarang | Penerbit | Tahun | Kategori | Rak | Stok | KeteranganSheet ANGGOTA
ID Anggota | NISN | Nama | Kelas | Jenis Kelamin | No. WA | Email | StatusSheet PINJAM
ID Pinjam | ID Anggota | Nama | ID Buku | Judul | Tgl Pinjam | Tgl Kembali | Keterangan | EmailSheet KEMBALI
ID Pinjam | ID Anggota | Nama | ID Buku | Judul | Tgl Pinjam | Tgl Kembali | Keterangan | EmailSheet LOG
Timestamp | Aksi | Email | Detail | StatusDua Script yang Dibutuhkan
Aplikasi ini menggunakan dua bagian utama.
Script 1 — Code2.gs
Script pertama merupakan backend aplikasi.
Fungsinya menangani komunikasi dengan Google Sheets, pengelolaan buku, anggota, peminjaman, pengembalian, dashboard, email, dan log aktivitas.
Code2.gs Lengkap
Catatan: Gunakan script asli berikut secara utuh. Jangan mengubah nama fungsi karena
Index.htmlmemanggil fungsi-fungsi tersebut.
// ============================================// APLIKASI PERPUSTAKAAN - WEB APP// SMK DWIJA PRAJA// ============================================const SPREADSHEET_ID = 'GANTI SPREADSHEET ID DISINI';// ============================================// WEB APP ENTRY POINTS// ============================================function doGet(e) {return HtmlService.createHtmlOutputFromFile('Index').setTitle('Aplikasi Perpustakaan - SMK Dwija Praja').setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL).addMetaTag('viewport', 'width=device-width, initial-scale=1');}function doPost(e) {try {const params = JSON.parse(e.postData.contents);const action = params.action;const data = params.data || {};switch(action) {case 'getDashboard': return jsonResponse(getDashboardData());case 'getBuku': return jsonResponse(getAllBuku());case 'getAnggota': return jsonResponse(getAllAnggota());case 'getPinjam': return jsonResponse(getAllPinjam());case 'getKembali': return jsonResponse(getAllKembali());case 'getLog': return jsonResponse(getAllLog());case 'tambahBuku': return jsonResponse(tambahBuku(data));case 'editBuku': return jsonResponse(editBuku(data));case 'hapusBuku': return jsonResponse(hapusBuku(data));case 'tambahAnggota': return jsonResponse(tambahAnggota(data));case 'editAnggota': return jsonResponse(editAnggota(data));case 'hapusAnggota': return jsonResponse(hapusAnggota(data));case 'pinjamBuku': return jsonResponse(pinjamBuku(data));case 'kembalikanBuku': return jsonResponse(kembalikanBuku(data));case 'cariBuku': return jsonResponse(cariBuku(data));case 'getKategori': return jsonResponse(getKategoriList());case 'getRak': return jsonResponse(getRakList());default: return jsonResponse({success: false,error: 'Action tidak dikenal: ' + action});}} catch(err) {return jsonResponse({success: false,error: err.toString(),stack: err.stack});}}function jsonResponse(data) {return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON);}// ============================================// HELPER FUNCTIONS// ============================================function getSheet(sheetName) {const ss = SpreadsheetApp.openById(SPREADSHEET_ID);const sheet = ss.getSheetByName(sheetName);if (!sheet) {throw new Error('Sheet "' + sheetName + '" tidak ditemukan di spreadsheet');}return sheet;}function getNextId(sheet, prefix, idCol) {const data = sheet.getDataRange().getValues();if (data.length <= 1) return prefix + '0001';let maxNum = 0;for (let i = 1; i < data.length; i++) {const id = data[i][idCol]? data[i][idCol].toString(): '';const num = parseInt(id.replace(prefix, '').replace(/\D/g, ''));if (!isNaN(num) && num > maxNum) {maxNum = num;}}return prefix + String(maxNum + 1).padStart(4, '0');}function writeLog(aksi, detail, status) {try {const sheet = getSheet('LOG');const timestamp = new Date();const email =Session.getActiveUser().getEmail() ||'admin.smkdwijapraja@gmail.com';sheet.appendRow([timestamp,aksi,email,detail,status]);} catch(e) {console.log('Write log error: ' + e);}}function sendEmail(to, subject, body) {try {if (to && to.includes('@')) {MailApp.sendEmail(to, subject, body);}} catch(e) {console.log('Email gagal: ' + e);}}// ============================================// DASHBOARD// ============================================function getDashboardData() {try {const bukuSheet = getSheet('BUKU');const anggotaSheet = getSheet('ANGGOTA');const pinjamSheet = getSheet('PINJAM');const kembaliSheet = getSheet('KEMBALI');const totalBuku =Math.max(0, bukuSheet.getLastRow() - 1);const totalAnggota =Math.max(0, anggotaSheet.getLastRow() - 1);const totalPinjam =Math.max(0, pinjamSheet.getLastRow() - 1);const totalKembali =Math.max(0, kembaliSheet.getLastRow() - 1);let stokTersedia = 0;let stokDipinjam = 0;const bukuData =bukuSheet.getDataRange().getValues();for (let i = 1; i < bukuData.length; i++) {stokTersedia += parseInt(bukuData[i][7]) || 0;}const pinjamData =pinjamSheet.getDataRange().getValues();for (let i = 1; i < pinjamData.length; i++) {if (pinjamData[i][7] === 'DIPINJAM') {stokDipinjam++;}}const kategoriCount = {};for (let i = 1; i < bukuData.length; i++) {const kat = bukuData[i][5];if (kat) {kategoriCount[kat] =(kategoriCount[kat] || 0) + 1;}}const pinjamPerBulan = {};for (let i = 1; i < pinjamData.length; i++) {const tgl = pinjamData[i][5];if (tgl && tgl instanceof Date) {const key =tgl.getFullYear() +'-' +String(tgl.getMonth() + 1).padStart(2, '0');pinjamPerBulan[key] =(pinjamPerBulan[key] || 0) + 1;}}return {success: true,totalBuku,totalAnggota,totalPinjam,totalKembali,stokTersedia,stokDipinjam,kategoriPopuler: kategoriCount,pinjamPerBulan: pinjamPerBulan};} catch(err) {return {success: false,error: err.toString()};}}// ============================================// BUKU CRUD// ============================================function getAllBuku() {try {const sheet = getSheet('BUKU');const data = sheet.getDataRange().getValues();const result = [];for (let i = 1; i < data.length; i++) {result.push({id_buku: data[i][0] || '',judul: data[i][1] || '',pengarang: data[i][2] || '',penerbit: data[i][3] || '',tahun: data[i][4] || '',kategori: data[i][5] || '',rak: data[i][6] || '',stok: data[i][7] || 0,ket: data[i][8] || ''});}return {success: true,data: result};} catch(err) {return {success: false,error: err.toString()};}}function tambahBuku(data) {try {const sheet = getSheet('BUKU');const idBuku = getNextId(sheet, 'BK', 0);sheet.appendRow([idBuku,data.judul,data.pengarang,data.penerbit,data.tahun,data.kategori,data.rak,data.stok || 50,'TERSEDIA']);writeLog('TAMBAH BUKU','Buku: ' + data.judul +' (ID: ' + idBuku + ')','SUKSES');return {success: true,id: idBuku,message: 'Buku berhasil ditambahkan'};} catch(err) {return {success: false,error: err.toString()};}}function editBuku(data) {try {const sheet = getSheet('BUKU');const values = sheet.getDataRange().getValues();for (let i = 1; i < values.length; i++) {if (values[i][0] === data.id_buku) {sheet.getRange(i + 1, 2).setValue(data.judul);sheet.getRange(i + 1, 3).setValue(data.pengarang);sheet.getRange(i + 1, 4).setValue(data.penerbit);sheet.getRange(i + 1, 5).setValue(data.tahun);sheet.getRange(i + 1, 6).setValue(data.kategori);sheet.getRange(i + 1, 7).setValue(data.rak);sheet.getRange(i + 1, 8).setValue(data.stok);writeLog('EDIT BUKU','Buku: ' + data.judul +' (ID: ' + data.id_buku + ')','SUKSES');return {success: true,message: 'Buku berhasil diupdate'};}}return {success: false,message: 'Buku tidak ditemukan'};} catch(err) {return {success: false,error: err.toString()};}}function hapusBuku(data) {try {const sheet = getSheet('BUKU');const values = sheet.getDataRange().getValues();for (let i = 1; i < values.length; i++) {if (values[i][0] === data.id_buku) {sheet.deleteRow(i + 1);writeLog('HAPUS BUKU','ID: ' + data.id_buku,'SUKSES');return {success: true,message: 'Buku berhasil dihapus'};}}return {success: false,message: 'Buku tidak ditemukan'};} catch(err) {return {success: false,error: err.toString()};}}function cariBuku(keyword) {try {const result = getAllBuku();if (!result.success) return result;const all = result.data;const lower =(keyword || '').toLowerCase();const filtered = all.filter(b =>(b.judul || '').toLowerCase().includes(lower) ||(b.pengarang || '').toLowerCase().includes(lower) ||(b.kategori || '').toLowerCase().includes(lower));return {success: true,data: filtered};} catch(err) {return {success: false,error: err.toString()};}}function getKategoriList() {try {const result = getAllBuku();if (!result.success) return result;const kategori = [...new Set(result.data.map(b => b.kategori).filter(k => k))];return {success: true,data: kategori.sort()};} catch(err) {return {success: false,error: err.toString()};}}function getRakList() {try {const result = getAllBuku();if (!result.success) return result;const rak = [...new Set(result.data.map(b => b.rak).filter(r => r))];return {success: true,data: rak.sort()};} catch(err) {return {success: false,error: err.toString()};}}// ============================================// ANGGOTA CRUD// ============================================function getAllAnggota() {try {const sheet = getSheet('ANGGOTA');const data = sheet.getDataRange().getValues();const result = [];for (let i = 1; i < data.length; i++) {result.push({id_anggota: data[i][0] || '',nisn: data[i][1] || '',nama: data[i][2] || '',kelas: data[i][3] || '',jenis_kelamin: data[i][4] || '',no_wa: data[i][5] || '',email: data[i][6] || '',status: data[i][7] || 'AKTIF'});}return {success: true,data: result};} catch(err) {return {success: false,error: err.toString()};}}function tambahAnggota(data) {try {const sheet = getSheet('ANGGOTA');const idAnggota =getNextId(sheet, 'A', 0);sheet.appendRow([idAnggota,data.nisn,data.nama,data.kelas,data.jenis_kelamin,data.no_wa,data.email,'AKTIF']);writeLog('TAMBAH ANGGOTA','Anggota: ' + data.nama +' (ID: ' + idAnggota + ')','SUKSES');return {success: true,id: idAnggota,message: 'Anggota berhasil ditambahkan'};} catch(err) {return {success: false,error: err.toString()};}}function editAnggota(data) {try {const sheet = getSheet('ANGGOTA');const values = sheet.getDataRange().getValues();for (let i = 1; i < values.length; i++) {if (values[i][0] === data.id_anggota) {sheet.getRange(i + 1, 2).setValue(data.nisn);sheet.getRange(i + 1, 3).setValue(data.nama);sheet.getRange(i + 1, 4).setValue(data.kelas);sheet.getRange(i + 1, 5).setValue(data.jenis_kelamin);sheet.getRange(i + 1, 6).setValue(data.no_wa);sheet.getRange(i + 1, 7).setValue(data.email);writeLog('EDIT ANGGOTA','Anggota: ' + data.nama +' (ID: ' + data.id_anggota + ')','SUKSES');return {success: true,message: 'Anggota berhasil diupdate'};}}return {success: false,message: 'Anggota tidak ditemukan'};} catch(err) {return {success: false,error: err.toString()};}}function hapusAnggota(data) {try {const sheet = getSheet('ANGGOTA');const values = sheet.getDataRange().getValues();for (let i = 1; i < values.length; i++) {if (values[i][0] === data.id_anggota) {sheet.deleteRow(i + 1);writeLog('HAPUS ANGGOTA','ID: ' + data.id_anggota,'SUKSES');return {success: true,message: 'Anggota berhasil dihapus'};}}return {success: false,message: 'Anggota tidak ditemukan'};} catch(err) {return {success: false,error: err.toString()};}}// ============================================// PEMINJAMAN// ============================================function getAllPinjam() {try {const sheet = getSheet('PINJAM');const data = sheet.getDataRange().getValues();const result = [];for (let i = 1; i < data.length; i++) {result.push({id_pinjam: data[i][0] || '',id_anggota: data[i][1] || '',nama: data[i][2] || '',id_buku: data[i][3] || '',judul: data[i][4] || '',tgl_pinjam: formatDate(data[i][5]),tgl_kembali: formatDate(data[i][6]),ket: data[i][7] || '',email: data[i][8] || ''});}return {success: true,data: result};} catch(err) {return {success: false,error: err.toString()};}}function pinjamBuku(data) {try {// Cek stokconst bukuSheet = getSheet('BUKU');const bukuData =bukuSheet.getDataRange().getValues();let stokNow = 0;let bukuRow = -1;for (let i = 1; i < bukuData.length; i++) {if (bukuData[i][0] === data.id_buku) {stokNow =parseInt(bukuData[i][7]) || 0;bukuRow = i;break;}}if (stokNow <= 0) {return {success: false,message: 'Stok buku habis atau tidak tersedia'};}const sheet = getSheet('PINJAM');const idPinjam =getNextId(sheet, 'P', 0);const tglPinjam = new Date();const tglKembali = new Date();tglKembali.setDate(tglKembali.getDate() + 7);sheet.appendRow([idPinjam,data.id_anggota,data.nama,data.id_buku,data.judul,tglPinjam,tglKembali,'DIPINJAM',data.email]);// Kurangi stokif (bukuRow > 0) {bukuSheet.getRange(bukuRow + 1, 8).setValue(stokNow - 1);}const subject ='Konfirmasi Peminjaman Buku - Perpustakaan SMK Dwija Praja';const body ='Yth. ' + data.nama + ',\n\n' +'Anda telah meminjam buku:\n' +'Judul: ' + data.judul + '\n' +'ID Peminjaman: ' + idPinjam + '\n' +'Tanggal Pinjam: ' +formatDate(tglPinjam) + '\n' +'Tanggal Kembali: ' +formatDate(tglKembali) + '\n\n' +'Harap mengembalikan buku tepat waktu.\n\n' +'Terima kasih.\n' +'Perpustakaan SMK Dwija Praja';sendEmail(data.email,subject,body);writeLog('PINJAM','ID Pinjam: ' +idPinjam +' - ' +data.judul,'SUKSES');return {success: true,id: idPinjam,message: 'Peminjaman berhasil'};} catch(err) {return {success: false,error: err.toString()};}}function kembalikanBuku(data) {try {const pinjamSheet = getSheet('PINJAM');const kembaliSheet = getSheet('KEMBALI');const pinjamData =pinjamSheet.getDataRange().getValues();for (let i = 1; i < pinjamData.length; i++) {if (pinjamData[i][0] === data.id_pinjam) {kembaliSheet.appendRow([pinjamData[i][0],pinjamData[i][1],pinjamData[i][2],pinjamData[i][3],pinjamData[i][4],pinjamData[i][5],new Date(),'DIKEMBALIKAN',pinjamData[i][8]]);pinjamSheet.deleteRow(i + 1);// Tambah stokconst bukuSheet = getSheet('BUKU');const bukuData =bukuSheet.getDataRange().getValues();for (let j = 1; j < bukuData.length; j++) {if (bukuData[j][0] === pinjamData[i][3]) {const stokNow =parseInt(bukuData[j][7]) || 0;bukuSheet.getRange(j + 1, 8).setValue(stokNow + 1);break;}}writeLog('KEMBALI','ID Pinjam: ' +data.id_pinjam +' - ' +pinjamData[i][4],'SUKSES');return {success: true,message: 'Buku berhasil dikembalikan'};}}return {success: false,message: 'Data peminjaman tidak ditemukan'};} catch(err) {return {success: false,error: err.toString()};}}function getAllKembali() {try {const sheet = getSheet('KEMBALI');const data = sheet.getDataRange().getValues();const result = [];for (let i = 1; i < data.length; i++) {result.push({id_pinjam: data[i][0] || '',id_anggota: data[i][1] || '',nama: data[i][2] || '',id_buku: data[i][3] || '',judul: data[i][4] || '',tgl_pinjam: formatDate(data[i][5]),tgl_kembali: formatDate(data[i][6]),ket: data[i][7] || '',email: data[i][8] || ''});}return {success: true,data: result};} catch(err) {return {success: false,error: err.toString()};}}// ============================================// LOG// ============================================function getAllLog() {try {const sheet = getSheet('LOG');const data = sheet.getDataRange().getValues();const result = [];for (let i = 1; i < data.length; i++) {result.push({timestamp: formatDateTime(data[i][0]),aksi: data[i][1] || '',email: data[i][2] || '',detail: data[i][3] || '',status: data[i][4] || ''});}return {success: true,data: result.reverse()};} catch(err) {return {success: false,error: err.toString()};}}// ============================================// UTILITIES// ============================================function formatDate(dateVal) {if (!dateVal) return '-';const d = new Date(dateVal);if (isNaN(d.getTime())) return '-';return d.toLocaleDateString('id-ID');}function formatDateTime(dateVal) {if (!dateVal) return '-';const d = new Date(dateVal);if (isNaN(d.getTime())) return '-';return d.toLocaleString('id-ID');}
Script backend asli memang menggunakan SPREADSHEET_ID, doGet(), fungsi CRUD buku dan anggota, transaksi peminjaman/pengembalian, serta log aktivitas.
Script 2 — Index.html
Script kedua adalah frontend aplikasi.
File HTML ini bertanggung jawab terhadap tampilan dashboard, menu, tabel, form tambah/edit, pencarian, peminjaman, pengembalian, loading, notifikasi, dan grafik.
Nama file harus:
Index.htmlkarena doGet() pada Code2.gs memanggil file dengan nama Index.
Script Index2.html asli Anda terdiri dari 1.225 baris, sehingga untuk artikel Blogspot saya sarankan script tersebut ditampilkan dalam satu blok kode terpisah setelah penjelasan ini, bukan dipotong menjadi beberapa bagian.
Frontend tersebut menggunakan:
google.script.rununtuk memanggil fungsi backend seperti:
getDashboardData()getAllBuku()getAllAnggota()getAllPinjam()getAllKembali()getAllLog()tambahBuku()editBuku()hapusBuku()tambahAnggota()editAnggota()hapusAnggota()pinjamBuku()kembalikanBuku()
Komunikasi frontend dan backend tersebut terlihat pada fungsi callAPI().
Frontend juga menggunakan Chart.js untuk menampilkan statistik peminjaman dalam bentuk grafik.
Cara Memasang Aplikasi
1. Buat Google Spreadsheet
Buat spreadsheet baru dan buat lima sheet:
BUKUANGGOTAPINJAMKEMBALILOG
Masukkan header sesuai struktur database di atas.
2. Buka Apps Script
Dari Google Spreadsheet pilih:
Extensions → Apps Script
3. Masukkan Code2.gs
Hapus kode bawaan Apps Script.
Masukkan script backend pada bagian:
Script 1 — Code2.gs
4. Buat Index.html
Klik:
+ → HTML
Berikan nama:
IndexKemudian masukkan seluruh kode Index2.html.
Jangan mengganti nama file menjadi Index2 karena doGet() memanggil:
HtmlService.createHtmlOutputFromFile('Index')Cara Deploy Aplikasi
Setelah kedua script selesai:
Klik Deploy
Pilih New deployment
Pada tipe deployment pilih Web app
Atur akses sesuai kebutuhan
Klik Deploy
Berikan izin yang diminta Google
Salin URL Web App
Buka URL tersebut melalui browser
Aplikasi kemudian dapat digunakan sebagai sistem perpustakaan berbasis web.
Kesimpulan
Aplikasi perpustakaan berbasis Google Apps Script dan Google Sheets dapat menjadi solusi sederhana untuk membantu digitalisasi administrasi perpustakaan sekolah.
Dengan aplikasi ini, pengelolaan buku, anggota, peminjaman, pengembalian, stok, statistik, dan log aktivitas dapat dilakukan melalui satu halaman Web App.
Keunggulan lainnya adalah aplikasi tidak membutuhkan hosting tradisional karena backend berjalan menggunakan Google Apps Script dan database menggunakan Google Sheets.
Aplikasi ini juga masih dapat dikembangkan lebih lanjut dengan fitur:
Login administrator
Hak akses petugas
Barcode buku
QR Code
Kartu anggota
Cetak kartu anggota
Cetak laporan
Export PDF
Export Excel
Denda keterlambatan otomatis
Backup database
Laporan peminjaman per kelas
Laporan buku paling sering dipinjam
Dengan pengembangan tersebut, aplikasi sederhana ini dapat menjadi sistem informasi perpustakaan sekolah yang lebih lengkap dan profesional.

Post a Comment