Prompt dan Source Code Aplikasi Pustaka Sekolah Berbasis Apps Script & Blogger

Manajemen perpustakaan di era digital kini tidak lagi membutuhkan biaya server yang mahal atau infrastruktur yang rumit. Dengan memanfaatkan ekosistem Google, kita bisa membangun aplikasi tingkat profesional secara gratis. Artikel ini akan membahas konsep, memberikan prompt pembuatan mandiri, hingga source code dan template yang siap Anda gunakan.

Apa itu Aplikasi Pustaka Sekolah (SI-PUS)?

Pustaka Sekolah (SI-PUS) adalah Sistem Informasi Manajemen Perpustakaan berbasis web (Single Page Application) yang dirancang untuk mendata inventaris buku, meregistrasi anggota (siswa/guru), dan memproses sirkulasi (peminjaman dan pengembalian) secara otomatis. Aplikasi ini dilengkapi dengan fitur cetak Kartu Anggota, cetak Barcode Buku massal, hingga teknologi pemindai barcode menggunakan kamera smartphone maupun scanner eksternal.

Kolaborasi Google Apps Script dan Blogger

Google Apps Script (GAS) berfungsi sebagai Backend (Server & Database). GAS membaca, menulis, dan menghapus data langsung ke dalam Google Spreadsheet. Ini menjadikan Spreadsheet bertindak sebagai database yang sangat familiar dan mudah dibackup.

Blogger bertindak murni sebagai Frontend (User Interface). Alih-alih membuat postingan blog biasa, kita menggunakan tema XML kustom (berbasis Tailwind CSS) untuk mengubah blog menjadi antarmuka aplikasi interaktif yang memanggil data dari URL Apps Script Anda tanpa perlu biaya *hosting* sepeser pun.

Manfaat Menggunakan Sistem Ini

  • 100% Gratis Tanpa Biaya Hosting: Memanfaatkan Blogger dan Google Drive (Spreadsheet).
  • Aman & Responsif: Terlindungi oleh server Google, serta desain yang menyesuaikan layar HP (Mobile Friendly).
  • Fitur Profesional: Mendukung cetak PDF massal (html2pdf), scan barcode kamera (html5-qrcode), dan upload anggota massal via Excel (XLSX).
  • Database Terbuka: Data tersimpan rapi di Google Sheets, mudah diedit langsung jika terjadi kesalahan.

Prompt AI Detail & Lengkap (Bikin Sendiri)

Jika Anda ingin meminta AI (seperti ChatGPT, Gemini, atau Claude) untuk membangun sistem serupa dari awal, gunakan struktur prompt berikut:

"Buatkan saya Sistem Informasi Perpustakaan Sekolah (SI-PUS) berarsitektur SPA (Single Page Application). Gunakan Google Apps Script sebagai Backend dengan Google Sheets sebagai database (Books, Members, Loans, Settings). Gunakan Blogger XML Template murni sebagai Frontend dengan styling Tailwind CSS (tema Platinum/Emerald). Frontend harus memiliki fitur:

1. Dashboard statistik otomatis.
2. Manajemen Buku (dengan fitur duplikasi/eksemplar otomatis) dan Anggota (dengan fitur upload massal Excel/XLSX).
3. Sirkulasi peminjaman dan pengembalian yang terintegrasi dengan library scanner kamera (html5-qrcode).
4. Menu Laporan untuk mencetak PDF menggunakan html2pdf (Kartu Anggota depan-belakang dengan barcode JsBarcode, Barcode Buku matrix 3 kolom, dan tabel riwayat sirkulasi).
5. Pengaturan dinamis untuk Kop Surat dan Tanda Tangan.

Sistem harus anti-blank di Blogger (letakkan div utama di luar b:section), gunakan SweetAlert2 untuk notifikasi, dan pastikan print laporan dilengkapi page-break-inside avoid."

Source Code Backend (Google Apps Script)

Salin kode di bawah ini tanpa mengubah satu baris pun, lalu tempelkan ke editor Google Apps Script Anda.

Code.gs
/**
 * SISTEM INFORMASI PERPUSTAKAAN SEKOLAH (SI-PUS)
 * Backend - Google Apps Script (Production Ready v4.0)
 * 2026 desain oleh Yefri Haryanto
 */

function setupDatabase() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheets = [
    { name: "Books", headers: ["ID", "Title", "Author", "Category", "ISBN", "Status", "CreatedAt"] },
    { name: "Members", headers: ["ID", "MemberID", "Name", "Class", "PhotoUrl", "ValidUntil", "CreatedAt"] },
    { name: "Loans", headers: ["ID", "BookID", "BookTitle", "MemberID", "MemberName", "LoanDate", "ReturnDate", "Status"] },
    { name: "Settings", headers: ["Key", "Value"] }
  ];

  sheets.forEach(s => {
    let sheet = ss.getSheetByName(s.name);
    if (!sheet) {
      sheet = ss.insertSheet(s.name);
      sheet.appendRow(s.headers);
      sheet.getRange(1, 1, 1, s.headers.length).setFontWeight("bold").setBackground("#e2e8f0");
    }
  });
}

function doGet(e) { return ContentService.createTextOutput(JSON.stringify({ status: 'success', message: 'API Aktif!' })).setMimeType(ContentService.MimeType.JSON); }

function doPost(e) {
  if (!e || !e.parameter || !e.parameter.action) return ContentService.createTextOutput(JSON.stringify({ status: 'error', message: 'Invalid Request' })).setMimeType(ContentService.MimeType.JSON);

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss.getSheetByName("Books")) setupDatabase();

  const action = e.parameter.action;
  let response = { status: 'error', message: 'Action not found' };

  try {
    if (action === 'get_all_data') {
      response = { status: 'success', data: { books: getSheetData("Books"), members: getSheetData("Members"), loans: getSheetData("Loans"), settings: getSettingsData() } };
    } 
    else if (action === 'add_book') {
      const p = JSON.parse(e.parameter.payload);
      const sheet = ss.getSheetByName("Books");
      const qty = parseInt(p.qty) || 1;
      const baseIsbn = p.isbn || ('B' + new Date().getTime());
      
      for(let i = 1; i <= qty; i++) {
        let finalIsbn = qty > 1 ? `${baseIsbn}-${i}` : baseIsbn;
        let id = 'b_' + new Date().getTime() + i;
        sheet.appendRow([id, p.title, p.author, p.category, finalIsbn, 'available', new Date().toISOString()]);
      }
      response = { status: 'success', message: `${qty} Eksemplar Buku ditambahkan` };
    }
    else if (action === 'add_member') {
      const p = JSON.parse(e.parameter.payload);
      const sheet = ss.getSheetByName("Members");
      const existingData = sheet.getDataRange().getValues();
      let exists = false;
      
      // Cek duplikasi ID Anggota / NISN
      for(let i = 1; i < existingData.length; i++) {
        if(String(existingData[i][1]).trim() === String(p.memberId).trim()) { exists = true; break; }
      }
      
      if(exists) {
        response = { status: 'error', message: `Gagal: NISN / ID Anggota '${p.memberId}' sudah terdaftar!` };
      } else {
        sheet.appendRow(['m_' + new Date().getTime(), p.memberId, p.name, p.class, p.photoUrl, p.validUntil, new Date().toISOString()]);
        response = { status: 'success', message: 'Anggota ditambahkan' };
      }
    }
    else if (action === 'bulk_add_members') {
      const p = JSON.parse(e.parameter.payload); 
      const sheet = ss.getSheetByName("Members");
      const existingData = sheet.getDataRange().getValues();
      const existingIds = new Set();
      
      for(let i = 1; i < existingData.length; i++) existingIds.add(String(existingData[i][1]).trim());
      
      let addedCount = 0;
      let duplicateCount = 0;
      
      p.forEach((m, idx) => {
         const mId = String(m.memberId).trim();
         if(!existingIds.has(mId)) {
           sheet.appendRow(['m_' + new Date().getTime() + idx, mId, m.name, m.class, m.photoUrl, m.validUntil, new Date().toISOString()]);
           existingIds.add(mId); // Tambahkan ke Set agar tidak ganda di dalam file Excel yang sama
           addedCount++;
         } else {
           duplicateCount++;
         }
      });
      response = { status: 'success', message: `Import Selesai! ${addedCount} Data Baru Ditambahkan. ${duplicateCount} Data Ganda Dilewati.` };
    }
    else if (action === 'process_circulation') {
      response = processCirculation(JSON.parse(e.parameter.payload));
    }
    else if (action === 'save_settings') {
      saveSettingsData(JSON.parse(e.parameter.payload));
      response = { status: 'success', message: 'Pengaturan disimpan' };
    }
    else if (action === 'delete_item') {
      deleteRowById(e.parameter.sheet, e.parameter.id);
      response = { status: 'success', message: 'Data dihapus' };
    }
  } catch (error) { response = { status: 'error', message: error.toString() }; }

  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}

function getSheetData(sheetName) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  if (!sheet) return [];
  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return [];
  const headers = data[0]; const result = [];
  for (let i = 1; i < data.length; i++) {
    let obj = {}; for (let j = 0; j < headers.length; j++) obj[headers[j]] = data[i][j];
    result.push(obj);
  }
  return result;
}

function getSettingsData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Settings");
  if (!sheet) return {};
  const data = sheet.getDataRange().getValues();
  let settings = {}; for (let i = 1; i < data.length; i++) settings[data[i][0]] = data[i][1];
  return settings;
}

function saveSettingsData(newSettings) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Settings");
  if (!sheet) return;
  sheet.getDataRange().clearContent(); sheet.appendRow(["Key", "Value"]);
  for (let key in newSettings) sheet.appendRow([key, newSettings[key]]);
}

function deleteRowById(sheetName, id) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  if (!sheet) return;
  const data = sheet.getDataRange().getValues();
  for (let i = 1; i < data.length; i++) { if (data[i][0] === id) { sheet.deleteRow(i + 1); break; } }
}

function processCirculation(payload) {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const booksData = ss.getSheetByName("Books").getDataRange().getValues();
  const searchId = String(payload.bookId).trim();
  const altId = searchId.replace(/\s/g, '_'); 
  let bookRow = -1, bookObj = null;

  for (let i = 1; i < booksData.length; i++) {
    if (booksData[i][4] == searchId || booksData[i][0] == searchId || booksData[i][4] == altId || booksData[i][0] == altId) {
      bookRow = i + 1; bookObj = { id: booksData[i][0], title: booksData[i][1], status: booksData[i][5] }; break;
    }
  }
  if (!bookObj) throw new Error("Gagal: ID Buku tidak ditemukan di database.");

  // Pengecekan Member
  const membersData = ss.getSheetByName("Members").getDataRange().getValues();
  let memberObj = null;
  const memSearch = String(payload.memberId).trim();
  for (let i = 1; i < membersData.length; i++) {
    if (membersData[i][1] == memSearch) {
      memberObj = { id: membersData[i][0], name: membersData[i][2] }; break;
    }
  }
  if (!memberObj) throw new Error("Gagal: ID Anggota tidak terdaftar / salah scan.");

  if (payload.mode === 'loan') {
    if (bookObj.status !== 'available') throw new Error("Gagal: Buku ini sedang dipinjam.");
    ss.getSheetByName("Loans").appendRow(['L' + new Date().getTime(), bookObj.id, bookObj.title, memberObj.id, memberObj.name, new Date().toISOString(), "", "borrowed"]);
    ss.getSheetByName("Books").getRange(bookRow, 6).setValue("borrowed");
    return { status: 'success', message: 'Peminjaman Berhasil!' };
  } else {
    if (bookObj.status === 'available') throw new Error("Gagal: Buku tidak dalam status dipinjam.");
    const loansSheet = ss.getSheetByName("Loans");
    const loansData = loansSheet.getDataRange().getValues();
    let loanRow = -1;
    
    for (let i = 1; i < loansData.length; i++) {
      if (loansData[i][1] === bookObj.id && loansData[i][3] === memberObj.id && loansData[i][7] === 'borrowed') {
        loanRow = i + 1; break;
      }
    }
    if (loanRow === -1) throw new Error("Gagal: Buku ini tidak sedang dipinjam oleh Siswa tersebut.");
    
    loansSheet.getRange(loanRow, 7).setValue(new Date().toISOString());
    loansSheet.getRange(loanRow, 8).setValue("returned");
    ss.getSheetByName("Books").getRange(bookRow, 6).setValue("available");
    return { status: 'success', message: 'Pengembalian Berhasil!' };
  }
}

Download Tema XML Frontend (Blogger)

Gunakan tombol di bawah ini untuk mengunduh kode tema XML Blogger yang sudah diatur dengan desain Platinum Modern, dan terhindar dari *bug rendering*.

Langkah-langkah Pemasangan Sistem

  1. Buka Google Sheets dan buat Spreadsheet kosong baru. Beri nama "Database SI-PUS".
  2. Pada menu Spreadsheet, klik Ekstensi > Apps Script.
  3. Hapus kode bawaan function myFunction() {}, lalu tempel (paste) kode Backend dari kotak hitam di atas. Klik ikon disket (Save).
  4. Di kanan atas layar Apps Script, klik Terapkan (Deploy) > Deployment Baru. Pilih jenis "Aplikasi Web", atur "Jalankan sebagai: Saya", dan yang terpenting "Akses: Siapa saja (Anyone)". Klik Terapkan.
  5. Salin URL Aplikasi Web (URL Web App) yang diberikan.
  6. Buka file Tema XML yang telah Anda unduh menggunakan Notepad atau Text Editor.
  7. Cari bagian const GAS_URL = "YOUR_GOOGLE_APPS_SCRIPT_URL_HERE"; dan ganti tulisan di dalam tanda kutip dengan URL Web App Anda.
  8. Masuk ke Dashboard Blogger > menu Tema > klik tanda panah ke bawah di sebelah tombol Sesuaikan > pilih Edit HTML.
  9. Hapus seluruh isi kode Blogger bawaan, tempel kode Tema XML yang sudah disesuaikan tadi, lalu klik Simpan.
  10. Buka Blog Anda. Aplikasi Pustaka Sekolah siap digunakan secara profesional!