355 lines
13 KiB
JavaScript
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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
})();
|