diff --git a/app/Http/Controllers/QuickSearchController.php b/app/Http/Controllers/QuickSearchController.php index fb2b5ad..2d995a5 100644 --- a/app/Http/Controllers/QuickSearchController.php +++ b/app/Http/Controllers/QuickSearchController.php @@ -67,30 +67,64 @@ class QuickSearchController extends Controller public function public_search_datatable(Request $request) { 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 $query = PbgTask::select([ '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 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'); - if ($request->filled('search')) { - $search = trim($request->get('search')); - $query->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%") - ->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%"); - }); - }); + $result = $query->paginate(); + + // Tambahkan message jika tidak ada hasil + if ($result->total() === 0) { + $result = $result->toArray(); + $result['message'] = 'Tidak ada data yang ditemukan'; } - return response()->json($query->paginate()); + return response()->json($result); } catch (\Throwable $e) { Log::error("Error fetching datatable data: " . $e->getMessage()); return response()->json([ diff --git a/resources/js/public-search/index.js b/resources/js/public-search/index.js index 5499c62..f19edeb 100644 --- a/resources/js/public-search/index.js +++ b/resources/js/public-search/index.js @@ -8,12 +8,42 @@ class PublicSearch { this.baseUrl = baseInput ? baseInput.value.split("?")[0] : ""; this.keywordInput = document.getElementById("search_input"); 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() { 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(); } @@ -21,8 +51,21 @@ class PublicSearch { const handleSearch = () => { 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 this.datatableUrl = this.buildUrl(newKeyword); + this.showSearchResults(); this.initDatatable(); // 2. Update URL query string (tanpa reload page) @@ -46,13 +89,64 @@ class PublicSearch { 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) { const url = new URL(this.baseUrl, window.location.origin); - if (keyword && keyword.trim() !== "") { - url.searchParams.set("search", keyword); + // Validasi keyword tidak kosong dan tidak hanya spasi + if (keyword && keyword.trim() !== "" && keyword.trim().length >= 3) { + url.searchParams.set("search", keyword.trim()); } else { url.searchParams.delete("search"); // pastikan tidak ada search param } @@ -60,6 +154,73 @@ class PublicSearch { 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() { const tableContainer = document.getElementById( "datatable-public-search" @@ -67,17 +228,17 @@ class PublicSearch { const config = { columns: [ - "ID", - { name: "Nama Pemohon" }, - { name: "Nama Pemilik" }, - { name: "Kondisi" }, - "Nomor Registrasi", - "Status", - "Jenis Fungsi", - { name: "Nama Bangunan" }, - "Jenis Konsultasi", - { name: "Tanggal Jatuh Tempo" }, - { name: "Retribusi" }, + { name: "ID", width: "80px" }, + { name: "Nama Pemohon", width: "150px" }, + { name: "Nama Pemilik", width: "150px" }, + { name: "Kondisi", width: "120px" }, + { name: "Nomor Registrasi", width: "180px" }, + { name: "Status", width: "120px" }, + { name: "Jenis Fungsi", width: "150px" }, + { name: "Nama Bangunan", width: "200px" }, + { name: "Jenis Konsultasi", width: "150px" }, + { name: "Tanggal Jatuh Tempo", width: "140px" }, + { name: "Retribusi", width: "120px" }, ], search: false, pagination: { @@ -92,21 +253,40 @@ class PublicSearch { sort: true, server: { url: this.datatableUrl, - then: (data) => - data.data.map((item) => [ - item.id, - item.name, - item.owner_name, - item.condition, - item.registration_number, - item.status_name, - item.function_type, - item.name_building, - item.consultation_type, - item.due_date, - addThousandSeparators(item.nilai_retribusi_bangunan), - ]), - total: (data) => data.total, + then: (data) => { + // Check if data is empty + if (!data.data || data.data.length === 0) { + this.showEmptyState( + data.message || "Tidak ada data yang ditemukan" + ); + return []; + } + + return data.data.map((item) => [ + item.id || "-", + item.name || "-", + item.owner_name || "-", + item.condition || "-", + 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" + ); + }, }, }; diff --git a/resources/scss/pages/public-search/index.scss b/resources/scss/pages/public-search/index.scss index 645f686..60c7204 100644 --- a/resources/scss/pages/public-search/index.scss +++ b/resources/scss/pages/public-search/index.scss @@ -1,149 +1,281 @@ .qs-wrapper { + padding: 20px; width: 100%; + max-width: 100%; margin: 0 auto; - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; - color: #2c3e50; + min-height: 100vh; } .qs-toolbar { - border-bottom: 1px solid #e0e0e0; - margin-bottom: 1.5rem; + background: #f8f9fa; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .qs-search-form { width: 100%; - max-width: 1000px; // biar kotak ga terlalu panjang di layar besar - margin: 0 auto; // center - - position: relative; .gsp-input { - width: 100%; - height: 48px; + flex: 1; + padding: 12px 16px; + border: 2px solid #e9ecef; + border-radius: 6px; font-size: 16px; - padding: 0 48px 0 48px; // kasih space kiri buat icon - 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; + transition: all 0.3s ease; &:focus { - border-color: transparent; - box-shadow: 0 1px 6px rgba(32, 33, 36, 0.28); 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 { - margin-left: 12px; - height: 44px; - padding: 0 24px; - font-size: 14px; + padding: 12px 24px; + background: #007bff; + color: white; border: none; - border-radius: 999px; - background-color: #007c61; - color: #ffffff; + border-radius: 6px; + font-size: 16px; + font-weight: 600; cursor: pointer; - transition: background-color 0.2s ease-in-out; + transition: all 0.3s ease; &: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 { - margin-bottom: 30px; - text-align: center; + background: white; + 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 { - font-size: 24px; + color: #2c3e50; + margin-bottom: 8px; font-weight: 600; - color: #1a237e; - - em { - font-style: normal; - color: #0d47a1; - } } p { + color: #6c757d; + margin: 0; font-size: 16px; - color: #555; } } .qs-table-wrapper { - background-color: #fff; - border-radius: 10px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - overflow-x: auto; // allow horizontal scroll on small screens - padding: 30px 20px; // 🔑 kasih jarak kanan, kiri, atas, bawah + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + overflow: hidden; } -/* Grid.js overrides */ -.qs-table-wrapper .gridjs { - font-size: 14px; - color: #333; +.qs-empty-state { + background: white; + border-radius: 8px; + 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 { - width: 100%; - border-collapse: collapse; +// GridJS customization +.gridjs-wrapper { + border: none !important; + box-shadow: none !important; + width: 100% !important; + max-width: 100% !important; } -.qs-table-wrapper .gridjs-th, -.qs-table-wrapper .gridjs-td { - padding: 12px 16px; - border: 1px solid #e0e0e0; - text-align: left; - vertical-align: middle; +.gridjs-table { + border: none !important; + width: 100% !important; + table-layout: auto !important; } -.qs-table-wrapper .gridjs-th { - background-color: #f5f5f5; - font-weight: 600; - color: #1b1b1b; +// Ensure table cells don't wrap unnecessarily +.gridjs-td { + border-bottom: 1px solid #e9ecef !important; + 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 { - background-color: #f9f9f9; +// Allow specific columns to wrap if needed +.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 { - margin-top: 16px; - justify-content: center; +.gridjs-th { + background: #f8f9fa !important; + 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) { - .qs-header h2 { - font-size: 20px; - } - .qs-wrapper { - padding: 20px 10px; - } - - .qs-table-wrapper { padding: 15px; } - .qs-table-wrapper .gridjs-th, - .qs-table-wrapper .gridjs-td { - padding: 10px 12px; - font-size: 13px; + .qs-toolbar { + padding: 15px; + } + + .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; } } diff --git a/resources/views/public-search/index.blade.php b/resources/views/public-search/index.blade.php index e4649bb..cb15e0e 100644 --- a/resources/views/public-search/index.blade.php +++ b/resources/views/public-search/index.blade.php @@ -23,14 +23,22 @@ -
+ -
+ + +
+
+ +
+

Mulai Pencarian

+

Masukkan kata kunci minimal 3 karakter untuk mencari data PBG

+
@endsection