update: datatables pengolahan

main
marszayn 2025-09-17 11:48:27 +07:00
parent 413cd6017a
commit 52094bcb07
3 changed files with 628 additions and 415 deletions

View File

@ -285,35 +285,78 @@
</div>
<div class="my-12 max-w-6xl mx-auto px-4">
<div class="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
<div class="mb-6">
<h2 class="text-2xl font-bold text-gray-800 mb-2">Daftar Jasa Pengolahan Sampah</h2>
<p class="text-gray-600">Data perusahaan yang memiliki izin pengolahan sampah di DKI Jakarta</p>
<div class="bg-gradient-to-br from-white to-gray-50 rounded-2xl shadow-xl border border-gray-100 overflow-hidden">
<div class="bg-gradient-to-r from-cyan-600 to-blue-600 p-6">
<h2 class="text-2xl font-bold text-white mb-2">Daftar Jasa Pengolahan Sampah</h2>
<p class="text-cyan-100">Data perusahaan yang memiliki izin pengolahan sampah di DKI Jakarta</p>
</div>
<div class="p-6">
<div class="mb-6 flex flex-col sm:flex-row justify-between items-center space-y-4 sm:space-y-0 sm:space-x-4">
<div class="flex items-center space-x-2">
<label class="text-sm text-gray-600 font-medium">Tampilkan:</label>
<select id="perPage" class="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-cyan-500 focus:border-transparent text-sm">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<span class="text-sm text-gray-600">data per halaman</span>
</div>
<div class="relative">
<input type="text" id="searchInput" placeholder="Cari perusahaan atau alamat..."
class="pl-10 pr-4 py-2 w-80 border border-gray-300 rounded-lg focus:ring-2 focus:ring-cyan-500 focus:border-transparent text-sm transition-all duration-200">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="overflow-x-auto">
<table id="pengolahanTable" class="w-full bg-white table-fixed" style="width:100%">
<thead class="bg-cyan-400 text-white">
<table id="pengolahanTable" class="w-full">
<thead class="bg-gradient-to-r from-cyan-500 to-blue-500">
<tr>
<th class="px-3 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider" style="width: 5%">No</th>
<th class="px-3 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider" style="width: 25%">Perusahaan</th>
<th class="px-3 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider" style="width: 50%">Alamat</th>
<th class="px-3 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider" style="width: 20%">Berlaku Hingga</th>
<th class="px-6 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider w-16">No</th>
<th class="px-6 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider">Perusahaan</th>
<th class="px-6 py-4 text-left text-white text-sm font-semibold uppercase tracking-wider">Alamat</th>
<th class="px-6 py-4 text-center text-white text-sm font-semibold uppercase tracking-wider w-48">Berlaku Hingga</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tbody id="tableBody" class="bg-white divide-y divide-gray-200">
</tbody>
</table>
</div>
<div class="mt-4 text-sm text-gray-500 text-center" id="loadingIndicator">
<div class="inline-flex items-center">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<div id="loadingIndicator" class="text-center py-8">
<div class="inline-flex items-center text-cyan-600">
<svg class="animate-spin -ml-1 mr-3 h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Memuat data...
<span class="text-sm font-medium">Memuat data...</span>
</div>
</div>
<div id="emptyState" class="hidden text-center py-8">
<div class="text-gray-400 mb-4">
<svg class="mx-auto h-12 w-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<p class="text-sm text-gray-500">Tidak ada data yang ditemukan</p>
</div>
</div>
<div class="mt-6 flex flex-col sm:flex-row justify-between items-center space-y-4 sm:space-y-0">
<div id="tableInfo" class="text-sm text-gray-600">
</div>
<nav id="pagination" class="flex space-x-1">
</nav>
</div>
</div>
</div>

View File

@ -85,17 +85,14 @@
--color-indigo-50: oklch(96.2% 0.018 272.314);
--color-indigo-100: oklch(93% 0.034 272.788);
--color-indigo-300: oklch(78.5% 0.115 274.713);
--color-indigo-800: oklch(39.8% 0.195 277.366);
--color-purple-50: oklch(97.7% 0.014 308.299);
--color-purple-100: oklch(94.6% 0.033 307.174);
--color-purple-200: oklch(90.2% 0.063 306.703);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-purple-600: oklch(55.8% 0.288 302.321);
--color-purple-700: oklch(49.6% 0.265 301.924);
--color-purple-800: oklch(43.8% 0.218 303.724);
--color-purple-900: oklch(38.1% 0.176 304.987);
--color-pink-50: oklch(97.1% 0.014 343.198);
--color-pink-500: oklch(65.6% 0.241 354.308);
--color-rose-50: oklch(96.9% 0.015 12.422);
--color-rose-500: oklch(64.5% 0.246 16.439);
--color-rose-600: oklch(58.6% 0.253 17.585);
@ -117,9 +114,6 @@
--color-gray-700: oklch(37.3% 0.034 259.733);
--color-gray-800: oklch(27.8% 0.033 256.848);
--color-gray-900: oklch(21% 0.034 264.665);
--color-neutral-200: oklch(92.2% 0 0);
--color-neutral-600: oklch(43.9% 0 0);
--color-neutral-800: oklch(26.9% 0 0);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
@ -408,9 +402,6 @@
.top-0 {
top: calc(var(--spacing) * 0);
}
.top-1 {
top: calc(var(--spacing) * 1);
}
.top-1\/2 {
top: calc(1/2 * 100%);
}
@ -465,9 +456,6 @@
.right-full {
right: 100%;
}
.-bottom-0 {
bottom: calc(var(--spacing) * -0);
}
.-bottom-0\.5 {
bottom: calc(var(--spacing) * -0.5);
}
@ -504,9 +492,6 @@
.left-0 {
left: calc(var(--spacing) * 0);
}
.left-1 {
left: calc(var(--spacing) * 1);
}
.left-1\/2 {
left: calc(1/2 * 100%);
}
@ -624,9 +609,6 @@
.col-span-2 {
grid-column: span 2 / span 2;
}
.col-span-12 {
grid-column: span 12 / span 12;
}
.float-end {
float: inline-end;
}
@ -918,9 +900,6 @@
.aspect-video {
aspect-ratio: var(--aspect-video);
}
.h-0 {
height: calc(var(--spacing) * 0);
}
.h-0\.5 {
height: calc(var(--spacing) * 0.5);
}
@ -1095,6 +1074,9 @@
.w-75 {
width: calc(var(--spacing) * 75);
}
.w-80 {
width: calc(var(--spacing) * 80);
}
.w-100 {
width: calc(var(--spacing) * 100);
}
@ -1161,9 +1143,6 @@
.shrink {
flex-shrink: 1;
}
.shrink-0 {
flex-shrink: 0;
}
.flex-grow {
flex-grow: 1;
}
@ -1185,16 +1164,9 @@
.border-collapse {
border-collapse: collapse;
}
.border-separate {
border-collapse: separate;
}
.origin-top {
transform-origin: top;
}
.-translate-x-1 {
--tw-translate-x: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-x-1\/2 {
--tw-translate-x: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@ -1207,10 +1179,6 @@
--tw-translate-x: calc(var(--spacing) * 16);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1 {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
.-translate-y-1\/2 {
--tw-translate-y: calc(calc(1/2 * 100%) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
@ -1289,9 +1257,6 @@
.grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.grid-cols-12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.flex-col {
flex-direction: column;
}
@ -1397,6 +1362,13 @@
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
}
}
.space-x-1 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)));
}
}
.space-x-2 {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
@ -1651,9 +1623,6 @@
border-color: color-mix(in oklab, var(--color-cyan-600) 50%, transparent);
}
}
.border-cyan-700 {
border-color: var(--color-cyan-700);
}
.border-cyan-700\/50 {
border-color: color-mix(in srgb, oklch(52% 0.105 223.128) 50%, transparent);
@supports (color: color-mix(in lab, red, red)) {
@ -1693,9 +1662,6 @@
.border-green-400 {
border-color: var(--color-green-400);
}
.border-neutral-200 {
border-color: var(--color-neutral-200);
}
.border-orange-200 {
border-color: var(--color-orange-200);
}
@ -1807,9 +1773,6 @@
.bg-blue-600 {
background-color: var(--color-blue-600);
}
.bg-current {
background-color: currentcolor;
}
.bg-cyan-50 {
background-color: var(--color-cyan-50);
}
@ -1825,6 +1788,9 @@
.bg-cyan-500 {
background-color: var(--color-cyan-500);
}
.bg-cyan-600 {
background-color: var(--color-cyan-600);
}
.bg-cyan-700 {
background-color: var(--color-cyan-700);
}
@ -2247,6 +2213,10 @@
--tw-gradient-to: var(--color-emerald-600);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-gray-50 {
--tw-gradient-to: var(--color-gray-50);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.to-gray-100 {
--tw-gradient-to: var(--color-gray-100);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@ -2571,6 +2541,9 @@
.pl-9 {
padding-left: calc(var(--spacing) * 9);
}
.pl-10 {
padding-left: calc(var(--spacing) * 10);
}
.text-center {
text-align: center;
}
@ -2694,9 +2667,6 @@
.break-all {
word-break: break-all;
}
.whitespace-nowrap {
white-space: nowrap;
}
.text-amber-400 {
color: var(--color-amber-400);
}
@ -2796,9 +2766,6 @@
.text-green-900 {
color: var(--color-green-900);
}
.text-neutral-800 {
color: var(--color-neutral-800);
}
.text-orange-100 {
color: var(--color-orange-100);
}
@ -2826,6 +2793,9 @@
.text-purple-900 {
color: var(--color-purple-900);
}
.text-red-400 {
color: var(--color-red-400);
}
.text-red-500 {
color: var(--color-red-500);
}
@ -3085,10 +3055,6 @@
--tw-outline-style: none;
outline-style: none;
}
.select-all {
-webkit-user-select: all;
user-select: all;
}
.select-none {
-webkit-user-select: none;
user-select: none;
@ -3228,6 +3194,13 @@
}
}
}
.hover\:border-cyan-300 {
&:hover {
@media (hover: hover) {
border-color: var(--color-cyan-300);
}
}
}
.hover\:border-cyan-400 {
&:hover {
@media (hover: hover) {
@ -3284,6 +3257,13 @@
}
}
}
.hover\:bg-cyan-50 {
&:hover {
@media (hover: hover) {
background-color: var(--color-cyan-50);
}
}
}
.hover\:bg-cyan-500 {
&:hover {
@media (hover: hover) {
@ -3298,6 +3278,13 @@
}
}
}
.hover\:bg-cyan-700 {
&:hover {
@media (hover: hover) {
background-color: var(--color-cyan-700);
}
}
}
.hover\:bg-gray-50 {
&:hover {
@media (hover: hover) {
@ -3700,6 +3687,20 @@
max-width: var(--container-md);
}
}
.sm\:flex-row {
@media (width >= 40rem) {
flex-direction: row;
}
}
.sm\:space-y-0 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
--tw-space-y-reverse: 0;
margin-block-start: calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));
margin-block-end: calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)));
}
}
}
.sm\:space-y-6 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
@ -3709,6 +3710,15 @@
}
}
}
.sm\:space-x-4 {
@media (width >= 40rem) {
:where(& > :not(:last-child)) {
--tw-space-x-reverse: 0;
margin-inline-start: calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));
margin-inline-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)));
}
}
}
.sm\:rounded-3xl {
@media (width >= 40rem) {
border-radius: var(--radius-3xl);
@ -3972,16 +3982,6 @@
padding-inline: calc(var(--spacing) * 8);
}
}
.dark\:border-neutral-600 {
@media (prefers-color-scheme: dark) {
border-color: var(--color-neutral-600);
}
}
.dark\:text-white {
@media (prefers-color-scheme: dark) {
color: var(--color-white);
}
}
}
@property --tw-translate-x {
syntax: "*";

View File

@ -1,249 +1,473 @@
document.addEventListener("DOMContentLoaded", function () {
initializePengolahanDataTable();
});
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;
// helper: parse date string in format dd/mm/yyyy to a Date object (local)
function parseDateDMY(dateStr) {
if (!dateStr) return new Date(dateStr);
// accept either dd/mm/yyyy or ISO
if (dateStr.indexOf("/") !== -1) {
const parts = dateStr.split("/");
// parts[0]=dd, [1]=mm, [2]=yyyy
const d = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10) - 1;
const y = parseInt(parts[2], 10);
return new Date(y, m, d);
}
return new Date(dateStr);
}
function initializePengolahanDataTable() {
if (!document.getElementById("pengolahanTable")) return;
if (typeof $ !== "undefined" && $.fn.DataTable) {
$("#pengolahanTable").DataTable({
ajax: {
url: "/api/web/jasa/olah",
type: "POST",
dataSrc: function (json) {
if (!json) return [];
return json.data || json;
},
error: function (xhr, error, thrown) {
console.error("Error fetching pengolahan data:", error || thrown);
},
},
columns: [
{
data: null,
className: "px-3 py-4 text-sm text-gray-900 text-center font-medium",
title: "No",
width: "5%",
render: function (data, type, row, meta) {
return meta.row + 1;
},
},
{
data: "nama",
className: "px-3 py-4 text-sm text-gray-900 font-medium break-words",
title: "Nama Perusahaan",
width: "25%",
},
{
data: "alamat",
className: "px-3 py-4 text-sm text-gray-700 break-words",
title: "Alamat",
width: "50%",
},
{
data: "tglBerlakuIzin",
className: "px-3 py-4 text-sm text-gray-900 text-center",
title: "Berlaku Hingga",
width: "20%",
render: function (data, type, row) {
if (type === "display") {
const date = parseDateDMY(data);
const formattedDate = date.toLocaleDateString("id-ID", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
const today = new Date();
const expiry = parseDateDMY(data);
const daysUntilExpiry = Math.ceil(
(expiry - today) / (1000 * 60 * 60 * 24)
);
let statusClass = "bg-green-100 text-green-800";
if (daysUntilExpiry < 30) {
statusClass = "bg-red-100 text-red-800";
} else if (daysUntilExpiry < 90) {
statusClass = "bg-yellow-100 text-yellow-800";
this.init();
}
return `<div class="text-center">
<div class="text-sm font-medium text-gray-900 mb-1">${formattedDate}</div>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${statusClass}">
${
daysUntilExpiry > 0
? `${daysUntilExpiry} hari`
: "Expired"
}
</span>
</div>`;
}
return data;
},
},
],
language: {
search: "Cari:",
lengthMenu: "Tampilkan _MENU_ data per halaman",
info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ data",
infoEmpty: "Menampilkan 0 sampai 0 dari 0 data",
infoFiltered: "(disaring dari _MAX_ total data)",
paginate: {
first: "Pertama",
last: "Terakhir",
next: "Selanjutnya",
previous: "Sebelumnya",
},
emptyTable: "Tidak ada data yang tersedia",
zeroRecords: "Tidak ditemukan data yang sesuai",
},
pageLength: 10,
responsive: false,
ordering: true,
searching: true,
paging: true,
info: true,
autoWidth: false,
scrollX: false,
layout: {
topStart: {
pageLength: {
text: "Tampilkan _MENU_ data",
},
},
topEnd: {
search: {
placeholder: "Cari perusahaan pengolahan...",
},
},
bottomStart: "info",
bottomEnd: "paging",
},
initComplete: function () {
$("#loadingIndicator").hide();
const wrapper = $(".dataTables_wrapper");
wrapper
.find('input[type="search"]')
.addClass(
"block w-full px-4 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
)
.attr("placeholder", "Cari perusahaan pengolahan...");
wrapper
.find("select")
.addClass(
"px-3 py-2 text-sm border border-gray-300 rounded-lg bg-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
);
wrapper
.find(".dt-paging .dt-paging-button")
.addClass(
"px-3 py-2 mx-1 text-sm border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-300 transition-all duration-200"
);
wrapper
.find(".dt-paging .dt-paging-button.current")
.addClass("bg-blue-600 text-white border-blue-600 hover:bg-blue-700")
.removeClass("bg-white text-gray-700");
wrapper
.find(".dt-paging .dt-paging-button.disabled")
.addClass("opacity-50 cursor-not-allowed");
if (window.innerWidth <= 768) {
wrapper.find(".dt-paging").addClass("text-center");
wrapper.find(".dt-info").addClass("text-center text-sm");
wrapper.find(".dt-length").addClass("text-center");
wrapper.find(".dt-search").addClass("text-center");
init() {
this.injectStyles();
this.createTableStructure();
this.createPaginationControls();
this.createSearchInput();
this.bindEvents();
this.loadData();
}
wrapper.find(".dt-length").addClass("flex items-center space-x-2");
wrapper.find(".dt-search").addClass("flex items-center space-x-2");
wrapper.find(".dt-info").addClass("text-sm text-gray-600");
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;
}
$("#pengolahanTable").removeClass("dataTable").addClass("w-full");
$("#pengolahanTable thead th").addClass(
"bg-cyan-400 text-white font-semibold text-sm uppercase tracking-wider border-b"
);
#pagination button {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
</style>
`;
wrapper.find(".dtr-control").remove();
const existingStyles = document.getElementById("custom-table-styles");
if (existingStyles) {
existingStyles.remove();
}
document.head.insertAdjacentHTML("beforeend", styles);
}
$("#pengolahanTable tbody tr").addClass("transition-all duration-200");
createTableStructure() {
const table = document.getElementById(this.tableId);
table.classList.add("modern-table");
$(window).on("resize", function () {
const wrapper = $(".dataTables_wrapper");
if (window.innerWidth <= 768) {
wrapper.find(".dt-paging").addClass("text-center");
wrapper.find(".dt-info").addClass("text-center text-sm");
wrapper.find(".dt-length").addClass("text-center");
wrapper.find(".dt-search").addClass("text-center");
} else {
wrapper.find(".dt-paging").removeClass("text-center");
wrapper.find(".dt-info").removeClass("text-center text-sm");
wrapper.find(".dt-length").removeClass("text-center");
wrapper.find(".dt-search").removeClass("text-center");
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();
}
});
},
drawCallback: function () {
$("#pengolahanTable tbody tr")
.removeClass("bg-blue-50")
.hover(
function () {
$(this).addClass(
"bg-blue-50 shadow-sm transform transition-all duration-200"
);
},
function () {
$(this).removeClass("bg-blue-50 shadow-sm transform");
}
);
$("#pengolahanTable tbody tr:even").addClass("bg-gray-50");
$("#pengolahanTable tbody tr:odd").addClass("bg-white");
},
searchInput.addEventListener("input", (e) => {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.searchTerm = searchInput.value;
this.currentPage = 1;
this.draw++;
this.loadData();
}, 500);
});
} else {
loadPengolahanDataManually();
}
}
function loadPengolahanDataManually() {
fetch("/website/data/pengolahan-data.json")
.then((response) => {
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}`);
}
return response.json();
})
.then((data) => {
const tbody = document.querySelector("#pengolahanTable tbody");
tbody.innerHTML = "";
data.forEach((item, index) => {
const row = document.createElement("tr");
row.className = `${
index % 2 === 0 ? "bg-gray-50" : "bg-white"
} hover:bg-blue-50 transition-all duration-200`;
const result = await response.json();
const date = parseDateDMY(item.tglBerlakuIzin);
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",
@ -251,10 +475,7 @@ function loadPengolahanDataManually() {
});
const today = new Date();
const expiry = new Date(item.tglBerlakuIzin);
const daysUntilExpiry = Math.ceil(
(expiry - today) / (1000 * 60 * 60 * 24)
);
const daysUntilExpiry = Math.ceil((date - today) / (1000 * 60 * 60 * 24));
let statusClass = "bg-green-100 text-green-800";
let statusText = `${daysUntilExpiry} hari`;
@ -268,86 +489,35 @@ function loadPengolahanDataManually() {
statusClass = "bg-yellow-100 text-yellow-800";
}
row.innerHTML = `
<td class="px-3 py-4 text-sm text-gray-900 text-center font-medium" style="width: 5%">${
index + 1
}</td>
<td class="px-3 py-4 text-sm text-gray-900 font-medium break-words" style="width: 25%">${
item.nama
}</td>
<td class="px-3 py-4 text-sm text-gray-700 break-words" style="width: 50%">${
item.alamat
}</td>
<td class="px-3 py-4 text-sm text-gray-900 text-center" style="width: 20%">
<div class="text-sm font-medium text-gray-900">${formattedDate}</div>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${statusClass} mt-1">
${statusText}
</span>
</td>
`;
tbody.appendChild(row);
});
const loadingIndicator = document.getElementById("loadingIndicator");
if (loadingIndicator) {
loadingIndicator.style.display = "none";
return {
formatted: formattedDate,
statusClass: statusClass,
statusText: statusText,
};
} catch (error) {
return {
formatted: dateString,
statusClass: "bg-gray-100 text-gray-800",
statusText: "Invalid",
};
}
}
addManualSearch(data);
})
.catch((error) => {
console.error("Error loading data:", error);
const loadingIndicator = document.getElementById("loadingIndicator");
if (loadingIndicator) {
loadingIndicator.innerHTML = `
<div class="text-red-500 text-center">
<i class="fas fa-exclamation-triangle mr-2"></i>
Error memuat data: ${error.message}
</div>
`;
escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
});
}
function addManualSearch(data) {
const tableContainer = document
.querySelector("#pengolahanTable")
.closest(".bg-white");
if (!document.getElementById("manualSearchInput")) {
const searchContainer = document.createElement("div");
searchContainer.className = "mb-4 flex justify-end";
searchContainer.innerHTML = `
<div class="relative">
<input type="text" id="manualSearchInput" placeholder="Cari perusahaan pengolahan..."
class="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent pr-10">
<div class="absolute inset-y-0 right-0 pr-3 flex items-center">
<svg class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
`;
tableContainer.insertBefore(
searchContainer,
document.querySelector(".overflow-x-auto")
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"
);
document
.getElementById("manualSearchInput")
.addEventListener("input", function (e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll("#pengolahanTable tbody tr");
rows.forEach((row) => {
const text = row.textContent.toLowerCase();
if (text.includes(searchTerm)) {
row.style.display = "";
} else {
row.style.display = "none";
}
});
});
}
}
});