/** * CRUD Stasiun Kerja - Vanilla JS (fetch API) * * Catatan penting soal CSRF & session (biar tidak kejadian lagi * seperti "CSRF token mismatch" / auto-logout): * 1. Selalu kirim header X-CSRF-TOKEN dari meta tag . * Pastikan tag ini ADA di layout utama: * * 2. Selalu set credentials: 'same-origin' supaya cookie session ikut terkirim. * 3. Endpoint diambil dari window.STASIUNKERJA_ENDPOINT (di-inject dari Blade * pakai url()), BUKAN di-hardcode, supaya otomatis ikut host/port yang aktif. */ (function () { "use strict"; const ENDPOINT = window.STASIUNKERJA_ENDPOINT; // contoh: http://localhost:8888/stasiunkerja const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.content; if (!CSRF_TOKEN) { console.error( 'Meta tag csrf-token tidak ditemukan. Tambahkan di head layout.', ); } const tbody = document.getElementById("tbodyStasiunKerja"); const modalEl = document.getElementById("modalStasiunKerja"); const modal = new bootstrap.Modal(modalEl); const form = document.getElementById("formStasiunKerja"); const btnTambah = document.getElementById("btnTambah"); const modalTitle = document.getElementById("modalStasiunKerjaTitle"); const inputMode = document.getElementById("inputMode"); const inputKodeOriginal = document.getElementById("inputKodeOriginal"); const inputKode = document.getElementById("inputKode"); const inputNama = document.getElementById("inputNama"); const inputMargin = document.getElementById("inputMargin"); const inputTarget = document.getElementById("inputTarget"); const inputStatus = document.getElementById("inputStatus"); const inputKodeAkses = document.getElementById("inputKodeAkses"); const inputSpv = document.getElementById("inputSpv"); const inputIdLokasi = document.getElementById("inputIdLokasi"); const inputLantai = document.getElementById("inputLantai"); const inputBank = document.getElementById("inputBank"); const inputTelp = document.getElementById("inputTelp"); const inputAlamat = document.getElementById("inputAlamat"); /** * Notifikasi interaktif pengganti alert()/confirm() bawaan browser. * SweetAlert2 wajib di-load di view (vendor/sweetalert2). */ function info(icon, title, text) { return Swal.fire({ icon, title, text }); } function loadingRow(colspan, text) { return ` ${text}`; } /** * Helper fetch generik dengan header CSRF + credentials. */ async function apiFetch(url, options = {}) { const headers = Object.assign( { Accept: "application/json", "X-Requested-With": "XMLHttpRequest", "X-CSRF-TOKEN": CSRF_TOKEN, }, options.headers || {}, ); // Kalau body berupa object biasa (bukan FormData), kirim sebagai JSON. if (options.body && !(options.body instanceof FormData)) { headers["Content-Type"] = "application/json"; options.body = JSON.stringify(options.body); } const response = await fetch(url, { credentials: "same-origin", // WAJIB supaya cookie session ikut terkirim ...options, headers, }); // Kalau sesi habis / tidak ter-autentikasi, kasih info lalu redirect ke login // daripada diam-diam gagal. if (response.status === 401 || response.status === 419) { info("warning", "Sesi habis", "Sesi kamu sudah habis, silakan login ulang.").then(() => { window.location.href = "/login"; }); throw new Error("Unauthenticated / session expired"); } const json = await response.json().catch(() => null); if (!response.ok) { const error = new Error(json?.message || "Terjadi kesalahan pada server."); error.status = response.status; error.payload = json; throw error; } return json; } /** * Escape teks sederhana biar aman ditaruh ke innerHTML (mencegah XSS dasar). */ function escapeHtml(text) { if (text === null || text === undefined) return ""; return String(text) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } /** * Render baris tabel dari array data. */ function renderTable(rows) { tbody.innerHTML = ""; if (!rows.length) { tbody.innerHTML = ` Belum ada data stasiun kerja. `; return; } rows.forEach((row) => { const tr = document.createElement("tr"); tr.innerHTML = ` ${escapeHtml(row.kode)} ${escapeHtml(row.nama)} ${escapeHtml(row.margin)} ${escapeHtml(row.spv)} ${escapeHtml(row.target)} ${escapeHtml(row.id_lokasi)} ${escapeHtml(row.lantai)} ${ row.status == 1 ? 'Aktif' : 'Nonaktif' } ${escapeHtml(row.alamat)} ${escapeHtml(row.bank)} ${escapeHtml(row.telp)} ${escapeHtml(row.kode_akses)} `; tbody.appendChild(tr); }); } /** * Ambil semua data dari server dan render ke tabel. */ async function loadData() { tbody.innerHTML = loadingRow(13, "Memuat data stasiun kerja..."); try { const json = await apiFetch(`${ENDPOINT}/data`, { method: "GET" }); renderTable(json.data || []); } catch (err) { console.error(err); tbody.innerHTML = ` Gagal memuat data: ${escapeHtml(err.message)} `; } } /** * Bersihkan semua pesan error validasi & class is-invalid di form. */ function clearFormErrors() { form.querySelectorAll(".is-invalid").forEach((el) => el.classList.remove("is-invalid")); form.querySelectorAll("[data-error-for]").forEach((el) => (el.textContent = "")); } /** * Tampilkan error validasi (422) dari Laravel ke masing-masing field. */ function showFormErrors(errors) { if (!errors) return; Object.keys(errors).forEach((field) => { const errorBox = form.querySelector(`[data-error-for="${field}"]`); const inputEl = form.querySelector( `#input${field.charAt(0).toUpperCase()}${field.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase())}`, ); if (errorBox) errorBox.textContent = errors[field][0]; if (inputEl) inputEl.classList.add("is-invalid"); }); } /** * Reset form ke kondisi kosong (mode create). */ function resetForm() { form.reset(); clearFormErrors(); inputMode.value = "create"; inputKodeOriginal.value = ""; inputKode.disabled = false; modalTitle.textContent = "Tambah Stasiun Kerja"; } /** * Isi form dengan data existing untuk mode edit. */ function fillForm(data) { inputMode.value = "edit"; inputKodeOriginal.value = data.kode; inputKode.value = data.kode; inputKode.disabled = true; // kode adalah primary key, tidak boleh diubah saat edit inputNama.value = data.nama ?? ""; inputMargin.value = data.margin ?? ""; inputTarget.value = data.target ?? ""; inputStatus.value = data.status ?? 1; inputKodeAkses.value = data.kode_akses ?? ""; inputSpv.value = data.spv ?? ""; inputIdLokasi.value = data.id_lokasi ?? ""; inputLantai.value = data.lantai ?? ""; inputBank.value = data.bank ?? ""; inputTelp.value = data.telp ?? ""; inputAlamat.value = data.alamat ?? ""; modalTitle.textContent = `Edit Stasiun Kerja - ${data.kode}`; } /** * Kumpulkan payload dari isi form. */ function collectPayload() { return { kode: inputKode.value.trim(), nama: inputNama.value.trim(), margin: inputMargin.value === "" ? null : Number(inputMargin.value), target: inputTarget.value === "" ? null : Number(inputTarget.value), status: Number(inputStatus.value), kode_akses: inputKodeAkses.value.trim() || null, spv: inputSpv.value.trim() || null, id_lokasi: inputIdLokasi.value.trim() || null, lantai: inputLantai.value.trim() || null, bank: inputBank.value.trim() || null, telp: inputTelp.value.trim() || null, alamat: inputAlamat.value.trim() || null, }; } // ============== EVENT: Buka modal tambah ============== btnTambah.addEventListener("click", () => { resetForm(); modal.show(); }); // ============== EVENT: Klik tombol Edit / Hapus di tabel (delegasi) ============== tbody.addEventListener("click", async (e) => { const editBtn = e.target.closest(".btn-edit"); const hapusBtn = e.target.closest(".btn-hapus"); if (editBtn) { const kode = editBtn.dataset.kode; editBtn.disabled = true; const origText = editBtn.textContent; editBtn.textContent = "Memuat..."; try { const json = await apiFetch(`${ENDPOINT}/${encodeURIComponent(kode)}`, { method: "GET" }); clearFormErrors(); fillForm(json.data); modal.show(); } catch (err) { info("error", "Gagal mengambil data", err.message); } finally { editBtn.disabled = false; editBtn.textContent = origText; } } if (hapusBtn) { const kode = hapusBtn.dataset.kode; const tanya = await Swal.fire({ title: `Hapus stasiun kerja "${kode}"?`, icon: "warning", showCancelButton: true, confirmButtonText: "Ya, hapus", cancelButtonText: "Batal", confirmButtonColor: "#dc3545", }); if (!tanya.isConfirmed) return; try { await apiFetch(`${ENDPOINT}/${encodeURIComponent(kode)}`, { method: "DELETE" }); await loadData(); info("success", "Berhasil", `Stasiun kerja "${kode}" dihapus.`); } catch (err) { info("error", "Gagal menghapus data", err.message); } } }); // ============== EVENT: Submit form (create / update) ============== form.addEventListener("submit", async (e) => { e.preventDefault(); clearFormErrors(); const payload = collectPayload(); const isEdit = inputMode.value === "edit"; const url = isEdit ? `${ENDPOINT}/${encodeURIComponent(inputKodeOriginal.value)}` : ENDPOINT; const method = isEdit ? "PUT" : "POST"; const btnSimpan = document.getElementById("btnSimpan"); btnSimpan.disabled = true; btnSimpan.innerHTML = 'Menyimpan...'; try { await apiFetch(url, { method, body: payload }); modal.hide(); await loadData(); info("success", "Berhasil", isEdit ? "Perubahan stasiun kerja disimpan." : "Stasiun kerja baru ditambahkan."); } catch (err) { if (err.status === 422 && err.payload?.errors) { showFormErrors(err.payload.errors); } else { info("error", "Gagal menyimpan data", err.message); } } finally { btnSimpan.disabled = false; btnSimpan.textContent = "Simpan"; } }); // ============== INIT ============== document.addEventListener("DOMContentLoaded", loadData); // Kalau script ini di-load setelah DOMContentLoaded (misal via @push di akhir body), // panggil juga langsung supaya tidak tertinggal. if (document.readyState !== "loading") { loadData(); } })();