Files

355 lines
13 KiB
JavaScript

/**
* 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 <meta name="csrf-token">.
* Pastikan tag ini ADA di <head> layout utama:
* <meta name="csrf-token" content="{{ csrf_token() }}">
* 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 <meta name="csrf-token" content="{{ csrf_token() }}"> 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 `<tr><td colspan="${colspan}" class="text-center text-muted py-4">
<span class="spinner-border spinner-border-sm me-2"></span>${text}</td></tr>`;
}
/**
* 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Render baris tabel dari array data.
*/
function renderTable(rows) {
tbody.innerHTML = "";
if (!rows.length) {
tbody.innerHTML = `
<tr>
<td colspan="13" class="text-center text-muted py-4">
Belum ada data stasiun kerja.
</td>
</tr>`;
return;
}
rows.forEach((row) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${escapeHtml(row.kode)}</td>
<td>${escapeHtml(row.nama)}</td>
<td>${escapeHtml(row.margin)}</td>
<td>${escapeHtml(row.spv)}</td>
<td>${escapeHtml(row.target)}</td>
<td>${escapeHtml(row.id_lokasi)}</td>
<td>${escapeHtml(row.lantai)}</td>
<td>${
row.status == 1
? '<span class="badge bg-success">Aktif</span>'
: '<span class="badge bg-secondary">Nonaktif</span>'
}
</td>
<td>${escapeHtml(row.alamat)}</td>
<td>${escapeHtml(row.bank)}</td>
<td>${escapeHtml(row.telp)}</td>
<td>${escapeHtml(row.kode_akses)}</td>
<td class="text-nowrap">
<button class="btn btn-sm btn-warning btn-edit" data-kode="${escapeHtml(row.kode)}">
Edit
</button>
<button class="btn btn-sm btn-danger btn-hapus" data-kode="${escapeHtml(row.kode)}">
Hapus
</button>
</td>
`;
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 = `
<tr>
<td colspan="13" class="text-center text-danger py-4">
Gagal memuat data: ${escapeHtml(err.message)}
</td>
</tr>`;
}
}
/**
* 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 = '<span class="spinner-border spinner-border-sm me-1"></span>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();
}
})();