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,382 @@
|
|||||||
|
let editing = false;
|
||||||
|
/** Create */
|
||||||
|
function openCreateModal() {
|
||||||
|
editing = false;
|
||||||
|
|
||||||
|
document.getElementById("reservationModalTitle").innerText = "Add DHCP Reservation";
|
||||||
|
|
||||||
|
document.getElementById("reservationForm").reset();
|
||||||
|
document.getElementById("reservationId").value = "";
|
||||||
|
document.getElementById("enabled").checked = true;
|
||||||
|
|
||||||
|
clearValidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Edit*/
|
||||||
|
function editReservation(hostname, mac, ip, enabled) {
|
||||||
|
editing = true;
|
||||||
|
document.getElementById("reservationModalTitle").innerText = "Edit DHCP Reservation";
|
||||||
|
document.getElementById("hostname").value = hostname;
|
||||||
|
document.getElementById("macAddress").value = mac;
|
||||||
|
document.getElementById("ipAddress").value = ip;
|
||||||
|
document.getElementById("enabled").checked = enabled;
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById("reservationModal"));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** MAC formatting */
|
||||||
|
document.getElementById("macAddress").addEventListener("input", function () {
|
||||||
|
let value = this.value.replace(/[^a-fA-F0-9]/g, "").toUpperCase();
|
||||||
|
|
||||||
|
let pairs = value.match(/.{1,2}/g);
|
||||||
|
|
||||||
|
this.value = pairs ? pairs.join(":").substring(0, 17) : "";
|
||||||
|
});
|
||||||
|
function clearValidation() {
|
||||||
|
document.getElementById("macAddress").classList.remove("is-invalid");
|
||||||
|
document.getElementById("ipAddress").classList.remove("is-invalid");
|
||||||
|
document.getElementById("macError").innerText = "Invalid MAC address.";
|
||||||
|
document.getElementById("ipError").innerText = "Invalid IP address.";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("reservationForm").addEventListener("submit", function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
clearValidation();
|
||||||
|
|
||||||
|
const id = document.getElementById("reservationId").value;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
hostname: document.getElementById("hostname").value.trim(),
|
||||||
|
|
||||||
|
mac_address: document.getElementById("macAddress").value.trim(),
|
||||||
|
|
||||||
|
ip_address: document.getElementById("ipAddress").value.trim(),
|
||||||
|
|
||||||
|
enabled: document.getElementById("enabled").checked,
|
||||||
|
};
|
||||||
|
|
||||||
|
const macValid = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/i.test(payload.mac_address);
|
||||||
|
|
||||||
|
const ipValid = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/.test(
|
||||||
|
payload.ip_address,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!macValid) {
|
||||||
|
document.getElementById("macAddress").classList.add("is-invalid");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ipValid) {
|
||||||
|
document.getElementById("ipAddress").classList.add("is-invalid");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = id ? dhcpConfig.reservationBaseUrl + "/" + id : dhcpConfig.reservationStoreUrl;
|
||||||
|
|
||||||
|
const method = id ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const submitButton = this.querySelector('button[type="submit"]');
|
||||||
|
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = true;
|
||||||
|
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>' + "Saving...";
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
|
||||||
|
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (!data.success) {
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById("reservationModal"));
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Reservation save error:", error);
|
||||||
|
|
||||||
|
if (error.errors && error.errors.mac_address) {
|
||||||
|
document.getElementById("macAddress").classList.add("is-invalid");
|
||||||
|
|
||||||
|
document.getElementById("macError").innerText = error.errors.mac_address[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.errors && error.errors.ip_address) {
|
||||||
|
document.getElementById("ipAddress").classList.add("is-invalid");
|
||||||
|
|
||||||
|
document.getElementById("ipError").innerText = error.errors.ip_address[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({icon: 'error', title: 'Gagal menyimpan', text: error.message || "Failed to save reservation."});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (submitButton) {
|
||||||
|
submitButton.disabled = false;
|
||||||
|
submitButton.innerHTML = '<i class="bi bi-check-lg me-1"></i>' + "Save Reservation";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*** Search*/
|
||||||
|
document.getElementById("searchReservation").addEventListener("input", function () {
|
||||||
|
const keyword = this.value.toLowerCase();
|
||||||
|
document.querySelectorAll("#reservationTable tbody tr").forEach(function (row) {
|
||||||
|
row.style.display = row.innerText.toLowerCase().includes(keyword) ? "" : "none";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*** Delete*/
|
||||||
|
function deleteReservation(hostname) {
|
||||||
|
Swal.fire({title: 'Hapus reservasi "' + hostname + '"?', icon: 'warning', showCancelButton: true, confirmButtonText: 'Ya, hapus', cancelButtonText: 'Batal', confirmButtonColor: '#dc3545'})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.isConfirmed) return;
|
||||||
|
Swal.fire({icon: 'info', title: 'Mode demo', text: 'Reservation "' + hostname + '" deleted.'});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test DHCP
|
||||||
|
*/
|
||||||
|
function testDhcp() {
|
||||||
|
Swal.fire({icon: 'info', title: 'Mode demo', text: "DHCP configuration is valid."});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply
|
||||||
|
*/
|
||||||
|
function applyDhcp() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById("applyModal"));
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Confirm Apply */
|
||||||
|
function confirmApply() {
|
||||||
|
const modalElement = document.getElementById("applyModal");
|
||||||
|
|
||||||
|
const modal = bootstrap.Modal.getInstance(modalElement);
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = document.querySelector('#applyModal [onclick="confirmApply()"]');
|
||||||
|
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>' + "Applying...";
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(dhcpConfig.applyUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]').getAttribute("content"),
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
if (!data.success) {
|
||||||
|
throw data;
|
||||||
|
}
|
||||||
|
|
||||||
|
Swal.fire({icon: 'success', title: 'Berhasil', text: data.message || "DHCP configuration applied successfully."});
|
||||||
|
|
||||||
|
refreshDhcpStatus();
|
||||||
|
window.location.reload();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("DHCP apply error:", error);
|
||||||
|
|
||||||
|
Swal.fire({icon: 'error', title: 'Gagal apply', text: error.message || "Failed to apply DHCP configuration."});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = "Apply Configuration";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDhcpStatus() {
|
||||||
|
fetch(dhcpConfig.statusUrl)
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("HTTP " + response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
document.getElementById("dhcpServerHostname").innerText = data.hostname ?? "DHCP Server";
|
||||||
|
|
||||||
|
const badge = document.getElementById("dhcpStatusBadge");
|
||||||
|
|
||||||
|
const clients = document.getElementById("activeClients");
|
||||||
|
|
||||||
|
const lastApplied = document.getElementById("lastApplied");
|
||||||
|
const serverIp = document.getElementById("dhcpServerIp");
|
||||||
|
|
||||||
|
const serverInterface = document.getElementById("dhcpServerInterface");
|
||||||
|
|
||||||
|
serverIp.innerText = data.ip ?? "-";
|
||||||
|
|
||||||
|
serverInterface.innerText = data.interface ?? "-";
|
||||||
|
|
||||||
|
// STATUS
|
||||||
|
if (data.running) {
|
||||||
|
badge.innerHTML = `
|
||||||
|
<span class="badge rounded-pill bg-success bg-opacity-10 text-success">
|
||||||
|
<span
|
||||||
|
class="d-inline-block rounded-circle bg-success me-1"
|
||||||
|
style="width:7px;height:7px;"
|
||||||
|
></span>
|
||||||
|
Running
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
badge.innerHTML = `
|
||||||
|
<span class="badge rounded-pill bg-danger bg-opacity-10 text-danger">
|
||||||
|
<span
|
||||||
|
class="d-inline-block rounded-circle bg-danger me-1"
|
||||||
|
style="width:7px;height:7px;"
|
||||||
|
></span>
|
||||||
|
Stopped
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACTIVE CLIENTS
|
||||||
|
clients.innerHTML =
|
||||||
|
(data.reservations_count ?? data.active_clients ?? 0) +
|
||||||
|
' <span class="text-muted fw-normal small">registered</span>';
|
||||||
|
|
||||||
|
// LAST APPLIED
|
||||||
|
if (data.last_applied) {
|
||||||
|
const date = new Date(data.last_applied.replace(" ", "T"));
|
||||||
|
|
||||||
|
lastApplied.innerText = date.toLocaleString("id-ID", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
lastApplied.innerText = "Never";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("DHCP status error:", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDhcpLogs() {
|
||||||
|
const container = document.getElementById("dhcpLogs");
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-muted p-4">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch(dhcpConfig.logsUrl)
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("HTTP " + response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
|
||||||
|
.then((data) => {
|
||||||
|
if (!data.logs || !data.logs.length) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-muted p-4">
|
||||||
|
No DHCP events found.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = data.logs
|
||||||
|
.map((log) => {
|
||||||
|
const escaped = escapeHtml(log);
|
||||||
|
|
||||||
|
let className = "text-muted";
|
||||||
|
|
||||||
|
if (log.includes("DHCPACK")) {
|
||||||
|
className = "text-success";
|
||||||
|
} else if (log.includes("DHCPNAK")) {
|
||||||
|
className = "text-danger";
|
||||||
|
} else if (log.includes("DHCPREQUEST")) {
|
||||||
|
className = "text-primary";
|
||||||
|
} else if (log.includes("DHCPDISCOVER")) {
|
||||||
|
className = "text-warning";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="px-4 py-2 border-bottom
|
||||||
|
${className}">
|
||||||
|
${escaped}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
})
|
||||||
|
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("DHCP logs error:", error);
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="text-danger p-4">
|
||||||
|
Failed to load DHCP logs.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
|
||||||
|
div.textContent = value;
|
||||||
|
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshDhcpStatus();
|
||||||
|
loadDhcpLogs();
|
||||||
|
setInterval(refreshDhcpStatus, 5000, loadDhcpLogs, 5000);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,514 @@
|
|||||||
|
/**
|
||||||
|
* CRUD Tarif Stiker - Vanilla JS dengan pola OPTIMISTIC UI.
|
||||||
|
*
|
||||||
|
* Konsep optimistic UI:
|
||||||
|
* - Begitu user submit/edit/hapus, DOM langsung diupdate SEKETIKA
|
||||||
|
* (tanpa nunggu response server), supaya terasa instan.
|
||||||
|
* - Request ke server tetap dikirim di belakang layar.
|
||||||
|
* - Kalau server balas SUKSES -> data "disegel" jadi permanen
|
||||||
|
* (nomor/ID asli dari server dipakai buat gantiin ID sementara).
|
||||||
|
* - Kalau server balas GAGAL -> perubahan di DOM di-ROLLBACK
|
||||||
|
* (baris balik ke kondisi semula / dihapus lagi / dikembalikan).
|
||||||
|
*
|
||||||
|
* Trade-off yang harus disadari:
|
||||||
|
* - UX terasa lebih cepat & responsif.
|
||||||
|
* - Tapi kalau internet lambat / gagal, user sempat lihat data yang
|
||||||
|
* "belum tentu benar" sebelum akhirnya di-rollback. Makanya kita
|
||||||
|
* kasih indikator visual (opacity pudar) untuk baris yang masih
|
||||||
|
* "pending" konfirmasi dari server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const ENDPOINT = window.TARIFSTIKER_ENDPOINT;
|
||||||
|
const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.content;
|
||||||
|
|
||||||
|
if (!CSRF_TOKEN) {
|
||||||
|
console.error("Meta tag csrf-token tidak ditemukan di head layout.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbody = document.getElementById("tbodyTarifStiker");
|
||||||
|
const modalEl = document.getElementById("modalTarifStiker");
|
||||||
|
const modal = new bootstrap.Modal(modalEl);
|
||||||
|
const form = document.getElementById("formTarifStiker");
|
||||||
|
const btnTambah = document.getElementById("btnTambah");
|
||||||
|
const modalTitle = document.getElementById("modalTarifStikerTitle");
|
||||||
|
const btnSimpan = document.getElementById("btnSimpan");
|
||||||
|
|
||||||
|
const inputMode = document.getElementById("inputMode");
|
||||||
|
const inputTarifIdOriginal = document.getElementById("inputTarifIdOriginal");
|
||||||
|
const inputTarifId = document.getElementById("inputTarifId");
|
||||||
|
const inputIdMobil = document.getElementById("inputIdMobil");
|
||||||
|
const inputJenisLangganan = document.getElementById("inputJenisLangganan");
|
||||||
|
const inputTarif = document.getElementById("inputTarif");
|
||||||
|
const inputLastMember = document.getElementById("inputLastMember");
|
||||||
|
const inputTglAkhir = document.getElementById("inputTglAkhir");
|
||||||
|
const inputJenis = document.getElementById("inputJenis");
|
||||||
|
const inputJenisMember = document.getElementById("inputJenisMember");
|
||||||
|
const inputHitungAwalBulan = document.getElementById("inputHitungAwalBulan");
|
||||||
|
const inputAvailableOnline = document.getElementById("inputAvailableOnline");
|
||||||
|
const inputLangganan = document.getElementById("inputLangganan");
|
||||||
|
const inputKodeAkses = document.getElementById("inputKodeAkses");
|
||||||
|
|
||||||
|
// Menyimpan salinan data asli dari server. Dipakai sebagai
|
||||||
|
// "source of truth" untuk rollback kalau ada request yang gagal.
|
||||||
|
let cacheData = [];
|
||||||
|
|
||||||
|
// Counter untuk ID sementara pada baris baru yang belum dikonfirmasi server.
|
||||||
|
let tempIdCounter = 0;
|
||||||
|
|
||||||
|
// State sorting berdasarkan tarif_id. 'desc' = data terbaru di atas (default).
|
||||||
|
let sortDirection = "desc";
|
||||||
|
const thTarifId = document.getElementById("thTarifId");
|
||||||
|
const sortIconTarifId = document.getElementById("sortIconTarifId");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Urutkan cacheData berdasarkan tarif_id sesuai sortDirection saat ini.
|
||||||
|
* Dipanggil sebelum setiap renderAll() supaya urutan selalu konsisten,
|
||||||
|
* termasuk setelah create/update/delete optimistic.
|
||||||
|
*/
|
||||||
|
function applySorting() {
|
||||||
|
cacheData.sort((a, b) => {
|
||||||
|
const idA = Number(a.tarif_id);
|
||||||
|
const idB = Number(b.tarif_id);
|
||||||
|
return sortDirection === "asc" ? idA - idB : idB - idA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update ikon panah di header sesuai arah sort aktif.
|
||||||
|
*/
|
||||||
|
function updateSortIcon() {
|
||||||
|
if (sortIconTarifId) {
|
||||||
|
sortIconTarifId.textContent = sortDirection === "asc" ? "▲" : "▼";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 + 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 || {},
|
||||||
|
);
|
||||||
|
|
||||||
|
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",
|
||||||
|
...options,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (text === null || text === undefined) return "";
|
||||||
|
return String(text)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render satu baris <tr> dari objek data.
|
||||||
|
* `pending = true` -> baris ditampilkan pudar (opacity) menandakan
|
||||||
|
* masih menunggu konfirmasi dari server (optimistic state).
|
||||||
|
*/
|
||||||
|
function buildRow(row, pending = false) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.dataset.tarifId = row.tarif_id;
|
||||||
|
if (pending) tr.classList.add("opacity-50");
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${escapeHtml(row.tarif_id)}</td>
|
||||||
|
<td>${escapeHtml(row.id_mobil)}</td>
|
||||||
|
<td>${escapeHtml(row.jenis_langganan)}</td>
|
||||||
|
<td>${escapeHtml(row.tarif)}</td>
|
||||||
|
<td>${escapeHtml(row.jenis)}</td>
|
||||||
|
<td>${escapeHtml(row.jenis_member)}</td>
|
||||||
|
<td>${escapeHtml(row.stasiun_kerja?.nama ?? row.langganan ?? "-")}</td>
|
||||||
|
<td>${
|
||||||
|
row.available_online
|
||||||
|
? '<span class="badge bg-success">Ya</span>'
|
||||||
|
: '<span class="badge bg-secondary">Tidak</span>'
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>${escapeHtml(row.kode_akses)}</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
<button class="btn btn-sm btn-warning btn-edit" data-tarif-id="${escapeHtml(row.tarif_id)}" ${pending ? "disabled" : ""}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger btn-hapus" data-tarif-id="${escapeHtml(row.tarif_id)}" ${pending ? "disabled" : ""}>
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEmptyState() {
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="text-center text-muted py-4">
|
||||||
|
Belum ada data tarif stiker.
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAll() {
|
||||||
|
applySorting();
|
||||||
|
updateSortIcon();
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
if (!cacheData.length) {
|
||||||
|
renderEmptyState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cacheData.forEach((row) => tbody.appendChild(buildRow(row)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load data awal dari server. Ini SATU-SATUNYA tempat yang
|
||||||
|
* "menunggu" server secara normal (bukan optimistic), karena
|
||||||
|
* belum ada apa-apa untuk ditampilkan sebelum data pertama datang.
|
||||||
|
*/
|
||||||
|
async function loadData() {
|
||||||
|
tbody.innerHTML = loadingRow(10, "Memuat data tarif stiker...");
|
||||||
|
try {
|
||||||
|
const json = await apiFetch(`${ENDPOINT}/data`, { method: "GET" });
|
||||||
|
cacheData = json.data || [];
|
||||||
|
renderAll();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="text-center text-danger py-4">
|
||||||
|
Gagal memuat data: ${escapeHtml(err.message)}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFormErrors() {
|
||||||
|
form.querySelectorAll(".is-invalid").forEach((el) => el.classList.remove("is-invalid"));
|
||||||
|
form.querySelectorAll("[data-error-for]").forEach((el) => (el.textContent = ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFormErrors(errors) {
|
||||||
|
if (!errors) return;
|
||||||
|
const map = {
|
||||||
|
tarif_id: inputTarifId,
|
||||||
|
id_mobil: inputIdMobil,
|
||||||
|
jenis_langganan: inputJenisLangganan,
|
||||||
|
tarif: inputTarif,
|
||||||
|
last_member: inputLastMember,
|
||||||
|
tgl_akhir: inputTglAkhir,
|
||||||
|
jenis: inputJenis,
|
||||||
|
jenis_member: inputJenisMember,
|
||||||
|
hitung_awal_bulan: inputHitungAwalBulan,
|
||||||
|
langganan: inputLangganan,
|
||||||
|
kode_akses: inputKodeAkses,
|
||||||
|
};
|
||||||
|
Object.keys(errors).forEach((field) => {
|
||||||
|
const errorBox = form.querySelector(`[data-error-for="${field}"]`);
|
||||||
|
if (errorBox) errorBox.textContent = errors[field][0];
|
||||||
|
if (map[field]) map[field].classList.add("is-invalid");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.reset();
|
||||||
|
clearFormErrors();
|
||||||
|
inputMode.value = "create";
|
||||||
|
inputTarifIdOriginal.value = "";
|
||||||
|
inputTarifId.disabled = false;
|
||||||
|
modalTitle.textContent = "Tambah Tarif Stiker";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(data) {
|
||||||
|
inputMode.value = "edit";
|
||||||
|
inputTarifIdOriginal.value = data.tarif_id;
|
||||||
|
inputTarifId.value = data.tarif_id;
|
||||||
|
inputTarifId.disabled = true; // tarif_id adalah PK, tidak boleh diubah saat edit
|
||||||
|
inputIdMobil.value = data.id_mobil ?? "";
|
||||||
|
inputJenisLangganan.value = data.jenis_langganan ?? "";
|
||||||
|
inputTarif.value = data.tarif ?? "";
|
||||||
|
inputLastMember.value = data.last_member ?? "";
|
||||||
|
inputTglAkhir.value = data.tgl_akhir ?? "";
|
||||||
|
inputJenis.value = data.jenis ?? "";
|
||||||
|
inputJenisMember.value = data.jenis_member ?? "";
|
||||||
|
inputHitungAwalBulan.value = data.hitung_awal_bulan ?? "";
|
||||||
|
inputAvailableOnline.checked = !!data.available_online;
|
||||||
|
inputLangganan.value = data.langganan ?? "";
|
||||||
|
inputKodeAkses.value = data.kode_akses ?? "";
|
||||||
|
modalTitle.textContent = `Edit Tarif Stiker - ${data.tarif_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPayload() {
|
||||||
|
return {
|
||||||
|
tarif_id: inputTarifId.value === "" ? null : Number(inputTarifId.value),
|
||||||
|
id_mobil: inputIdMobil.value.trim(),
|
||||||
|
jenis_langganan: inputJenisLangganan.value,
|
||||||
|
tarif: inputTarif.value === "" ? null : Number(inputTarif.value),
|
||||||
|
last_member: inputLastMember.value.trim() || null,
|
||||||
|
tgl_akhir: inputTglAkhir.value.trim() || null,
|
||||||
|
jenis: inputJenis.value.trim() || null,
|
||||||
|
jenis_member: inputJenisMember.value.trim() || null,
|
||||||
|
hitung_awal_bulan: inputHitungAwalBulan.value === "" ? null : Number(inputHitungAwalBulan.value),
|
||||||
|
available_online: inputAvailableOnline.checked,
|
||||||
|
langganan: inputLangganan.value || null,
|
||||||
|
kode_akses: inputKodeAkses.value.trim() || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== EVENT: Buka modal tambah ==============
|
||||||
|
btnTambah.addEventListener("click", () => {
|
||||||
|
resetForm();
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============== EVENT: Klik header "Tarif ID" untuk toggle sort ==============
|
||||||
|
if (thTarifId) {
|
||||||
|
thTarifId.addEventListener("click", () => {
|
||||||
|
sortDirection = sortDirection === "asc" ? "desc" : "asc";
|
||||||
|
renderAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== EVENT: Edit / Hapus (delegasi) ==============
|
||||||
|
tbody.addEventListener("click", async (e) => {
|
||||||
|
const editBtn = e.target.closest(".btn-edit");
|
||||||
|
const hapusBtn = e.target.closest(".btn-hapus");
|
||||||
|
|
||||||
|
if (editBtn) {
|
||||||
|
const tarifId = editBtn.dataset.tarifId;
|
||||||
|
const existing = cacheData.find((r) => String(r.tarif_id) === String(tarifId));
|
||||||
|
if (existing) {
|
||||||
|
clearFormErrors();
|
||||||
|
fillForm(existing);
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hapusBtn) {
|
||||||
|
const tarifId = hapusBtn.dataset.tarifId;
|
||||||
|
const tanya = await Swal.fire({
|
||||||
|
title: `Hapus tarif stiker #${tarifId}?`,
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: "Ya, hapus",
|
||||||
|
cancelButtonText: "Batal",
|
||||||
|
confirmButtonColor: "#dc3545",
|
||||||
|
});
|
||||||
|
if (!tanya.isConfirmed) return;
|
||||||
|
await handleOptimisticDelete(tarifId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ============== OPTIMISTIC DELETE ==============
|
||||||
|
* Langsung hapus baris dari cache + DOM, baru kirim request.
|
||||||
|
* Kalau gagal, baris dikembalikan ke posisi semula.
|
||||||
|
*/
|
||||||
|
async function handleOptimisticDelete(tarifId) {
|
||||||
|
const index = cacheData.findIndex((r) => String(r.tarif_id) === String(tarifId));
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const backup = cacheData[index];
|
||||||
|
|
||||||
|
// 1. Update UI seketika (optimis)
|
||||||
|
cacheData.splice(index, 1);
|
||||||
|
renderAll();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Kirim request ke server di belakang layar
|
||||||
|
await apiFetch(`${ENDPOINT}/${encodeURIComponent(tarifId)}`, { method: "DELETE" });
|
||||||
|
// 3a. Sukses -> tidak perlu apa-apa, state sudah benar
|
||||||
|
info("success", "Berhasil", `Tarif stiker #${tarifId} dihapus.`);
|
||||||
|
} catch (err) {
|
||||||
|
// 3b. Gagal -> ROLLBACK: kembalikan data ke posisi semula
|
||||||
|
cacheData.splice(index, 0, backup);
|
||||||
|
renderAll();
|
||||||
|
info("error", "Gagal menghapus data", "Perubahan dibatalkan: " + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== EVENT: Submit form (create / update) — OPTIMISTIC ==============
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearFormErrors();
|
||||||
|
|
||||||
|
const payload = collectPayload();
|
||||||
|
const isEdit = inputMode.value === "edit";
|
||||||
|
|
||||||
|
if (isEdit) {
|
||||||
|
await handleOptimisticUpdate(payload);
|
||||||
|
} else {
|
||||||
|
await handleOptimisticCreate(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ============== OPTIMISTIC CREATE ==============
|
||||||
|
* Baris baru langsung ditampilkan dengan ID sementara (pending),
|
||||||
|
* baru "disegel" pakai tarif_id asli setelah server merespons sukses.
|
||||||
|
*/
|
||||||
|
async function handleOptimisticCreate(payload) {
|
||||||
|
// ID sementara negatif supaya gak bentrok sama tarif_id asli (yang selalu positif).
|
||||||
|
const tempId = --tempIdCounter;
|
||||||
|
|
||||||
|
const optimisticRow = {
|
||||||
|
...payload,
|
||||||
|
tarif_id: payload.tarif_id ?? tempId,
|
||||||
|
stasiun_kerja: null, // belum tau nama stasiun kerja sebelum konfirmasi server
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Tampilkan baris baru seketika (pudar, menandakan "pending")
|
||||||
|
cacheData.unshift(optimisticRow);
|
||||||
|
renderAll();
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
btnSimpan.disabled = true;
|
||||||
|
btnSimpan.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Menyimpan...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Kirim ke server
|
||||||
|
const json = await apiFetch(ENDPOINT, { method: "POST", body: payload });
|
||||||
|
|
||||||
|
// 3a. Sukses -> ganti data optimis dengan data asli dari server
|
||||||
|
const idx = cacheData.findIndex((r) => r.tarif_id === optimisticRow.tarif_id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
cacheData[idx] = json.data;
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
info("success", "Berhasil", "Tarif stiker baru ditambahkan.");
|
||||||
|
} catch (err) {
|
||||||
|
// 3b. Gagal -> ROLLBACK: buang baris optimis dari cache
|
||||||
|
cacheData = cacheData.filter((r) => r.tarif_id !== optimisticRow.tarif_id);
|
||||||
|
renderAll();
|
||||||
|
|
||||||
|
if (err.status === 422 && err.payload?.errors) {
|
||||||
|
// Buka lagi modal supaya user bisa perbaiki input
|
||||||
|
modal.show();
|
||||||
|
showFormErrors(err.payload.errors);
|
||||||
|
} else {
|
||||||
|
info("error", "Gagal menyimpan data", "Perubahan dibatalkan: " + err.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
btnSimpan.disabled = false;
|
||||||
|
btnSimpan.textContent = "Simpan";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ============== OPTIMISTIC UPDATE ==============
|
||||||
|
* Baris langsung diperbarui di DOM, baru dikonfirmasi ke server.
|
||||||
|
* Kalau gagal, baris dikembalikan ke data lama (backup).
|
||||||
|
*/
|
||||||
|
async function handleOptimisticUpdate(payload) {
|
||||||
|
const tarifId = inputTarifIdOriginal.value;
|
||||||
|
const index = cacheData.findIndex((r) => String(r.tarif_id) === String(tarifId));
|
||||||
|
if (index === -1) return;
|
||||||
|
|
||||||
|
const backup = { ...cacheData[index] };
|
||||||
|
|
||||||
|
// Cari nama stasiun kerja dari dropdown untuk tampilan sementara (opsional, kosmetik saja)
|
||||||
|
const selectedOption = inputLangganan.selectedOptions[0];
|
||||||
|
const optimisticStasiunNama =
|
||||||
|
selectedOption && selectedOption.value ? selectedOption.textContent.replace(/\s*\([^)]*\)\s*$/, "") : null;
|
||||||
|
|
||||||
|
// 1. Update UI seketika
|
||||||
|
cacheData[index] = {
|
||||||
|
...backup,
|
||||||
|
...payload,
|
||||||
|
tarif_id: backup.tarif_id, // tarif_id tidak berubah
|
||||||
|
stasiun_kerja: payload.langganan ? { kode: payload.langganan, nama: optimisticStasiunNama } : null,
|
||||||
|
};
|
||||||
|
renderAll();
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
btnSimpan.disabled = true;
|
||||||
|
btnSimpan.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Menyimpan...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. Kirim ke server
|
||||||
|
const json = await apiFetch(`${ENDPOINT}/${encodeURIComponent(tarifId)}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: payload,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3a. Sukses -> selaraskan dengan data resmi dari server
|
||||||
|
const idx = cacheData.findIndex((r) => String(r.tarif_id) === String(tarifId));
|
||||||
|
if (idx !== -1) {
|
||||||
|
cacheData[idx] = json.data;
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
info("success", "Berhasil", "Perubahan tarif stiker disimpan.");
|
||||||
|
} catch (err) {
|
||||||
|
// 3b. Gagal -> ROLLBACK ke data lama
|
||||||
|
const idx = cacheData.findIndex((r) => String(r.tarif_id) === String(tarifId));
|
||||||
|
if (idx !== -1) {
|
||||||
|
cacheData[idx] = backup;
|
||||||
|
renderAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.status === 422 && err.payload?.errors) {
|
||||||
|
fillForm(backup);
|
||||||
|
modal.show();
|
||||||
|
showFormErrors(err.payload.errors);
|
||||||
|
} else {
|
||||||
|
info("error", "Gagal memperbarui data", "Perubahan dibatalkan: " + err.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
btnSimpan.disabled = false;
|
||||||
|
btnSimpan.textContent = "Simpan";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============== INIT ==============
|
||||||
|
document.addEventListener("DOMContentLoaded", loadData);
|
||||||
|
if (document.readyState !== "loading") {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
$("#example tbody").on("click", "tr", function () {
|
||||||
|
console.log($("#example tbody tr").length);
|
||||||
|
const notrans = $(this).data("notrans");
|
||||||
|
|
||||||
|
// hapus semua highlight
|
||||||
|
$("#example tbody tr").removeClass("selected");
|
||||||
|
|
||||||
|
// highlight semua row dalam 1 grup
|
||||||
|
$(`#example tbody tr[data-notrans="${notrans}"]`).addClass("selected");
|
||||||
|
|
||||||
|
// ambil data dari row pertama grup
|
||||||
|
const firstRow = $(`#example tbody tr[data-notrans="${notrans}"]`).first();
|
||||||
|
|
||||||
|
selectedRowData = {
|
||||||
|
notrans: firstRow.data("notrans"),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Selected detail trans :", selectedRowData);
|
||||||
|
|
||||||
|
$("#rowActions").removeClass("d-none");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".detail-button").on("click", function () {
|
||||||
|
$("#modal-details").show();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
// var vehicle = @json($vehicleSelect);
|
||||||
|
|
||||||
|
// let selectedRowData = null;
|
||||||
|
let harga = 0;
|
||||||
|
|
||||||
|
$("#example tbody").on("click", "tr", function () {
|
||||||
|
console.log($("#example tbody tr").length);
|
||||||
|
const notrans = $(this).data("notrans");
|
||||||
|
|
||||||
|
// hapus semua highlight
|
||||||
|
$("#example tbody tr").removeClass("selected");
|
||||||
|
|
||||||
|
// highlight semua row dalam 1 grup
|
||||||
|
$(`#example tbody tr[data-notrans="${notrans}"]`).addClass("selected");
|
||||||
|
|
||||||
|
// ambil data dari row pertama grup
|
||||||
|
const firstRow = $(`#example tbody tr[data-notrans="${notrans}"]`).first();
|
||||||
|
|
||||||
|
selectedRowData = {
|
||||||
|
notrans: firstRow.data("notrans"),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Selected transaksi:", selectedRowData);
|
||||||
|
|
||||||
|
$("#rowActions").removeClass("d-none");
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on("click", ".extend-button", function () {
|
||||||
|
console.log("Tombol extend ditekan");
|
||||||
|
if (!selectedRowData) {
|
||||||
|
alert("Pilih data terlebih dahulu");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var rawId = $(this).data("id");
|
||||||
|
// let id = rawId.substring(0, rawId.indexOf('/'));
|
||||||
|
let id = selectedRowData.notrans;
|
||||||
|
var kendaraan = vehicle;
|
||||||
|
console.log("Data ID : ", id);
|
||||||
|
let actionUrl = $(this).data("url");
|
||||||
|
|
||||||
|
// var actionUrl = '{{ route("getStikerMember.show", ":id") }}';
|
||||||
|
// actionUrl = actionUrl.replace(":id", "" + encodeURIComponent(id));
|
||||||
|
actionUrl = actionUrl.replace(":id", "" + encodeURIComponent(id));
|
||||||
|
console.log("Action URL : ", actionUrl);
|
||||||
|
// $('#extend-member').attr('action'), actionUrl;
|
||||||
|
$.ajax({
|
||||||
|
url: actionUrl,
|
||||||
|
type: "GET",
|
||||||
|
success: function (r) {
|
||||||
|
console.log("Datanya : ", r);
|
||||||
|
if (r) {
|
||||||
|
$("#extend-member").val("");
|
||||||
|
$("#list-nopol-ext").empty();
|
||||||
|
$("#list-card-ext").empty();
|
||||||
|
$(".notransnya").val(r.notrans);
|
||||||
|
$(".namanya").val(r.nama);
|
||||||
|
$(".address").val(r.alamat);
|
||||||
|
$("#periode-ext").val(
|
||||||
|
`${moment(r.awal).format("DD-MM-YYYY HH:mm")} - ${moment(r.akhir).format("DD-MM-YYYY HH:mm")}`,
|
||||||
|
);
|
||||||
|
$("#ext-jenisLang")
|
||||||
|
.val(
|
||||||
|
$("#ext-jenisLang option")
|
||||||
|
.filter(function () {
|
||||||
|
return $(this).text().trim() === r.jmember;
|
||||||
|
})
|
||||||
|
.val(),
|
||||||
|
)
|
||||||
|
.trigger("change")
|
||||||
|
.prop("disabled", true);
|
||||||
|
$("#ext-jenisLang-value").val(r.jenis_langganan);
|
||||||
|
$("#ext-department")
|
||||||
|
.val(
|
||||||
|
$("#ext-department option")
|
||||||
|
.filter(function () {
|
||||||
|
return $(this).text().trim() === r.dept;
|
||||||
|
})
|
||||||
|
.val(),
|
||||||
|
)
|
||||||
|
.trigger("change")
|
||||||
|
.prop("disabled", true);
|
||||||
|
$("#ext-department-value").val(r.unit_kerja);
|
||||||
|
$("#member-ext").val(r.unit_kerja);
|
||||||
|
$("#department-ext").val(r.jenis_langganan);
|
||||||
|
var rows = r.stiker_details;
|
||||||
|
if (rows.length !== 0) {
|
||||||
|
rows.forEach((row) => {
|
||||||
|
var vehicleName = String(row.jenis_mobil)?.nama || "Unknown";
|
||||||
|
harga += Number(row.tarif);
|
||||||
|
$("#list-nopol-ext").append(`
|
||||||
|
<tr>
|
||||||
|
<td class="text-center p-2">${row.nopol}</td>
|
||||||
|
<td class="text-center p-2" data-value="${row.jenis_mobil}">
|
||||||
|
${row.vehicle}
|
||||||
|
</td>
|
||||||
|
<td class="text-center p-2">${row.jenis_member} - ${row.tarif}</td>
|
||||||
|
<td class="text-center p-2">${row.merk}</td>
|
||||||
|
<td class="text-center p-2">
|
||||||
|
<button class="btn btn-danger btn-xs delete-row" disabled>
|
||||||
|
Hapus
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
$(".harga")
|
||||||
|
.val(harga ?? r.harga ?? 0)
|
||||||
|
.prop("disabled", true);
|
||||||
|
} else {
|
||||||
|
$("#list-nopol-ext").append(`
|
||||||
|
<tr>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rowss = r.memberCard;
|
||||||
|
if (rowss.length !== 0) {
|
||||||
|
rowss.forEach((ro) => {
|
||||||
|
$("#list-card-ext").append(`
|
||||||
|
<tr>
|
||||||
|
<td class="text-center p-2">${ro.no_card}</td>
|
||||||
|
<td class="text-center p-2">${ro.no_urut}</td>
|
||||||
|
<td class="text-center p-2"><button class="btn btn-danger btn-xs delete-row" disabled>Hapus<buttion></td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$("#list-card-ext").append(`
|
||||||
|
<tr>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
<td class="text-center p-2">Tidak ada data</td>
|
||||||
|
</tr>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
console.log("Data : ", r);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error("Error : ", xhr);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#modal-extend").modal("show");
|
||||||
|
});
|
||||||
|
|
||||||
|
const today = moment();
|
||||||
|
const startDate = moment(today).startOf("day").format("DD-MM-YYYY 00:00");
|
||||||
|
const endDate = moment(today).endOf("day").format("DD-MM-YYYY 23:59");
|
||||||
|
|
||||||
|
$(".input-daterange-timepicker-ext").daterangepicker(
|
||||||
|
// 👈 method-nya "daterangepicker", bukan "datepicker"
|
||||||
|
{
|
||||||
|
timePicker: true,
|
||||||
|
locale: {
|
||||||
|
format: "DD-MM-YYYY HH:mm",
|
||||||
|
},
|
||||||
|
timePicker24Hour: true,
|
||||||
|
timePickerSeconds: false,
|
||||||
|
buttonClasses: ["btn", "btn-sm"],
|
||||||
|
applyClass: "btn-danger",
|
||||||
|
cancelClass: "btn-inverse",
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
parentEl: "#extend-member", // 👈 daterangepicker pakai "parentEl", bukan "container"
|
||||||
|
},
|
||||||
|
function (startDate, endDate) {
|
||||||
|
console.log(
|
||||||
|
`Range selected: ${startDate.format("DD-MM-YYYY HH:mm")} s/d ${endDate.format("DD-MM-YYYY HH:mm")}`,
|
||||||
|
);
|
||||||
|
$(".input-daterange-timepicker-ext").val(
|
||||||
|
`${startDate.format("DD-MM-YYYY HH:mm")} s/d ${endDate.format("DD-MM-YYYY HH:mm")}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
$(".input-daterange-timepicker-ext").val(`${startDate} - ${endDate}`);
|
||||||
|
|
||||||
|
$(".nopol-add").on("input", function () {
|
||||||
|
$(this).val($(this).val().replace(/\s/g, ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".merk").on("input", function () {
|
||||||
|
$(this).val($(this).val().replace(/\s/g, ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".kartu").on("input", function () {
|
||||||
|
$(this).val($(this).val().replace(/\s/g, ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".nourut").on("input", function () {
|
||||||
|
$(this).val($(this).val().replace(/\s/g, ""));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#jenisVehicleExt").on("change", function () {
|
||||||
|
const jenisMembers = member;
|
||||||
|
const vehicleSelected = $(this).val();
|
||||||
|
const jenisMemberExt = $("#jenisMemberExt");
|
||||||
|
|
||||||
|
jenisMemberExt.empty();
|
||||||
|
jenisMemberExt.append('<option selected value="">Pilih Jenis</option>');
|
||||||
|
|
||||||
|
jenisMembers.forEach((item) => {
|
||||||
|
if (String(item.id_mobil) === String(vehicleSelected)) {
|
||||||
|
jenisMemberExt.append(`
|
||||||
|
<option value="${item.tarif}">
|
||||||
|
${item.jenis_langganan} - ${item.tarif}
|
||||||
|
</option>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kalau tidak ada pilihan
|
||||||
|
if (jenisMemberExt.children("option").length === 1) {
|
||||||
|
jenisMemberExt.append('<option value="">Tidak ada pilihan</option>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#ext-nopol-add").on("click", function () {
|
||||||
|
var nopolExt = $(".ext-nopol-add").val().trim();
|
||||||
|
// var vehicle = $('#jenisVehicle option:selected').text();
|
||||||
|
var vehicleExtName = $("#jenisVehicleExt option:selected").text();
|
||||||
|
var vehicleExt = $("#jenisVehicleExt").val();
|
||||||
|
var memberExt = $("#jenisMemberExt option:selected").text();
|
||||||
|
var memberExtValue = $("#jenisMemberExt option:selected").val();
|
||||||
|
var merkExt = $(".merkExt").val().trim();
|
||||||
|
console.log(merkExt);
|
||||||
|
$(".ext-nopol-add").removeClass("is-invalid"); // Hapus kelas jika ada sebelumnya
|
||||||
|
$(".ext-nopol-add").next(".invalid-feedback").hide();
|
||||||
|
|
||||||
|
if (!vehicleExt) {
|
||||||
|
alert("Wajib pilih jenis kendaraan");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "stiker/checkField",
|
||||||
|
type: "POST",
|
||||||
|
data: { nopol: nopolExt },
|
||||||
|
headers: { "X-CSRF-token": $('meta[name="csrf-token"]').attr("content") },
|
||||||
|
success: function (r) {
|
||||||
|
if (r.response === true) {
|
||||||
|
harga += Number(memberExtValue);
|
||||||
|
var rowhtml = `
|
||||||
|
<tr>
|
||||||
|
<td class="text-center">${nopolExt}</td>
|
||||||
|
<td class="text-center" data-value="${vehicleExt}">${vehicleExtName}</td>
|
||||||
|
<td class="text-center">${memberExt}</td>
|
||||||
|
<td class="text-center">${merkExt}</td>
|
||||||
|
<td class="text-center p-1" style="text-align: center;">
|
||||||
|
<button class="btn btn-danger btn-xs delete-row">Hapus<buttion>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
$("#list-nopol-ext").append(rowhtml);
|
||||||
|
console.log("Berhasil menambahkan " + nopolExt);
|
||||||
|
|
||||||
|
$(".ext-nopol-add").val("");
|
||||||
|
$("#jenisVehicleExt").val("");
|
||||||
|
$("#jenisMemberExt").val("");
|
||||||
|
$(".merkExt").val("");
|
||||||
|
$(".harga")
|
||||||
|
.val(harga ?? r.harga ?? 0)
|
||||||
|
.text(`Rp. ${harga}` ?? `${r.harga}` ?? 0)
|
||||||
|
.prop("disabled", true);
|
||||||
|
} else {
|
||||||
|
// var warn = `<div class="invalid-feedback">Nopol sudah ada</div>`;
|
||||||
|
$(".ext-nopol-add").addClass("is-invalid"); // Tambahkan kelas invalid
|
||||||
|
$(".ext-nopol-add").next(".invalid-feedback").text(r.message).show(); // Tampilkan pesan error
|
||||||
|
console.log(r.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
alert("Terjadi kesalahan saat memeriksa nopol");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".simpan-ext").on("click", function () {
|
||||||
|
let url = $("#extend-member").attr("action");
|
||||||
|
let extend = $(this);
|
||||||
|
let formExtend = $("#extend-member").serialize();
|
||||||
|
let nopolData = [];
|
||||||
|
let cardData = [];
|
||||||
|
|
||||||
|
$("#list-nopol-ext tr").each(function () {
|
||||||
|
var row = $(this);
|
||||||
|
var rowData = {
|
||||||
|
nopol: row.find("td:nth-child(1)").text().trim(),
|
||||||
|
vehicle: row.find("td:nth-child(2)").data("value"),
|
||||||
|
member: row.find("td:nth-child(3)").text().trim(),
|
||||||
|
merk: row.find("td:nth-child(4)").text().trim(),
|
||||||
|
};
|
||||||
|
nopolData.push(rowData);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(nopolData);
|
||||||
|
|
||||||
|
$("#list-card-ext tr").each(function () {
|
||||||
|
var row = $(this);
|
||||||
|
// var rowCard = {
|
||||||
|
// nocard: row.find('td:nth-child(1)').text(),
|
||||||
|
// nourut: row.find('td:nth-child(2)').text(),
|
||||||
|
// };
|
||||||
|
const nocard = row.find("td:nth-child(1)").text().trim();
|
||||||
|
const nourut = row.find("td:nth-child(2)").text().trim();
|
||||||
|
|
||||||
|
if (!nocard || nocard === "Tidak ada data" || !nourut || nourut === "Tidak ada data") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardData.push({
|
||||||
|
nocard: nocard,
|
||||||
|
nourut: nourut,
|
||||||
|
});
|
||||||
|
// cardData.push(rowCard);
|
||||||
|
cardData.push({
|
||||||
|
nocard: nocard,
|
||||||
|
nourut: nourut,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const stringDataExt = formExtend;
|
||||||
|
|
||||||
|
// Mengonversi query string menjadi objek
|
||||||
|
const formDataExt = {};
|
||||||
|
const searchExtend = new URLSearchParams(stringDataExt);
|
||||||
|
|
||||||
|
searchExtend.forEach((value, key) => {
|
||||||
|
formDataExt[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
var extData = {
|
||||||
|
formData: formDataExt,
|
||||||
|
nopolData: nopolData,
|
||||||
|
cardNumber: cardData,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(extData);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: "POST",
|
||||||
|
data: extData,
|
||||||
|
headers: {
|
||||||
|
"X-CSRF-TOKEN": $('meta[name="csrf-token"]').attr("content"),
|
||||||
|
},
|
||||||
|
berforeSend: function () {
|
||||||
|
extend.text("Loading...");
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
if (data.status === true) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "success",
|
||||||
|
title: "Berhasil",
|
||||||
|
text: "Member berhasil di daftarkan.. !!" + data.message,
|
||||||
|
});
|
||||||
|
$("#modal-member-extend").modal("hide");
|
||||||
|
} else {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: "Gagal",
|
||||||
|
text: data.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log(data);
|
||||||
|
},
|
||||||
|
error: function (xhr) {
|
||||||
|
console.error("Error : ", xhr);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// $(document).on('click', '.edit-button', function () {
|
||||||
|
// let id = selectedRowData;
|
||||||
|
// $('#edit-member').attr('action', '/transaksi/member/' + id);
|
||||||
|
// $('#ubah-member').modal('show');
|
||||||
|
// // console.log(selectedRowData);
|
||||||
|
// // console.log(id);
|
||||||
|
// $.get(`/transaksi/member/${selectedRowData}`, function(res){
|
||||||
|
|
||||||
|
// fillMemberModal(res);
|
||||||
|
|
||||||
|
// $('#modal-member').modal('show');
|
||||||
|
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// $('#btnEdit').on('click', function(){
|
||||||
|
|
||||||
|
// if(!selectedRowData) {
|
||||||
|
// alert('Pilih data dulu');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// console.log(selectedRowData);
|
||||||
|
|
||||||
|
// $.get(`/member/${selectedRowData.notrans}`, function(res){
|
||||||
|
|
||||||
|
// fillMemberModal(res);
|
||||||
|
|
||||||
|
// $('#modal-member').modal('show');
|
||||||
|
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// function fillMemberModal(res){
|
||||||
|
|
||||||
|
// let m = res.member;
|
||||||
|
|
||||||
|
// // TAB 1 - DATA PENGUNJUNG
|
||||||
|
// $('[name="nama"]').val(m.nama);
|
||||||
|
// $('[name="address"]').val(m.alamat);
|
||||||
|
// $('[name="daterange"]').val(m.awal + ' - ' + m.akhir);
|
||||||
|
// $('[name="member"]').val(m.jenis_langganan).trigger('change');
|
||||||
|
// $('[name="depart"]').val(m.dept).trigger('change');
|
||||||
|
// $('[name="note"]').val(m.note);
|
||||||
|
// $('[name="harga"]').val(m.harga);
|
||||||
|
|
||||||
|
// // =========================
|
||||||
|
// // TAB 2 - KENDARAAN
|
||||||
|
// // =========================
|
||||||
|
// $('#list-nopol').empty();
|
||||||
|
|
||||||
|
// res.kendaraan.forEach(v => {
|
||||||
|
// $('#list-nopol').append(`
|
||||||
|
// <tr>
|
||||||
|
// <td>${v.nopol}</td>
|
||||||
|
// <td>${v.jenis}</td>
|
||||||
|
// <td>${v.merk}</td>
|
||||||
|
// <td>${v.member}</td>
|
||||||
|
// <td>
|
||||||
|
// <button class="btn btn-danger btn-sm remove-nopol" data-id="${v.id}">Hapus</button>
|
||||||
|
// </td>
|
||||||
|
// </tr>
|
||||||
|
// `);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // =========================
|
||||||
|
// // TAB 3 - KARTU
|
||||||
|
// // =========================
|
||||||
|
// $('#list-card').empty();
|
||||||
|
|
||||||
|
// res.kartu.forEach(k => {
|
||||||
|
// $('#list-card').append(`
|
||||||
|
// <tr>
|
||||||
|
// <td>${k.kartu}</td>
|
||||||
|
// <td>${k.nourut}</td>
|
||||||
|
// <td>
|
||||||
|
// <button class="btn btn-danger btn-sm remove-card" data-id="${k.id}">Hapus</button>
|
||||||
|
// </td>
|
||||||
|
// </tr>
|
||||||
|
// `);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// $('#modal-member').on('hidden.bs.modal', function () {
|
||||||
|
// $(this).find('form')[0].reset();
|
||||||
|
// $('#list-nopol').empty();
|
||||||
|
// $('#list-card').empty();
|
||||||
|
// });
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
(function ($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
/* Code for attribute data-custom-class for adding custom class to tooltip */
|
||||||
|
if (typeof $.fn.tooltip.Constructor === "undefined") {
|
||||||
|
throw new Error("Bootstrap Tooltip must be included first!");
|
||||||
|
}
|
||||||
|
|
||||||
|
var Tooltip = $.fn.tooltip.Constructor;
|
||||||
|
|
||||||
|
// add customClass option to Bootstrap Tooltip
|
||||||
|
$.extend(Tooltip.Default, {
|
||||||
|
customClass: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
var _show = Tooltip.prototype.show;
|
||||||
|
|
||||||
|
Tooltip.prototype.show = function () {
|
||||||
|
// invoke parent method
|
||||||
|
_show.apply(this, Array.prototype.slice.apply(arguments));
|
||||||
|
|
||||||
|
if (this.config.customClass) {
|
||||||
|
var tip = this.getTipElement();
|
||||||
|
$(tip).addClass(this.config.customClass);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
Reference in New Issue
Block a user