diff --git a/public/js/setup/dhcp.js b/public/js/setup/dhcp.js
new file mode 100644
index 0000000..98791f6
--- /dev/null
+++ b/public/js/setup/dhcp.js
@@ -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 = '' + "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 = '' + "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 = '' + "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 = `
+
+
+ Running
+
+ `;
+ } else {
+ badge.innerHTML = `
+
+
+ Stopped
+
+ `;
+ }
+
+ // ACTIVE CLIENTS
+ clients.innerHTML =
+ (data.reservations_count ?? data.active_clients ?? 0) +
+ ' registered';
+
+ // 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 = `
+
+ Loading...
+
+ `;
+
+ 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 = `
+
+ No DHCP events found.
+
+ `;
+
+ 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 `
+
+ ${escaped}
+
+ `;
+ })
+ .join("");
+ })
+
+ .catch((error) => {
+ console.error("DHCP logs error:", error);
+
+ container.innerHTML = `
+
+ Failed to load DHCP logs.
+
+ `;
+ });
+}
+
+function escapeHtml(value) {
+ const div = document.createElement("div");
+
+ div.textContent = value;
+
+ return div.innerHTML;
+}
+
+refreshDhcpStatus();
+loadDhcpLogs();
+setInterval(refreshDhcpStatus, 5000, loadDhcpLogs, 5000);
diff --git a/public/js/setup/stasiunkerja.js b/public/js/setup/stasiunkerja.js
new file mode 100644
index 0000000..cd496c6
--- /dev/null
+++ b/public/js/setup/stasiunkerja.js
@@ -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 .
+ * Pastikan tag ini ADA di layout utama:
+ *
+ * 2. Selalu set credentials: 'same-origin' supaya cookie session ikut terkirim.
+ * 3. Endpoint diambil dari window.STASIUNKERJA_ENDPOINT (di-inject dari Blade
+ * pakai url()), BUKAN di-hardcode, supaya otomatis ikut host/port yang aktif.
+ */
+
+(function () {
+ "use strict";
+
+ const ENDPOINT = window.STASIUNKERJA_ENDPOINT; // contoh: http://localhost:8888/stasiunkerja
+ const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.content;
+
+ if (!CSRF_TOKEN) {
+ console.error(
+ 'Meta tag csrf-token tidak ditemukan. Tambahkan di head layout.',
+ );
+ }
+
+ const tbody = document.getElementById("tbodyStasiunKerja");
+ const modalEl = document.getElementById("modalStasiunKerja");
+ const modal = new bootstrap.Modal(modalEl);
+ const form = document.getElementById("formStasiunKerja");
+ const btnTambah = document.getElementById("btnTambah");
+ const modalTitle = document.getElementById("modalStasiunKerjaTitle");
+
+ const inputMode = document.getElementById("inputMode");
+ const inputKodeOriginal = document.getElementById("inputKodeOriginal");
+ const inputKode = document.getElementById("inputKode");
+ const inputNama = document.getElementById("inputNama");
+ const inputMargin = document.getElementById("inputMargin");
+ const inputTarget = document.getElementById("inputTarget");
+ const inputStatus = document.getElementById("inputStatus");
+ const inputKodeAkses = document.getElementById("inputKodeAkses");
+ const inputSpv = document.getElementById("inputSpv");
+ const inputIdLokasi = document.getElementById("inputIdLokasi");
+ const inputLantai = document.getElementById("inputLantai");
+ const inputBank = document.getElementById("inputBank");
+ const inputTelp = document.getElementById("inputTelp");
+ const inputAlamat = document.getElementById("inputAlamat");
+
+ /**
+ * Notifikasi interaktif pengganti alert()/confirm() bawaan browser.
+ * SweetAlert2 wajib di-load di view (vendor/sweetalert2).
+ */
+ function info(icon, title, text) {
+ return Swal.fire({ icon, title, text });
+ }
+
+ function loadingRow(colspan, text) {
+ return `|
+ ${text} |
`;
+ }
+
+ /**
+ * Helper fetch generik dengan header CSRF + credentials.
+ */
+ async function apiFetch(url, options = {}) {
+ const headers = Object.assign(
+ {
+ Accept: "application/json",
+ "X-Requested-With": "XMLHttpRequest",
+ "X-CSRF-TOKEN": CSRF_TOKEN,
+ },
+ options.headers || {},
+ );
+
+ // Kalau body berupa object biasa (bukan FormData), kirim sebagai JSON.
+ if (options.body && !(options.body instanceof FormData)) {
+ headers["Content-Type"] = "application/json";
+ options.body = JSON.stringify(options.body);
+ }
+
+ const response = await fetch(url, {
+ credentials: "same-origin", // WAJIB supaya cookie session ikut terkirim
+ ...options,
+ headers,
+ });
+
+ // Kalau sesi habis / tidak ter-autentikasi, kasih info lalu redirect ke login
+ // daripada diam-diam gagal.
+ if (response.status === 401 || response.status === 419) {
+ info("warning", "Sesi habis", "Sesi kamu sudah habis, silakan login ulang.").then(() => {
+ window.location.href = "/login";
+ });
+ throw new Error("Unauthenticated / session expired");
+ }
+
+ const json = await response.json().catch(() => null);
+
+ if (!response.ok) {
+ const error = new Error(json?.message || "Terjadi kesalahan pada server.");
+ error.status = response.status;
+ error.payload = json;
+ throw error;
+ }
+
+ return json;
+ }
+
+ /**
+ * Escape teks sederhana biar aman ditaruh ke innerHTML (mencegah XSS dasar).
+ */
+ function escapeHtml(text) {
+ if (text === null || text === undefined) return "";
+ return String(text)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ /**
+ * Render baris tabel dari array data.
+ */
+ function renderTable(rows) {
+ tbody.innerHTML = "";
+
+ if (!rows.length) {
+ tbody.innerHTML = `
+
+ |
+ Belum ada data stasiun kerja.
+ |
+
`;
+ return;
+ }
+
+ rows.forEach((row) => {
+ const tr = document.createElement("tr");
+ tr.innerHTML = `
+ ${escapeHtml(row.kode)} |
+ ${escapeHtml(row.nama)} |
+ ${escapeHtml(row.margin)} |
+ ${escapeHtml(row.spv)} |
+ ${escapeHtml(row.target)} |
+ ${escapeHtml(row.id_lokasi)} |
+ ${escapeHtml(row.lantai)} |
+ ${
+ row.status == 1
+ ? 'Aktif'
+ : 'Nonaktif'
+ }
+ |
+ ${escapeHtml(row.alamat)} |
+ ${escapeHtml(row.bank)} |
+ ${escapeHtml(row.telp)} |
+ ${escapeHtml(row.kode_akses)} |
+
+
+
+ |
+ `;
+ tbody.appendChild(tr);
+ });
+ }
+
+ /**
+ * Ambil semua data dari server dan render ke tabel.
+ */
+ async function loadData() {
+ tbody.innerHTML = loadingRow(13, "Memuat data stasiun kerja...");
+ try {
+ const json = await apiFetch(`${ENDPOINT}/data`, { method: "GET" });
+ renderTable(json.data || []);
+ } catch (err) {
+ console.error(err);
+ tbody.innerHTML = `
+
+ |
+ Gagal memuat data: ${escapeHtml(err.message)}
+ |
+
`;
+ }
+ }
+
+ /**
+ * Bersihkan semua pesan error validasi & class is-invalid di form.
+ */
+ function clearFormErrors() {
+ form.querySelectorAll(".is-invalid").forEach((el) => el.classList.remove("is-invalid"));
+ form.querySelectorAll("[data-error-for]").forEach((el) => (el.textContent = ""));
+ }
+
+ /**
+ * Tampilkan error validasi (422) dari Laravel ke masing-masing field.
+ */
+ function showFormErrors(errors) {
+ if (!errors) return;
+ Object.keys(errors).forEach((field) => {
+ const errorBox = form.querySelector(`[data-error-for="${field}"]`);
+ const inputEl = form.querySelector(
+ `#input${field.charAt(0).toUpperCase()}${field.slice(1).replace(/_([a-z])/g, (_, c) => c.toUpperCase())}`,
+ );
+ if (errorBox) errorBox.textContent = errors[field][0];
+ if (inputEl) inputEl.classList.add("is-invalid");
+ });
+ }
+
+ /**
+ * Reset form ke kondisi kosong (mode create).
+ */
+ function resetForm() {
+ form.reset();
+ clearFormErrors();
+ inputMode.value = "create";
+ inputKodeOriginal.value = "";
+ inputKode.disabled = false;
+ modalTitle.textContent = "Tambah Stasiun Kerja";
+ }
+
+ /**
+ * Isi form dengan data existing untuk mode edit.
+ */
+ function fillForm(data) {
+ inputMode.value = "edit";
+ inputKodeOriginal.value = data.kode;
+ inputKode.value = data.kode;
+ inputKode.disabled = true; // kode adalah primary key, tidak boleh diubah saat edit
+ inputNama.value = data.nama ?? "";
+ inputMargin.value = data.margin ?? "";
+ inputTarget.value = data.target ?? "";
+ inputStatus.value = data.status ?? 1;
+ inputKodeAkses.value = data.kode_akses ?? "";
+ inputSpv.value = data.spv ?? "";
+ inputIdLokasi.value = data.id_lokasi ?? "";
+ inputLantai.value = data.lantai ?? "";
+ inputBank.value = data.bank ?? "";
+ inputTelp.value = data.telp ?? "";
+ inputAlamat.value = data.alamat ?? "";
+ modalTitle.textContent = `Edit Stasiun Kerja - ${data.kode}`;
+ }
+
+ /**
+ * Kumpulkan payload dari isi form.
+ */
+ function collectPayload() {
+ return {
+ kode: inputKode.value.trim(),
+ nama: inputNama.value.trim(),
+ margin: inputMargin.value === "" ? null : Number(inputMargin.value),
+ target: inputTarget.value === "" ? null : Number(inputTarget.value),
+ status: Number(inputStatus.value),
+ kode_akses: inputKodeAkses.value.trim() || null,
+ spv: inputSpv.value.trim() || null,
+ id_lokasi: inputIdLokasi.value.trim() || null,
+ lantai: inputLantai.value.trim() || null,
+ bank: inputBank.value.trim() || null,
+ telp: inputTelp.value.trim() || null,
+ alamat: inputAlamat.value.trim() || null,
+ };
+ }
+
+ // ============== EVENT: Buka modal tambah ==============
+ btnTambah.addEventListener("click", () => {
+ resetForm();
+ modal.show();
+ });
+
+ // ============== EVENT: Klik tombol Edit / Hapus di tabel (delegasi) ==============
+ tbody.addEventListener("click", async (e) => {
+ const editBtn = e.target.closest(".btn-edit");
+ const hapusBtn = e.target.closest(".btn-hapus");
+
+ if (editBtn) {
+ const kode = editBtn.dataset.kode;
+ editBtn.disabled = true;
+ const origText = editBtn.textContent;
+ editBtn.textContent = "Memuat...";
+ try {
+ const json = await apiFetch(`${ENDPOINT}/${encodeURIComponent(kode)}`, { method: "GET" });
+ clearFormErrors();
+ fillForm(json.data);
+ modal.show();
+ } catch (err) {
+ info("error", "Gagal mengambil data", err.message);
+ } finally {
+ editBtn.disabled = false;
+ editBtn.textContent = origText;
+ }
+ }
+
+ if (hapusBtn) {
+ const kode = hapusBtn.dataset.kode;
+ const tanya = await Swal.fire({
+ title: `Hapus stasiun kerja "${kode}"?`,
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Ya, hapus",
+ cancelButtonText: "Batal",
+ confirmButtonColor: "#dc3545",
+ });
+ if (!tanya.isConfirmed) return;
+
+ try {
+ await apiFetch(`${ENDPOINT}/${encodeURIComponent(kode)}`, { method: "DELETE" });
+ await loadData();
+ info("success", "Berhasil", `Stasiun kerja "${kode}" dihapus.`);
+ } catch (err) {
+ info("error", "Gagal menghapus data", err.message);
+ }
+ }
+ });
+
+ // ============== EVENT: Submit form (create / update) ==============
+ form.addEventListener("submit", async (e) => {
+ e.preventDefault();
+ clearFormErrors();
+
+ const payload = collectPayload();
+ const isEdit = inputMode.value === "edit";
+ const url = isEdit ? `${ENDPOINT}/${encodeURIComponent(inputKodeOriginal.value)}` : ENDPOINT;
+ const method = isEdit ? "PUT" : "POST";
+
+ const btnSimpan = document.getElementById("btnSimpan");
+ btnSimpan.disabled = true;
+ btnSimpan.innerHTML = 'Menyimpan...';
+
+ try {
+ await apiFetch(url, { method, body: payload });
+ modal.hide();
+ await loadData();
+ info("success", "Berhasil", isEdit ? "Perubahan stasiun kerja disimpan." : "Stasiun kerja baru ditambahkan.");
+ } catch (err) {
+ if (err.status === 422 && err.payload?.errors) {
+ showFormErrors(err.payload.errors);
+ } else {
+ info("error", "Gagal menyimpan data", err.message);
+ }
+ } finally {
+ btnSimpan.disabled = false;
+ btnSimpan.textContent = "Simpan";
+ }
+ });
+
+ // ============== INIT ==============
+ document.addEventListener("DOMContentLoaded", loadData);
+ // Kalau script ini di-load setelah DOMContentLoaded (misal via @push di akhir body),
+ // panggil juga langsung supaya tidak tertinggal.
+ if (document.readyState !== "loading") {
+ loadData();
+ }
+})();
diff --git a/public/js/setup/tarifstiker.js b/public/js/setup/tarifstiker.js
new file mode 100644
index 0000000..95e3632
--- /dev/null
+++ b/public/js/setup/tarifstiker.js
@@ -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 `|
+ ${text} |
`;
+ }
+
+ /**
+ * 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, "'");
+ }
+
+ /**
+ * Render satu baris 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 = `
+ | ${escapeHtml(row.tarif_id)} |
+ ${escapeHtml(row.id_mobil)} |
+ ${escapeHtml(row.jenis_langganan)} |
+ ${escapeHtml(row.tarif)} |
+ ${escapeHtml(row.jenis)} |
+ ${escapeHtml(row.jenis_member)} |
+ ${escapeHtml(row.stasiun_kerja?.nama ?? row.langganan ?? "-")} |
+ ${
+ row.available_online
+ ? 'Ya'
+ : 'Tidak'
+ }
+ |
+ ${escapeHtml(row.kode_akses)} |
+
+
+
+ |
+ `;
+ return tr;
+ }
+
+ function renderEmptyState() {
+ tbody.innerHTML = `
+
+ |
+ Belum ada data tarif stiker.
+ |
+
`;
+ }
+
+ 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 = `
+
+ |
+ Gagal memuat data: ${escapeHtml(err.message)}
+ |
+
`;
+ }
+ }
+
+ 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 = '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 = '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();
+ }
+})();
diff --git a/public/js/stikerMember/member-details.js b/public/js/stikerMember/member-details.js
new file mode 100644
index 0000000..fcbbc91
--- /dev/null
+++ b/public/js/stikerMember/member-details.js
@@ -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();
+ });
+});
diff --git a/public/js/stikerMember/member-extend.js b/public/js/stikerMember/member-extend.js
new file mode 100644
index 0000000..b872fb8
--- /dev/null
+++ b/public/js/stikerMember/member-extend.js
@@ -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(`
+
+ | ${row.nopol} |
+
+ ${row.vehicle}
+ |
+ ${row.jenis_member} - ${row.tarif} |
+ ${row.merk} |
+
+
+ |
+
+ `);
+ });
+ $(".harga")
+ .val(harga ?? r.harga ?? 0)
+ .prop("disabled", true);
+ } else {
+ $("#list-nopol-ext").append(`
+
+ | Tidak ada data |
+ Tidak ada data |
+ Tidak ada data |
+ Tidak ada data |
+
+ `);
+ }
+
+ var rowss = r.memberCard;
+ if (rowss.length !== 0) {
+ rowss.forEach((ro) => {
+ $("#list-card-ext").append(`
+
+ | ${ro.no_card} |
+ ${ro.no_urut} |
+ |
+
+ `);
+ });
+ } else {
+ $("#list-card-ext").append(`
+
+ | Tidak ada data |
+ Tidak ada data |
+ Tidak ada data |
+
+ `);
+ }
+ 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('');
+
+ jenisMembers.forEach((item) => {
+ if (String(item.id_mobil) === String(vehicleSelected)) {
+ jenisMemberExt.append(`
+
+ `);
+ }
+ });
+
+ // Kalau tidak ada pilihan
+ if (jenisMemberExt.children("option").length === 1) {
+ jenisMemberExt.append('');
+ }
+ });
+
+ $("#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 = `
+
+ | ${nopolExt} |
+ ${vehicleExtName} |
+ ${memberExt} |
+ ${merkExt} |
+
+ |
+
+ `;
+ $("#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 = `Nopol sudah ada
`;
+ $(".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(`
+ //
+ // | ${v.nopol} |
+ // ${v.jenis} |
+ // ${v.merk} |
+ // ${v.member} |
+ //
+ //
+ // |
+ //
+ // `);
+ // });
+
+ // // =========================
+ // // TAB 3 - KARTU
+ // // =========================
+ // $('#list-card').empty();
+
+ // res.kartu.forEach(k => {
+ // $('#list-card').append(`
+ //
+ // | ${k.kartu} |
+ // ${k.nourut} |
+ //
+ //
+ // |
+ //
+ // `);
+ // });
+
+ // }
+
+ // $('#modal-member').on('hidden.bs.modal', function () {
+ // $(this).find('form')[0].reset();
+ // $('#list-nopol').empty();
+ // $('#list-card').empty();
+ // });
+});
diff --git a/public/js/tooltips.js b/public/js/tooltips.js
new file mode 100644
index 0000000..38d0f87
--- /dev/null
+++ b/public/js/tooltips.js
@@ -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);