Source Code Aplikasi Tabungan Siswa Dengan Appscript
Mengelola tabungan siswa di sekolah kini tidak perlu lagi menggunakan cara manual yang merepotkan. Dengan kemajuan teknologi, Anda dapat membangun sistem perbankan mini di sekolah Anda sendiri hanya dengan bermodalkan Google Sheets dan Google Apps Script (GAS). Pada artikel ini, kami akan membagikan Source Code Aplikasi E-Tabungan Siswa yang bisa langsung Anda gunakan.
Apa Itu Tabungan dan E-Tabungan?
Tabungan Konvensional:
Tabungan adalah simpanan uang yang penarikannya hanya dapat dilakukan menurut syarat tertentu yang disepakati. Di sekolah, tabungan biasanya dicatat secara manual di buku tulis tebal.
Tabungan adalah simpanan uang yang penarikannya hanya dapat dilakukan menurut syarat tertentu yang disepakati. Di sekolah, tabungan biasanya dicatat secara manual di buku tulis tebal.
Beda Buku Tabungan Manual dengan E-Tabungan:
- Keamanan Data: Buku manual rentan hilang, terselip, atau rusak terkena air. E-Tabungan menyimpan data dengan aman di server *Cloud* (Google Drive) yang kebal terhadap kerusakan fisik.
- Akurasi Perhitungan: Menghitung manual sangat rawan *human error* (salah hitung selisih). E-Tabungan menghitung saldo, pemasukan, dan pengeluaran secara otomatis dan presisi (Anti-Selisih).
- Efisiensi Waktu: Mencari data satu siswa di tumpukan buku membutuhkan waktu lama. Dengan E-Tabungan, data dapat diakses, difilter, dan dicetak laporan PDF-nya hanya dalam hitungan detik.
- Profesionalitas: E-Tabungan dilengkapi dengan cetakan mutasi digital lengkap dengan *Barcode* dan *QR Code* pengesahan layaknya bank sungguhan.
Fitur Unggulan Aplikasi Ini
Aplikasi yang akan Anda buat ini memiliki fitur setara aplikasi berbayar, di antaranya:
- Dashboard Interaktif: Dilengkapi dengan grafik statistik (*Chart.js*) untuk memantau sirkulasi keuangan kelas secara *real-time*.
- Auto-Generate Sheets: Sistem cerdas yang akan otomatis membuat tabel database (*Siswa, Transaksi, Config*) jika belum ada.
- Cetak PDF Anti-Blank & Presisi: Menggunakan engine *jsPDF* murni yang menjamin cetakan Buku Tabungan A4 tidak akan terpotong meski diakses lewat layar HP (*Mobile Friendly*).
- Rekapitulasi Bulanan: Fitur rekap laporan keuangan per bulan yang siap dicetak untuk diserahkan ke Kepala Sekolah atau Wali Murid.
Langkah-Langkah Instalasi E-Tabungan
Ikuti panduan mudah berikut untuk menginstal aplikasi ini ke akun Google Anda:
- Buka Google Drive Anda dan buat file Google Sheets baru. Beri nama (misal: "Database Tabungan Kelas 9").
- Pada menu bagian atas Google Sheets, klik Ekstensi (Extensions) > Apps Script.
- Akan terbuka tab baru. Hapus semua kode bawaan yang ada di file
Code.gs. - Salin kode Backend (Code.gs) di bawah ini dan *paste* ke dalam file tersebut.
- Selanjutnya, buat file baru dengan cara klik ikon Tambah (+) di menu kiri, pilih HTML, dan beri nama Index (huruf 'I' besar).
- Salin kode Frontend (Index.html) di bawah ini dan *paste* ke dalam file tersebut.
- Klik ikon Simpan (Save/Disket).
- Refresh/Muat ulang halaman Google Sheets Anda. Akan muncul menu baru bertuliskan Aplikasi Tabungan di sebelah menu 'Bantuan'. Klik menu tersebut, pilih Buka Aplikasi, lalu berikan Izin Akses (Authorize).
Source Code E-Tabungan
📄 Code.gs (Backend)
/**
* @OnlyCurrentDoc
*/
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Aplikasi Tabungan')
.addItem('Buka Aplikasi', 'showSidebar')
.addToUi();
}
function doGet() {
return HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('Pembukuan Tabungan Siswa')
.addMetaTag('viewport', 'width=device-width, initial-scale=1');
}
function showSidebar() {
var html = HtmlService.createTemplateFromFile('Index')
.evaluate()
.setTitle('Pembukuan Tabungan')
.setWidth(850);
SpreadsheetApp.getUi().showModalDialog(html, 'Aplikasi Tabungan Siswa');
}
function ensureSheetsExist() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var isNew = false;
if (!ss.getSheetByName('Siswa')) {
ss.insertSheet('Siswa').appendRow(['Nama Siswa', 'Saldo Akhir']); isNew = true;
}
if (!ss.getSheetByName('Transaksi')) {
ss.insertSheet('Transaksi').appendRow(['Tanggal', 'Nama Siswa', 'Jenis', 'Jumlah']); isNew = true;
}
if (!ss.getSheetByName('Config')) {
var sheetConfig = ss.insertSheet('Config');
sheetConfig.appendRow(['Key', 'Value']);
sheetConfig.appendRow(['NamaSekolah', 'SMP Negeri 3 Kerinci']);
sheetConfig.appendRow(['Kelas', '9']);
sheetConfig.appendRow(['NamaGuru', 'Yefri Haryanto, M.Pd.']);
sheetConfig.appendRow(['TempatTTD', 'Sungai Penuh']);
isNew = true;
}
return isNew;
}
function getConfig() {
try {
ensureSheetsExist();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Config');
var data = sheet.getDataRange().getValues();
var config = {};
for (var i = 1; i < data.length; i++) config[data[i][0]] = data[i][1];
return { success: true, data: config };
} catch (e) { return { success: false, message: e.toString() }; }
}
function saveConfig(configData) {
try {
ensureSheetsExist();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Config');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
var key = data[i][0];
if (configData[key] !== undefined) sheet.getRange(i + 1, 2).setValue(configData[key]);
}
return { success: true, message: 'Konfigurasi berhasil disimpan!' };
} catch (e) { return { success: false, message: e.toString() }; }
}
function bulkInsertSiswa(siswaString) {
try {
ensureSheetsExist();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Siswa');
var names = siswaString.split(/[\n,]+/).map(n => n.trim()).filter(n => n.length > 0);
if (names.length === 0) return { success: false, message: 'Tidak ada nama yang valid.' };
var currentData = sheet.getDataRange().getValues();
var existingNames = currentData.map(row => String(row[0]).trim().toLowerCase());
var added = 0;
names.forEach(name => {
if (existingNames.indexOf(name.toLowerCase()) === -1) { sheet.appendRow([name, 0]); added++; }
});
return { success: true, message: added + ' siswa berhasil ditambahkan!' };
} catch (e) { return { success: false, message: e.toString() }; }
}
// --- FUNGSI DATA DASHBOARD & SALDO ---
function getSiswaData() {
try {
ensureSheetsExist();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetSiswa = ss.getSheetByName('Siswa');
var sheetTrx = ss.getSheetByName('Transaksi');
var siswaValues = sheetSiswa.getDataRange().getValues();
var siswaListRaw = siswaValues.length > 1 ? siswaValues.slice(1) : [];
var trxValues = sheetTrx.getDataRange().getValues();
var trxData = trxValues.length > 1 ? trxValues.slice(1) : [];
var balances = {};
siswaListRaw.forEach(row => balances[String(row[0]).trim()] = 0);
var totalSaldo = 0;
var totalSetor = 0;
var totalTarik = 0;
trxData.forEach(row => {
var nama = String(row[1]).trim();
var jenis = row[2];
var jml = parseFloat(row[3]) || 0;
if(balances.hasOwnProperty(nama)) {
if(jenis === 'Setor') { balances[nama] += jml; totalSaldo += jml; totalSetor += jml; }
else if(jenis === 'Tarik') { balances[nama] -= jml; totalSaldo -= jml; totalTarik += jml; }
}
});
var finalSiswaList = [];
for (const [nama, saldo] of Object.entries(balances)) {
finalSiswaList.push([nama, saldo]);
}
return {
siswaList: finalSiswaList,
totalSiswa: finalSiswaList.length,
totalSaldo: totalSaldo,
totalSetor: totalSetor,
totalTarik: totalTarik
};
} catch (e) { return { error: e.toString() }; }
}
function recordTransaction(data) {
try {
ensureSheetsExist();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var transaksiSheet = ss.getSheetByName('Transaksi');
var siswaSheet = ss.getSheetByName('Siswa');
var jumlah = parseFloat(data.jumlah);
if (isNaN(jumlah) || jumlah <= 0) return { success: false, message: 'Jumlah tidak valid.' };
var siswaData = siswaSheet.getDataRange().getValues();
var rowIndex = -1;
for (var i = 1; i < siswaData.length; i++) {
if (String(siswaData[i][0]).trim() === String(data.namaSiswa).trim()) { rowIndex = i; break; }
}
if (rowIndex === -1) return { success: false, message: 'Nama Siswa tidak ditemukan!' };
var trxValues = transaksiSheet.getDataRange().getValues();
var currentSaldo = 0;
if (trxValues.length > 1) {
trxValues.slice(1).forEach(row => {
if(String(row[1]).trim() === String(data.namaSiswa).trim()){
if(row[2] === 'Setor') currentSaldo += (parseFloat(row[3])||0);
if(row[2] === 'Tarik') currentSaldo -= (parseFloat(row[3])||0);
}
});
}
if (data.jenisTransaksi === 'Tarik') {
if (currentSaldo < jumlah) return { success: false, message: 'Saldo tidak mencukupi!' };
}
siswaSheet.getRange(rowIndex + 1, 2).setValue(data.jenisTransaksi === 'Setor' ? currentSaldo + jumlah : currentSaldo - jumlah);
transaksiSheet.appendRow([new Date(data.tanggal), data.namaSiswa, data.jenisTransaksi, jumlah]);
return { success: true, message: 'Transaksi berhasil disimpan!' };
} catch (e) { return { success: false, message: e.toString() }; }
}
function getStudentPassbookData(namaSiswa) {
try {
ensureSheetsExist();
var trxValues = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Transaksi').getDataRange().getValues();
if (trxValues.length <= 1) return { success: true, data: [] };
var trxData = trxValues.slice(1);
var studentTrx = [];
var runningBalance = 0;
trxData.forEach(row => {
if (String(row[1]).trim() === String(namaSiswa).trim()) {
var debit = row[2] === 'Setor' ? (parseFloat(row[3]) || 0) : 0;
var kredit = row[2] === 'Tarik' ? (parseFloat(row[3]) || 0) : 0;
runningBalance += (debit - kredit);
studentTrx.push({
tanggal: Utilities.formatDate(new Date(row[0]), "Asia/Jakarta", "dd/MM/yyyy"),
sandi: row[2], debit: debit, kredit: kredit, saldo: runningBalance
});
}
});
return { success: true, data: studentTrx };
} catch (e) { return { success: false, message: e.toString() }; }
}
function getMonthlyReportData(year, monthName) {
try {
ensureSheetsExist();
var allTrxValues = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Transaksi').getDataRange().getValues();
var allTrx = allTrxValues.length > 1 ? allTrxValues.slice(1) : [];
var months = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
var mIndex = months.indexOf(monthName);
var saldoAwal = 0;
var monthlyData = [];
allTrx.forEach(row => {
var tgl = new Date(row[0]);
var nama = row[1];
var jenis = row[2];
var jml = parseFloat(row[3]) || 0;
if (tgl.getFullYear() < year || (tgl.getFullYear() == year && tgl.getMonth() < mIndex)) {
if(jenis === 'Setor') saldoAwal += jml;
if(jenis === 'Tarik') saldoAwal -= jml;
}
else if (tgl.getFullYear() == year && tgl.getMonth() == mIndex) {
monthlyData.push({
tanggal: Utilities.formatDate(tgl, "Asia/Jakarta", "dd/MM/yyyy"),
nama: nama, jenis: jenis, debet: jenis === 'Setor' ? jml : 0, kredit: jenis === 'Tarik' ? jml : 0
});
}
});
var saldoBerjalan = saldoAwal;
var totalSetor = 0;
var totalTarik = 0;
monthlyData.forEach(item => {
saldoBerjalan += item.debet - item.kredit;
item.saldo = saldoBerjalan;
totalSetor += item.debet;
totalTarik += item.kredit;
});
return { success: true, saldoAwal: saldoAwal, data: monthlyData, totalSetor: totalSetor, totalTarik: totalTarik, saldoAkhir: saldoBerjalan };
} catch (e) { return { success: false, message: e.toString() }; }
}
📄 Index.html (Frontend)
<!DOCTYPE html>
<html lang="id">
<head>
<base target="_top">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aplikasi Tabungan Siswa</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11.7.12/dist/sweetalert2.min.css" rel="stylesheet">
<style>
body { background-color: #f4f7f9; font-family: 'Poppins', sans-serif; overflow-x: hidden; margin: 0; padding: 0; }
#main-app-content { position: relative; z-index: 10; background-color: #f4f7f9; min-height: 100vh; padding-bottom: 20px; }
/* Disembunyikan karena digunakan untuk menampung canvas/gambar QR dan Barcode untuk jsPDF */
#hiddenAssets { position: absolute; top: -10000px; left: -10000px; visibility: hidden; }
#masterPrintStorage { display: none; }
.app-header { background: linear-gradient(135deg, #1e3c72, #2a5298); color: white; padding: 15px; text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.1); margin-bottom: 20px; }
.app-header h3 { margin: 0; font-weight: 700; letter-spacing: 1px; font-size: 1.4rem; }
.custom-tabs {
display: flex; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch;
margin-bottom: 25px; gap: 5px; scrollbar-width: none; justify-content: flex-start;
}
@media (min-width: 768px) { .custom-tabs { justify-content: center; } }
.custom-tabs::-webkit-scrollbar { display: none; }
.custom-tabs .nav-link {
flex: 0 0 auto; white-space: nowrap; text-align: center; border-radius: 10px; color: #34495e; font-weight: 600;
padding: 10px 18px; background: #ffffff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); border: none; font-size: 0.9rem;
}
.custom-tabs .nav-link.active { background: linear-gradient(135deg, #1abc9c, #16a085); color: white; box-shadow: 0 4px 10px rgba(22, 160, 133, 0.3); }
.custom-tabs i { font-size: 1.1rem; margin-bottom: 3px; display: block; }
@media (max-width: 576px) {
.custom-tabs .nav-link { font-size: 0.75rem; padding: 8px 12px; }
.custom-tabs i { font-size: 1rem; }
}
.card { border: none; border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.05); margin-bottom: 20px; }
.card-header { background-color: #fff; border-bottom: 2px solid #f4f7f9; border-radius: 12px 12px 0 0 !important; font-weight: 600; }
.btn-custom { border-radius: 8px; font-weight: 500; padding: 10px 20px; }
.dash-card { transition: transform 0.2s; border-radius: 12px; }
.dash-card:hover { transform: translateY(-3px); }
.dash-quick-btn {
border: 1px solid #e4e9ed !important; background: #ffffff; color: #2c3e50; border-radius: 10px;
padding: 12px 10px; font-size: 0.85rem; font-weight: 600; transition: all 0.2s;
}
.dash-quick-btn:hover { background: #f8f9fa; transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.05) !important; color: #16a085;}
.dash-quick-btn i { font-size: 1.2rem; }
/* Gaya untuk HTML Pratinjau (Preview) Modal Saja */
.print-template { background: #ffffff; color: #000000; padding: 20px; border-radius: 8px; font-family: 'Poppins', sans-serif; }
.pb-header { text-align: center; background-color: #1e3c72; color: #ffffff; padding: 15px; border-radius: 8px 8px 0 0; }
.pb-header h3 { margin: 0; font-weight: bold; font-size: 20px; }
.pb-header p { margin: 5px 0 0; font-size: 14px; opacity: 0.9;}
.pb-body { border: 1px solid #eee; border-top: none; padding: 20px; border-radius: 0 0 8px 8px; }
.pb-info { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px dashed #eee; }
.pb-table th { background-color: #2c3e50; color: #ffffff; text-align: center; font-size: 12px; }
.pb-table td { text-align: center; font-size: 12px; vertical-align: middle; }
.uang-masuk { color: #27ae60; font-weight: 600; } .uang-keluar { color: #c0392b; font-weight: 600; }
#app-footer { text-align: center; padding: 20px; font-size: 0.85em; color: #7f8c8d; border-top: 1px solid #e4e9ed; margin-top: 30px;}
</style>
</head>
<body>
<div id="hiddenAssets">
<canvas id="hiddenBarcode"></canvas>
<div id="hiddenQrCode"></div>
</div>
<div id="main-app-content">
<div class="app-header">
<h3><i class="fas fa-wallet me-2"></i>E-Tabungan Siswa</h3>
</div>
<div class="container">
<ul class="nav custom-tabs" id="pills-tab" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" id="dashboard-tab" data-bs-toggle="pill" data-bs-target="#dashboard" type="button"><i class="fas fa-home"></i> Dashboard</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="transaksi-tab" data-bs-toggle="pill" data-bs-target="#transaksi" type="button"><i class="fas fa-exchange-alt"></i> Transaksi</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="data-tab" data-bs-toggle="pill" data-bs-target="#data" type="button"><i class="fas fa-users"></i> Data Saldo</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="laporan-tab" data-bs-toggle="pill" data-bs-target="#laporan" type="button"><i class="fas fa-file-invoice"></i> Laporan</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" id="config-tab" data-bs-toggle="pill" data-bs-target="#config" type="button"><i class="fas fa-cogs"></i> Setup</button></li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="dashboard" role="tabpanel">
<div class="card border-0 shadow-sm mb-4 bg-white" style="border-radius: 15px;">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="bg-primary bg-opacity-10 p-3 rounded-circle me-3">
<i class="fas fa-chart-line fa-2x text-primary"></i>
</div>
<div>
<h4 class="mb-0 fw-bold" style="color: #2c3e50;">Selamat Datang, <span id="welcomeGuru" class="text-primary">Memuat...</span></h4>
<p class="text-muted mb-0 small">Sistem Administrasi Tabungan Cerdas</p>
</div>
</div>
<div class="row g-2">
<div class="col-12"><p class="fw-bold text-secondary mb-1 small"><i class="fas fa-compass me-1"></i> Akses Cepat & Petunjuk</p></div>
<div class="col-6 col-md-3"><button class="btn w-100 text-start dash-quick-btn" onclick="document.getElementById('transaksi-tab').click()"><i class="fas fa-plus-circle text-primary me-2"></i> Input Transaksi</button></div>
<div class="col-6 col-md-3"><button class="btn w-100 text-start dash-quick-btn" onclick="document.getElementById('data-tab').click()"><i class="fas fa-users text-success me-2"></i> Cek Saldo Siswa</button></div>
<div class="col-6 col-md-3"><button class="btn w-100 text-start dash-quick-btn" onclick="document.getElementById('laporan-tab').click()"><i class="fas fa-file-invoice text-warning me-2"></i> Rekap Laporan</button></div>
<div class="col-6 col-md-3"><button class="btn w-100 text-start dash-quick-btn" onclick="document.getElementById('config-tab').click()"><i class="fas fa-cogs text-info me-2"></i> Pengaturan</button></div>
</div>
</div>
</div>
<div class="row g-2 mb-4">
<div class="col-6 col-md-3"><div class="card dash-card bg-primary text-white h-100 border-0 shadow-sm"><div class="card-body text-center p-3"><i class="fas fa-users fs-4 mb-1 opacity-75"></i><div class="small">Total Siswa</div><h4 class="mb-0 fw-bold mt-1" id="dashTotalSiswa">0</h4></div></div></div>
<div class="col-6 col-md-3"><div class="card dash-card bg-success text-white h-100 border-0 shadow-sm"><div class="card-body text-center p-3"><i class="fas fa-arrow-down fs-4 mb-1 opacity-75"></i><div class="small">Penyetoran</div><h6 class="mb-0 fw-bold mt-1" id="dashTotalSetor">Rp 0</h6></div></div></div>
<div class="col-6 col-md-3"><div class="card dash-card bg-danger text-white h-100 border-0 shadow-sm"><div class="card-body text-center p-3"><i class="fas fa-arrow-up fs-4 mb-1 opacity-75"></i><div class="small">Penarikan</div><h6 class="mb-0 fw-bold mt-1" id="dashTotalTarik">Rp 0</h6></div></div></div>
<div class="col-6 col-md-3"><div class="card dash-card bg-info text-white h-100 border-0 shadow-sm"><div class="card-body text-center p-3"><i class="fas fa-wallet fs-4 mb-1 opacity-75"></i><div class="small">Total Saldo</div><h6 class="mb-0 fw-bold mt-1" id="dashTotalSaldo">Rp 0</h6></div></div></div>
</div>
<div class="row g-3 mb-2">
<div class="col-md-5"><div class="card border-0 shadow-sm h-100"><div class="card-body" style="position: relative; height:250px;"><canvas id="chartTx"></canvas></div></div></div>
<div class="col-md-7"><div class="card border-0 shadow-sm h-100"><div class="card-body" style="position: relative; height:250px;"><canvas id="chartSaldo"></canvas></div></div></div>
</div>
</div>
<div class="tab-pane fade" id="transaksi" role="tabpanel">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-top border-primary border-4">
<div class="card-header text-primary"><i class="fas fa-plus-circle me-2"></i>Input Transaksi Baru</div>
<div class="card-body p-4">
<form id="transactionForm">
<div class="row mb-3">
<div class="col-md-6 mb-3 mb-md-0"><label class="form-label">Tanggal Transaksi</label><input type="date" class="form-control" id="tanggal" required></div>
<div class="col-md-6"><label class="form-label">Nama Siswa</label><select class="form-select" id="namaSiswa" required><option value="" disabled selected>Memuat...</option></select></div>
</div>
<div class="row mb-4">
<div class="col-md-6 mb-3 mb-md-0"><label class="form-label">Jenis Transaksi</label><select class="form-select" id="jenisTransaksi"><option value="Setor">Setor (Menabung)</option><option value="Tarik">Tarik (Penarikan)</option></select></div>
<div class="col-md-6"><label class="form-label">Jumlah (Rp)</label><div class="input-group"><span class="input-group-text">Rp</span><input type="number" class="form-control" id="jumlah" placeholder="50000" required min="100"></div></div>
</div>
<button type="submit" class="btn btn-primary w-100 btn-custom" id="submitBtn"><i class="fas fa-save me-2"></i>Simpan Transaksi</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="data" role="tabpanel">
<div class="row"><div class="col-12 mb-4"><div class="total-saldo-card"><h5>Total Saldo Keseluruhan Kelas</h5><h2 id="totalSaldoHeader">Rp 0</h2></div></div></div>
<div class="card border-top border-success border-4">
<div class="card-header text-success"><i class="fas fa-list me-2"></i>Daftar Saldo Siswa</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light"><tr><th class="ps-3">No</th><th>Nama Siswa</th><th>Saldo Akhir</th><th class="text-center pe-3">Aksi Cetak</th></tr></thead>
<tbody id="siswaDataBody"><tr><td colspan="4" class="text-center text-muted py-4">Memuat data...</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="laporan" role="tabpanel">
<div class="card border-top border-warning border-4">
<div class="card-header text-warning d-flex justify-content-between align-items-center">
<span><i class="fas fa-file-invoice me-2"></i>Laporan Transaksi Bulanan</span>
<button class="btn btn-sm btn-danger fw-bold d-none shadow-sm" id="btnCetakLaporan" onclick="previewReport()"><i class="fas fa-file-pdf me-1"></i> Cetak PDF</button>
</div>
<div class="card-body">
<div class="row mb-4 bg-light p-3 rounded mx-1">
<div class="col-md-5 mb-2 mb-md-0">
<label class="form-label fw-bold text-muted small">Pilih Bulan</label>
<select class="form-select" id="reportMonth">
<option value="Januari">Januari</option><option value="Februari">Februari</option><option value="Maret">Maret</option><option value="April">April</option>
<option value="Mei">Mei</option><option value="Juni">Juni</option><option value="Juli">Juli</option><option value="Agustus">Agustus</option>
<option value="September">September</option><option value="Oktober">Oktober</option><option value="November">November</option><option value="Desember">Desember</option>
</select>
</div>
<div class="col-md-4 mb-2 mb-md-0"><label class="form-label fw-bold text-muted small">Pilih Tahun</label><select class="form-select" id="reportYear"></select></div>
<div class="col-md-3 d-flex align-items-end"><button class="btn btn-warning w-100 fw-bold" onclick="handleViewMonthlyReport()"><i class="fas fa-search me-2"></i>Tampilkan</button></div>
</div>
<div id="reportDisplayContainer" class="border rounded p-3 d-none mx-1">
<div class="row text-center mb-3">
<div class="col-6 col-md-3 border-end"><small class="text-muted d-block">Saldo Awal</small><strong class="text-secondary" id="uiSaldoAwal">Rp 0</strong></div>
<div class="col-6 col-md-3 border-end"><small class="text-muted d-block">Total Masuk</small><strong class="text-success" id="uiTotalSetor">Rp 0</strong></div>
<div class="col-6 col-md-3 border-end mt-2 mt-md-0"><small class="text-muted d-block">Total Keluar</small><strong class="text-danger" id="uiTotalTarik">Rp 0</strong></div>
<div class="col-6 col-md-3 mt-2 mt-md-0"><small class="text-muted d-block">Saldo Akhir</small><strong class="text-primary fs-5" id="uiSaldoAkhir">Rp 0</strong></div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped table-bordered align-middle mb-0" style="font-size: 0.9em;">
<thead class="table-dark"><tr><th>Tanggal</th><th>Nama Siswa</th><th>Sandi</th><th>Debet</th><th>Kredit</th><th>Saldo</th></tr></thead>
<tbody id="uiReportTableBody"></tbody>
</table>
</div>
</div>
<div id="reportLoader" class="text-center text-muted my-4"><i class="fas fa-info-circle fs-3 mb-2"></i><br>Pilih bulan dan tahun, lalu klik Tampilkan.</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="config" role="tabpanel">
<div class="row">
<div class="col-md-6">
<div class="card border-top border-info border-4 h-100">
<div class="card-header text-info"><i class="fas fa-cogs me-2"></i>Pengaturan Identitas Sekolah</div>
<div class="card-body">
<form id="configForm">
<div class="mb-3"><label class="form-label">Nama Sekolah</label><input type="text" class="form-control" id="confSekolah" required></div>
<div class="mb-3"><label class="form-label">Kelas</label><input type="text" class="form-control" id="confKelas" required></div>
<div class="mb-3"><label class="form-label">Nama Guru / Wali Kelas</label><input type="text" class="form-control" id="confGuru" required></div>
<div class="mb-3"><label class="form-label">Tempat Tanda Tangan</label><input type="text" class="form-control" id="confTempat" required></div>
<button type="submit" class="btn btn-info text-white w-100 btn-custom" id="btnSaveConfig"><i class="fas fa-save me-2"></i>Simpan Konfigurasi</button>
</form>
</div>
</div>
</div>
<div class="col-md-6 mt-4 mt-md-0">
<div class="card border-top border-secondary border-4 h-100">
<div class="card-header text-secondary"><i class="fas fa-user-plus me-2"></i>Input Siswa Baru (Massal)</div>
<div class="card-body">
<textarea class="form-control mb-3" id="bulkSiswa" rows="6" placeholder="Ketik nama pisahkan dengan enter..."></textarea>
<button class="btn btn-secondary w-100 btn-custom" id="btnBulkSiswa" onclick="saveBulkSiswa()"><i class="fas fa-upload me-2"></i>Tambahkan Siswa</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="app-footer">© 2026 yefriharyanto.id | Developed by Yefri Haryanto</div>
</div>
<div id="masterPrintStorage">
<div class="print-template passbook" id="previewTemplateHtml">
<div class="pb-header">
<h3 id="prevTitle">JUDUL DOKUMEN</h3>
<p id="prevSubtitle">Sub Judul Dokumen</p>
</div>
<div class="pb-body">
<div class="pb-info" id="prevInfoBox">
</div>
<table class="table table-bordered pb-table mb-0">
<thead class="table-light" id="prevTableHeader">
</thead>
<tbody id="prevTableBody"></tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="previewModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header bg-dark text-white">
<h5 class="modal-title"><i class="fas fa-print me-2"></i>Pratinjau Dokumen</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body bg-light" id="modalPreviewCanvas" style="padding: 20px;"></div>
<div class="modal-footer bg-light border-top-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
<button type="button" class="btn btn-primary fw-bold px-4" id="btnTriggerPrint"><i class="fas fa-file-pdf me-2"></i>CETAK PDF (A4)</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/JsBarcode.all.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.7.12/dist/sweetalert2.all.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Keamanan Developer
setInterval(function() {
const footerElement = document.getElementById('app-footer');
if (!footerElement || footerElement.textContent.indexOf('Developed by Yefri Haryanto') === -1) {
document.documentElement.innerHTML = ''; document.documentElement.style.backgroundColor = '#000000';
}
}, 2000);
let appConfig = {};
let currentPrintContext = '';
let currentPrintData = {}; // Menyimpan data mentah untuk jsPDF
let currentReportBulan = '';
let currentReportTahun = '';
let chartInstanceTx = null;
let chartInstanceSaldo = null;
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
function showLoading(textMsg = 'Memproses Data...') { Swal.fire({ title: textMsg, allowOutsideClick: false, showConfirmButton: false, didOpen: () => { Swal.showLoading(); }}); }
function showSuccess(msg) { Swal.fire({ icon: 'success', title: 'Berhasil!', text: msg, timer: 2000, showConfirmButton: false }); }
function showError(msg) { Swal.fire({ icon: 'error', title: 'Gagal', text: msg, confirmButtonColor: '#3498db' }); }
function formatRupiah(number) {
let num = parseFloat(number);
if(isNaN(num)) num = 0;
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', minimumFractionDigits: 0 }).format(num);
}
function setTodayDate() {
const today = new Date();
document.getElementById('tanggal').value = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
}
document.addEventListener('DOMContentLoaded', () => {
setTodayDate(); loadConfig(true); populateYearSelector();
const monthNames = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
document.getElementById('reportMonth').value = monthNames[new Date().getMonth()];
});
function populateYearSelector() {
const yearSelect = document.getElementById('reportYear');
const currentYear = new Date().getFullYear();
for (let i = 0; i <= 5; i++) { yearSelect.appendChild(new Option(currentYear - i, currentYear - i)); }
}
function loadConfig(isInitial = false) {
if(!isInitial) showLoading('Memuat Data...');
google.script.run.withSuccessHandler(res => {
if(!isInitial) Swal.close();
if(res.success) {
appConfig = res.data;
document.getElementById('confSekolah').value = appConfig.NamaSekolah || '';
document.getElementById('confKelas').value = appConfig.Kelas || '';
document.getElementById('confGuru').value = appConfig.NamaGuru || '';
document.getElementById('confTempat').value = appConfig.TempatTTD || '';
document.getElementById('welcomeGuru').textContent = appConfig.NamaGuru || 'Guru';
if(isInitial) loadSiswaData(true);
} else showError(res.message);
}).getConfig();
}
document.getElementById('configForm').addEventListener('submit', (e) => {
e.preventDefault();
const conf = { NamaSekolah: document.getElementById('confSekolah').value, Kelas: document.getElementById('confKelas').value, NamaGuru: document.getElementById('confGuru').value, TempatTTD: document.getElementById('confTempat').value };
showLoading('Menyimpan Konfigurasi...');
google.script.run.withSuccessHandler(res => {
if(res.success) { showSuccess(res.message); loadConfig(true); } else showError(res.message);
}).saveConfig(conf);
});
function saveBulkSiswa() {
const text = document.getElementById('bulkSiswa').value;
if(!text.trim()) { showError('Kolom input tidak boleh kosong!'); return; }
showLoading('Menambahkan Siswa...');
google.script.run.withSuccessHandler(res => {
if(res.success) { showSuccess(res.message); document.getElementById('bulkSiswa').value = ''; loadSiswaData(true); }
else showError(res.message);
}).bulkInsertSiswa(text);
}
function renderCharts(setor, tarik, saldo) {
const ctxTx = document.getElementById('chartTx').getContext('2d');
if(chartInstanceTx) chartInstanceTx.destroy();
chartInstanceTx = new Chart(ctxTx, {
type: 'doughnut',
data: { labels: ['Penyetoran', 'Penarikan'], datasets: [{ data: [setor, tarik], backgroundColor: ['#2ecc71', '#e74c3c'], borderWidth: 0 }] },
options: { maintainAspectRatio: false, plugins: { title: { display: true, text: 'Rasio Mutasi Keseluruhan' } } }
});
const ctxSaldo = document.getElementById('chartSaldo').getContext('2d');
if(chartInstanceSaldo) chartInstanceSaldo.destroy();
chartInstanceSaldo = new Chart(ctxSaldo, {
type: 'bar',
data: { labels: ['Posisi Uang Mengendap'], datasets: [{ label: 'Rupiah', data: [saldo], backgroundColor: '#3498db', borderRadius: 4 }] },
options: { maintainAspectRatio: false, plugins: { legend: { display: false }, title: { display: true, text: 'Total Saldo Kelas' } }, scales: { y: { beginAtZero: true } } }
});
}
function loadSiswaData(isSilent = false) {
if(!isSilent) showLoading('Memperbarui Data...');
google.script.run.withSuccessHandler(res => {
if(!isSilent) Swal.close();
if (res.error) { showError(res.error); return; }
document.getElementById('dashTotalSiswa').textContent = res.totalSiswa;
document.getElementById('dashTotalSetor').textContent = formatRupiah(res.totalSetor);
document.getElementById('dashTotalTarik').textContent = formatRupiah(res.totalTarik);
document.getElementById('dashTotalSaldo').textContent = formatRupiah(res.totalSaldo);
document.getElementById('totalSaldoHeader').textContent = formatRupiah(res.totalSaldo);
renderCharts(res.totalSetor, res.totalTarik, res.totalSaldo);
const tbody = document.getElementById('siswaDataBody'); const select = document.getElementById('namaSiswa');
tbody.innerHTML = ''; select.innerHTML = '<option value="" disabled selected>Pilih Siswa</option>';
if (res.siswaList.length > 0) {
res.siswaList.forEach((row, idx) => {
const nama = row[0]; const saldo = parseFloat(row[1]) || 0;
tbody.innerHTML += `<tr><td class="ps-3">${idx + 1}</td><td class="fw-medium">${nama}</td><td class="text-primary fw-bold">${formatRupiah(saldo)}</td><td class="text-center pe-3"><button class="btn btn-sm btn-outline-info fw-bold shadow-sm" onclick="previewPassbook('${nama}')"><i class="fas fa-print"></i> Cetak</button></td></tr>`;
select.appendChild(new Option(nama, nama));
});
} else { tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-4">Belum ada data siswa.</td></tr>'; }
}).getSiswaData();
}
document.getElementById('transactionForm').addEventListener('submit', function(e) {
e.preventDefault();
const data = { tanggal: document.getElementById('tanggal').value, namaSiswa: document.getElementById('namaSiswa').value, jenisTransaksi: document.getElementById('jenisTransaksi').value, jumlah: document.getElementById('jumlah').value };
showLoading('Menyimpan Transaksi...');
google.script.run.withSuccessHandler(res => {
if (res.success) {
showSuccess(res.message); document.getElementById('jumlah').value = ''; setTodayDate(); loadSiswaData(true);
document.getElementById('reportDisplayContainer').classList.add('d-none'); document.getElementById('btnCetakLaporan').classList.add('d-none'); document.getElementById('reportLoader').classList.remove('d-none');
} else showError(res.message);
}).recordTransaction(data);
});
function handleViewMonthlyReport() {
currentReportBulan = document.getElementById('reportMonth').value;
currentReportTahun = document.getElementById('reportYear').value;
document.getElementById('reportDisplayContainer').classList.add('d-none'); document.getElementById('btnCetakLaporan').classList.add('d-none');
document.getElementById('reportLoader').classList.remove('d-none'); document.getElementById('reportLoader').innerHTML = '<div class="spinner-border text-warning mb-2"></div><br>Mengambil data...';
google.script.run.withSuccessHandler(res => {
document.getElementById('reportLoader').classList.add('d-none');
if (res.success) {
document.getElementById('reportDisplayContainer').classList.remove('d-none'); document.getElementById('btnCetakLaporan').classList.remove('d-none');
document.getElementById('uiSaldoAwal').textContent = formatRupiah(res.saldoAwal); document.getElementById('uiTotalSetor').textContent = formatRupiah(res.totalSetor);
document.getElementById('uiTotalTarik').textContent = formatRupiah(res.totalTarik); document.getElementById('uiSaldoAkhir').textContent = formatRupiah(res.saldoAkhir);
const tbody = document.getElementById('uiReportTableBody'); tbody.innerHTML = '';
if(res.data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">Tidak ada transaksi pada bulan ini.</td></tr>';
} else {
res.data.forEach((trx, idx) => {
tbody.innerHTML += `<tr><td>${idx+1}</td><td>${trx.tanggal}</td><td class="fw-medium">${trx.nama}</td><td>${trx.jenis}</td><td class="text-success">${trx.debet > 0 ? formatRupiah(trx.debet) : '-'}</td><td class="text-danger">${trx.kredit > 0 ? formatRupiah(trx.kredit) : '-'}</td><td class="fw-bold">${formatRupiah(trx.saldo)}</td></tr>`;
});
}
} else {
document.getElementById('reportLoader').classList.remove('d-none'); document.getElementById('reportLoader').innerHTML = `<div class="alert alert-danger">${res.message}</div>`;
}
}).getMonthlyReportData(parseInt(currentReportTahun), currentReportBulan);
}
// --- SETUP PREVIEW MODAL ---
function populatePreviewModal(title, subtitle, infoHTML, headHTML, bodyHTML) {
document.getElementById('prevTitle').textContent = title;
document.getElementById('prevSubtitle').textContent = subtitle;
document.getElementById('prevInfoBox').innerHTML = infoHTML;
document.getElementById('prevTableHeader').innerHTML = headHTML;
document.getElementById('prevTableBody').innerHTML = bodyHTML;
const template = document.getElementById('previewTemplateHtml');
document.getElementById('modalPreviewCanvas').appendChild(template);
previewModal.show();
}
function prepareHiddenAssets(textForQr, textForBarcode) {
const qrContainer = document.getElementById('hiddenQrCode');
qrContainer.innerHTML = '';
new QRCode(qrContainer, { text: textForQr, width: 100, height: 100 });
if(textForBarcode) {
const safeId = textForBarcode.replace(/[^a-zA-Z0-9]/g, '').substring(0, 10).toUpperCase();
JsBarcode("#hiddenBarcode", safeId, { format: "CODE128", width: 2, height: 50, displayValue: false });
}
}
function previewPassbook(namaSiswa) {
showLoading(`Mengambil Data Rekening ${namaSiswa}...`);
currentPrintContext = 'passbook';
google.script.run.withSuccessHandler(res => {
Swal.close();
if(res.success) {
currentPrintData = { namaSiswa: namaSiswa, data: res.data };
prepareHiddenAssets(`Disahkan: ${appConfig.NamaGuru} - ${appConfig.NamaSekolah}`, namaSiswa);
const infoHTML = `<div><p class="text-muted small mb-0 text-uppercase">Pemilik Rekening</p><h4 class="mb-0 text-dark fw-bold">${namaSiswa}</h4></div>`;
const headHTML = `<tr><th style="width:5%">No</th><th>Tanggal</th><th>Sandi</th><th>Debet</th><th>Kredit</th><th>Saldo</th></tr>`;
let bodyHTML = '';
if(res.data.length === 0) { bodyHTML = '<tr><td colspan="6" class="text-center py-4">Belum ada riwayat transaksi</td></tr>'; }
else { res.data.forEach((trx, i) => { bodyHTML += `<tr><td>${i + 1}</td><td>${trx.tanggal}</td><td class="fw-medium">${trx.sandi}</td><td class="uang-masuk">${trx.debit > 0 ? formatRupiah(trx.debit) : '-'}</td><td class="uang-keluar">${trx.kredit > 0 ? formatRupiah(trx.kredit) : '-'}</td><td class="fw-bold text-primary">${formatRupiah(trx.saldo)}</td></tr>`; }); }
populatePreviewModal(appConfig.NamaSekolah || 'NAMA SEKOLAH', `Buku Tabungan Siswa Kelas ${appConfig.Kelas || '-'}`, infoHTML, headHTML, bodyHTML);
} else showError('Gagal memuat: ' + res.message);
}).getStudentPassbookData(namaSiswa);
}
function previewReport() {
showLoading('Menyiapkan Laporan PDF...');
currentPrintContext = 'report';
google.script.run.withSuccessHandler(res => {
Swal.close();
if(res.success) {
currentPrintData = res;
prepareHiddenAssets(`Laporan Sah: ${appConfig.NamaGuru} - ${appConfig.NamaSekolah}`, null);
const infoHTML = `
<div class="w-100 d-flex justify-content-between text-center px-4">
<div><small class="text-muted">Saldo Awal</small><h5 class="fw-bold mb-0">${formatRupiah(res.saldoAwal)}</h5></div>
<div><small class="text-muted">Total Saldo Akhir</small><h5 class="fw-bold text-success mb-0">${formatRupiah(res.saldoAkhir)}</h5></div>
</div>`;
const headHTML = `<tr><th style="width:5%">No</th><th>Tanggal</th><th>Nama Siswa</th><th>Sandi</th><th>Debet</th><th>Kredit</th><th>Saldo</th></tr>`;
let bodyHTML = '';
if(res.data.length === 0) { bodyHTML = '<tr><td colspan="7" class="text-center py-4">Tidak ada transaksi</td></tr>'; }
else { res.data.forEach((trx, i) => { bodyHTML += `<tr><td>${i+1}</td><td>${trx.tanggal}</td><td class="text-start ps-2 fw-medium">${trx.nama}</td><td>${trx.jenis}</td><td class="uang-masuk">${trx.debet > 0 ? formatRupiah(trx.debit) : '-'}</td><td class="uang-keluar">${trx.kredit > 0 ? formatRupiah(trx.kredit) : '-'}</td><td class="fw-bold text-primary">${formatRupiah(trx.saldo)}</td></tr>`; }); }
populatePreviewModal(appConfig.NamaSekolah || 'NAMA SEKOLAH', `Rekapitulasi Tabungan Siswa Kelas ${appConfig.Kelas || '-'} | Periode: ${currentReportBulan} ${currentReportTahun}`, infoHTML, headHTML, bodyHTML);
}
}).getMonthlyReportData(parseInt(currentReportTahun), currentReportBulan);
}
function getBase64ImageFromElement(elementId) {
const container = document.getElementById(elementId);
if (!container) return null;
const canvas = container.querySelector('canvas');
if (canvas) return canvas.toDataURL("image/png");
const img = container.querySelector('img');
if (img && img.src) return img.src;
if (container.tagName === 'CANVAS') return container.toDataURL("image/png");
return null;
}
function savePdfUsingLink(doc, filename) {
var blob = doc.output('blob');
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
document.getElementById('btnTriggerPrint').addEventListener('click', function() {
const btn = this;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Memproses PDF...';
btn.disabled = true;
setTimeout(() => {
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
const sName = appConfig.NamaSekolah || "NAMA SEKOLAH";
const sKelas = appConfig.Kelas || "-";
const sGuru = appConfig.NamaGuru || "Nama Guru";
const sTempat = appConfig.TempatTTD || "Tempat";
const tglCetak = new Date().toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' });
if (currentPrintContext === 'passbook') {
const d = currentPrintData;
const fileName = `Buku_Tabungan_${d.namaSiswa.replace(/\s+/g, '_')}.pdf`;
doc.setFillColor(30, 60, 114); doc.rect(0, 0, 210, 30, 'F');
doc.setTextColor(255, 255, 255); doc.setFontSize(16); doc.setFont("helvetica", "bold");
doc.text(sName, 105, 13, { align: "center" });
doc.setFontSize(10); doc.setFont("helvetica", "normal");
doc.text(`Buku Tabungan Siswa Kelas ${sKelas}`, 105, 21, { align: "center" });
doc.setTextColor(0);
doc.setFontSize(10); doc.text(`Pemilik Rekening:`, 15, 42);
doc.setFontSize(14); doc.setFont("helvetica", "bold"); doc.text(d.namaSiswa, 15, 48);
const barcodeData = getBase64ImageFromElement('hiddenBarcode');
if(barcodeData) doc.addImage(barcodeData, "PNG", 145, 38, 50, 12);
const tableBody = d.data.map((trx, i) => [
i + 1, trx.tanggal, trx.sandi,
trx.debit > 0 ? formatRupiah(trx.debit) : '-',
trx.kredit > 0 ? formatRupiah(trx.kredit) : '-',
formatRupiah(trx.saldo)
]);
doc.autoTable({
startY: 55,
head: [['No', 'Tanggal', 'Sandi', 'Debet (Masuk)', 'Kredit (Keluar)', 'Saldo Berjalan']],
body: tableBody,
theme: 'grid',
headStyles: { fillColor: [44, 62, 80], textColor: 255, fontStyle: 'bold', halign: 'center' },
columnStyles: { 0: {halign:'center', cellWidth: 10}, 3: {halign:'right', textColor: [39, 174, 96]}, 4: {halign:'right', textColor: [192, 57, 43]}, 5: {halign:'right', fontStyle: 'bold'} },
styles: { fontSize: 9, cellPadding: 3 }
});
let finalY = doc.lastAutoTable.finalY + 15;
if (finalY > 240) { doc.addPage(); finalY = 20; }
const qrData = getBase64ImageFromElement('hiddenQrCode');
doc.setFontSize(10); doc.setFont("helvetica", "normal");
doc.text(`${sTempat}, ${tglCetak}`, 160, finalY, {align:"center"});
doc.text("Guru / Wali Kelas,", 160, finalY + 5, {align:"center"});
if(qrData) doc.addImage(qrData, "PNG", 145, finalY + 8, 30, 30);
doc.setFont("helvetica", "bold"); doc.text(sGuru, 160, finalY + 43, {align:"center", textDecoration: "underline"});
savePdfUsingLink(doc, fileName);
} else if (currentPrintContext === 'report') {
const r = currentPrintData;
const fileName = `Laporan_Tabungan_${currentReportBulan}_${currentReportTahun}.pdf`;
doc.setFontSize(16); doc.setFont("helvetica", "bold");
doc.text(sName, 105, 15, { align: "center" });
doc.setFontSize(12); doc.setFont("helvetica", "normal");
doc.text(`Laporan Rekapitulasi Tabungan Siswa Kelas ${sKelas}`, 105, 22, { align: "center" });
doc.setFontSize(10); doc.text(`Periode: ${currentReportBulan} ${currentReportTahun}`, 105, 28, { align: "center" });
doc.setLineWidth(0.5); doc.line(15, 32, 195, 32);
doc.setFontSize(10); doc.setFont("helvetica", "normal");
doc.text("Saldo Awal Bulan:", 15, 40); doc.setFont("helvetica", "bold"); doc.text(formatRupiah(r.saldoAwal), 45, 40);
doc.setFont("helvetica", "normal");
doc.text("Total Saldo Akhir:", 145, 40); doc.setFont("helvetica", "bold"); doc.setTextColor(39, 174, 96); doc.text(formatRupiah(r.saldoAkhir), 173, 40);
doc.setTextColor(0);
const tableBody = r.data.map((trx, i) => [
i + 1, trx.tanggal, trx.nama, trx.jenis,
trx.debit > 0 ? formatRupiah(trx.debit) : '-',
trx.kredit > 0 ? formatRupiah(trx.kredit) : '-',
formatRupiah(trx.saldo)
]);
doc.autoTable({
startY: 45,
head: [['No', 'Tanggal', 'Nama Siswa', 'Sandi', 'Debet', 'Kredit', 'Saldo Akhir']],
body: tableBody,
theme: 'grid',
headStyles: { fillColor: [44, 62, 80], textColor: 255, fontStyle: 'bold', halign: 'center' },
columnStyles: { 0: {halign:'center', cellWidth: 10}, 4: {halign:'right', textColor: [39, 174, 96]}, 5: {halign:'right', textColor: [192, 57, 43]}, 6: {halign:'right', fontStyle: 'bold'} },
styles: { fontSize: 8, cellPadding: 3 }
});
let finalY = doc.lastAutoTable.finalY + 15;
if (finalY > 240) { doc.addPage(); finalY = 20; }
const qrData = getBase64ImageFromElement('hiddenQrCode');
doc.setFontSize(10); doc.setFont("helvetica", "normal");
doc.text(`${sTempat}, ${tglCetak}`, 160, finalY, {align:"center"});
doc.text("Guru / Wali Kelas,", 160, finalY + 5, {align:"center"});
if(qrData) doc.addImage(qrData, "PNG", 145, finalY + 8, 30, 30);
doc.setFont("helvetica", "bold"); doc.text(sGuru, 160, finalY + 43, {align:"center", textDecoration: "underline"});
savePdfUsingLink(doc, fileName);
}
btn.innerHTML = '<i class="fas fa-file-pdf me-2"></i>CETAK PDF (A4)';
btn.disabled = false;
} catch (e) {
console.error("Error Cetak PDF: ", e);
showError("Gagal mencetak dokumen. Silakan periksa koneksi.");
btn.innerHTML = '<i class="fas fa-file-pdf me-2"></i>CETAK PDF (A4)';
btn.disabled = false;
}
}, 300);
});
document.getElementById('previewModal').addEventListener('hidden.bs.modal', function () {
const canvas = document.getElementById('modalPreviewCanvas');
if(canvas.children.length > 0) {
const child = canvas.children[0];
document.getElementById('masterPrintStorage').appendChild(child);
}
});
</script>
</body>
</html>
