<script>
/**
* SISTEM MANAJEMEN INVENTARIS - FRONTEND JAVASCRIPT (JavaScript.html)
* Ditulis oleh: Apps Script Expert
* Deskripsi: State management, filtering, render table, dan integrasi dengan Backend GAS.
*/
// State Utama Aplikasi Sisi Klien
let dbItems = [];
const KATEGORI_LIST = ["Elektronik", "Alat Tulis Kantor", "Furnitur", "Konsumsi", "Lainnya"];
let selectedDeleteId = null;
// Mulai Proses Saat Window Selesai Memuat DOM
window.onload = function() {
initKategoriSelect();
loadInventarisData();
};
/**
* Mengisi elemen dropdown kategori secara dinamis
*/
function initKategoriSelect() {
const filterSelect = document.getElementById('categoryFilter');
const formSelect = document.getElementById('itemCategory');
// Reset konten
filterSelect.innerHTML = '<option value="">Semua Kategori</option>';
formSelect.innerHTML = '<option value="" disabled selected>Pilih Kategori...</option>';
KATEGORI_LIST.forEach(cat => {
// Tambah ke filter dropdown
const optFilter = document.createElement('option');
optFilter.value = cat;
optFilter.textContent = cat;
filterSelect.appendChild(optFilter);
// Tambah ke form input dropdown
const optForm = document.createElement('option');
optForm.value = cat;
optForm.textContent = cat;
formSelect.appendChild(optForm);
});
}
/**
* Ambil data dari backend (Kode.gs -> readAll)
*/
function loadInventarisData() {
showTableLoading();
google.script.run
.withSuccessHandler(function(response) {
dbItems = response;
renderDashboardStats(dbItems);
renderTable(dbItems);
})
.withFailureHandler(function(error) {
showToast("Terjadi kesalahan sistem: " + error.message, "error");
renderTable([]); // Tampilkan placeholder kosong
})
.readAll();
}
/**
* Merender Kalkulasi Angka Dashboard Statistik
*/
function renderDashboardStats(items) {
const totalJenis = items.length;
let totalStok = 0;
let totalNilaiAset = 0;
let stokMenipisCount = 0;
items.forEach(item => {
const qty = Number(item.jumlah) || 0;
const price = Number(item.harga) || 0;
totalStok += qty;
totalNilaiAset += (qty * price);
if (qty < 5) {
stokMenipisCount++;
}
});
document.getElementById('statTotalJenis').textContent = totalJenis;
document.getElementById('statTotalStok').textContent = totalStok;
document.getElementById('statNilaiAset').textContent = formatRupiah(totalNilaiAset);
document.getElementById('statStokMenipis').textContent = stokMenipisCount;
// Ubah visualisasi visual card peringatan stok menipis jika > 0
const cardStok = document.getElementById('cardStokMenipis');
const iconStok = document.getElementById('iconStokMenipis');
if (stokMenipisCount > 0) {
cardStok.className = "bg-amber-50 p-6 rounded-2xl shadow-sm border border-amber-200 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1";
iconStok.className = "bg-amber-500 text-white p-4 rounded-xl";
} else {
cardStok.className = "bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex items-center justify-between transition-all duration-300 hover:shadow-md hover:-translate-y-1";
iconStok.className = "bg-slate-100 text-slate-400 p-4 rounded-xl";
}
}
/**
* Merender Baris Tabel Data dari Array
*/
function renderTable(itemsToRender) {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
if (itemsToRender.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-20">
<div class="flex flex-col items-center justify-center space-y-2">
<i class="fa-solid fa-box-open text-slate-300 text-5xl"></i>
<span class="text-slate-400 font-medium">Tidak ada data inventaris ditemukan</span>
</div>
</td>
</tr>
`;
return;
}
itemsToRender.forEach(item => {
const tr = document.createElement('tr');
tr.className = "hover:bg-slate-50/50 transition duration-150";
// Badge Kategori Kustomisasi Visual
let catBadgeClass = "bg-slate-100 text-slate-800";
if (item.kategori === "Elektronik") catBadgeClass = "bg-blue-50 text-blue-700 border border-blue-100";
else if (item.kategori === "Alat Tulis Kantor") catBadgeClass = "bg-indigo-50 text-indigo-700 border border-indigo-100";
else if (item.kategori === "Furnitur") catBadgeClass = "bg-purple-50 text-purple-700 border border-purple-100";
else if (item.kategori === "Konsumsi") catBadgeClass = "bg-amber-50 text-amber-700 border border-amber-100";
// Stok Text Styling jika menipis
const isLowStock = Number(item.jumlah) < 5;
const stokCell = isLowStock
? `<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-bold bg-rose-50 text-rose-700 border border-rose-100"><span class="w-1.5 h-1.5 rounded-full bg-rose-600 animate-ping"></span>${item.jumlah} Unit</span>`
: `<span class="text-sm font-semibold text-slate-700">${item.jumlah} Unit</span>`;
tr.innerHTML = `
<td class="py-4 px-6 text-sm font-semibold text-slate-500">${item.id}</td>
<td class="py-4 px-6 text-sm font-bold text-slate-800">${item.namabarang}</td>
<td class="py-4 px-6"><span class="px-2.5 py-1 rounded-lg text-xs font-semibold ${catBadgeClass}">${item.kategori}</span></td>
<td class="py-4 px-6 text-center">${stokCell}</td>
<td class="py-4 px-6 text-sm font-bold text-slate-700 text-right">${formatRupiah(item.harga)}</td>
<td class="py-4 px-6 text-sm font-medium text-slate-600">${item.lokasi}</td>
<td class="py-4 px-6 text-xs text-slate-400 font-mono">${item.tanggalupdate}</td>
<td class="py-4 px-6 text-center">
<div class="inline-flex items-center space-x-2">
<button onclick="editBarang('${item.id}')" class="p-2 text-blue-600 hover:text-blue-800 hover:bg-blue-50 rounded-lg transition" title="Edit Barang">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button onclick="bukaDeleteModal('${item.id}', '${item.namabarang}')" class="p-2 text-rose-600 hover:text-rose-800 hover:bg-rose-50 rounded-lg transition" title="Hapus Barang">
<i class="fa-solid fa-trash-can"></i>
</button>
</div>
</td>
`;
tbody.appendChild(tr);
});
}
/**
* Filter & Search Real-Time
*/
function filterData() {
const q = document.getElementById('searchQuery').value.toLowerCase();
const cat = document.getElementById('categoryFilter').value;
const filtered = dbItems.filter(item => {
const matchSearch = item.id.toLowerCase().includes(q) ||
item.namabarang.toLowerCase().includes(q) ||
item.lokasi.toLowerCase().includes(q);
const matchCat = cat === "" || item.kategori === cat;
return matchSearch && matchCat;
});
renderTable(filtered);
}
/**
* Handler untuk Menyimpan Data (Create / Update)
*/
function simpanData(e) {
e.preventDefault();
const id = document.getElementById('itemId').value;
const name = document.getElementById('itemName').value;
const cat = document.getElementById('itemCategory').value;
const qty = document.getElementById('itemQuantity').value;
const price = document.getElementById('itemPrice').value;
const loc = document.getElementById('itemLocation').value;
// Tombol Loading State
const btnSubmit = document.getElementById('btnSubmitForm');
const originalText = btnSubmit.innerText;
btnSubmit.disabled = true;
btnSubmit.innerText = "Memproses...";
const itemData = {
namabarang: name,
kategori: cat,
jumlah: qty,
harga: price,
lokasi: loc
};
if (id) {
// MODE UPDATE BARANG
itemData.id = id;
google.script.run
.withSuccessHandler(function(msg) {
showToast(msg, "success");
tutupModal();
loadInventarisData();
})
.withFailureHandler(function(error) {
showToast("Update gagal: " + error.message, "error");
btnSubmit.disabled = false;
btnSubmit.innerText = originalText;
})
.updateRow(itemData);
} else {
// MODE CREATE BARANG BARU
google.script.run
.withSuccessHandler(function(msg) {
showToast(msg, "success");
tutupModal();
loadInventarisData();
})
.withFailureHandler(function(error) {
showToast("Tambah gagal: " + error.message, "error");
btnSubmit.disabled = false;
btnSubmit.innerText = originalText;
})
.createRow(itemData);
}
}
/**
* MODE EDIT: Mengisi Form modal dengan data lama
*/
function editBarang(id) {
const matchedItem = dbItems.find(item => item.id === id);
if (!matchedItem) return;
bukaModal(true); // Buka modal dalam edit mode
document.getElementById('itemId').value = matchedItem.id;
document.getElementById('itemName').value = matchedItem.namabarang;
document.getElementById('itemCategory').value = matchedItem.kategori;
document.getElementById('itemQuantity').value = matchedItem.jumlah;
document.getElementById('itemPrice').value = matchedItem.harga;
document.getElementById('itemLocation').value = matchedItem.lokasi;
}
/**
* DELETE - Pemicu aksi hapus baris barang
*/
function eksekusiHapus() {
if (!selectedDeleteId) return;
const btn = document.getElementById('btnConfirmDelete');
btn.disabled = true;
btn.innerText = "Menghapus...";
google.script.run
.withSuccessHandler(function(msg) {
showToast(msg, "success");
tutupDeleteModal();
loadInventarisData();
})
.withFailureHandler(function(error) {
showToast("Gagal menghapus: " + error.message, "error");
tutupDeleteModal();
})
.deleteRow(selectedDeleteId);
}
/* --- MANAJEMEN MODAL DAN POPUP UI --- */
function bukaModal(isEdit = false) {
const modal = document.getElementById('formModal');
const container = modal.querySelector('.modal-container');
document.getElementById('modalTitle').textContent = isEdit ? "Edit Data Barang" : "Tambah Barang Baru";
document.getElementById('btnSubmitForm').textContent = isEdit ? "Simpan Perubahan" : "Simpan Barang";
// Pastikan Form Bersih dari Data Sebelumnya jika form tambah
if (!isEdit) {
document.getElementById('itemForm').reset();
document.getElementById('itemId').value = '';
}
modal.classList.remove('hidden');
setTimeout(() => {
container.classList.add('modal-open-anim');
}, 10);
}
function tutupModal() {
const modal = document.getElementById('formModal');
const container = modal.querySelector('.modal-container');
container.classList.remove('modal-open-anim');
setTimeout(() => {
modal.classList.add('hidden');
}, 200);
}
function bukaDeleteModal(id, nama) {
selectedDeleteId = id;
document.getElementById('deleteTargetName').textContent = id + " (" + nama + ")";
const modal = document.getElementById('deleteModal');
const container = modal.querySelector('.delete-modal-container');
const btn = document.getElementById('btnConfirmDelete');
btn.disabled = false;
btn.innerText = "Hapus Permanen";
btn.onclick = eksekusiHapus;
modal.classList.remove('hidden');
setTimeout(() => {
container.classList.add('modal-open-anim');
}, 10);
}
function tutupDeleteModal() {
const modal = document.getElementById('deleteModal');
const container = modal.querySelector('.delete-modal-container');
container.classList.remove('modal-open-anim');
setTimeout(() => {
modal.classList.add('hidden');
selectedDeleteId = null;
}, 200);
}
function showTableLoading() {
document.getElementById('tableBody').innerHTML = `
<tr>
<td colspan="8" class="text-center py-20">
<div class="flex flex-col items-center justify-center space-y-3">
<div class="w-10 h-10 border-4 border-emerald-500 border-t-transparent rounded-full animate-spin"></div>
<span class="text-slate-400 font-medium">Sedang memproses data...</span>
</div>
</td>
</tr>
`;
}
/* --- UTILITY HELPER FUNCTIONS --- */
/**
* Memformat angka standar menjadi format rupiah lokal Indonesia
*/
function formatRupiah(num) {
return new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0
}).format(num);
}
/**
* Membuat dan memunculkan toast notifikasi kustom
*/
function showToast(message, type = "success") {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
const isSuccess = type === "success";
const bgClass = isSuccess ? "bg-emerald-900 border-emerald-800 text-emerald-100" : "bg-red-900 border-red-800 text-red-100";
const icon = isSuccess ? "fa-circle-check text-emerald-400" : "fa-triangle-exclamation text-red-400";
toast.className = `flex items-center gap-3 p-4 rounded-2xl shadow-xl border ${bgClass} toast-enter pointer-events-auto min-w-[300px] max-w-sm`;
toast.innerHTML = `
<i class="fa-solid ${icon} text-lg"></i>
<span class="text-sm font-semibold flex-1">${message}</span>
`;
container.appendChild(toast);
// Otomatis hapus toast dalam waktu 4 detik
setTimeout(() => {
toast.classList.add('toast-leave');
setTimeout(() => {
toast.remove();
}, 300);
}, 4000);
}
</script>