Track public JS yg dipakai view (tooltips, dhcp, stasiunkerja, tarifstiker, member-details/extend) biar ikut ke-deploy
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user