524 lines
17 KiB
JavaScript
524 lines
17 KiB
JavaScript
class CustomTable {
|
|
constructor(tableId, apiUrl) {
|
|
this.tableId = tableId;
|
|
this.apiUrl = apiUrl;
|
|
this.currentPage = 1;
|
|
this.pageSize = 10;
|
|
this.draw = 1;
|
|
this.searchTerm = "";
|
|
this.totalRecords = 0;
|
|
this.filteredRecords = 0;
|
|
this.data = [];
|
|
this.isLoading = false;
|
|
this.paginationClickTimeout = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.injectStyles();
|
|
this.createTableStructure();
|
|
this.createPaginationControls();
|
|
this.createSearchInput();
|
|
this.bindEvents();
|
|
this.loadData();
|
|
}
|
|
|
|
injectStyles() {
|
|
const styles = `
|
|
<style id="custom-table-styles">
|
|
/* Pagination touch handling untuk mobile */
|
|
#pagination {
|
|
touch-action: manipulation;
|
|
-webkit-touch-callout: none;
|
|
-webkit-user-select: none;
|
|
-khtml-user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
#pagination button {
|
|
touch-action: manipulation;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
const existingStyles = document.getElementById("custom-table-styles");
|
|
if (existingStyles) {
|
|
existingStyles.remove();
|
|
}
|
|
document.head.insertAdjacentHTML("beforeend", styles);
|
|
}
|
|
|
|
createTableStructure() {
|
|
const table = document.getElementById(this.tableId);
|
|
table.classList.add("modern-table");
|
|
|
|
let tbody = table.querySelector("tbody");
|
|
if (tbody) {
|
|
tbody.id = `${this.tableId}-tbody`;
|
|
}
|
|
|
|
this.showLoading();
|
|
}
|
|
|
|
createSearchInput() {}
|
|
|
|
createPaginationControls() {}
|
|
|
|
bindEvents() {
|
|
const searchInput = document.getElementById("searchInput");
|
|
const pageSizeSelect = document.getElementById("perPage");
|
|
|
|
if (searchInput) {
|
|
searchInput.addEventListener("keypress", (e) => {
|
|
if (e.key === "Enter") {
|
|
this.searchTerm = searchInput.value;
|
|
this.currentPage = 1;
|
|
this.draw++;
|
|
this.loadData();
|
|
}
|
|
});
|
|
|
|
searchInput.addEventListener("input", (e) => {
|
|
clearTimeout(this.searchTimeout);
|
|
this.searchTimeout = setTimeout(() => {
|
|
this.searchTerm = searchInput.value;
|
|
this.currentPage = 1;
|
|
this.draw++;
|
|
this.loadData();
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
if (pageSizeSelect) {
|
|
pageSizeSelect.addEventListener("change", (e) => {
|
|
this.pageSize = parseInt(e.target.value);
|
|
this.currentPage = 1;
|
|
this.draw++;
|
|
this.loadData();
|
|
});
|
|
}
|
|
}
|
|
|
|
async loadData() {
|
|
if (this.isLoading) {
|
|
console.log("Already loading, skipping...");
|
|
return;
|
|
}
|
|
|
|
console.log("Starting loadData...", {
|
|
currentPage: this.currentPage,
|
|
pageSize: this.pageSize,
|
|
searchTerm: this.searchTerm,
|
|
draw: this.draw,
|
|
});
|
|
|
|
this.isLoading = true;
|
|
this.showLoading();
|
|
|
|
try {
|
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
|
|
const formData = new URLSearchParams();
|
|
formData.append("draw", this.draw.toString());
|
|
formData.append("start", start.toString());
|
|
formData.append("length", this.pageSize.toString());
|
|
|
|
if (this.searchTerm) {
|
|
formData.append("search[value]", this.searchTerm);
|
|
}
|
|
|
|
const response = await fetch(this.apiUrl, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
this.data = result.data || [];
|
|
this.totalRecords = result.recordsTotal || 0;
|
|
this.filteredRecords = result.recordsFiltered || 0;
|
|
|
|
console.log("Data loaded successfully:", {
|
|
dataLength: this.data.length,
|
|
totalRecords: this.totalRecords,
|
|
filteredRecords: this.filteredRecords,
|
|
});
|
|
|
|
this.renderTable();
|
|
this.renderPagination();
|
|
this.hideLoading();
|
|
} catch (error) {
|
|
console.error("Error loading data:", error);
|
|
this.showError("Gagal memuat data. Silakan coba lagi.");
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
showLoading() {
|
|
const loadingIndicator = document.getElementById("loadingIndicator");
|
|
const emptyState = document.getElementById("emptyState");
|
|
const tableContainer = document.querySelector(".overflow-x-auto");
|
|
const pagination = document.getElementById("pagination");
|
|
|
|
if (loadingIndicator) loadingIndicator.classList.remove("hidden");
|
|
if (emptyState) emptyState.classList.add("hidden");
|
|
if (tableContainer) tableContainer.style.display = "none";
|
|
|
|
if (pagination) {
|
|
const buttons = pagination.querySelectorAll("button");
|
|
buttons.forEach((btn) => {
|
|
btn.style.opacity = "0.5";
|
|
btn.style.pointerEvents = "none";
|
|
});
|
|
}
|
|
}
|
|
|
|
showError(message) {
|
|
const loadingIndicator = document.getElementById("loadingIndicator");
|
|
const emptyState = document.getElementById("emptyState");
|
|
const tableContainer = document.querySelector(".overflow-x-auto");
|
|
|
|
if (loadingIndicator) {
|
|
loadingIndicator.classList.add("hidden");
|
|
}
|
|
if (emptyState) {
|
|
emptyState.classList.remove("hidden");
|
|
emptyState.innerHTML = `
|
|
<div class="text-center py-8">
|
|
<div class="text-red-400 mb-4">
|
|
<i class="fas fa-exclamation-triangle text-4xl"></i>
|
|
</div>
|
|
<p class="text-sm text-red-600">${message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
if (tableContainer) tableContainer.style.display = "none";
|
|
}
|
|
|
|
hideLoading() {
|
|
const loadingIndicator = document.getElementById("loadingIndicator");
|
|
const emptyState = document.getElementById("emptyState");
|
|
const tableContainer = document.querySelector(".overflow-x-auto");
|
|
const pagination = document.getElementById("pagination");
|
|
|
|
if (loadingIndicator) loadingIndicator.classList.add("hidden");
|
|
if (emptyState) emptyState.classList.add("hidden");
|
|
if (tableContainer) tableContainer.style.display = "block";
|
|
|
|
if (pagination) {
|
|
const buttons = pagination.querySelectorAll("button");
|
|
buttons.forEach((btn) => {
|
|
if (!btn.hasAttribute("disabled")) {
|
|
btn.style.opacity = "1";
|
|
btn.style.pointerEvents = "auto";
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
renderTable() {
|
|
const tbody =
|
|
document.querySelector(`#${this.tableId} tbody`) ||
|
|
document.getElementById(`${this.tableId}-tbody`);
|
|
const emptyState = document.getElementById("emptyState");
|
|
const tableContainer = document.querySelector(".overflow-x-auto");
|
|
|
|
if (this.data.length === 0) {
|
|
if (tableContainer) tableContainer.style.display = "none";
|
|
if (emptyState) emptyState.classList.remove("hidden");
|
|
this.updateTableInfo();
|
|
this.renderPagination();
|
|
return;
|
|
}
|
|
|
|
if (tableContainer) tableContainer.style.display = "block";
|
|
if (emptyState) emptyState.classList.add("hidden");
|
|
|
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
let html = "";
|
|
|
|
this.data.forEach((row, index) => {
|
|
const no = start + index + 1;
|
|
const dateInfo = this.formatDate(row.tglBerlakuIzin);
|
|
|
|
html += `
|
|
<tr class="hover:bg-cyan-50 transition-all duration-200">
|
|
<td class="px-6 py-4 text-sm text-gray-900 text-center font-medium">${no}</td>
|
|
<td class="px-6 py-4 text-sm text-gray-900 font-medium">
|
|
<div class="font-medium text-gray-900">${this.escapeHtml(
|
|
row.nama || "-"
|
|
)}</div>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-gray-700">
|
|
<div class="text-gray-700">${this.escapeHtml(
|
|
row.alamat || "-"
|
|
)}</div>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-gray-900 text-center">
|
|
<div class="text-center">
|
|
<div class="text-sm font-medium text-gray-900 mb-1">${
|
|
dateInfo.formatted
|
|
}</div>
|
|
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
|
dateInfo.statusClass
|
|
}">
|
|
${dateInfo.statusText}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
|
|
this.updateTableInfo();
|
|
}
|
|
|
|
renderPagination() {
|
|
const container = document.getElementById("pagination");
|
|
const totalPages = Math.ceil(this.filteredRecords / this.pageSize);
|
|
|
|
if (!container) return;
|
|
|
|
let paginationHtml = "";
|
|
|
|
paginationHtml += `
|
|
<button ${this.currentPage === 1 ? 'disabled="disabled"' : ""}
|
|
data-page="${this.currentPage - 1}"
|
|
style="touch-action: manipulation;"
|
|
class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-cyan-50 hover:text-cyan-600 hover:border-cyan-300 transition-all duration-200 ${
|
|
this.currentPage === 1
|
|
? "opacity-50 cursor-not-allowed"
|
|
: ""
|
|
}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
</svg>
|
|
</button>
|
|
`;
|
|
|
|
const startPage = Math.max(1, this.currentPage - 2);
|
|
const endPage = Math.min(totalPages, this.currentPage + 2);
|
|
|
|
if (startPage > 1) {
|
|
paginationHtml += `<button data-page="1" style="touch-action: manipulation;" class="px-3 py-2 mx-1 text-sm border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-cyan-50 hover:text-cyan-600 hover:border-cyan-300 transition-all duration-200">1</button>`;
|
|
if (startPage > 2) {
|
|
paginationHtml += `<span class="px-3 py-2 text-sm text-gray-500">...</span>`;
|
|
}
|
|
}
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
paginationHtml += `
|
|
<button data-page="${i}"
|
|
style="touch-action: manipulation;"
|
|
class="px-3 py-2 mx-1 text-sm border rounded-md transition-all duration-200 ${
|
|
i === this.currentPage
|
|
? "bg-cyan-600 text-white border-cyan-600 hover:bg-cyan-700"
|
|
: "border-gray-300 bg-white text-gray-700 hover:bg-cyan-50 hover:text-cyan-600 hover:border-cyan-300"
|
|
}">
|
|
${i}
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
if (endPage < totalPages) {
|
|
if (endPage < totalPages - 1) {
|
|
paginationHtml += `<span class="px-3 py-2 text-sm text-gray-500">...</span>`;
|
|
}
|
|
paginationHtml += `<button data-page="${totalPages}" style="touch-action: manipulation;" class="px-3 py-2 mx-1 text-sm border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-cyan-50 hover:text-cyan-600 hover:border-cyan-300 transition-all duration-200">${totalPages}</button>`;
|
|
}
|
|
|
|
paginationHtml += `
|
|
<button ${
|
|
this.currentPage === totalPages ? 'disabled="disabled"' : ""
|
|
}
|
|
data-page="${this.currentPage + 1}"
|
|
style="touch-action: manipulation;"
|
|
class="px-3 py-2 text-sm border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-cyan-50 hover:text-cyan-600 hover:border-cyan-300 transition-all duration-200 ${
|
|
this.currentPage === totalPages
|
|
? "opacity-50 cursor-not-allowed"
|
|
: ""
|
|
}">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
</svg>
|
|
</button>
|
|
`;
|
|
|
|
container.innerHTML = paginationHtml;
|
|
|
|
if (container._clickHandler) {
|
|
container.removeEventListener("click", container._clickHandler);
|
|
}
|
|
if (container._touchHandler) {
|
|
container.removeEventListener("touchstart", container._touchHandler);
|
|
}
|
|
if (container._touchEndHandler) {
|
|
container.removeEventListener("touchend", container._touchEndHandler);
|
|
}
|
|
|
|
container._clickHandler = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
console.log("Pagination clicked:", e.target);
|
|
|
|
let clickedButton = e.target;
|
|
if (e.target.tagName === "SVG" || e.target.tagName === "PATH") {
|
|
clickedButton = e.target.closest("button");
|
|
}
|
|
|
|
if (
|
|
clickedButton &&
|
|
clickedButton.tagName === "BUTTON" &&
|
|
clickedButton.dataset.page
|
|
) {
|
|
console.log("Button clicked:", {
|
|
page: clickedButton.dataset.page,
|
|
disabled: clickedButton.disabled,
|
|
hasAttribute: clickedButton.hasAttribute("disabled"),
|
|
});
|
|
|
|
const page = parseInt(clickedButton.dataset.page);
|
|
|
|
if (
|
|
page &&
|
|
page !== this.currentPage &&
|
|
page >= 1 &&
|
|
page <= totalPages &&
|
|
!clickedButton.disabled &&
|
|
!clickedButton.hasAttribute("disabled") &&
|
|
!this.isLoading
|
|
) {
|
|
console.log("Navigating to page:", page);
|
|
|
|
if (this.paginationClickTimeout) {
|
|
clearTimeout(this.paginationClickTimeout);
|
|
}
|
|
|
|
this.paginationClickTimeout = setTimeout(() => {
|
|
this.currentPage = page;
|
|
this.draw++;
|
|
this.loadData();
|
|
}, 100);
|
|
} else {
|
|
console.log("Click ignored:", {
|
|
samePage: page === this.currentPage,
|
|
outOfRange: page < 1 || page > totalPages,
|
|
disabled:
|
|
clickedButton.disabled || clickedButton.hasAttribute("disabled"),
|
|
loading: this.isLoading,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
container._touchHandler = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
container.addEventListener("click", container._clickHandler);
|
|
container.addEventListener("touchstart", container._touchHandler, {
|
|
passive: false,
|
|
});
|
|
|
|
container._touchEndHandler = (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
container.addEventListener("touchend", container._touchEndHandler, {
|
|
passive: false,
|
|
});
|
|
}
|
|
|
|
updateTableInfo() {
|
|
const infoContainer = document.getElementById("tableInfo");
|
|
if (infoContainer) {
|
|
const start = (this.currentPage - 1) * this.pageSize + 1;
|
|
const end = Math.min(
|
|
this.currentPage * this.pageSize,
|
|
this.filteredRecords
|
|
);
|
|
infoContainer.innerHTML = `Menampilkan ${start} hingga ${end} dari ${this.filteredRecords} data`;
|
|
}
|
|
}
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString || dateString === "0001-01-01") {
|
|
return {
|
|
formatted: "-",
|
|
statusClass: "bg-gray-100 text-gray-800",
|
|
statusText: "Tidak diketahui",
|
|
};
|
|
}
|
|
|
|
try {
|
|
const date = new Date(dateString);
|
|
const formattedDate = date.toLocaleDateString("id-ID", {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
});
|
|
|
|
const today = new Date();
|
|
const daysUntilExpiry = Math.ceil((date - today) / (1000 * 60 * 60 * 24));
|
|
|
|
let statusClass = "bg-green-100 text-green-800";
|
|
let statusText = `${daysUntilExpiry} hari`;
|
|
|
|
if (daysUntilExpiry < 0) {
|
|
statusClass = "bg-red-100 text-red-800";
|
|
statusText = "Expired";
|
|
} else if (daysUntilExpiry < 30) {
|
|
statusClass = "bg-red-100 text-red-800";
|
|
} else if (daysUntilExpiry < 90) {
|
|
statusClass = "bg-yellow-100 text-yellow-800";
|
|
}
|
|
|
|
return {
|
|
formatted: formattedDate,
|
|
statusClass: statusClass,
|
|
statusText: statusText,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
formatted: dateString,
|
|
statusClass: "bg-gray-100 text-gray-800",
|
|
statusText: "Invalid",
|
|
};
|
|
}
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const tableElement = document.getElementById("pengolahanTable");
|
|
if (tableElement && !tableElement.customTableInitialized) {
|
|
tableElement.customTableInitialized = true;
|
|
console.log("Initializing CustomTable...");
|
|
new CustomTable(
|
|
"pengolahanTable",
|
|
"https://pesapakawan.dinaslhdki.id/api/web/jasa/olah"
|
|
);
|
|
}
|
|
});
|