fix no data and handle default page load no data

This commit is contained in:
arifal
2025-08-26 13:59:36 +07:00
parent c6257b79bf
commit 63310f2748
4 changed files with 487 additions and 133 deletions

View File

@@ -67,30 +67,64 @@ class QuickSearchController extends Controller
public function public_search_datatable(Request $request) public function public_search_datatable(Request $request)
{ {
try { try {
// Hanya proses jika ada keyword search
if (!$request->filled('search') || trim($request->get('search')) === '') {
return response()->json([
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => 15,
'from' => null,
'to' => null
]);
}
$search = trim($request->get('search'));
// Validasi minimal 3 karakter
if (strlen($search) < 3) {
return response()->json([
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => 15,
'from' => null,
'to' => null,
'message' => 'Minimal 3 karakter untuk pencarian'
]);
}
// Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi // Gunakan subquery untuk performa yang lebih baik dan menghindari duplikasi
$query = PbgTask::select([ $query = PbgTask::select([
'pbg_task.*', 'pbg_task.*',
DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'), DB::raw('(SELECT name_building FROM pbg_task_details WHERE pbg_task_details.pbg_task_uid = pbg_task.uuid LIMIT 1) as name_building'),
DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan') DB::raw('(SELECT nilai_retribusi_bangunan FROM pbg_task_retributions WHERE pbg_task_retributions.pbg_task_uid = pbg_task.uuid LIMIT 1) as nilai_retribusi_bangunan')
]) ])
->where(function ($q) use ($search) {
$q->where('pbg_task.registration_number', 'LIKE', "%$search%")
->orWhere('pbg_task.name', 'LIKE', "%$search%")
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%")
->orWhere('pbg_task.address', 'LIKE', "%$search%")
->orWhereExists(function ($subQuery) use ($search) {
$subQuery->select(DB::raw(1))
->from('pbg_task_details')
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
});
})
->orderBy('pbg_task.id', 'desc'); ->orderBy('pbg_task.id', 'desc');
if ($request->filled('search')) { $result = $query->paginate();
$search = trim($request->get('search'));
$query->where(function ($q) use ($search) { // Tambahkan message jika tidak ada hasil
$q->where('pbg_task.registration_number', 'LIKE', "%$search%") if ($result->total() === 0) {
->orWhere('pbg_task.name', 'LIKE', "%$search%") $result = $result->toArray();
->orWhere('pbg_task.owner_name', 'LIKE', "%$search%") $result['message'] = 'Tidak ada data yang ditemukan';
->orWhereExists(function ($subQuery) use ($search) {
$subQuery->select(DB::raw(1))
->from('pbg_task_details')
->whereColumn('pbg_task_details.pbg_task_uid', 'pbg_task.uuid')
->where('pbg_task_details.name_building', 'LIKE', "%$search%");
});
});
} }
return response()->json($query->paginate()); return response()->json($result);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::error("Error fetching datatable data: " . $e->getMessage()); Log::error("Error fetching datatable data: " . $e->getMessage());
return response()->json([ return response()->json([

View File

@@ -8,12 +8,42 @@ class PublicSearch {
this.baseUrl = baseInput ? baseInput.value.split("?")[0] : ""; this.baseUrl = baseInput ? baseInput.value.split("?")[0] : "";
this.keywordInput = document.getElementById("search_input"); this.keywordInput = document.getElementById("search_input");
this.searchButton = document.getElementById("search_button"); this.searchButton = document.getElementById("search_button");
this.searchHeader = document.getElementById("search-header");
this.tableWrapper = document.getElementById("table-wrapper");
this.emptyState = document.getElementById("empty-state");
this.datatableUrl = this.buildUrl(this.keywordInput.value); // Tidak inisialisasi datatable sampai ada pencarian
this.datatableUrl = null;
} }
init() { init() {
this.bindSearchButton(); this.bindSearchButton();
// Check if there's a keyword in URL
const urlParams = new URLSearchParams(window.location.search);
const keyword = urlParams.get("keyword");
if (keyword && keyword.trim() !== "") {
this.keywordInput.value = keyword.trim();
this.handleSearchFromUrl(keyword.trim());
}
}
handleSearchFromUrl(keyword) {
// Validasi input kosong atau hanya spasi
if (!keyword || keyword.trim().length === 0) {
this.showEmptyState("Mulai Pencarian");
return;
}
// Validasi minimal 3 karakter
if (keyword.trim().length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
return;
}
this.datatableUrl = this.buildUrl(keyword.trim());
this.showSearchResults();
this.initDatatable(); this.initDatatable();
} }
@@ -21,8 +51,21 @@ class PublicSearch {
const handleSearch = () => { const handleSearch = () => {
const newKeyword = this.keywordInput.value.trim(); const newKeyword = this.keywordInput.value.trim();
// Validasi input kosong atau hanya spasi
if (!newKeyword || newKeyword.length === 0) {
this.showEmptyState("Mulai Pencarian");
return;
}
// Validasi minimal 3 karakter (setelah trim)
if (newKeyword.length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
return;
}
// 1. Update datatable URL and reload // 1. Update datatable URL and reload
this.datatableUrl = this.buildUrl(newKeyword); this.datatableUrl = this.buildUrl(newKeyword);
this.showSearchResults();
this.initDatatable(); this.initDatatable();
// 2. Update URL query string (tanpa reload page) // 2. Update URL query string (tanpa reload page)
@@ -46,13 +89,64 @@ class PublicSearch {
handleSearch(); handleSearch();
} }
}); });
// Handle input change untuk real-time validation
this.keywordInput.addEventListener("input", (event) => {
const value = event.target.value.trim();
// Remove existing classes
this.keywordInput.classList.remove("valid", "warning", "invalid");
// Jika input kosong atau hanya spasi, show empty state
if (!value || value.length === 0) {
this.showEmptyState("Mulai Pencarian");
return;
}
// Jika kurang dari 3 karakter, show warning
if (value.length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
this.keywordInput.classList.add("warning");
return;
}
// Jika valid, add valid class
this.keywordInput.classList.add("valid");
});
// Handle input focus untuk clear warning state
this.keywordInput.addEventListener("focus", () => {
const value = this.keywordInput.value.trim();
if (value.length >= 3) {
// Jika sudah valid, hide empty state
this.emptyState.style.display = "none";
}
});
// Handle input blur untuk final validation
this.keywordInput.addEventListener("blur", () => {
const value = this.keywordInput.value.trim();
if (!value || value.length === 0) {
this.showEmptyState("Mulai Pencarian");
} else if (value.length < 3) {
this.showEmptyState("Minimal 3 karakter untuk pencarian");
}
});
// Handle Escape key untuk clear search
this.keywordInput.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
this.clearSearch();
}
});
} }
buildUrl(keyword) { buildUrl(keyword) {
const url = new URL(this.baseUrl, window.location.origin); const url = new URL(this.baseUrl, window.location.origin);
if (keyword && keyword.trim() !== "") { // Validasi keyword tidak kosong dan tidak hanya spasi
url.searchParams.set("search", keyword); if (keyword && keyword.trim() !== "" && keyword.trim().length >= 3) {
url.searchParams.set("search", keyword.trim());
} else { } else {
url.searchParams.delete("search"); // pastikan tidak ada search param url.searchParams.delete("search"); // pastikan tidak ada search param
} }
@@ -60,6 +154,73 @@ class PublicSearch {
return url.toString(); return url.toString();
} }
showSearchResults() {
this.searchHeader.style.display = "block";
this.tableWrapper.style.display = "block";
this.emptyState.style.display = "none";
}
showEmptyState(message = "Tidak ada data yang ditemukan") {
this.searchHeader.style.display = "none";
this.tableWrapper.style.display = "none";
this.emptyState.style.display = "block";
// Update empty state message and icon
const emptyStateTitle = this.emptyState.querySelector("h4");
const emptyStateDesc = this.emptyState.querySelector("p");
const emptyIcon = this.emptyState.querySelector(".empty-icon i");
if (emptyStateTitle) {
emptyStateTitle.textContent = message;
}
if (emptyStateDesc) {
if (message === "Mulai Pencarian") {
emptyStateDesc.textContent =
"Masukkan kata kunci minimal 3 karakter untuk mencari data PBG";
} else if (message === "Minimal 3 karakter untuk pencarian") {
emptyStateDesc.textContent =
"Masukkan kata kunci minimal 3 karakter untuk mencari data PBG";
} else {
emptyStateDesc.textContent =
"Coba gunakan kata kunci yang berbeda atau lebih spesifik";
}
}
// Update icon based on message
if (emptyIcon) {
if (message === "Mulai Pencarian") {
emptyIcon.className = "fas fa-search fa-3x text-muted";
} else if (message === "Minimal 3 karakter untuk pencarian") {
emptyIcon.className =
"fas fa-exclamation-triangle fa-3x text-warning";
} else {
emptyIcon.className = "fas fa-search fa-3x text-muted";
}
}
// Clear existing table if any
if (this.table) {
this.table.destroy();
this.table = null;
}
}
clearSearch() {
this.keywordInput.value = "";
this.showEmptyState("Mulai Pencarian");
// Reset CSS classes
this.keywordInput.classList.remove("valid", "warning", "invalid");
// Clear URL parameter
const newUrl = window.location.pathname;
window.history.pushState({ path: newUrl }, "", newUrl);
// Reset datatable URL
this.datatableUrl = null;
}
initDatatable() { initDatatable() {
const tableContainer = document.getElementById( const tableContainer = document.getElementById(
"datatable-public-search" "datatable-public-search"
@@ -67,17 +228,17 @@ class PublicSearch {
const config = { const config = {
columns: [ columns: [
"ID", { name: "ID", width: "80px" },
{ name: "Nama Pemohon" }, { name: "Nama Pemohon", width: "150px" },
{ name: "Nama Pemilik" }, { name: "Nama Pemilik", width: "150px" },
{ name: "Kondisi" }, { name: "Kondisi", width: "120px" },
"Nomor Registrasi", { name: "Nomor Registrasi", width: "180px" },
"Status", { name: "Status", width: "120px" },
"Jenis Fungsi", { name: "Jenis Fungsi", width: "150px" },
{ name: "Nama Bangunan" }, { name: "Nama Bangunan", width: "200px" },
"Jenis Konsultasi", { name: "Jenis Konsultasi", width: "150px" },
{ name: "Tanggal Jatuh Tempo" }, { name: "Tanggal Jatuh Tempo", width: "140px" },
{ name: "Retribusi" }, { name: "Retribusi", width: "120px" },
], ],
search: false, search: false,
pagination: { pagination: {
@@ -92,21 +253,40 @@ class PublicSearch {
sort: true, sort: true,
server: { server: {
url: this.datatableUrl, url: this.datatableUrl,
then: (data) => then: (data) => {
data.data.map((item) => [ // Check if data is empty
item.id, if (!data.data || data.data.length === 0) {
item.name, this.showEmptyState(
item.owner_name, data.message || "Tidak ada data yang ditemukan"
item.condition, );
item.registration_number, return [];
item.status_name, }
item.function_type,
item.name_building, return data.data.map((item) => [
item.consultation_type, item.id || "-",
item.due_date, item.name || "-",
addThousandSeparators(item.nilai_retribusi_bangunan), item.owner_name || "-",
]), item.condition || "-",
total: (data) => data.total, item.registration_number || "-",
item.status_name || "-",
item.function_type || "-",
item.name_building || "-",
item.consultation_type || "-",
item.due_date || "-",
item.nilai_retribusi_bangunan
? addThousandSeparators(
item.nilai_retribusi_bangunan
)
: "-",
]);
},
total: (data) => data.total || 0,
error: (error) => {
console.error("Datatable error:", error);
this.showEmptyState(
"Terjadi kesalahan saat mengambil data"
);
},
}, },
}; };

View File

@@ -1,149 +1,281 @@
.qs-wrapper { .qs-wrapper {
padding: 20px;
width: 100%; width: 100%;
max-width: 100%;
margin: 0 auto; margin: 0 auto;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; min-height: 100vh;
color: #2c3e50;
} }
.qs-toolbar { .qs-toolbar {
border-bottom: 1px solid #e0e0e0; background: #f8f9fa;
margin-bottom: 1.5rem; border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.qs-search-form { .qs-search-form {
width: 100%; width: 100%;
max-width: 1000px; // biar kotak ga terlalu panjang di layar besar
margin: 0 auto; // center
position: relative;
.gsp-input { .gsp-input {
width: 100%; flex: 1;
height: 48px; padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 16px; font-size: 16px;
padding: 0 48px 0 48px; // kasih space kiri buat icon transition: all 0.3s ease;
border-radius: 999px; // pill shape
border: 1px solid #dfe1e5;
background-color: #fff;
box-shadow: none;
transition: box-shadow 0.2s ease-in-out, border-color 0.2s;
&:focus { &:focus {
border-color: transparent;
box-shadow: 0 1px 6px rgba(32, 33, 36, 0.28);
outline: none; outline: none;
border-color: #007bff;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
}
&:invalid {
border-color: #dc3545;
box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
}
&::placeholder {
color: #6c757d;
}
// Style untuk input yang valid
&.valid {
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
}
// Style untuk input yang warning
&.warning {
border-color: #ffc107;
box-shadow: 0 0 0 3px rgba(255, 193, 7, 0.1);
} }
} }
// ikon search di kiri input
&::before {
content: "🔍";
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
color: #5f6368;
pointer-events: none;
}
.gsp-btn { .gsp-btn {
margin-left: 12px; padding: 12px 24px;
height: 44px; background: #007bff;
padding: 0 24px; color: white;
font-size: 14px;
border: none; border: none;
border-radius: 999px; border-radius: 6px;
background-color: #007c61; font-size: 16px;
color: #ffffff; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease-in-out; transition: all 0.3s ease;
&:hover { &:hover {
background-color: #36a852; background: #0056b3;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
&:active {
transform: translateY(0);
} }
} }
} }
.qs-header { .qs-header {
margin-bottom: 30px; background: white;
text-align: center; border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid #007bff;
h2 { h2 {
font-size: 24px; color: #2c3e50;
margin-bottom: 8px;
font-weight: 600; font-weight: 600;
color: #1a237e;
em {
font-style: normal;
color: #0d47a1;
}
} }
p { p {
color: #6c757d;
margin: 0;
font-size: 16px; font-size: 16px;
color: #555;
} }
} }
.qs-table-wrapper { .qs-table-wrapper {
background-color: #fff; background: white;
border-radius: 10px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow-x: auto; // allow horizontal scroll on small screens overflow: hidden;
padding: 30px 20px; // 🔑 kasih jarak kanan, kiri, atas, bawah
} }
/* Grid.js overrides */ .qs-empty-state {
.qs-table-wrapper .gridjs { background: white;
font-size: 14px; border-radius: 8px;
color: #333; padding: 60px 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.empty-icon {
color: #dee2e6;
i {
opacity: 0.7;
&.text-warning {
color: #ffc107 !important;
}
}
}
h4 {
font-weight: 500;
margin-bottom: 12px;
}
p {
font-size: 16px;
line-height: 1.5;
max-width: 400px;
margin: 0 auto;
}
} }
.qs-table-wrapper .gridjs-table { // GridJS customization
width: 100%; .gridjs-wrapper {
border-collapse: collapse; border: none !important;
box-shadow: none !important;
width: 100% !important;
max-width: 100% !important;
} }
.qs-table-wrapper .gridjs-th, .gridjs-table {
.qs-table-wrapper .gridjs-td { border: none !important;
padding: 12px 16px; width: 100% !important;
border: 1px solid #e0e0e0; table-layout: auto !important;
text-align: left;
vertical-align: middle;
} }
.qs-table-wrapper .gridjs-th { // Ensure table cells don't wrap unnecessarily
background-color: #f5f5f5; .gridjs-td {
font-weight: 600; border-bottom: 1px solid #e9ecef !important;
color: #1b1b1b; padding: 16px 12px !important;
vertical-align: middle !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
max-width: 200px !important;
} }
.qs-table-wrapper .gridjs-tr:hover { // Allow specific columns to wrap if needed
background-color: #f9f9f9; .gridjs-td:nth-child(4), // Kondisi
.gridjs-td:nth-child(7), // Jenis Fungsi
.gridjs-td:nth-child(8), // Nama Bangunan
.gridjs-td:nth-child(9), // Jenis Konsultasi
.gridjs-td:nth-child(10) {
// Tanggal Jatuh Tempo
white-space: normal !important;
word-wrap: break-word !important;
max-width: 150px !important;
} }
.qs-table-wrapper .gridjs-pagination { .gridjs-th {
margin-top: 16px; background: #f8f9fa !important;
justify-content: center; border-bottom: 2px solid #dee2e6 !important;
font-weight: 600 !important;
color: #495057 !important;
padding: 16px 12px !important;
} }
.gridjs-td {
border-bottom: 1px solid #e9ecef !important;
padding: 16px 12px !important;
vertical-align: middle !important;
}
.gridjs-pagination {
border-top: 1px solid #e9ecef !important;
padding: 20px !important;
.gridjs-pages {
button {
border: 1px solid #dee2e6 !important;
border-radius: 4px !important;
padding: 8px 12px !important;
margin: 0 2px !important;
&:hover {
background: #e9ecef !important;
}
&.gridjs-currentPage {
background: #007bff !important;
color: white !important;
border-color: #007bff !important;
}
}
}
}
// Responsive design
@media (max-width: 768px) { @media (max-width: 768px) {
.qs-header h2 {
font-size: 20px;
}
.qs-wrapper { .qs-wrapper {
padding: 20px 10px;
}
.qs-table-wrapper {
padding: 15px; padding: 15px;
} }
.qs-table-wrapper .gridjs-th, .qs-toolbar {
.qs-table-wrapper .gridjs-td { padding: 15px;
padding: 10px 12px; }
font-size: 13px;
.qs-search-form {
flex-direction: column;
gap: 15px;
.gsp-input {
width: 100%;
}
.gsp-btn {
width: 100%;
}
}
.qs-header {
padding: 15px;
h2 {
font-size: 20px;
}
}
.qs-empty-state {
padding: 40px 15px;
.empty-icon i {
font-size: 2.5rem !important;
}
h4 {
font-size: 18px;
}
p {
font-size: 14px;
}
}
}
// Table responsive improvements
@media (max-width: 1200px) {
.gridjs-wrapper {
overflow-x: auto !important;
}
.gridjs-table {
min-width: 1000px !important;
}
}
// Ensure full width on larger screens
@media (min-width: 1201px) {
.qs-wrapper {
padding: 20px 40px;
}
.gridjs-wrapper {
max-width: none !important;
} }
} }

View File

@@ -23,14 +23,22 @@
</div> </div>
</div> </div>
<div class="qs-header mb-3"> <div class="qs-header mb-3" id="search-header" style="display: none;">
<h2>Hasil Pencarian</h2> <h2>Hasil Pencarian</h2>
<p>Berikut adalah data hasil pencarian berdasarkan kata kunci yang Anda masukkan.</p> <p>Berikut adalah data hasil pencarian berdasarkan kata kunci yang Anda masukkan.</p>
</div> </div>
<div class="qs-table-wrapper"> <div class="qs-table-wrapper" id="table-wrapper" style="display: none;">
<div class="p-3" id="datatable-public-search"></div> <div class="p-3" id="datatable-public-search"></div>
</div> </div>
<div class="qs-empty-state text-center py-5" id="empty-state">
<div class="empty-icon mb-3">
<i class="fas fa-search fa-3x text-muted"></i>
</div>
<h4 class="text-muted mb-2">Mulai Pencarian</h4>
<p class="text-muted">Masukkan kata kunci minimal 3 karakter untuk mencari data PBG</p>
</div>
</div> </div>
@endsection @endsection