diff --git a/app/Http/Controllers/AkreditasiInstrumenController.php b/app/Http/Controllers/AkreditasiInstrumenController.php index de81fdf..055c916 100644 --- a/app/Http/Controllers/AkreditasiInstrumenController.php +++ b/app/Http/Controllers/AkreditasiInstrumenController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Models\FileDirectory; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -149,6 +150,90 @@ class AkreditasiInstrumenController extends Controller return true; } + private function hasFilesInPath(string $folderPath): bool + { + $escapedPath = addcslashes($folderPath, '\\%_'); + + return FileDirectory::query() + ->where('statusenabled', true) + ->where('is_akre', true) + ->where('file', 'ILIKE', $escapedPath . '/%') + ->exists(); + } + + private function removeNodeByPath(array &$data, string $folderPath): bool + { + $parts = array_values(array_filter(explode('/', $folderPath), fn ($value) => trim((string) $value) !== '')); + $depth = count($parts); + if ($depth === 0) { + return false; + } + + if ($depth === 1) { + foreach ($data as $index => $type) { + if (($type['name'] ?? null) !== $parts[0]) { + continue; + } + + $segments = $type['segment'] ?? []; + if (is_array($segments) && count($segments) > 0) { + return false; + } + + array_splice($data, $index, 1); + return true; + } + + return false; + } + + foreach ($data as $typeIndex => $type) { + if (($type['name'] ?? null) !== $parts[0]) { + continue; + } + + $segments = $data[$typeIndex]['segment'] ?? []; + if (!is_array($segments)) { + return false; + } + + foreach ($segments as $segmentIndex => $segment) { + if (($segment['name'] ?? null) !== $parts[1]) { + continue; + } + + if ($depth === 2) { + $children = $segment['turunan'] ?? []; + if (is_array($children) && count($children) > 0) { + return false; + } + + array_splice($segments, $segmentIndex, 1); + $data[$typeIndex]['segment'] = array_values($segments); + return true; + } + + $children = $segments[$segmentIndex]['turunan'] ?? []; + if (!is_array($children)) { + return false; + } + + foreach ($children as $childIndex => $child) { + if (($child['name'] ?? null) !== $parts[2]) { + continue; + } + + array_splice($children, $childIndex, 1); + $segments[$segmentIndex]['turunan'] = array_values($children); + $data[$typeIndex]['segment'] = array_values($segments); + return true; + } + } + } + + return false; + } + /** * Read akreditasi.jff (array) */ @@ -313,4 +398,97 @@ class AkreditasiInstrumenController extends Controller @fclose($lockFp); } } + + public function destroy(Request $request) + { + $validated = $request->validate([ + 'path' => ['required', 'string', 'max:2000'], + ]); + + $folderPath = trim((string) ($validated['path'] ?? '')); + if ($folderPath === '') { + return response()->json([ + 'status' => false, + 'message' => 'Path folder wajib diisi.', + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + if ($this->hasFilesInPath($folderPath)) { + return response()->json([ + 'status' => false, + 'message' => 'Folder tidak bisa dihapus karena masih memiliki file di dalamnya.', + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + $this->ensureJsonDirExists(); + $path = $this->akreditasiJsonPath(); + $lockPath = $this->akreditasiLockPath(); + + $lockFp = @fopen($lockPath, 'c+'); + if (!$lockFp) { + return response()->json([ + 'status' => false, + 'message' => 'Gagal membuka file lock untuk akreditasi.jff.', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + try { + if (!flock($lockFp, LOCK_EX)) { + return response()->json([ + 'status' => false, + 'message' => 'Gagal mengunci file akreditasi.jff.', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $raw = @file_get_contents($path); + if ($raw === false) { + $data = []; + } else { + $data = $this->decodeJsonArray($raw, true); + if ($data === null) { + $repaired = $this->tryRepairJsonArray($raw); + if ($repaired === null) { + return response()->json([ + 'status' => false, + 'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak).', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + $data = $repaired; + } + } + + $removed = $this->removeNodeByPath($data, $folderPath); + if (!$removed) { + return response()->json([ + 'status' => false, + 'message' => 'Folder tidak ditemukan atau masih memiliki turunan.', + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if ($json === false) { + return response()->json([ + 'status' => false, + 'message' => 'Gagal mengubah data menjadi JSON.', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + $json .= "\n"; + + if (!$this->atomicWrite($path, $json)) { + return response()->json([ + 'status' => false, + 'message' => 'Gagal menulis file akreditasi.jff (atomic write).', + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return response()->json([ + 'status' => true, + 'message' => 'Folder akreditasi berhasil dihapus.', + 'data' => $data, + ]); + } finally { + @flock($lockFp, LOCK_UN); + @fclose($lockFp); + } + } } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 03f2855..a977706 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -88,27 +88,6 @@ class PdfFpdiWithAlpha extends Fpdi class DashboardController extends Controller { - // public function index(){ - // $katDok = MasterKategori::where('statusenabled', true)->select('master_kategori_directory_id', 'nama_kategori_directory')->get(); - // $klasifikasiDok = MasterKlasifikasi::where('statusenabled', true)->select('master_klasifikasi_directory_id', 'nama_klasifikasi_directory')->get(); - // $prefillFilter = session()->pull('dashboard_prefill'); - - // $authMapping = auth()->user()?->dataUser?->mappingUnitKerjaPegawai[0]; - // $authUnitKerja = $authMapping->objectunitkerjapegawaifk; - // $authSubUnitKerja = $authMapping->objectsubunitkerjapegawaifk; - - // $allAkses = AksesFile::where(['statusenabled' => true, 'pegawai_id' => auth()->user()?->dataUser->id])->first(); - // $payload = [ - // 'title' => 'Dashboard', - // 'katDok' => $katDok, - // 'klasifikasiDok' => $klasifikasiDok, - // 'authUnitKerja' => $authUnitKerja, - // 'authSubUnitKerja' => $authSubUnitKerja, - // 'allAkses' => $allAkses ?? null, - // 'prefillFilter' => $prefillFilter - // ]; - // return view('dashboard.index', $payload); - // } public function setDashboardPrefill(Request $request) { @@ -132,8 +111,8 @@ class DashboardController extends Controller public function index(){ $katDok = MasterKategori::where('statusenabled', true)->select('master_kategori_directory_id', 'nama_kategori_directory')->get(); $authMapping = auth()->user()?->dataUser?->mappingUnitKerjaPegawai() - ->where('isprimary', true) - ->first(); + ->where('isprimary', true) + ->first(); // dd(auth()->user()?->dataUser); $authUnitKerja = $authMapping->objectunitkerjapegawaifk ?? null; $authSubUnitKerja = $authMapping->objectsubunitkerjapegawaifk ?? null; @@ -2791,9 +2770,18 @@ class DashboardController extends Controller // Filter spesifik berdasarkan path folder dari tree/explorer. // Gunakan prefix match agar "KPS 9.c" tidak ikut mengambil "KPS 9.f". - $query->when($folderPath !== '', function ($q) use ($folderPath) { + $query->when($folderPath !== '', function ($q) use ($folderPath, $keyword) { $escapedPath = addcslashes($folderPath, '\\%_'); + $slashCount = substr_count($folderPath, '/') + 1; + $q->where('file', 'ILIKE', $escapedPath . '/%'); + + if (!$keyword) { + $q->whereRaw( + "(length(file) - length(replace(file, '/', ''))) = ?", + [$slashCount] + ); + } }); // Pencarian nama file/dokumen bisa digabung dengan filter folder. diff --git a/public/json/akreditasi.jff b/public/json/akreditasi.jff index 9ac6c57..0808b00 100644 --- a/public/json/akreditasi.jff +++ b/public/json/akreditasi.jff @@ -108,22 +108,22 @@ "name": "TKRS 7", "turunan": [ { - "name": "TKRS 7.a" + "name": "TKRS 7.a" }, { - "name": "TKRS 7.b" + "name": "TKRS 7.b" }, { - "name": "TKRS 7.c" + "name": "TKRS 7.c" }, { - "name": "TKRS 7.d" + "name": "TKRS 7.d" }, { - "name": "TKRS 7.e" + "name": "TKRS 7.e" }, { - "name": "TKRS 7.f" + "name": "TKRS 7.f" } ] }, diff --git a/public/json/akreditasi.jff.bak b/public/json/akreditasi.jff.bak index 15269ec..07f64fc 100644 --- a/public/json/akreditasi.jff.bak +++ b/public/json/akreditasi.jff.bak @@ -103,6 +103,188 @@ "name": "TKRS 6.f" } ] + }, + { + "name": "TKRS 7", + "turunan": [ + { + "name": "TKRS 7.a" + }, + { + "name": "TKRS 7.b" + }, + { + "name": "TKRS 7.c" + }, + { + "name": "TKRS 7.d" + }, + { + "name": "TKRS 7.e" + }, + { + "name": "TKRS 7.f" + } + ] + }, + { + "name": "TKRS 7.1", + "turunan": [ + { + "name": "TKRS 7.1.a" + }, + { + "name": "TKRS 7.1.b" + }, + { + "name": "TKRS 7.1.c" + }, + { + "name": "TKRS 7.1.d" + } + ] + }, + { + "name": "TKRS 8", + "turunan": [ + { + "name": "TKRS 8.a" + }, + { + "name": "TKRS 8.b" + }, + { + "name": "TKRS 8.c" + } + ] + }, + { + "name": "TKRS 9", + "turunan": [ + { + "name": "TKRS 9.a" + }, + { + "name": "TKRS 9.b" + }, + { + "name": "TKRS 9.c" + }, + { + "name": "TKRS 9.d" + }, + { + "name": "TKRS 9.e" + } + ] + }, + { + "name": "TKRS 10", + "turunan": [ + { + "name": "TKRS 10.a" + }, + { + "name": "TKRS 10.b" + }, + { + "name": "TKRS 10.c" + }, + { + "name": "TKRS 10.d" + } + ] + }, + { + "name": "TKRS 11", + "turunan": [ + { + "name": "TKRS 11.a" + }, + { + "name": "TKRS 11.b" + }, + { + "name": "TKRS 11.c" + } + ] + }, + { + "name": "TKRS 12", + "turunan": [ + { + "name": "TKRS 12.a" + }, + { + "name": "TKRS 12.b" + }, + { + "name": "TKRS 12.c" + }, + { + "name": "TKRS 12.d" + } + ] + }, + { + "name": "TKRS 13", + "turunan": [ + { + "name": "TKRS 13.a" + }, + { + "name": "TKRS 13.b" + }, + { + "name": "TKRS 13.c" + }, + { + "name": "TKRS 13.d" + }, + { + "name": "TKRS 13.e" + }, + { + "name": "TKRS 13.f" + } + ] + }, + { + "name": "TKRS 14", + "turunan": [ + { + "name": "TKRS 14.a" + }, + { + "name": "TKRS 14.b" + } + ] + }, + { + "name": "TKRS 15", + "turunan": [ + { + "name": "TKRS 15.a" + }, + { + "name": "TKRS 15.b" + }, + { + "name": "TKRS 15.c" + }, + { + "name": "TKRS 15.d" + }, + { + "name": "TKRS 15.e" + }, + { + "name": "TKRS 15.f" + }, + { + "name": "TKRS 15.g" + } + ] } ] }, @@ -1298,7 +1480,7 @@ }, { "name": "PPI 8.c" - }, + } ] }, { @@ -3178,4 +3360,8 @@ } ] }, + { + "name": "tst", + "segment": [] + } ] diff --git a/resources/views/dataAkreditasi/index.blade.php b/resources/views/dataAkreditasi/index.blade.php index 7293dfc..7d2fb0f 100644 --- a/resources/views/dataAkreditasi/index.blade.php +++ b/resources/views/dataAkreditasi/index.blade.php @@ -24,6 +24,9 @@ max-height: 150vh; min-height: 520px; overflow-y: auto; + scroll-behavior: smooth; + scrollbar-width: thin; + scrollbar-color: #b8c7db transparent; } .akre-tree-row.level-0 { padding-left: 16px; @@ -45,6 +48,18 @@ text-align: left; color: #1f2937; } + .akre-tree-row-main { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + } + .akre-tree-row-main span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .akre-tree-row:hover { background: #f5f9ff; } @@ -78,6 +93,42 @@ width: 12px; flex: 0 0 12px; } + .akre-tree-actions { + display: inline-flex; + align-items: center; + gap: 6px; + flex: 0 0 auto; + } + .akre-tree-action { + width: 28px; + height: 28px; + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid #d7e3f4; + border-radius: 999px; + background: #fff; + color: #2563eb; + opacity: 0; + transition: opacity 0.18s ease, background 0.18s ease, border-color 0.18s ease; + flex: 0 0 28px; + } + .akre-tree-row:hover .akre-tree-action, + .akre-tree-row.active .akre-tree-action { + opacity: 1; + } + .akre-tree-action:hover { + background: #eff6ff; + border-color: #bfdbfe; + } + .akre-tree-action.delete { + color: #dc2626; + border-color: #fecaca; + } + .akre-tree-action.delete:hover { + background: #fef2f2; + border-color: #fca5a5; + } .akre-tree-children { background: #fff; } @@ -146,7 +197,7 @@ .akre-browser-area { border: 1px solid #edf2f7; border-radius: 12px; - overflow: hidden; + /* overflow: hidden; */ background: #fff; margin-bottom: 14px; } @@ -162,8 +213,33 @@ font-weight: 700; } .akre-browser-body { - max-height: 320px; + max-height: 720px; overflow-y: auto; + scroll-behavior: smooth; + scrollbar-width: thin; + scrollbar-color: #b8c7db transparent; + } + .akre-tree-body::-webkit-scrollbar, + .akre-browser-body::-webkit-scrollbar { + width: 8px; + } + .akre-tree-body::-webkit-scrollbar-track, + .akre-browser-body::-webkit-scrollbar-track { + background: transparent; + border-radius: 999px; + } + .akre-tree-body::-webkit-scrollbar-thumb, + .akre-browser-body::-webkit-scrollbar-thumb { + background: rgba(148, 163, 184, 0.7); + border-radius: 999px; + border: 2px solid transparent; + background-clip: padding-box; + } + .akre-tree-body::-webkit-scrollbar-thumb:hover, + .akre-browser-body::-webkit-scrollbar-thumb:hover { + background: rgba(100, 116, 139, 0.85); + border: 2px solid transparent; + background-clip: padding-box; } .akre-browser-row { display: grid; @@ -229,6 +305,23 @@ .akre-selection-status { color: #7b8aa0; } + .akre-parent-chip { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 44px; + width: 100%; + padding: 10px 12px; + border: 1px solid #dbe5f0; + border-radius: 10px; + background: #f8fbff; + color: #1e293b; + font-weight: 600; + } + .akre-parent-chip small { + color: #64748b; + font-weight: 600; + } @media (max-width: 991.98px) { .akre-tree-body { min-height: 320px; @@ -324,29 +417,37 @@
- - +
+ + +
- 0 dipilih + Belum ada file dipilih
@if(Auth::guard('admin')->check())
@@ -445,8 +548,12 @@ let selectedIds = []; let searchTimer; let activeFileSearch = ''; + let isAkreDataLoading = false; + let currentAkreFetchController = null; + let latestAkreFetchToken = 0; const isAdminUser = @json(Auth::guard('admin')->check()); const btn = document.getElementById('btnDownloadMultiple'); + const clearSelectionBtn = document.getElementById('btnClearSelection'); // === INITIALIZATION === document.addEventListener('DOMContentLoaded', () => { loadAkreData().then(() => initAkreTree(true)); @@ -454,6 +561,7 @@ renderAkreSubtree(); initEventListeners(); initTooltips(); + configureAkreModal(''); }); function initTooltips() { @@ -475,7 +583,10 @@ activeFileSearch = String(value || '').trim(); if (activeAkrePath) { fetchData(activeAkrePath, 1); + } else if (activeFileSearch) { + fetchData('', 1); } else { + currentData = []; renderAkreSubtree(); } }; @@ -506,6 +617,12 @@ }); updateDownloadButton(); }); + + if (clearSelectionBtn) { + clearSelectionBtn.addEventListener('click', function() { + clearSelectedFiles(); + }); + } } // === CORE FUNCTIONS === @@ -515,11 +632,17 @@ */ function fetchData(keyword = '', page = 1) { const perPage = 100; - const subtreeEl = document.getElementById('akreSubtreeContainer'); - if (subtreeEl) { - subtreeEl.innerHTML = '
Memuat isi folder...
'; + const requestToken = ++latestAkreFetchToken; + + if (currentAkreFetchController) { + currentAkreFetchController.abort(); } + currentAkreFetchController = new AbortController(); + isAkreDataLoading = true; + currentData = []; + renderAkreSubtree(); + const params = new URLSearchParams({ per_page: String(perPage), page: String(page), @@ -532,21 +655,33 @@ params.set('keyword', activeFileSearch); } - fetch(`/datatable-akreditasi?${params.toString()}`) + return fetch(`/datatable-akreditasi?${params.toString()}`, { + signal: currentAkreFetchController.signal, + }) .then(res => res.json()) .then(response => { + if (requestToken !== latestAkreFetchToken) return; if (response.status) { currentData = response.data || []; - renderAkreSubtree(); } else { currentData = []; - renderAkreSubtree(); } + isAkreDataLoading = false; + renderAkreSubtree(); }) .catch(err => { + if (err?.name === 'AbortError') return; + if (requestToken !== latestAkreFetchToken) return; console.error('Error fetching data:', err); currentData = []; + isAkreDataLoading = false; renderAkreSubtree(); + }) + .finally(() => { + if (requestToken === latestAkreFetchToken) { + isAkreDataLoading = false; + currentAkreFetchController = null; + } }); } // === UTILITY FUNCTIONS === @@ -557,27 +692,65 @@ if (!selectedIds.includes(val)) selectedIds.push(val); } else { selectedIds = selectedIds.filter(id => id !== val); - document.getElementById('checkAllRows').checked = false; + const checkAllRows = document.getElementById('checkAllRows'); + if (checkAllRows) checkAllRows.checked = false; } updateDownloadButton(); } - function updateDownloadButton() { + function clearSelectedFiles() { + selectedIds = []; + const checkAllRows = document.getElementById('checkAllRows'); + if (checkAllRows) checkAllRows.checked = false; + document.querySelectorAll('.row-checkbox').forEach(cb => { + cb.checked = false; + }); + updateDownloadButton(); + } + function updateDownloadButton() { const countLabel = document.getElementById('selectedCount'); - btn.disabled = selectedIds.length === 0; - countLabel.innerText = `${selectedIds.length} dipilih`; + const totalSelected = selectedIds.length; + const hasSelection = totalSelected > 0; + if (btn) { + btn.disabled = !hasSelection; + btn.innerHTML = `Download Terpilih (${totalSelected})`; + } + if (clearSelectionBtn) { + clearSelectionBtn.disabled = !hasSelection; + } + if (countLabel) { + countLabel.innerText = hasSelection + ? `${totalSelected} file siap diunduh` + : 'Belum ada file dipilih'; + } } const downloadBtn = document.getElementById('btnDownloadMultiple'); if (downloadBtn) { - downloadBtn.addEventListener('click', function(){ + downloadBtn.addEventListener('click', async function(){ if(selectedIds.length === 0){ return; } + + const confirmation = await Swal.fire({ + icon: 'question', + title: 'Download file terpilih?', + text: `${selectedIds.length} file akan digabung ke dalam ZIP.`, + showCancelButton: true, + confirmButtonText: 'Ya, download', + cancelButtonText: 'Batal', + reverseButtons: true, + }); + + if (!confirmation.isConfirmed) { + return; + } + const payload = { ids: Array.from(selectedIds) }; downloadBtn.disabled = true; - downloadBtn.textContent = 'Menyiapkan...'; + if (clearSelectionBtn) clearSelectionBtn.disabled = true; + downloadBtn.innerHTML = 'Menyiapkan ZIP...'; fetch('/download-multiple', { method: 'POST', headers: { @@ -604,13 +777,20 @@ a.click(); a.remove(); window.URL.revokeObjectURL(url); + Swal.fire({ + icon: 'success', + title: 'Berhasil', + text: `${selectedIds.length} file sedang diunduh.`, + timer: 1500, + showConfirmButton: false + }); + clearSelectedFiles(); }) .catch(err => { Swal.fire({ icon: 'error', title: 'Gagal', text: err.message || 'Gagal download file' }); }) .finally(() => { - downloadBtn.disabled = selectedIds.length === 0; - downloadBtn.textContent = 'Download Terpilih'; + updateDownloadButton(); }); }); } @@ -643,6 +823,7 @@ let expandedAkreNodes = new Set(); let activeAkreNodeKey = null; let activeAkrePath = ''; + let pendingAkreFocusPath = ''; function loadAkreData(){ return Promise.resolve(akreData); @@ -654,10 +835,24 @@ .then(res => res.json()) .then(data => { akreData = Array.isArray(data) ? data : []; - expandedAkreNodes = new Set(); - activeAkreNodeKey = null; - activeAkrePath = ''; + if (pendingAkreFocusPath) { + const nextNode = findAkreNodeMetaByPath(pendingAkreFocusPath); + if (nextNode) { + expandedAkreNodes = new Set(nextNode.ancestors || []); + activeAkreNodeKey = nextNode.key; + activeAkrePath = nextNode.path; + } else { + expandedAkreNodes = new Set(); + activeAkreNodeKey = null; + activeAkrePath = ''; + } + } else { + expandedAkreNodes = new Set(); + activeAkreNodeKey = null; + activeAkrePath = ''; + } initAkreTree(true); + pendingAkreFocusPath = ''; return akreData; }) .catch(() => { @@ -666,6 +861,7 @@ activeAkreNodeKey = null; activeAkrePath = ''; initAkreTree(true); + pendingAkreFocusPath = ''; return akreData; }); } @@ -761,10 +957,30 @@ data-node-key="${e(key)}" data-path="${e(path || '')}" data-is-leaf="${isLeaf ? 'true' : 'false'}"> - ${isLeaf - ? '' - : ``} - ${label} + + ${isLeaf + ? '' + : ``} + ${label} + + ${isAdminUser ? ` + + ${level < 2 ? ` + + + + ` : ''} + + + + + ` : ''} `; } @@ -796,6 +1012,22 @@ if (!treeEl || treeEl.dataset.bound === 'true') return; treeEl.addEventListener('click', function(e) { + const addFolderBtn = e.target.closest('[data-add-folder="true"]'); + if (addFolderBtn) { + e.preventDefault(); + e.stopPropagation(); + openAddAkreFromFolder(addFolderBtn.dataset.folderPath || ''); + return; + } + + const deleteFolderBtn = e.target.closest('[data-delete-folder="true"]'); + if (deleteFolderBtn) { + e.preventDefault(); + e.stopPropagation(); + deleteAkreFolder(deleteFolderBtn.dataset.folderPath || ''); + return; + } + const row = e.target.closest('.akre-tree-row'); if (!row) return; @@ -810,7 +1042,6 @@ if (!isLeaf) { toggleAkreNode(nodeKey, false, false); } - renderAkreSubtree(); fetchData(path, 1); syncAkreTreeState(); }); @@ -847,7 +1078,6 @@ activeAkreNodeKey = nodeKey; activeAkrePath = path || ''; fetchData(activeAkrePath, 1); - renderAkreSubtree(); if (syncTree) syncAkreTreeState(); syncAkreSubtreeState(); } @@ -866,6 +1096,57 @@ } } + function findAkreNodeMetaByPath(path) { + const targetPath = String(path || '').trim(); + if (!targetPath) return null; + + for (let typeIndex = 0; typeIndex < akreData.length; typeIndex++) { + const typeItem = akreData[typeIndex]; + const typePath = typeItem.name || ''; + const typeKey = `type-${typeIndex}`; + if (typePath === targetPath) { + return { + key: typeKey, + path: typePath, + level: 0, + ancestors: [] + }; + } + + const segments = Array.isArray(typeItem.segment) ? typeItem.segment : []; + for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) { + const segment = segments[segmentIndex]; + const segmentPath = `${typePath}/${segment.name || ''}`; + const segmentKey = `${typeKey}-segment-${segmentIndex}`; + if (segmentPath === targetPath) { + return { + key: segmentKey, + path: segmentPath, + level: 1, + ancestors: [typeKey] + }; + } + + const children = Array.isArray(segment.turunan) ? segment.turunan : []; + for (let childIndex = 0; childIndex < children.length; childIndex++) { + const child = children[childIndex]; + const childPath = `${segmentPath}/${child.name || ''}`; + const childKey = `${segmentKey}-child-${childIndex}`; + if (childPath === targetPath) { + return { + key: childKey, + path: childPath, + level: 2, + ancestors: [typeKey, segmentKey] + }; + } + } + } + } + + return null; + } + function findAkreNodeByKey() { if (!activeAkreNodeKey) return null; @@ -925,38 +1206,12 @@ const subtreeEl = document.getElementById('akreSubtreeContainer'); const hintEl = document.getElementById('akreSubtreeHint'); const pathLabelEl = document.getElementById('akrePathLabel'); - if (!subtreeEl || !hintEl) return; + const metaHeaderEl = document.getElementById('akreMetaHeader'); + if (!subtreeEl) return; const activeNode = findAkreNodeByKey(); const isSearchingFiles = activeFileSearch.length > 0; - - if (!activeNode) { - hintEl.textContent = 'Buka struktur di kiri sampai folder tujuan, lalu dokumen akan tampil di kanan.'; - if (pathLabelEl) pathLabelEl.textContent = 'Belum ada folder dipilih'; - subtreeEl.innerHTML = '
Pilih folder pada struktur kiri untuk melihat isinya.
'; - return; - } - - hintEl.textContent = isSearchingFiles - ? `Hasil pencarian file di ${activeNode.label}.` - : `Isi folder untuk ${activeNode.label}.`; - if (pathLabelEl) pathLabelEl.textContent = activeNode.path || 'Belum ada folder dipilih'; - - const childRows = isSearchingFiles ? '' : (Array.isArray(activeNode.children) ? activeNode.children : []).map(child => ` - - `).join(''); - - const fileRows = (currentData || []).map(item => { + const renderFileRows = () => (currentData || []).map(item => { const fileName = item.file ? item.file.split('/').pop() : 'Unknown'; const typeDok = fileName.split('.').pop().toUpperCase(); return ` @@ -983,10 +1238,55 @@ `; }).join(''); + if (!activeNode) { + if (metaHeaderEl) metaHeaderEl.textContent = isSearchingFiles ? 'Tanggal Unggah' : 'Struktur'; + if (isAkreDataLoading && isSearchingFiles) { + if (pathLabelEl) pathLabelEl.textContent = 'Semua folder akreditasi'; + subtreeEl.innerHTML = '
Mencari file...
'; + return; + } + + if (isSearchingFiles) { + if (pathLabelEl) pathLabelEl.textContent = 'Semua folder akreditasi'; + const globalFileRows = renderFileRows(); + subtreeEl.innerHTML = globalFileRows.trim().length + ? globalFileRows + : '
Tidak ada file yang cocok di semua folder.
'; + return; + } + if (pathLabelEl) pathLabelEl.textContent = 'Belum ada folder dipilih'; + subtreeEl.innerHTML = '
Pilih folder pada struktur kiri untuk melihat isinya.
'; + return; + } + + if (pathLabelEl) pathLabelEl.textContent = activeNode.path || 'Belum ada folder dipilih'; + if (metaHeaderEl) metaHeaderEl.textContent = (currentData || []).length > 0 ? 'Tanggal Unggah' : 'Struktur'; + + if (isAkreDataLoading) { + subtreeEl.innerHTML = '
Memuat isi folder...
'; + return; + } + + const childRows = isSearchingFiles ? '' : (Array.isArray(activeNode.children) ? activeNode.children : []).map(child => ` + + `).join(''); + + const fileRows = renderFileRows(); + const explorerHtml = `${childRows}${fileRows}`; subtreeEl.innerHTML = explorerHtml.trim().length ? explorerHtml - : '
Folder ini belum memiliki turunan atau file.
'; + : '
Folder ini belum memiliki file.
'; subtreeEl.dataset.bound = ''; bindAkreSubtreeEvents(); @@ -1363,33 +1663,195 @@ // Admin: tambah folder/instrumen akreditasi (disimpan di public/json/akreditasi.jff) const formAkreInstrumen = document.getElementById('formAkreInstrumen'); + const akreModeInput = document.getElementById('akreModeInput'); + const akreNameInput = document.getElementById('akreNameInput'); + const akreTypeInput = document.getElementById('akreTypeInput'); const akreSegmentInput = document.getElementById('akreSegmentInput'); const akreItemInput = document.getElementById('akreItemInput'); + const akreParentDisplay = document.getElementById('akreParentDisplay'); + const akreParentLabel = document.getElementById('akreParentLabel'); + const akreLevelDisplay = document.getElementById('akreLevelDisplay'); + const akreNameHint = document.getElementById('akreNameHint'); + const akreModalTitle = document.getElementById('akreInstrumenModalTitle'); - if (akreSegmentInput && akreItemInput) { - const updateItemEnabled = () => { - const hasSegment = String(akreSegmentInput.value || '').trim().length > 0; - akreItemInput.disabled = !hasSegment; - if (!hasSegment) akreItemInput.value = ''; - }; - akreSegmentInput.addEventListener('input', updateItemEnabled); - updateItemEnabled(); + function sanitizeAkreFolderName(value) { + return String(value || '').replace(/[\\\/:*?"<>|]/g, '').trim(); + } + + function buildAkrePayload() { + const mode = String(akreModeInput?.value || 'type'); + const typeValue = String(akreTypeInput?.value || '').trim(); + const segmentValue = String(akreSegmentInput?.value || '').trim(); + const itemValue = String(akreItemInput?.value || '').trim(); + + if (mode === 'segment') { + return { type: typeValue, segment: segmentValue }; + } + if (mode === 'item') { + return { type: typeValue, segment: segmentValue, item: itemValue }; + } + return { type: typeValue }; + } + + function getAkreParentPath(path) { + const parts = String(path || '').split('/').filter(Boolean); + if (parts.length <= 1) return ''; + parts.pop(); + return parts.join('/'); + } + + function getAkreCurrentParentPath() { + const mode = String(akreModeInput?.value || 'type'); + const typeValue = String(akreTypeInput?.value || '').trim(); + const segmentValue = String(akreSegmentInput?.value || '').trim(); + + if (mode === 'item') { + return [typeValue, segmentValue].filter(Boolean).join('/'); + } + if (mode === 'segment') { + return typeValue; + } + return ''; + } + + function configureAkreModal(folderPath = '') { + const parts = String(folderPath || '').split('/').filter(Boolean); + const depth = parts.length; + + if (akreTypeInput) akreTypeInput.value = parts[0] || ''; + if (akreSegmentInput) akreSegmentInput.value = parts[1] || ''; + if (akreItemInput) akreItemInput.value = ''; + if (akreNameInput) akreNameInput.value = ''; + + if (depth >= 2) { + if (akreModeInput) akreModeInput.value = 'item'; + if (akreModalTitle) akreModalTitle.textContent = 'Tambah Item Akreditasi'; + if (akreParentLabel) akreParentLabel.textContent = 'Akan ditambahkan ke'; + if (akreParentDisplay) akreParentDisplay.textContent = folderPath; + if (akreLevelDisplay) akreLevelDisplay.value = 'Item'; + if (akreNameHint) akreNameHint.textContent = 'Masukkan nama item baru untuk segment yang sedang dipilih.'; + const saveBtn = document.getElementById('btnSaveAkreInstrumen'); + if (saveBtn) saveBtn.textContent = 'Tambah Item'; + } else if (depth === 1) { + if (akreModeInput) akreModeInput.value = 'segment'; + if (akreModalTitle) akreModalTitle.textContent = 'Tambah Subfolder Akreditasi'; + if (akreParentLabel) akreParentLabel.textContent = 'Akan ditambahkan ke'; + if (akreParentDisplay) akreParentDisplay.textContent = folderPath; + if (akreLevelDisplay) akreLevelDisplay.value = 'Subfolder'; + if (akreNameHint) akreNameHint.textContent = 'Masukkan nama subfolder baru di bawah folder utama yang dipilih.'; + const saveBtn = document.getElementById('btnSaveAkreInstrumen'); + if (saveBtn) saveBtn.textContent = 'Tambah Subfolder'; + } else { + if (akreModeInput) akreModeInput.value = 'type'; + if (akreModalTitle) akreModalTitle.textContent = 'Tambah Folder Akreditasi'; + if (akreParentLabel) akreParentLabel.textContent = 'Akan ditambahkan ke'; + if (akreParentDisplay) akreParentDisplay.textContent = 'Root akreditasi'; + if (akreLevelDisplay) akreLevelDisplay.value = 'Folder Utama'; + if (akreNameHint) akreNameHint.textContent = 'Masukkan nama folder utama baru.'; + const saveBtn = document.getElementById('btnSaveAkreInstrumen'); + if (saveBtn) saveBtn.textContent = 'Tambah Folder'; + } + + if (akreNameInput) { + akreNameInput.placeholder = depth >= 2 + ? 'Contoh: PAP 1.3' + : depth === 1 + ? 'Contoh: PAP 2' + : 'Contoh: Tata Kelola Rumah Sakit (TKRS)'; + } + } + + async function deleteAkreFolder(folderPath) { + const normalizedPath = String(folderPath || '').trim(); + if (!normalizedPath) return; + + const confirmation = await Swal.fire({ + icon: 'warning', + title: 'Hapus folder ini?', + text: `Folder "${normalizedPath}" hanya bisa dihapus jika kosong.`, + showCancelButton: true, + confirmButtonText: 'Ya, hapus', + cancelButtonText: 'Batal', + reverseButtons: true, + }); + + if (!confirmation.isConfirmed) { + return; + } + + fetch('/akreditasi/instrumen', { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('input[name="_token"]')?.value || '', + }, + body: JSON.stringify({ path: normalizedPath }), + }) + .then(async (res) => { + const data = await res.json().catch(() => ({})); + if (!res.ok || data?.status === false) { + throw new Error(data?.message || 'Folder gagal dihapus.'); + } + + pendingAkreFocusPath = getAkreParentPath(normalizedPath); + if (activeAkrePath === normalizedPath) { + activeFileSearch = ''; + currentData = []; + } + + await refreshAkreSelects(); + + if (activeAkrePath && activeAkrePath !== normalizedPath) { + await fetchData(activeAkrePath, 1); + } else if (pendingAkreFocusPath) { + await fetchData(pendingAkreFocusPath, 1); + } else { + currentData = []; + renderAkreSubtree(); + } + + Swal.fire({ + icon: 'success', + title: 'Berhasil', + text: data?.message || 'Folder akreditasi berhasil dihapus.', + timer: 1500, + showConfirmButton: false, + }); + }) + .catch((err) => { + Swal.fire({ + icon: 'error', + title: 'Gagal', + text: err?.message || 'Folder gagal dihapus.', + }); + }); } if (formAkreInstrumen) { formAkreInstrumen.addEventListener('submit', (e) => { e.preventDefault(); - const btn = document.getElementById('btnSaveAkreInstrumen'); - if (btn) btn.disabled = true; - if (btn) btn.textContent = 'menyimpan...'; + const saveBtn = document.getElementById('btnSaveAkreInstrumen'); + const rawName = sanitizeAkreFolderName(akreNameInput?.value || ''); - const payload = { - type: String(document.getElementById('akreTypeInput')?.value || '').trim(), - segment: String(document.getElementById('akreSegmentInput')?.value || '').trim(), - item: String(document.getElementById('akreItemInput')?.value || '').trim(), - }; - if (!payload.segment) delete payload.segment; - if (!payload.item) delete payload.item; + if (akreNameInput) akreNameInput.value = rawName; + if (!rawName) { + Swal.fire({ + icon: 'warning', + title: 'Nama folder wajib diisi', + text: 'Isi satu nama folder yang valid tanpa karakter khusus seperti / atau \\.', + }); + akreNameInput?.focus(); + return; + } + + const mode = String(akreModeInput?.value || 'type'); + if (mode === 'type' && akreTypeInput) akreTypeInput.value = rawName; + if (mode === 'segment' && akreSegmentInput) akreSegmentInput.value = rawName; + if (mode === 'item' && akreItemInput) akreItemInput.value = rawName; + + const payload = buildAkrePayload(); + if (saveBtn) saveBtn.disabled = true; + if (saveBtn) saveBtn.textContent = 'menyimpan...'; fetch('/akreditasi/instrumen', { method: 'POST', @@ -1416,15 +1878,24 @@ showConfirmButton: false, }); + const createdPath = [payload.type, payload.segment, payload.item].filter(Boolean).join('/'); + pendingAkreFocusPath = createdPath; + const modalEl = document.getElementById('modalAkreInstrumen'); const modalInstance = modalEl ? bootstrap.Modal.getInstance(modalEl) : null; modalInstance?.hide(); formAkreInstrumen.reset(); - if (akreItemInput) akreItemInput.disabled = true; + configureAkreModal(''); // refresh dropdown instrumen di form upload - return refreshAkreSelects(); + return refreshAkreSelects().then(() => { + if (activeAkrePath) { + return fetchData(activeAkrePath, 1); + } + renderAkreSubtree(); + return null; + }); }) .catch((err) => { Swal.fire({ @@ -1434,8 +1905,8 @@ }); }) .finally(() => { - if (btn) btn.disabled = false; - if (btn) btn.textContent = 'Simpan'; + if (saveBtn) saveBtn.disabled = false; + configureAkreModal(getAkreCurrentParentPath()); }); }); } @@ -1447,46 +1918,11 @@ const modalEl = document.getElementById('modalAkreInstrumen'); if (!modalEl) return; - const typeInput = document.getElementById('akreTypeInput'); - const segmentInput = document.getElementById('akreSegmentInput'); - const itemInput = document.getElementById('akreItemInput'); - if (!typeInput || !segmentInput || !itemInput) return; - - const parts = String(folderPath || '').split('/').filter(Boolean); - const depth = parts.length; - - // reset state - typeInput.readOnly = false; - segmentInput.readOnly = false; - itemInput.disabled = true; - itemInput.readOnly = false; - - if (depth >= 1) { - typeInput.value = parts[0] || ''; - typeInput.readOnly = true; - } else { - typeInput.value = ''; - } - - if (depth >= 2) { - segmentInput.value = parts[1] || ''; - segmentInput.readOnly = true; - itemInput.disabled = false; - itemInput.value = ''; - } else { - segmentInput.value = ''; - itemInput.value = ''; - } + configureAkreModal(folderPath); const instance = bootstrap.Modal.getOrCreateInstance(modalEl); instance.show(); - - // fokus ke field yang tepat - if (depth >= 2) { - itemInput.focus(); - } else { - segmentInput.focus(); - } + setTimeout(() => akreNameInput?.focus(), 150); } document.addEventListener('change', function(e){ diff --git a/resources/views/layout/partials/footer.blade.php b/resources/views/layout/partials/footer.blade.php index 39214cc..4eda2ba 100644 --- a/resources/views/layout/partials/footer.blade.php +++ b/resources/views/layout/partials/footer.blade.php @@ -2,5 +2,5 @@

© made with by TIM SIMRS

+ made with by TIM SIRS

diff --git a/routes/web.php b/routes/web.php index 1fedf25..69cfb39 100644 --- a/routes/web.php +++ b/routes/web.php @@ -23,6 +23,7 @@ Route::middleware(['auth:admin,web'])->group(function(){ // Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database) Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']); Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']); + Route::delete('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'destroy']); Route::get('/download-excel/data-umum', [DashboardController::class, 'downloadDataUmumExcel']); Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']); Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']);