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 @@