1.58 59
7.26 35
11.31 27
13.37 85
59352785
Code.gs
// ==================== KONFIGURASI ====================
const SPREADSHEET_ID = '1FTLbiPwBR9y4x63RhsdlR5jfKimejhPt3AMz1kyFabE';// <<< GANTI DENGAN ID SPREADSHEET ANDA
const DRIVE_FOLDER_ID = '1E3YGMUwzqf8yFkle7EsFrAv9_DekXdAo'; // <<< GANTI DENGAN ID FOLDER GOOGLE DRIVE ANDA
function getSpreadsheet() {
return SpreadsheetApp.openById(SPREADSHEET_ID);
}
// ==================== INITIALIZE SHEETS ====================
/**
* Fungsi untuk membuat sheet dan mengisi data sampel
* Jalankan fungsi ini HANYA SEKALI saat pertama kali setup
*/
function initializeSheets() {
const ss = getSpreadsheet();
// ========== SHEET USERS ==========
let usersSheet = ss.getSheetByName('Users');
if (usersSheet) {
ss.deleteSheet(usersSheet);
}
usersSheet = ss.insertSheet('Users');
// Header
usersSheet.getRange('A1:D1').setValues([['email', 'password', 'role', 'nama']]);
usersSheet.getRange('A1:D1').setFontWeight('bold').setBackground('#1e40af').setFontColor('#ffffff');
// Data sampel
const usersData = [
['admin@perpus.com', 'admin123', 'Admin', 'Administrator Perpustakaan'],
['guru@sekolah.com', 'guru123', 'Admin', 'Ibu Siti Rahayu'],
['siswa1@sekolah.com', 'siswa123', 'Siswa', 'Ahmad Rizki Maulana'],
['siswa2@sekolah.com', 'siswa123', 'Siswa', 'Siti Nurhaliza'],
['siswa3@sekolah.com', 'siswa123', 'Siswa', 'Budi Santoso'],
['siswa4@sekolah.com', 'siswa123', 'Siswa', 'Dewi Lestari'],
['siswa5@sekolah.com', 'siswa123', 'Siswa', 'Rian Pratama']
];
usersSheet.getRange(2, 1, usersData.length, 4).setValues(usersData);
usersSheet.setFrozenRows(1);
usersSheet.autoResizeColumns(1, 4);
// ========== SHEET BOOKS ==========
let booksSheet = ss.getSheetByName('Books');
if (booksSheet) {
ss.deleteSheet(booksSheet);
}
booksSheet = ss.insertSheet('Books');
// Header
booksSheet.getRange('A1:E1').setValues([['id', 'judul', 'penulis', 'stok', 'coverURL']]);
booksSheet.getRange('A1:E1').setFontWeight('bold').setBackground('#1e40af').setFontColor('#ffffff');
// Data sampel
const booksData = [
['BK001', 'Laskar Pelangi', 'Andrea Hirata', 8, 'https://images.tokopedia.net/img/cache/500-square/VqbcmM/2021/5/28/d7e6c0e5-c6e7-4e7e-8e0d-0e5c7f4d6e7d.jpg'],
['BK002', 'Bumi Manusia', 'Pramoedya Ananta Toer', 5, 'https://cdn.gramedia.com/uploads/items/9789799731234.jpg'],
['BK003', 'Sang Pemimpi', 'Andrea Hirata', 6, 'https://cdn.gramedia.com/uploads/items/Sang_Pemimpi.jpg'],
['BK004', 'Negeri 5 Menara', 'Ahmad Fuadi', 7, 'https://cdn.gramedia.com/uploads/items/9786020331188_Negeri-5-Menara.jpg'],
['BK005', 'Perahu Kertas', 'Dee Lestari', 4, 'https://cdn.gramedia.com/uploads/items/9786024246945_Perahu-Kertas.jpg'],
['BK006', 'Ronggeng Dukuh Paruk', 'Ahmad Tohari', 5, 'https://cdn.gramedia.com/uploads/items/9789799101488.jpg'],
['BK007', 'Ayat-Ayat Cinta', 'Habiburrahman El Shirazy', 10, 'https://cdn.gramedia.com/uploads/items/9789793062792.jpg'],
['BK008', 'Saman', 'Ayu Utami', 3, 'https://cdn.gramedia.com/uploads/items/9786024803520.jpg'],
['BK009', 'Pulang', 'Leila S. Chudori', 6, 'https://cdn.gramedia.com/uploads/items/9786024246037.jpg'],
['BK010', 'Cantik Itu Luka', 'Eka Kurniawan', 4, 'https://cdn.gramedia.com/uploads/items/9786024246105.jpg'],
['BK011', 'The Da Vinci Code', 'Dan Brown', 5, 'https://cdn.gramedia.com/uploads/items/9780307474278.jpg'],
['BK012', 'Harry Potter and the Philosopher Stone', 'J.K. Rowling', 8, 'https://cdn.gramedia.com/uploads/items/9781408855652.jpg'],
['BK013', 'The Alchemist', 'Paulo Coelho', 7, 'https://cdn.gramedia.com/uploads/items/9780062315007.jpg'],
['BK014', '1984', 'George Orwell', 6, 'https://cdn.gramedia.com/uploads/items/9780451524935.jpg'],
['BK015', 'To Kill a Mockingbird', 'Harper Lee', 5, 'https://cdn.gramedia.com/uploads/items/9780061120084.jpg']
];
booksSheet.getRange(2, 1, booksData.length, 5).setValues(booksData);
booksSheet.setFrozenRows(1);
booksSheet.autoResizeColumns(1, 5);
// ========== SHEET BORROW ==========
let borrowSheet = ss.getSheetByName('Borrow');
if (borrowSheet) {
ss.deleteSheet(borrowSheet);
}
borrowSheet = ss.insertSheet('Borrow');
// Header
borrowSheet.getRange('A1:G1').setValues([['id', 'siswa', 'bukuId', 'judulBuku', 'tanggalPinjam', 'tanggalKembali', 'status']]);
borrowSheet.getRange('A1:G1').setFontWeight('bold').setBackground('#1e40af').setFontColor('#ffffff');
// Data sampel peminjaman
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const twoDaysAgo = new Date(today);
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const threeDaysAgo = new Date(today);
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
const weekAgo = new Date(today);
weekAgo.setDate(weekAgo.getDate() - 7);
const borrowData = [
['BR001', 'siswa1@sekolah.com', 'BK001', 'Laskar Pelangi', Utilities.formatDate(threeDaysAgo, Session.getScriptTimeZone(), 'dd/MM/yyyy'), '', 'Pending'],
['BR002', 'siswa2@sekolah.com', 'BK002', 'Bumi Manusia', Utilities.formatDate(twoDaysAgo, Session.getScriptTimeZone(), 'dd/MM/yyyy'), '', 'Approved'],
['BR003', 'siswa3@sekolah.com', 'BK003', 'Sang Pemimpi', Utilities.formatDate(yesterday, Session.getScriptTimeZone(), 'dd/MM/yyyy'), '', 'Pending'],
['BR004', 'siswa1@sekolah.com', 'BK004', 'Negeri 5 Menara', Utilities.formatDate(weekAgo, Session.getScriptTimeZone(), 'dd/MM/yyyy'), Utilities.formatDate(yesterday, Session.getScriptTimeZone(), 'dd/MM/yyyy'), 'Returned'],
['BR005', 'siswa4@sekolah.com', 'BK005', 'Perahu Kertas', Utilities.formatDate(twoDaysAgo, Session.getScriptTimeZone(), 'dd/MM/yyyy'), '', 'Approved'],
['BR006', 'siswa5@sekolah.com', 'BK007', 'Ayat-Ayat Cinta', Utilities.formatDate(today, Session.getScriptTimeZone(), 'dd/MM/yyyy'), '', 'Pending']
];
borrowSheet.getRange(2, 1, borrowData.length, 7).setValues(borrowData);
borrowSheet.setFrozenRows(1);
borrowSheet.autoResizeColumns(1, 7);
// Pindahkan sheet ke urutan yang benar
ss.setActiveSheet(usersSheet);
ss.moveActiveSheet(1);
ss.setActiveSheet(booksSheet);
ss.moveActiveSheet(2);
ss.setActiveSheet(borrowSheet);
ss.moveActiveSheet(3);
// Hapus sheet default jika ada
const defaultSheet = ss.getSheetByName('Sheet1');
if (defaultSheet && ss.getSheets().length > 3) {
ss.deleteSheet(defaultSheet);
}
Logger.log('✅ INISIALISASI BERHASIL!');
Logger.log('Sheet Users, Books, dan Borrow telah dibuat dengan data sampel.');
Logger.log('');
Logger.log('Login Admin:');
Logger.log('• Email: admin@perpus.com');
Logger.log('• Password: admin123');
Logger.log('');
Logger.log('Login Siswa:');
Logger.log('• Email: siswa1@sekolah.com');
Logger.log('• Password: siswa123');
Logger.log('');
Logger.log('Silakan deploy aplikasi sebagai Web App!');
return 'Inisialisasi berhasil! Lihat log untuk detail.';
}
// ==================== RENDER HTML ====================
function doGet() {
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('Perpustakaan Digital')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
.addMetaTag("viewport", "width=device-width, initial-scale=1.0")
.setFaviconUrl('https://w7.pngwing.com/pngs/184/833/png-transparent-exam-test-checklist-online-learning-education-online-document-online-learning-icon.png');
}
// ==================== AUTENTIKASI ====================
function login(email, password) {
const ss = getSpreadsheet();
const usersSheet = ss.getSheetByName('Users');
const data = usersSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === email && data[i][1] === password) {
return {
success: true,
user: {
email: data[i][0],
role: data[i][2],
nama: data[i][3]
}
};
}
}
return { success: false, message: 'Email atau password salah' };
}
// ==================== FUNGSI BUKU ====================
function getBooks() {
const ss = getSpreadsheet();
const booksSheet = ss.getSheetByName('Books');
const data = booksSheet.getDataRange().getValues();
const books = [];
for (let i = 1; i < data.length; i++) {
books.push({
id: data[i][0],
judul: data[i][1],
penulis: data[i][2],
stok: data[i][3],
coverURL: data[i][4]
});
}
return books;
}
// --- FUNGSI BANTUAN UNTUK UPLOAD KE DRIVE ---
function processFileUpload(fileData) {
try {
let folder;
if (DRIVE_FOLDER_ID) {
try {
folder = DriveApp.getFolderById(DRIVE_FOLDER_ID);
} catch (e) {
folder = DriveApp.getRootFolder();
}
} else {
folder = DriveApp.getRootFolder();
}
const contentType = fileData.mimeType;
const check = contentType.split('/');
if (check[0] !== 'image') {
throw new Error('File harus berupa gambar');
}
// Decode base64 string ke blob
const decoded = Utilities.base64Decode(fileData.data);
const blob = Utilities.newBlob(decoded, contentType, fileData.fileName);
// Simpan file ke Drive
const file = folder.createFile(blob);
// PENTING: Set permission agar gambar bisa dilihat di tag <img> HTML
file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
// Mengembalikan URL yang bisa di-render oleh tag <img>
return "https://lh3.googleusercontent.com/d/" + file.getId();
} catch (error) {
throw new Error('Gagal upload gambar: ' + error.toString());
}
}
// --- MODIFIKASI FUNGSI ADD BOOK ---
function addBook(judul, penulis, stok, coverURL, fileData) {
const ss = getSpreadsheet();
const booksSheet = ss.getSheetByName('Books');
// Jika ada file yang diupload, proses upload dulu
let finalCoverURL = coverURL;
if (fileData && fileData.data) {
finalCoverURL = processFileUpload(fileData);
} else if (!finalCoverURL) {
// Default image jika tidak ada URL dan tidak ada upload
finalCoverURL = 'https://via.placeholder.com/200x300/667eea/ffffff?text=No+Cover';
}
const lastRow = booksSheet.getLastRow();
// Generate ID unik yang lebih aman
const newId = 'BK' + (lastRow + Math.floor(Math.random() * 1000)).toString().padStart(3, '0');
booksSheet.appendRow([newId, judul, penulis, stok, finalCoverURL]);
return { success: true, message: 'Buku berhasil ditambahkan' };
}
// --- MODIFIKASI FUNGSI UPDATE BOOK ---
function updateBook(id, judul, penulis, stok, coverURL, fileData) {
const ss = getSpreadsheet();
const booksSheet = ss.getSheetByName('Books');
const data = booksSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === id) {
// Logika URL Gambar:
// 1. Jika ada upload baru (fileData), pakai itu.
// 2. Jika tidak ada upload, tapi ada coverURL (dari input hidden/text), pakai itu (url lama).
let finalCoverURL = coverURL;
if (fileData && fileData.data) {
finalCoverURL = processFileUpload(fileData);
}
booksSheet.getRange(i + 1, 2, 1, 4).setValues([[judul, penulis, stok, finalCoverURL]]);
return { success: true, message: 'Buku berhasil diupdate' };
}
}
return { success: false, message: 'Buku tidak ditemukan' };
}
function deleteBook(id) {
const ss = getSpreadsheet();
const booksSheet = ss.getSheetByName('Books');
const data = booksSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === id) {
booksSheet.deleteRow(i + 1);
return { success: true, message: 'Buku berhasil dihapus' };
}
}
return { success: false, message: 'Buku tidak ditemukan' };
}
// ==================== FUNGSI SISWA ====================
function getStudents() {
const ss = getSpreadsheet();
const usersSheet = ss.getSheetByName('Users');
const data = usersSheet.getDataRange().getValues();
const students = [];
for (let i = 1; i < data.length; i++) {
if (data[i][2] === 'Siswa') {
students.push({
email: data[i][0],
nama: data[i][3]
});
}
}
return students;
}
function addStudent(email, password, nama) {
const ss = getSpreadsheet();
const usersSheet = ss.getSheetByName('Users');
usersSheet.appendRow([email, "'" + password, 'Siswa', nama]);
return { success: true, message: 'Siswa berhasil ditambahkan' };
}
function updateStudent(oldEmail, newEmail, password, nama) {
const ss = getSpreadsheet();
const usersSheet = ss.getSheetByName('Users');
const data = usersSheet.getDataRange().getValues();
let studentFound = false;
for (let i = 1; i < data.length; i++) {
if (data[i][0] === oldEmail && data[i][2] === 'Siswa') {
// Update nama dan password jika ada
if (password) {
usersSheet.getRange(i + 1, 2).setValue("'" + password);
}
usersSheet.getRange(i + 1, 4).setValue(nama);
// Jika email juga diubah, panggil fungsi terpisah
if (oldEmail !== newEmail) {
updateStudentEmail(oldEmail, newEmail);
}
studentFound = true;
return { success: true, message: 'Data siswa berhasil diupdate' };
}
}
if (!studentFound) {
return { success: false, message: 'Siswa tidak ditemukan' };
}
}
function updateStudentEmail(oldEmail, newEmail) {
const ss = getSpreadsheet();
const usersSheet = ss.getSheetByName('Users');
const borrowSheet = ss.getSheetByName('Borrow');
// 1. Update di Sheet Users
const usersData = usersSheet.getDataRange().getValues();
for (let i = 1; i < usersData.length; i++) {
if (usersData[i][0] === oldEmail && usersData[i][2] === 'Siswa') {
usersSheet.getRange(i + 1, 1).setValue(newEmail);
break;
}
}
// 2. Update di Sheet Borrow
const borrowData = borrowSheet.getDataRange().getValues();
for (let i = 1; i < borrowData.length; i++) {
if (borrowData[i][1] === oldEmail) {
borrowSheet.getRange(i + 1, 2).setValue(newEmail);
}
}
}
function deleteStudent(email) {
const ss = getSpreadsheet();
const usersSheet = ss.getSheetByName('Users');
const data = usersSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === email && data[i][2] === 'Siswa') {
usersSheet.deleteRow(i + 1);
return { success: true, message: 'Siswa berhasil dihapus' };
}
}
return { success: false, message: 'Siswa tidak ditemukan' };
}
// ==================== FUNGSI PEMINJAMAN ====================
function borrowBook(siswa, bukuId) {
const ss = getSpreadsheet();
const borrowSheet = ss.getSheetByName('Borrow');
const booksSheet = ss.getSheetByName('Books');
// Cek stok buku
const booksData = booksSheet.getDataRange().getValues();
let bookFound = false;
let bookTitle = '';
for (let i = 1; i < booksData.length; i++) {
if (booksData[i][0] === bukuId) {
bookFound = true;
bookTitle = booksData[i][1];
if (booksData[i][3] <= 0) {
return { success: false, message: 'Stok buku tidak tersedia' };
}
break;
}
}
if (!bookFound) {
return { success: false, message: 'Buku tidak ditemukan' };
}
const lastRow = borrowSheet.getLastRow();
const newId = 'BR' + (lastRow).toString().padStart(3, '0');
const today = new Date();
borrowSheet.appendRow([
newId,
siswa,
bukuId,
bookTitle,
Utilities.formatDate(today, Session.getScriptTimeZone(), 'dd/MM/yyyy'),
'',
'Pending'
]);
return { success: true, message: 'Pengajuan peminjaman berhasil diajukan' };
}
function getBorrowHistory(email, role) {
const ss = getSpreadsheet();
const borrowSheet = ss.getSheetByName('Borrow');
const data = borrowSheet.getDataRange().getDisplayValues();
const history = [];
for (let i = 1; i < data.length; i++) {
if (role === 'Siswa' && data[i][1] !== email) continue;
history.push({
id: data[i][0],
siswa: data[i][1],
bukuId: data[i][2],
judulBuku: data[i][3],
tanggalPinjam: data[i][4],
tanggalKembali: data[i][5],
status: data[i][6]
});
}
return history;
}
function approveBorrow(borrowId) {
const ss = getSpreadsheet();
const borrowSheet = ss.getSheetByName('Borrow');
const booksSheet = ss.getSheetByName('Books');
const data = borrowSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === borrowId) {
const bukuId = data[i][2];
// Kurangi stok buku
const booksData = booksSheet.getDataRange().getValues();
for (let j = 1; j < booksData.length; j++) {
if (booksData[j][0] === bukuId) {
const currentStok = booksData[j][3];
booksSheet.getRange(j + 1, 4).setValue(currentStok - 1);
break;
}
}
borrowSheet.getRange(i + 1, 7).setValue('Approved');
return { success: true, message: 'Peminjaman disetujui' };
}
}
return { success: false, message: 'Data peminjaman tidak ditemukan' };
}
function rejectBorrow(borrowId) {
const ss = getSpreadsheet();
const borrowSheet = ss.getSheetByName('Borrow');
const data = borrowSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === borrowId) {
borrowSheet.getRange(i + 1, 7).setValue('Rejected');
return { success: true, message: 'Peminjaman ditolak' };
}
}
return { success: false, message: 'Data peminjaman tidak ditemukan' };
}
function returnBook(borrowId) {
const ss = getSpreadsheet();
const borrowSheet = ss.getSheetByName('Borrow');
const booksSheet = ss.getSheetByName('Books');
const data = borrowSheet.getDataRange().getValues();
for (let i = 1; i < data.length; i++) {
if (data[i][0] === borrowId) {
const bukuId = data[i][2];
const today = new Date();
// Tambah stok buku
const booksData = booksSheet.getDataRange().getValues();
for (let j = 1; j < booksData.length; j++) {
if (booksData[j][0] === bukuId) {
const currentStok = booksData[j][3];
booksSheet.getRange(j + 1, 4).setValue(currentStok + 1);
break;
}
}
borrowSheet.getRange(i + 1, 6).setValue(Utilities.formatDate(today, Session.getScriptTimeZone(), 'dd/MM/yyyy'));
borrowSheet.getRange(i + 1, 7).setValue('Returned');
return { success: true, message: 'Buku berhasil dikembalikan' };
}
}
return { success: false, message: 'Data peminjaman tidak ditemukan' };
}
// ==================== STATISTIK ====================
function getStatistics() {
const ss = getSpreadsheet();
const booksSheet = ss.getSheetByName('Books');
const borrowSheet = ss.getSheetByName('Borrow');
const usersSheet = ss.getSheetByName('Users');
const booksData = booksSheet.getDataRange().getValues();
const borrowData = borrowSheet.getDataRange().getDisplayValues();
const usersData = usersSheet.getDataRange().getValues();
let totalBooks = booksData.length - 1;
let totalStok = 0;
for (let i = 1; i < booksData.length; i++) {
totalStok += booksData[i][3];
}
let totalStudents = 0;
for (let i = 1; i < usersData.length; i++) {
if (usersData[i][2] === 'Siswa') {
totalStudents++;
}
}
let pending = 0;
let waitingReturn = 0;
let returned = 0;
let rejected = 0;
for (let i = 1; i < borrowData.length; i++) {
const status = borrowData[i][6];
if (status === 'Approved') {
waitingReturn++;
} else if (status === 'Pending') {
pending++;
} else if (status === 'Returned') {
returned++;
} else if (status === 'Rejected') {
rejected++;
}
}
return {
totalBooks: totalBooks,
totalStok: totalStok,
totalStudents: totalStudents,
pending: pending,
waitingReturn: waitingReturn,
returned: returned,
rejected: rejected
};
}
// ==================== FUNGSI OTORISASI ====================
/**
* Jalankan fungsi ini sekali dari editor Apps Script untuk memberikan izin akses ke Google Drive.
*/
function authorizeDrive() {
try {
DriveApp.getRootFolder();
Logger.log('✅ Otorisasi Google Drive berhasil!');
} catch (e) {
Logger.log('🛑 Gagal memberikan otorisasi: ' + e.toString());
Logger.log('Silakan jalankan fungsi ini lagi dan pastikan Anda menyetujui semua izin yang diminta.');
}
}
Index.html
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Perpustakaan Digital</title>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* ==================== LOGIN PAGE ==================== */
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
position: relative;
overflow: hidden;
}
.login-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('https://images.unsplash.com/photo-1521587760476-6c12a4b040da?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80') center/cover no-repeat;
filter: brightness(0.4);
z-index: 0;
}
.login-container::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.login-box {
position: relative;
z-index: 2;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 50px 40px;
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.3);
width: 100%;
max-width: 440px;
animation: fadeInUp 0.6s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
.login-header::before {
content: '📚';
font-size: 64px;
display: block;
margin-bottom: 16px;
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.login-header h1 {
color: white;
font-size: 32px;
margin-bottom: 8px;
font-weight: 700;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
letter-spacing: -0.5px;
}
.login-header p {
color: rgba(255, 255, 255, 0.9);
font-size: 15px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
margin-bottom: 10px;
color: white;
font-weight: 600;
font-size: 14px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
letter-spacing: 0.3px;
}
.form-group input {
width: 100%;
padding: 14px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 12px;
font-size: 15px;
transition: all 0.3s;
color: white;
font-weight: 500;
}
.form-group input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.form-group input:focus {
outline: none;
border-color: rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.1),
0 8px 20px rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
}
/* MODAL FORM OVERRIDES */
.modal .form-group label {
color: #475569;
text-shadow: none;
}
.modal .form-group input {
color: #1e293b;
background: #f8fafc;
border: 1px solid #e2e8f0;
backdrop-filter: none;
}
.modal .form-group input::placeholder {
color: #94a3b8;
}
.modal .form-group input:focus {
border-color: #667eea;
background: white;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
transform: none;
}
.modal .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.modal .btn-primary:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #683a92 100%);
}
.btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.btn-primary {
background: rgba(255, 255, 255, 0.95);
color: #667eea;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
position: relative;
overflow: hidden;
}
.btn-primary::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(102, 126, 234, 0.3), transparent);
transition: left 0.5s;
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
background: white;
}
.btn-primary:hover::before {
left: 100%;
}
.btn-primary:active {
transform: translateY(-1px);
}
.btn-loading {
cursor: not-allowed !important;
background: #ccc !important;
box-shadow: none !important;
transform: none !important;
}
.btn-loading .btn-text {
visibility: hidden;
opacity: 0;
}
.btn-loading .spinner {
display: inline-block;
opacity: 1;
}
.spinner {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 20px;
height: 20px;
border: 2px solid rgba(102, 126, 234, 0.5);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
opacity: 0;
transition: opacity 0.3s;
}
@keyframes spin {
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}
/* ==================== DASHBOARD LAYOUT ==================== */
.dashboard {
display: none;
min-height: 100vh;
background: #f1f5f9;
}
body.dashboard-open {
overflow: hidden; /* Prevent scrolling when dashboard is open */
}
.wrapper {
display: flex;
flex-direction: column;
min-height: 100vh;
background: #f1f5f9;
}
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: 260px;
background: linear-gradient(180deg, #2c3e95 0%, #1e2b6f 100%);
padding: 0;
overflow-y: auto;
z-index: 1000;
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.15);
transition: width 0.3s ease;
}
.main-header {
position: fixed;
top: 0;
left: 260px;
right: 0;
background: white;
padding: 20px;
z-index: 999;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: left 0.3s ease;
}
.content-wrapper {
margin-left: 260px;
margin-top: 100px; /* Height of main-header + some padding */
padding: 20px;
transition: margin-left 0.3s ease;
}
/* Collapsed state */
body.sidebar-collapsed .sidebar {
width: 80px;
}
body.sidebar-collapsed .sidebar .sidebar-header h2,
body.sidebar-collapsed .sidebar .sidebar-header p,
body.sidebar-collapsed .sidebar .menu-item span:not(.material-icons),
body.sidebar-collapsed .sidebar .menu-label,
body.sidebar-collapsed .sidebar .menu-badge {
display: none;
}
body.sidebar-collapsed .sidebar .menu-item {
justify-content: center;
}
body.sidebar-collapsed .sidebar .menu-item .material-icons {
margin-right: 0;
}
body.sidebar-collapsed .main-header {
left: 80px;
}
body.sidebar-collapsed .content-wrapper {
margin-left: 80px;
}
.sidebar-toggle {
background: none;
border: none;
color: #334155;
cursor: pointer;
font-size: 24px;
margin-right: 16px;
}
.header-left {
display: flex;
align-items: center;
}
.top-bar-left { /* Keep for backward compatibility */
display: flex;
align-items: center;
}
.sidebar-header {
background: linear-gradient(135deg, #4154b3 0%, #2c3e95 100%);
color: white;
padding: 24px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 8px;
text-align: center;
}
.sidebar-logo {
width: 50px;
height: 50px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
font-size: 28px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.sidebar-header h2 {
font-size: 18px;
margin-bottom: 4px;
font-weight: 700;
letter-spacing: 0.3px;
}
.sidebar-header p {
font-size: 12px;
opacity: 0.85;
font-weight: 400;
}
.menu-section {
padding: 0 12px;
margin-bottom: 8px;
}
.menu-label {
color: rgba(255, 255, 255, 0.5);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
padding: 16px 16px 8px;
}
.menu-item {
display: flex;
align-items: center;
padding: 13px 16px;
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
border-radius: 10px;
margin-bottom: 4px;
transition: all 0.3s;
cursor: pointer;
font-size: 14px;
font-weight: 500;
position: relative;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.1);
color: white;
transform: translateX(4px);
}
.menu-item.active {
background: linear-gradient(135deg, #5b6fd8 0%, #4154b3 100%);
color: white;
box-shadow: 0 4px 12px rgba(91, 111, 216, 0.4);
}
.menu-item.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 70%;
background: white;
border-radius: 0 4px 4px 0;
}
.menu-item .material-icons {
margin-right: 14px;
font-size: 22px;
}
.menu-badge {
margin-left: auto;
background: #ef4444;
color: white;
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
.top-bar-left h3 {
color: #1e293b;
font-size: 26px;
font-weight: 700;
margin-bottom: 4px;
}
.top-bar-subtitle {
color: #64748b;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.top-bar-subtitle .material-icons {
font-size: 16px;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
background: #f8fafc;
padding: 10px 16px;
border-radius: 12px;
border: 1px solid #e2e8f0;
cursor: pointer;
transition: background-color 0.2s;
}
.user-info:hover {
background-color: #f1f5f9;
}
.user-menu-container {
position: relative;
}
.dropdown-menu {
display: none;
position: absolute;
top: 110%;
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 8px;
z-index: 1200;
width: 100%;
}
.user-menu-container.open .dropdown-menu {
display: block;
}
.dropdown-menu .logout-btn {
width: 100%;
justify-content: center;
}
.user-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 16px;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 600;
color: #1e293b;
font-size: 14px;
}
.user-role {
font-size: 12px;
color: #64748b;
display: flex;
align-items: center;
gap: 4px;
}
.user-role .material-icons {
font-size: 14px;
color: #10b981;
}
.logout-btn {
padding: 10px 20px;
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 6px;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
}
.logout-btn:hover {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(239, 68, 68, 0.4);
}
.logout-btn .material-icons {
font-size: 18px;
}
/* ==================== STATS CARDS ==================== */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 16px;
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.stat-icon.blue {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
color: white;
}
.stat-icon.green {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.stat-icon.red {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
color: white;
}
.stat-icon.purple {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
color: white;
}
.stat-icon.orange {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
}
.stat-icon.teal {
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
color: white;
}
.stat-icon.cyan {
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
color: white;
}
.stat-icon.pink {
background: linear-gradient(135deg, #ec4899 0%, #d946ef 100%);
color: white;
}
.stat-info h4 {
color: #64748b;
font-size: 13px;
font-weight: 500;
margin-bottom: 4px;
}
.stat-info .stat-value {
color: #1e293b;
font-size: 28px;
font-weight: 700;
}
/* ==================== CONTENT CARD ==================== */
.content-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
margin-bottom: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h4 {
color: #1e293b;
font-size: 18px;
font-weight: 600;
}
.btn-add {
padding: 10px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s;
}
.btn-add:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.table-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.search-container {
position: relative;
}
.search-container input {
padding: 8px 12px 8px 36px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
}
.search-container .material-icons {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 20px;
}
.page-size-container {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #475569;
}
.page-size-container select {
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
}
.table-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e2e8f0;
flex-wrap: wrap;
gap: 12px;
}
.pagination-info {
font-size: 14px;
color: #475569;
}
.pagination-controls button {
padding: 6px 12px;
border: 1px solid #e2e8f0;
background: white;
border-radius: 6px;
cursor: pointer;
margin-left: 4px;
transition: background-color 0.2s;
}
.pagination-controls button:hover:not(:disabled) {
background-color: #f8fafc;
}
.pagination-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ==================== TABLE ==================== */
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: #f8fafc;
}
th {
padding: 12px;
text-align: left;
color: #475569;
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 12px;
border-top: 1px solid #e2e8f0;
color: #334155;
font-size: 14px;
}
tr:hover {
background: #f8fafc;
}
/* ==================== BADGES ==================== */
.badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.badge.pending {
background: #fef3c7;
color: #92400e;
}
.badge.approved {
background: #d1fae5;
color: #065f46;
}
.badge.rejected {
background: #fee2e2;
color: #991b1b;
}
.badge.returned {
background: #dbeafe;
color: #1e40af;
}
/* ==================== ACTION BUTTONS ==================== */
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
margin-right: 6px;
transition: all 0.3s;
}
.btn-approve {
background: #10b981;
color: white;
}
.btn-approve:hover {
background: #059669;
}
.btn-reject {
background: #ef4444;
color: white;
}
.btn-reject:hover {
background: #dc2626;
}
.btn-edit {
background: #3b82f6;
color: white;
}
.btn-edit:hover {
background: #2563eb;
}
.btn-delete {
background: #ef4444;
color: white;
}
.btn-delete:hover {
background: #dc2626;
}
.btn-return {
background: #8b5cf6;
color: white;
}
.btn-return:hover {
background: #7c3aed;
}
.btn-borrow {
background: #10b981;
color: white;
}
.btn-borrow:hover {
background: #059669;
}
/* ==================== MODAL ==================== */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 16px;
width: 90%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-height: 80vh; /* Add this line */
overflow-y: auto; /* Add this line */
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-header h3 {
color: #1e293b;
font-size: 20px;
}
.close-modal {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #64748b;
}
/* ==================== BOOKS GRID ==================== */
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.book-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
}
.book-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.book-cover {
width: 100%;
height: 250px;
object-fit: cover;
background: #e2e8f0;
}
.book-info {
padding: 16px;
}
.book-info h5 {
color: #1e293b;
font-size: 16px;
margin-bottom: 6px;
font-weight: 600;
}
.book-info p {
color: #64748b;
font-size: 13px;
margin-bottom: 4px;
}
.book-stok {
display: flex;
align-items: center;
gap: 6px;
color: #10b981;
font-size: 13px;
font-weight: 600;
margin-top: 8px;
}
.hidden {
display: none !important;
}
/* ==================== LOADING ==================== */
.loading {
text-align: center;
padding: 40px;
color: #64748b;
}
/* ==================== RESPONSIVE ==================== */
.sidebar-overlay {
display: none;
}
@media (max-width: 768px) {
body.sidebar-mobile-active .sidebar-overlay {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1099; /* Below sidebar but above content */
}
.main-header {
position: static;
left: 0;
width: 100%;
}
.content-wrapper {
margin-left: 0;
margin-top: 20px;
}
.sidebar {
transform: translateX(-100%);
z-index: 1100; /* Ensure sidebar is on top */
}
body.sidebar-mobile-active .sidebar {
transform: translateX(0);
}
body.sidebar-collapsed .sidebar {
transform: translateX(-100%); /* Ensure it's hidden when toggling on desktop then resizing */
}
body.sidebar-collapsed .main-header, body.sidebar-collapsed .content-wrapper {
left: 0;
margin-left: 0;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- ==================== LOGIN PAGE ==================== -->
<div id="loginPage" class="login-container">
<div class="login-box">
<div class="login-header">
<h1>Perpustakaan Digital</h1>
<p>Silakan masuk untuk melanjutkan</p>
</div>
<form id="loginForm">
<div class="form-group">
<label>Email</label>
<input type="email" id="emailInput" required placeholder="nama@email.com">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="passwordInput" required placeholder="Masukkan password">
</div>
<button type="submit" class="btn btn-primary">
<span class="btn-text">Masuk</span>
<span class="spinner"></span>
</button>
</form>
</div>
</div>
<!-- ==================== ADMIN DASHBOARD ==================== -->
<div id="adminDashboard" class="dashboard">
<div class="wrapper">
<div class="sidebar-overlay" onclick="toggleSidebar()"></div>
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">📚</div>
<h2>Perpustakaan Digital</h2>
<p>Admin Panel</p>
</div>
<div class="menu-section">
<div class="menu-label">Menu Utama</div>
<div class="menu-item active" onclick="showSection('dashboard')">
<span class="material-icons">dashboard</span>
<span>Dashboard</span>
</div>
<div class="menu-item" onclick="showSection('borrowRequests')">
<span class="material-icons">pending_actions</span>
<span>Peminjaman Pending</span>
<span class="menu-badge" id="pendingBadge">0</span>
</div>
<div class="menu-item" onclick="showSection('allBorrows')">
<span class="material-icons">history</span>
<span>Riwayat Peminjaman</span>
</div>
</div>
<div class="menu-section">
<div class="menu-label">Kelola Data</div>
<div class="menu-item" onclick="showSection('masterBooks')">
<span class="material-icons">menu_book</span>
<span>Master Buku</span>
</div>
<div class="menu-item" onclick="showSection('masterStudents')">
<span class="material-icons">people</span>
<span>Master Siswa</span>
</div>
</div>
</div>
<header class="main-header">
<div class="header-left">
<button class="sidebar-toggle" onclick="toggleSidebar()">
<span class="material-icons">menu</span>
</button>
<div class="top-bar-left">
<h3 id="pageTitle">Dashboard</h3>
</div>
</div>
<div class="user-menu-container">
<div class="user-info" onclick="toggleUserMenu()">
<div class="user-avatar" id="adminAvatar">A</div>
<div class="user-details">
<span class="user-name" id="adminName">Administrator</span>
<span class="user-role">
<span class="material-icons">verified</span>
Admin
</span>
</div>
</div>
<div class="dropdown-menu" id="adminUserMenu">
<button class="logout-btn" onclick="logout()">
<span class="material-icons">logout</span>
<span>Keluar</span>
</button>
</div>
</div>
</header>
<main class="content-wrapper">
<!-- Dashboard Stats -->
<div id="dashboardSection">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon blue">
<span class="material-icons">menu_book</span>
</div>
<div class="stat-info">
<h4>Total Judul Buku</h4>
<div class="stat-value" id="totalBooks">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon cyan">
<span class="material-icons">inventory_2</span>
</div>
<div class="stat-info">
<h4>Total Stok Buku</h4>
<div class="stat-value" id="totalStock">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<span class="material-icons">groups</span>
</div>
<div class="stat-info">
<h4>Total Siswa</h4>
<div class="stat-value" id="totalStudents">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon red">
<span class="material-icons">pending</span>
</div>
<div class="stat-info">
<h4>Peminjaman Pending</h4>
<div class="stat-value" id="pendingBorrows">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon purple">
<span class="material-icons">assignment_turned_in</span>
</div>
<div class="stat-info">
<h4>Menunggu Pengembalian</h4>
<div class="stat-value" id="waitingReturn">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon green">
<span class="material-icons">assignment_return</span>
</div>
<div class="stat-info">
<h4>Buku Dikembalikan</h4>
<div class="stat-value" id="returnedBooks">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon pink">
<span class="material-icons">cancel</span>
</div>
<div class="stat-info">
<h4>Peminjaman Ditolak</h4>
<div class="stat-value" id="rejectedBorrows">0</div>
</div>
</div>
</div>
</div>
<!-- Borrow Requests -->
<div id="borrowRequestsSection" class="hidden">
<div class="content-card">
<div class="card-header">
<h4>Pengajuan Peminjaman</h4>
<div class="table-controls">
<div class="search-container">
<span class="material-icons">search</span>
<input type="text" id="borrowRequestsSearch" placeholder="Cari...">
</div>
<div class="page-size-container">
<span>Tampilkan:</span>
<select id="borrowRequestsPageSize">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">Semua</option>
</select>
</div>
</div>
</div>
<div class="table-container">
<table id="borrowRequestsTable">
<thead>
<tr>
<th>ID</th>
<th>Siswa</th>
<th>Buku</th>
<th>Tanggal Pinjam</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="table-footer">
<div id="borrowRequestsPaginationInfo" class="pagination-info"></div>
<div class="pagination-controls">
<button id="borrowRequestsPrevPage">Sebelumnya</button>
<button id="borrowRequestsNextPage">Berikutnya</button>
</div>
</div>
</div>
</div>
<!-- All Borrows -->
<div id="allBorrowsSection" class="hidden">
<div class="content-card">
<div class="card-header">
<h4>Riwayat Peminjaman</h4>
<div class="table-controls">
<div class="search-container">
<span class="material-icons">search</span>
<input type="text" id="allBorrowsSearch" placeholder="Cari...">
</div>
<div class="page-size-container">
<span>Tampilkan:</span>
<select id="allBorrowsPageSize">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">Semua</option>
</select>
</div>
</div>
</div>
<div class="table-container">
<table id="allBorrowsTable">
<thead>
<tr>
<th>ID</th>
<th>Siswa</th>
<th>Buku</th>
<th>Tanggal Pinjam</th>
<th>Tanggal Kembali</th>
<th>Status</th>
<th>Aksi</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="table-footer">
<div id="allBorrowsPaginationInfo" class="pagination-info"></div>
<div class="pagination-controls">
<button id="allBorrowsPrevPage">Sebelumnya</button>
<button id="allBorrowsNextPage">Berikutnya</button>
</div>
</div>
</div>
</div>
<!-- Master Books -->
<div id="masterBooksSection" class="hidden">
<div class="content-card">
<div class="card-header">
<h4>Master Buku</h4>
<div class="table-controls">
<div class="search-container">
<span class="material-icons">search</span>
<input type="text" id="masterBooksSearch" placeholder="Cari...">
</div>
<div class="page-size-container">
<span>Tampilkan:</span>
<select id="masterBooksPageSize">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">Semua</option>
</select>
</div>
<button class="btn-add" onclick="openAddBookModal()">
<span class="material-icons">add</span>
Tambah Buku
</button>
</div>
</div>
<div class="table-container">
<table id="masterBooksTable">
<thead>
<tr>
<th>ID</th>
<th>Judul</th>
<th>Penulis</th>
<th>Stok</th>
<th>Aksi</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="table-footer">
<div id="masterBooksPaginationInfo" class="pagination-info"></div>
<div class="pagination-controls">
<button id="masterBooksPrevPage">Sebelumnya</button>
<button id="masterBooksNextPage">Berikutnya</button>
</div>
</div>
</div>
</div>
<!-- Master Students -->
<div id="masterStudentsSection" class="hidden">
<div class="content-card">
<div class="card-header">
<h4>Master Siswa</h4>
<div class="table-controls">
<div class="search-container">
<span class="material-icons">search</span>
<input type="text" id="masterStudentsSearch" placeholder="Cari...">
</div>
<div class="page-size-container">
<span>Tampilkan:</span>
<select id="masterStudentsPageSize">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">Semua</option>
</select>
</div>
<button class="btn-add" onclick="openAddStudentModal()">
<span class="material-icons">add</span>
Tambah Siswa
</button>
</div>
</div>
<div class="table-container">
<table id="masterStudentsTable">
<thead>
<tr>
<th>Email</th>
<th>Nama</th>
<th>Aksi</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="table-footer">
<div id="masterStudentsPaginationInfo" class="pagination-info"></div>
<div class="pagination-controls">
<button id="masterStudentsPrevPage">Sebelumnya</button>
<button id="masterStudentsNextPage">Berikutnya</button>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- ==================== STUDENT DASHBOARD ==================== -->
<div id="studentDashboard" class="dashboard">
<div class="wrapper">
<div class="sidebar-overlay" onclick="toggleSidebar()"></div>
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">📚</div>
<h2>Perpustakaan Digital</h2>
<p>Siswa Panel</p>
</div>
<div class="menu-section">
<div class="menu-label">Menu Utama</div>
<div class="menu-item active" onclick="showStudentSection('studentBooks')">
<span class="material-icons">menu_book</span>
<span>Daftar Buku</span>
</div>
<div class="menu-item" onclick="showStudentSection('studentBorrows')">
<span class="material-icons">history</span>
<span>Riwayat Peminjaman</span>
</div>
</div>
</div>
<header class="main-header">
<div class="header-left">
<button class="sidebar-toggle" onclick="toggleSidebar()">
<span class="material-icons">menu</span>
</button>
<div class="top-bar-left">
<h3 id="studentPageTitle">Daftar Buku</h3>
</div>
</div>
<div class="user-menu-container">
<div class="user-info" onclick="toggleUserMenu()">
<div class="user-avatar" id="studentAvatar">S</div>
<div class="user-details">
<span class="user-name" id="studentName">Siswa</span>
<span class="user-role">
<span class="material-icons">school</span>
Siswa
</span>
</div>
</div>
<div class="dropdown-menu" id="studentUserMenu">
<button class="logout-btn" onclick="logout()">
<span class="material-icons">logout</span>
<span>Keluar</span>
</button>
</div>
</div>
</header>
<main class="content-wrapper">
<!-- Student Books -->
<div id="studentBooksSection">
<div class="content-card">
<div class="card-header">
<h4>Daftar Buku Tersedia</h4>
</div>
<div class="books-grid" id="studentBooksGrid"></div>
</div>
</div>
<!-- Student Borrows -->
<div id="studentBorrowsSection" class="hidden">
<div class="content-card">
<div class="card-header">
<h4>Riwayat Peminjaman Saya</h4>
</div>
<div class="table-container">
<table id="studentBorrowsTable">
<thead>
<tr>
<th>ID</th>
<th>Buku</th>
<th>Tanggal Pinjam</th>
<th>Tanggal Kembali</th>
<th>Status</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- ==================== MODALS ==================== -->
<!-- Add/Edit Book Modal -->
<div id="bookModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="bookModalTitle">Tambah Buku</h3>
<button class="close-modal" onclick="closeModal('bookModal')">×</button>
</div>
<form id="bookForm">
<input type="hidden" id="bookId">
<input type="hidden" id="currentBookUrl">
<div class="form-group">
<label>Judul Buku</label>
<input type="text" id="bookJudul" required>
</div>
<div class="form-group">
<label>Penulis</label>
<input type="text" id="bookPenulis" required>
</div>
<div class="form-group">
<label>Stok</label>
<input type="number" id="bookStok" required min="0">
</div>
<div class="form-group">
<label>Cover Buku (Upload Gambar)</label>
<input type="file" id="bookCoverFile" accept="image/*">
<div style="margin-top: 5px; font-size: 12px; color: #64748b;">
Atau masukkan URL manual (opsional):
</div>
<input type="text" id="bookCoverUrlInput" placeholder="https://...">
<div id="imagePreviewContainer" style="margin-top: 10px; display: none;">
<p style="font-size: 12px; margin-bottom: 5px;">Preview:</p>
<img id="imagePreview" src="" style="max-width: 100px; max-height: 150px; border-radius: 8px; border: 1px solid #ddd;">
</div>
</div>
<button type="submit" class="btn btn-primary">Simpan</button>
</form>
</div>
</div>
<!-- Add/Edit Student Modal -->
<div id="studentModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="studentModalTitle">Tambah Siswa</h3>
<button class="close-modal" onclick="closeModal('studentModal')">×</button>
</div>
<form id="studentForm">
<input type="hidden" id="studentEmailOld">
<div class="form-group">
<label>Email</label>
<input type="email" id="studentEmail" required>
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="studentPassword" required>
</div>
<div class="form-group">
<label>Nama Lengkap</label>
<input type="text" id="studentNama" required>
</div>
<button type="submit" class="btn btn-primary">
<span class="btn-text">Simpan</span>
<span class="spinner"></span>
</button>
</form>
</div>
</div>
<script>
// ==================== GLOBAL VARIABLES ====================
let currentUser = null;
// ==================== UTILITY FUNCTIONS ====================
function toggleUserMenu() {
const menuContainer = event.currentTarget.closest('.user-menu-container');
if (menuContainer) {
menuContainer.classList.toggle('open');
}
}
window.addEventListener('click', function(e) {
document.querySelectorAll('.user-menu-container.open').forEach(function(menu) {
if (!menu.contains(e.target)) {
menu.classList.remove('open');
}
});
});
function updateCurrentDate() {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const date = new Date().toLocaleDateString('id-ID', options);
const dateElement = document.getElementById('currentDate'); // Pastikan ID ini ada di HTML header jika ingin tampil
// Opsional: Tampilkan tanggal di elemen lain jika ada
}
function toggleSidebar() {
if (window.innerWidth <= 768) {
document.body.classList.toggle('sidebar-mobile-active');
} else {
document.body.classList.toggle('sidebar-collapsed');
}
}
function showResultAlert(result) {
Swal.fire({
title: result.success ? 'Berhasil' : 'Gagal',
text: result.message,
icon: result.success ? 'success' : 'error',
timer: result.success ? 1500 : 3000,
showConfirmButton: false,
});
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
// Close modal when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
// ==================== IMAGE UPLOAD UTILITIES ====================
// Fungsi mengubah File menjadi Base64 String
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
// Ambil string setelah tanda koma (hapus prefix data:image/...)
resolve(reader.result.split(',')[1]);
};
reader.onerror = error => reject(error);
});
}
// Event listener untuk preview gambar saat file dipilih
const fileInput = document.getElementById('bookCoverFile');
if(fileInput){
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
const previewContainer = document.getElementById('imagePreviewContainer');
const previewImage = document.getElementById('imagePreview');
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
previewImage.src = e.target.result;
previewContainer.style.display = 'block';
}
reader.readAsDataURL(file);
}
});
}
// ==================== SESSION MANAGEMENT ====================
function checkSession() {
const storedUser = sessionStorage.getItem('currentUser');
if (storedUser) {
currentUser = JSON.parse(storedUser);
document.getElementById('loginPage').style.display = 'none';
if (currentUser.role === 'Admin') {
document.getElementById('adminDashboard').style.display = 'block';
document.getElementById('adminName').textContent = currentUser.nama;
document.getElementById('adminAvatar').textContent = currentUser.nama.charAt(0).toUpperCase();
updateCurrentDate();
loadAdminDashboard();
} else {
document.getElementById('studentDashboard').style.display = 'block';
document.getElementById('studentName').textContent = currentUser.nama;
document.getElementById('studentAvatar').textContent = currentUser.nama.charAt(0).toUpperCase();
updateCurrentDate();
loadStudentDashboard();
}
}
}
document.addEventListener('DOMContentLoaded', checkSession);
// ==================== LOGIN & LOGOUT ====================
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('emailInput').value;
const password = document.getElementById('passwordInput').value;
const loginButton = this.querySelector('button[type="submit"]');
loginButton.disabled = true;
loginButton.classList.add('btn-loading');
const handleLoginResponse = (result) => {
loginButton.disabled = false;
loginButton.classList.remove('btn-loading');
if (result && result.success) {
sessionStorage.setItem('currentUser', JSON.stringify(result.user));
currentUser = result.user;
document.getElementById('loginPage').style.display = 'none';
if (result.user.role === 'Admin') {
document.getElementById('adminDashboard').style.display = 'block';
document.getElementById('adminName').textContent = result.user.nama;
document.getElementById('adminAvatar').textContent = result.user.nama.charAt(0).toUpperCase();
loadAdminDashboard();
} else {
document.getElementById('studentDashboard').style.display = 'block';
document.getElementById('studentName').textContent = result.user.nama;
document.getElementById('studentAvatar').textContent = result.user.nama.charAt(0).toUpperCase();
loadStudentDashboard();
}
} else {
Swal.fire({
title: 'Gagal Masuk',
text: result.message || 'Terjadi kesalahan.',
icon: 'error'
});
}
};
const handleLoginError = (err) => {
loginButton.disabled = false;
loginButton.classList.remove('btn-loading');
Swal.fire({
title: 'Error',
text: 'Koneksi bermasalah: ' + err,
icon: 'error'
});
};
google.script.run
.withSuccessHandler(handleLoginResponse)
.withFailureHandler(handleLoginError)
.login(email, password);
});
function logout() {
sessionStorage.removeItem('currentUser');
currentUser = null;
document.getElementById('loginPage').style.display = 'flex';
document.getElementById('adminDashboard').style.display = 'none';
document.getElementById('studentDashboard').style.display = 'none';
document.getElementById('emailInput').value = '';
document.getElementById('passwordInput').value = '';
}
// ==================== ADMIN DASHBOARD LOGIC ====================
function loadAdminDashboard() {
loadStatistics();
loadBorrowRequests();
loadAllBorrows();
loadMasterBooks();
loadMasterStudents();
}
function showSection(section) {
// Hide all sections
document.querySelectorAll('.content-wrapper > div').forEach(div => div.classList.add('hidden'));
// Remove active class from menu
document.querySelectorAll('#adminDashboard .menu-item').forEach(item => item.classList.remove('active'));
// Show selected section & Activate menu
// Note: Mapping manual based on order or ID logic
const menuItems = document.querySelectorAll('#adminDashboard .menu-item');
if (section === 'dashboard') {
document.getElementById('dashboardSection').classList.remove('hidden');
document.getElementById('pageTitle').textContent = 'Dashboard';
menuItems[0].classList.add('active');
loadStatistics();
} else if (section === 'borrowRequests') {
document.getElementById('borrowRequestsSection').classList.remove('hidden');
document.getElementById('pageTitle').textContent = 'Peminjaman Pending';
menuItems[1].classList.add('active');
loadBorrowRequests();
} else if (section === 'allBorrows') {
document.getElementById('allBorrowsSection').classList.remove('hidden');
document.getElementById('pageTitle').textContent = 'Riwayat Peminjaman';
menuItems[2].classList.add('active');
loadAllBorrows();
} else if (section === 'masterBooks') {
document.getElementById('masterBooksSection').classList.remove('hidden');
document.getElementById('pageTitle').textContent = 'Master Buku';
menuItems[3].classList.add('active');
loadMasterBooks();
} else if (section === 'masterStudents') {
document.getElementById('masterStudentsSection').classList.remove('hidden');
document.getElementById('pageTitle').textContent = 'Master Siswa';
menuItems[4].classList.add('active');
loadMasterStudents();
}
}
function loadStatistics() {
google.script.run.withSuccessHandler(function(stats) {
document.getElementById('totalBooks').textContent = stats.totalBooks;
document.getElementById('totalStock').textContent = stats.totalStok;
document.getElementById('totalStudents').textContent = stats.totalStudents;
document.getElementById('pendingBorrows').textContent = stats.pending;
document.getElementById('waitingReturn').textContent = stats.waitingReturn;
document.getElementById('returnedBooks').textContent = stats.returned;
document.getElementById('rejectedBorrows').textContent = stats.rejected;
const badge = document.getElementById('pendingBadge');
if (badge) {
badge.textContent = stats.pending;
badge.style.display = stats.pending > 0 ? 'block' : 'none';
}
}).getStatistics();
}
// ==================== BORROW REQUESTS TABLE ====================
let pendingBorrowsData = [];
let borrowRequestsCurrentPage = 1;
let borrowRequestsPageSize = 10;
let borrowRequestsSearchQuery = '';
function loadBorrowRequests() {
google.script.run.withSuccessHandler(function(history) {
pendingBorrowsData = history.filter(h => h.status === 'Pending');
borrowRequestsCurrentPage = 1;
renderBorrowRequestsTable();
}).getBorrowHistory(currentUser.email, currentUser.role);
}
function renderBorrowRequestsTable() {
const query = borrowRequestsSearchQuery.toLowerCase();
const filteredData = pendingBorrowsData.filter(item => {
return item.id.toLowerCase().includes(query) ||
item.siswa.toLowerCase().includes(query) ||
item.judulBuku.toLowerCase().includes(query);
});
const totalItems = filteredData.length;
const pageSize = borrowRequestsPageSize === 'all' ? totalItems : parseInt(borrowRequestsPageSize);
const totalPages = Math.ceil(totalItems / pageSize);
const start = (borrowRequestsCurrentPage - 1) * pageSize;
const end = start + pageSize;
const paginatedData = filteredData.slice(start, end);
const tbody = document.querySelector('#borrowRequestsTable tbody');
tbody.innerHTML = '';
if (paginatedData.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px;">Tidak ada data</td></tr>';
} else {
paginatedData.forEach(item => {
const row = tbody.insertRow();
row.innerHTML = `
<td>${item.id}</td>
<td>${item.siswa}</td>
<td>${item.judulBuku}</td>
<td>${item.tanggalPinjam}</td>
<td><span class="badge pending">Pending</span></td>
<td>
<button class="action-btn btn-approve" onclick="approveBorrow('${item.id}')"><span class="material-icons" style="font-size:14px">check</span></button>
<button class="action-btn btn-reject" onclick="rejectBorrow('${item.id}')"><span class="material-icons" style="font-size:14px">close</span></button>
</td>
`;
});
}
// Pagination Controls
document.getElementById('borrowRequestsPaginationInfo').textContent = `Menampilkan ${totalItems > 0 ? start + 1 : 0}-${Math.min(end, totalItems)} dari ${totalItems}`;
document.getElementById('borrowRequestsPrevPage').disabled = borrowRequestsCurrentPage === 1;
document.getElementById('borrowRequestsNextPage').disabled = borrowRequestsCurrentPage === totalPages || totalItems === 0;
}
// Event Listeners for Borrow Request Table
document.getElementById('borrowRequestsSearch').addEventListener('input', (e) => { borrowRequestsSearchQuery = e.target.value; borrowRequestsCurrentPage = 1; renderBorrowRequestsTable(); });
document.getElementById('borrowRequestsPageSize').addEventListener('change', (e) => { borrowRequestsPageSize = e.target.value; borrowRequestsCurrentPage = 1; renderBorrowRequestsTable(); });
document.getElementById('borrowRequestsPrevPage').addEventListener('click', () => { if(borrowRequestsCurrentPage > 1) { borrowRequestsCurrentPage--; renderBorrowRequestsTable(); }});
document.getElementById('borrowRequestsNextPage').addEventListener('click', () => { borrowRequestsCurrentPage++; renderBorrowRequestsTable(); });
// ==================== ALL BORROWS TABLE ====================
let allBorrowsData = [];
let allBorrowsCurrentPage = 1;
let allBorrowsPageSize = 10;
let allBorrowsSearchQuery = '';
function loadAllBorrows() {
google.script.run.withSuccessHandler(function(history) {
allBorrowsData = history;
allBorrowsCurrentPage = 1;
renderAllBorrowsTable();
}).getBorrowHistory(currentUser.email, currentUser.role);
}
function renderAllBorrowsTable() {
const query = allBorrowsSearchQuery.toLowerCase();
const filteredData = allBorrowsData.filter(item => {
return item.id.toLowerCase().includes(query) || item.siswa.toLowerCase().includes(query) || item.judulBuku.toLowerCase().includes(query) || item.status.toLowerCase().includes(query);
});
const totalItems = filteredData.length;
const pageSize = allBorrowsPageSize === 'all' ? totalItems : parseInt(allBorrowsPageSize);
const totalPages = Math.ceil(totalItems / pageSize);
const start = (allBorrowsCurrentPage - 1) * pageSize;
const end = start + pageSize;
const paginatedData = filteredData.slice(start, end);
const tbody = document.querySelector('#allBorrowsTable tbody');
tbody.innerHTML = '';
if (paginatedData.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 40px;">Tidak ada data</td></tr>';
} else {
paginatedData.forEach(item => {
const row = tbody.insertRow();
let statusBadge = `<span class="badge ${item.status.toLowerCase()}">${item.status}</span>`;
let actionBtn = '';
if (item.status === 'Approved') {
actionBtn = `<button class="action-btn btn-return" onclick="returnBook('${item.id}')">Kembalikan</button>`;
}
row.innerHTML = `<td>${item.id}</td><td>${item.siswa}</td><td>${item.judulBuku}</td><td>${item.tanggalPinjam}</td><td>${item.tanggalKembali || '-'}</td><td>${statusBadge}</td><td>${actionBtn}</td>`;
});
}
document.getElementById('allBorrowsPaginationInfo').textContent = `Menampilkan ${totalItems > 0 ? start + 1 : 0}-${Math.min(end, totalItems)} dari ${totalItems}`;
document.getElementById('allBorrowsPrevPage').disabled = allBorrowsCurrentPage === 1;
document.getElementById('allBorrowsNextPage').disabled = allBorrowsCurrentPage === totalPages || totalItems === 0;
}
// Event Listeners for All Borrows
document.getElementById('allBorrowsSearch').addEventListener('input', (e) => { allBorrowsSearchQuery = e.target.value; allBorrowsCurrentPage = 1; renderAllBorrowsTable(); });
document.getElementById('allBorrowsPageSize').addEventListener('change', (e) => { allBorrowsPageSize = e.target.value; allBorrowsCurrentPage = 1; renderAllBorrowsTable(); });
document.getElementById('allBorrowsPrevPage').addEventListener('click', () => { if(allBorrowsCurrentPage > 1) { allBorrowsCurrentPage--; renderAllBorrowsTable(); }});
document.getElementById('allBorrowsNextPage').addEventListener('click', () => { allBorrowsCurrentPage++; renderAllBorrowsTable(); });
// ==================== MASTER BOOKS LOGIC (WITH UPLOAD) ====================
let masterBooksData = [];
let masterBooksCurrentPage = 1;
let masterBooksPageSize = 10;
let masterBooksSearchQuery = '';
function loadMasterBooks() {
google.script.run.withSuccessHandler(function(books) {
masterBooksData = books;
masterBooksCurrentPage = 1;
renderMasterBooksTable();
}).getBooks();
}
function renderMasterBooksTable() {
const query = masterBooksSearchQuery.toLowerCase();
const filteredData = masterBooksData.filter(item => {
return item.id.toLowerCase().includes(query) || item.judul.toLowerCase().includes(query) || item.penulis.toLowerCase().includes(query);
});
const totalItems = filteredData.length;
const pageSize = masterBooksPageSize === 'all' ? totalItems : parseInt(masterBooksPageSize);
const totalPages = Math.ceil(totalItems / pageSize);
const start = (masterBooksCurrentPage - 1) * pageSize;
const end = start + pageSize;
const paginatedData = filteredData.slice(start, end);
const tbody = document.querySelector('#masterBooksTable tbody');
tbody.innerHTML = '';
if (paginatedData.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;">Tidak ada data</td></tr>';
} else {
paginatedData.forEach(book => {
const row = tbody.insertRow();
row.innerHTML = `
<td>${book.id}</td>
<td>${book.judul}</td>
<td>${book.penulis}</td>
<td>${book.stok}</td>
<td>
<button class="action-btn btn-edit" onclick="openEditBookModal('${book.id}')">Edit</button>
<button class="action-btn btn-delete" onclick="deleteBook('${book.id}')">Hapus</button>
</td>
`;
});
}
document.getElementById('masterBooksPaginationInfo').textContent = `Menampilkan ${totalItems > 0 ? start + 1 : 0}-${Math.min(end, totalItems)} dari ${totalItems}`;
document.getElementById('masterBooksPrevPage').disabled = masterBooksCurrentPage === 1;
document.getElementById('masterBooksNextPage').disabled = masterBooksCurrentPage === totalPages || totalItems === 0;
}
// Event Listeners for Master Books
document.getElementById('masterBooksSearch').addEventListener('input', (e) => { masterBooksSearchQuery = e.target.value; masterBooksCurrentPage = 1; renderMasterBooksTable(); });
document.getElementById('masterBooksPageSize').addEventListener('change', (e) => { masterBooksPageSize = e.target.value; masterBooksCurrentPage = 1; renderMasterBooksTable(); });
document.getElementById('masterBooksPrevPage').addEventListener('click', () => { if(masterBooksCurrentPage > 1) { masterBooksCurrentPage--; renderMasterBooksTable(); }});
document.getElementById('masterBooksNextPage').addEventListener('click', () => { masterBooksCurrentPage++; renderMasterBooksTable(); });
function openAddBookModal() {
document.getElementById('bookForm').reset();
document.getElementById('bookId').value = '';
document.getElementById('bookModalTitle').textContent = 'Tambah Buku';
document.getElementById('imagePreviewContainer').style.display = 'none';
document.getElementById('imagePreview').src = '';
document.getElementById('bookModal').style.display = 'flex';
}
function openEditBookModal(id) {
const book = masterBooksData.find(b => b.id === id);
if (book) {
document.getElementById('bookId').value = book.id;
document.getElementById('bookJudul').value = book.judul;
document.getElementById('bookPenulis').value = book.penulis;
document.getElementById('bookStok').value = book.stok;
document.getElementById('currentBookUrl').value = book.coverURL;
document.getElementById('bookCoverUrlInput').value = book.coverURL;
const previewContainer = document.getElementById('imagePreviewContainer');
const previewImage = document.getElementById('imagePreview');
if (book.coverURL) {
previewImage.src = book.coverURL;
previewContainer.style.display = 'block';
} else {
previewContainer.style.display = 'none';
}
document.getElementById('bookModalTitle').textContent = 'Edit Buku';
document.getElementById('bookModal').style.display = 'flex';
}
}
function deleteBook(id) {
Swal.fire({
title: 'Anda yakin?',
text: "Data buku akan dihapus permanen!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Ya, hapus!',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res) {
showResultAlert(res);
if(res.success) {
loadMasterBooks();
}
}).deleteBook(id);
}
});
}
document.getElementById('bookForm').addEventListener('submit', async function(e) {
e.preventDefault();
const id = document.getElementById('bookId').value;
const judul = document.getElementById('bookJudul').value;
const penulis = document.getElementById('bookPenulis').value;
const stok = document.getElementById('bookStok').value;
const coverUrlInput = document.getElementById('bookCoverUrlInput').value;
const coverFile = document.getElementById('bookCoverFile').files[0];
let fileData = null;
if (coverFile) {
const base64Data = await getBase64(coverFile);
fileData = {
fileName: coverFile.name,
mimeType: coverFile.type,
data: base64Data
};
}
let coverURL = document.getElementById('currentBookUrl').value;
if(coverUrlInput !== coverURL) {
coverURL = coverUrlInput;
}
const handler = function(res) {
showResultAlert(res);
if(res.success) {
closeModal('bookModal');
loadMasterBooks();
loadStatistics();
}
};
if (id) {
google.script.run.withSuccessHandler(handler).updateBook(id, judul, penulis, stok, coverURL, fileData);
} else {
google.script.run.withSuccessHandler(handler).addBook(judul, penulis, stok, coverURL, fileData);
}
});
function loadMasterBooks() {
google.script.run.withSuccessHandler(function(books) {
masterBooksData = books;
masterBooksCurrentPage = 1;
renderMasterBooksTable();
}).getBooks();
}
function renderMasterBooksTable() {
const query = masterBooksSearchQuery.toLowerCase();
const filteredData = masterBooksData.filter(item => item.judul.toLowerCase().includes(query) || item.penulis.toLowerCase().includes(query));
const totalItems = filteredData.length;
const pageSize = masterBooksPageSize === 'all' ? totalItems : parseInt(masterBooksPageSize);
const totalPages = Math.ceil(totalItems / pageSize);
const start = (masterBooksCurrentPage - 1) * pageSize;
const end = start + pageSize;
const paginatedData = filteredData.slice(start, end);
const tbody = document.querySelector('#masterBooksTable tbody');
tbody.innerHTML = '';
if(paginatedData.length === 0){
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;">Tidak ada data</td></tr>';
} else {
paginatedData.forEach(book => {
const row = tbody.insertRow();
row.innerHTML = `
<td>${book.id}</td>
<td>${book.judul}</td>
<td>${book.penulis}</td>
<td>${book.stok}</td>
<td>
<button class="action-btn btn-edit" onclick='editBook(${JSON.stringify(book)})'>Edit</button>
<button class="action-btn btn-delete" onclick="deleteBook('${book.id}')">Hapus</button>
</td>
`;
});
}
document.getElementById('masterBooksPaginationInfo').textContent = `Menampilkan ${totalItems > 0 ? start + 1 : 0}-${Math.min(end, totalItems)} dari ${totalItems}`;
document.getElementById('masterBooksPrevPage').disabled = masterBooksCurrentPage === 1;
document.getElementById('masterBooksNextPage').disabled = masterBooksCurrentPage === totalPages || totalItems === 0;
}
// Event Listeners Master Books
document.getElementById('masterBooksSearch').addEventListener('input', (e) => { masterBooksSearchQuery = e.target.value; masterBooksCurrentPage = 1; renderMasterBooksTable(); });
document.getElementById('masterBooksPageSize').addEventListener('change', (e) => { masterBooksPageSize = e.target.value; masterBooksCurrentPage = 1; renderMasterBooksTable(); });
document.getElementById('masterBooksPrevPage').addEventListener('click', () => { if(masterBooksCurrentPage > 1) { masterBooksCurrentPage--; renderMasterBooksTable(); }});
document.getElementById('masterBooksNextPage').addEventListener('click', () => { if(masterBooksCurrentPage * parseInt(masterBooksPageSize) < masterBooksData.length) { masterBooksCurrentPage++; renderMasterBooksTable(); }});
// --- BOOK MODAL & FORM SUBMIT ---
function openAddBookModal() {
document.getElementById('bookForm').reset();
document.getElementById('bookModalTitle').textContent = 'Tambah Buku';
document.getElementById('bookId').value = '';
document.getElementById('currentBookUrl').value = '';
// Reset Preview
document.getElementById('imagePreviewContainer').style.display = 'none';
document.getElementById('imagePreview').src = '';
document.getElementById('bookModal').style.display = 'flex';
}
function editBook(book) {
document.getElementById('bookModalTitle').textContent = 'Edit Buku';
document.getElementById('bookId').value = book.id;
document.getElementById('bookJudul').value = book.judul;
document.getElementById('bookPenulis').value = book.penulis;
document.getElementById('bookStok').value = book.stok;
// Reset file input
document.getElementById('bookCoverFile').value = '';
// Handle URL & Preview
const urlInput = document.getElementById('bookCoverUrlInput');
const currentUrlHidden = document.getElementById('currentBookUrl');
const previewContainer = document.getElementById('imagePreviewContainer');
const previewImage = document.getElementById('imagePreview');
if (book.coverURL && book.coverURL !== 'null' && book.coverURL !== '') {
urlInput.value = book.coverURL;
currentUrlHidden.value = book.coverURL;
previewImage.src = book.coverURL;
previewContainer.style.display = 'block';
} else {
urlInput.value = '';
currentUrlHidden.value = '';
previewContainer.style.display = 'none';
}
document.getElementById('bookModal').style.display = 'flex';
}
// SUBMIT BUKU (ASYNC UPLOAD)
document.getElementById('bookForm').addEventListener('submit', async function(e) {
e.preventDefault();
const bookId = document.getElementById('bookId').value;
const judul = document.getElementById('bookJudul').value;
const penulis = document.getElementById('bookPenulis').value;
const stok = parseInt(document.getElementById('bookStok').value);
const manualUrl = document.getElementById('bookCoverUrlInput').value;
const oldUrl = document.getElementById('currentBookUrl').value;
// Prioritaskan URL manual jika diisi, jika tidak gunakan URL lama
const coverURL = manualUrl || oldUrl;
const fileInput = document.getElementById('bookCoverFile');
let fileData = null;
const submitButton = this.querySelector('button[type="submit"]');
submitButton.disabled = true;
submitButton.classList.add('btn-loading');
try {
// Proses File Upload jika ada
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
// Batasi ukuran file (misal 2MB) agar tidak timeout
if (file.size > 2 * 1024 * 1024) {
throw new Error("Ukuran gambar terlalu besar (Maks 2MB)");
}
const base64 = await getBase64(file);
fileData = {
data: base64,
mimeType: file.type,
fileName: file.name
};
}
const handleResponse = (result) => {
submitButton.disabled = false;
submitButton.classList.remove('btn-loading');
if (result.success) {
Swal.fire('Berhasil!', result.message, 'success');
closeModal('bookModal');
loadMasterBooks();
} else {
Swal.fire('Gagal!', result.message, 'error');
}
};
const handleError = (err) => {
submitButton.disabled = false;
submitButton.classList.remove('btn-loading');
Swal.fire('Error!', 'Terjadi kesalahan: ' + err.message, 'error');
};
// Panggil Fungsi Google Apps Script
if (bookId) {
// Update: Kirim data termasuk fileData
google.script.run
.withSuccessHandler(handleResponse)
.withFailureHandler(handleError)
.updateBook(bookId, judul, penulis, stok, coverURL, fileData);
} else {
// Add: Kirim data termasuk fileData
google.script.run
.withSuccessHandler(handleResponse)
.withFailureHandler(handleError)
.addBook(judul, penulis, stok, coverURL, fileData);
}
} catch (error) {
submitButton.disabled = false;
submitButton.classList.remove('btn-loading');
Swal.fire('Error Upload!', error.message, 'error');
}
});
function deleteBook(id) {
Swal.fire({
title: 'Hapus Buku?',
text: "Data tidak bisa dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
confirmButtonText: 'Hapus'
}).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res){
showResultAlert(res);
if(res.success) loadMasterBooks();
}).deleteBook(id);
}
});
}
// ==================== MASTER STUDENTS LOGIC ====================
let masterStudentsData = [];
let masterStudentsCurrentPage = 1;
let masterStudentsPageSize = 10;
let masterStudentsSearchQuery = '';
function loadMasterStudents() {
google.script.run.withSuccessHandler(function(students) {
masterStudentsData = students;
masterStudentsCurrentPage = 1;
renderMasterStudentsTable();
}).getStudents();
}
function renderMasterStudentsTable() {
const query = masterStudentsSearchQuery.toLowerCase();
const filteredData = masterStudentsData.filter(item => item.email.toLowerCase().includes(query) || item.nama.toLowerCase().includes(query));
const totalItems = filteredData.length;
const pageSize = masterStudentsPageSize === 'all' ? totalItems : parseInt(masterStudentsPageSize);
const totalPages = Math.ceil(totalItems / pageSize);
const start = (masterStudentsCurrentPage - 1) * pageSize;
const end = start + pageSize;
const paginatedData = filteredData.slice(start, end);
const tbody = document.querySelector('#masterStudentsTable tbody');
tbody.innerHTML = '';
if(paginatedData.length === 0){
tbody.innerHTML = '<tr><td colspan="3" style="text-align: center; padding: 40px;">Tidak ada data</td></tr>';
} else {
paginatedData.forEach(student => {
const row = tbody.insertRow();
row.innerHTML = `
<td>${student.email}</td>
<td>${student.nama}</td>
<td>
<button class="action-btn btn-edit" onclick='editStudent(${JSON.stringify(student)})'>Edit</button>
<button class="action-btn btn-delete" onclick="deleteStudent('${student.email}')">Hapus</button>
</td>
`;
});
}
document.getElementById('masterStudentsPaginationInfo').textContent = `Menampilkan ${totalItems > 0 ? start + 1 : 0}-${Math.min(end, totalItems)} dari ${totalItems}`;
document.getElementById('masterStudentsPrevPage').disabled = masterStudentsCurrentPage === 1;
document.getElementById('masterStudentsNextPage').disabled = masterStudentsCurrentPage === totalPages || totalItems === 0;
}
// Event Listeners Master Students
document.getElementById('masterStudentsSearch').addEventListener('input', (e) => { masterStudentsSearchQuery = e.target.value; masterStudentsCurrentPage = 1; renderMasterStudentsTable(); });
document.getElementById('masterStudentsPageSize').addEventListener('change', (e) => { masterStudentsPageSize = e.target.value; masterStudentsCurrentPage = 1; renderMasterStudentsTable(); });
document.getElementById('masterStudentsPrevPage').addEventListener('click', () => { if(masterStudentsCurrentPage > 1) { masterStudentsCurrentPage--; renderMasterStudentsTable(); }});
document.getElementById('masterStudentsNextPage').addEventListener('click', () => { masterStudentsCurrentPage++; renderMasterStudentsTable(); });
function openAddStudentModal() {
document.getElementById('studentModalTitle').textContent = 'Tambah Siswa';
document.getElementById('studentEmailOld').value = '';
document.getElementById('studentEmail').value = '';
document.getElementById('studentPassword').value = '';
document.getElementById('studentNama').value = '';
document.getElementById('studentModal').style.display = 'flex';
}
function editStudent(student) {
document.getElementById('studentModalTitle').textContent = 'Edit Siswa';
document.getElementById('studentEmailOld').value = student.email;
document.getElementById('studentEmail').value = student.email;
document.getElementById('studentPassword').value = '';
document.getElementById('studentPassword').required = false;
document.getElementById('studentNama').value = student.nama;
document.getElementById('studentModal').style.display = 'flex';
}
document.getElementById('studentForm').addEventListener('submit', function(e) {
e.preventDefault();
const oldEmail = document.getElementById('studentEmailOld').value;
const email = document.getElementById('studentEmail').value;
const password = document.getElementById('studentPassword').value;
const nama = document.getElementById('studentNama').value;
const submitButton = this.querySelector('button[type="submit"]');
submitButton.disabled = true;
submitButton.classList.add('btn-loading');
const handler = (result) => {
submitButton.disabled = false;
submitButton.classList.remove('btn-loading');
showResultAlert(result);
if(result.success) { closeModal('studentModal'); loadMasterStudents(); }
};
if (oldEmail) {
google.script.run.withSuccessHandler(handler).updateStudent(oldEmail, email, password, nama);
} else {
google.script.run.withSuccessHandler(handler).addStudent(email, password, nama);
}
});
function deleteStudent(email) {
Swal.fire({
title: 'Hapus Siswa?',
text: "Data tidak bisa dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
confirmButtonText: 'Hapus'
}).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res){
showResultAlert(res);
if(res.success) loadMasterStudents();
}).deleteStudent(email);
}
});
}
// ==================== APPROVE / REJECT / RETURN LOGIC ====================
function approveBorrow(borrowId) {
Swal.fire({ title: 'Setujui?', text: "Stok buku akan berkurang.", icon: 'question', showCancelButton: true, confirmButtonText: 'Ya' }).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res) {
showResultAlert(res);
if (res.success) { loadBorrowRequests(); loadAllBorrows(); loadStatistics(); }
}).approveBorrow(borrowId);
}
});
}
function rejectBorrow(borrowId) {
Swal.fire({ title: 'Tolak?', icon: 'warning', showCancelButton: true, confirmButtonText: 'Ya', confirmButtonColor: '#d33' }).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res) {
showResultAlert(res);
if (res.success) { loadBorrowRequests(); loadAllBorrows(); loadStatistics(); }
}).rejectBorrow(borrowId);
}
});
}
function returnBook(borrowId) {
Swal.fire({ title: 'Buku Kembali?', text: "Stok akan bertambah.", icon: 'question', showCancelButton: true, confirmButtonText: 'Ya' }).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res) {
showResultAlert(res);
if (res.success) { loadAllBorrows(); loadStatistics(); }
}).returnBook(borrowId);
}
});
}
// ==================== STUDENT DASHBOARD LOGIC ====================
function loadStudentDashboard() {
loadStudentBooks();
loadStudentBorrows();
}
function showStudentSection(section) {
document.getElementById('studentBooksSection').classList.add('hidden');
document.getElementById('studentBorrowsSection').classList.add('hidden');
document.querySelectorAll('#studentDashboard .menu-item').forEach(item => item.classList.remove('active'));
const menuItems = document.querySelectorAll('#studentDashboard .menu-item');
if (section === 'studentBooks') {
document.getElementById('studentBooksSection').classList.remove('hidden');
document.getElementById('studentPageTitle').textContent = 'Daftar Buku';
menuItems[0].classList.add('active');
loadStudentBooks();
} else if (section === 'studentBorrows') {
document.getElementById('studentBorrowsSection').classList.remove('hidden');
document.getElementById('studentPageTitle').textContent = 'Riwayat Peminjaman';
menuItems[1].classList.add('active');
loadStudentBorrows();
}
}
function loadStudentBooks() {
google.script.run.withSuccessHandler(function(books) {
const grid = document.getElementById('studentBooksGrid');
grid.innerHTML = '';
if (books.length === 0) {
grid.innerHTML = '<p style="text-align: center; padding: 40px; grid-column: 1/-1;">Belum ada buku tersedia</p>';
return;
}
books.forEach(book => {
const card = document.createElement('div');
card.className = 'book-card';
// Gunakan placeholder jika coverURL kosong/null
const imgUrl = (book.coverURL && book.coverURL !== 'null') ? book.coverURL : 'https://via.placeholder.com/200x300/667eea/ffffff?text=Buku';
card.innerHTML = `
<img src="${imgUrl}" alt="${book.judul}" class="book-cover">
<div class="book-info">
<h5>${book.judul}</h5>
<p>Penulis: ${book.penulis}</p>
<div class="book-stok">
<span class="material-icons" style="font-size: 16px;">inventory_2</span>
Stok: ${book.stok}
</div>
<button class="action-btn btn-borrow" onclick="borrowBookStudent('${book.id}')" ${book.stok <= 0 ? 'disabled' : ''} style="margin-top: 12px; width: 100%;">
${book.stok > 0 ? 'Pinjam Buku' : 'Stok Habis'}
</button>
</div>
`;
grid.appendChild(card);
});
}).getBooks();
}
function loadStudentBorrows() {
google.script.run.withSuccessHandler(function(history) {
const tbody = document.querySelector('#studentBorrowsTable tbody');
tbody.innerHTML = '';
if (history.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;">Belum ada riwayat</td></tr>';
return;
}
history.forEach(item => {
const row = tbody.insertRow();
let statusBadge = `<span class="badge ${item.status.toLowerCase()}">${item.status}</span>`;
row.innerHTML = `
<td>${item.id}</td>
<td>${item.judulBuku}</td>
<td>${item.tanggalPinjam}</td>
<td>${item.tanggalKembali || '-'}</td>
<td>${statusBadge}</td>
`;
});
}).getBorrowHistory(currentUser.email, currentUser.role);
}
function borrowBookStudent(bukuId) {
Swal.fire({ title: 'Pinjam Buku?', icon: 'question', showCancelButton: true, confirmButtonText: 'Ya' }).then((result) => {
if (result.isConfirmed) {
google.script.run.withSuccessHandler(function(res) {
showResultAlert(res);
if (res.success) { loadStudentBooks(); loadStudentBorrows(); }
}).borrowBook(currentUser.email, bukuId);
}
});
}
</script>
</body>
</html>
Tidak ada komentar:
Posting Komentar