new version view dokumen akreditasi
This commit is contained in:
parent
c7544b1868
commit
9803b27972
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\FileDirectory;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
|
||||||
@ -149,6 +150,90 @@ class AkreditasiInstrumenController extends Controller
|
|||||||
return true;
|
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)
|
* Read akreditasi.jff (array)
|
||||||
*/
|
*/
|
||||||
@ -313,4 +398,97 @@ class AkreditasiInstrumenController extends Controller
|
|||||||
@fclose($lockFp);
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,27 +88,6 @@ class PdfFpdiWithAlpha extends Fpdi
|
|||||||
|
|
||||||
class DashboardController extends Controller
|
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)
|
public function setDashboardPrefill(Request $request)
|
||||||
{
|
{
|
||||||
@ -2791,9 +2770,18 @@ class DashboardController extends Controller
|
|||||||
|
|
||||||
// Filter spesifik berdasarkan path folder dari tree/explorer.
|
// Filter spesifik berdasarkan path folder dari tree/explorer.
|
||||||
// Gunakan prefix match agar "KPS 9.c" tidak ikut mengambil "KPS 9.f".
|
// 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, '\\%_');
|
$escapedPath = addcslashes($folderPath, '\\%_');
|
||||||
|
$slashCount = substr_count($folderPath, '/') + 1;
|
||||||
|
|
||||||
$q->where('file', 'ILIKE', $escapedPath . '/%');
|
$q->where('file', 'ILIKE', $escapedPath . '/%');
|
||||||
|
|
||||||
|
if (!$keyword) {
|
||||||
|
$q->whereRaw(
|
||||||
|
"(length(file) - length(replace(file, '/', ''))) = ?",
|
||||||
|
[$slashCount]
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pencarian nama file/dokumen bisa digabung dengan filter folder.
|
// Pencarian nama file/dokumen bisa digabung dengan filter folder.
|
||||||
|
|||||||
@ -103,6 +103,188 @@
|
|||||||
"name": "TKRS 6.f"
|
"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"
|
"name": "PPI 8.c"
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -3178,4 +3360,8 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "tst",
|
||||||
|
"segment": []
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -24,6 +24,9 @@
|
|||||||
max-height: 150vh;
|
max-height: 150vh;
|
||||||
min-height: 520px;
|
min-height: 520px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #b8c7db transparent;
|
||||||
}
|
}
|
||||||
.akre-tree-row.level-0 {
|
.akre-tree-row.level-0 {
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
@ -45,6 +48,18 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
color: #1f2937;
|
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 {
|
.akre-tree-row:hover {
|
||||||
background: #f5f9ff;
|
background: #f5f9ff;
|
||||||
}
|
}
|
||||||
@ -78,6 +93,42 @@
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
flex: 0 0 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 {
|
.akre-tree-children {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@ -146,7 +197,7 @@
|
|||||||
.akre-browser-area {
|
.akre-browser-area {
|
||||||
border: 1px solid #edf2f7;
|
border: 1px solid #edf2f7;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
overflow: hidden;
|
/* overflow: hidden; */
|
||||||
background: #fff;
|
background: #fff;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
@ -162,8 +213,33 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.akre-browser-body {
|
.akre-browser-body {
|
||||||
max-height: 320px;
|
max-height: 720px;
|
||||||
overflow-y: auto;
|
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 {
|
.akre-browser-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -229,6 +305,23 @@
|
|||||||
.akre-selection-status {
|
.akre-selection-status {
|
||||||
color: #7b8aa0;
|
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) {
|
@media (max-width: 991.98px) {
|
||||||
.akre-tree-body {
|
.akre-tree-body {
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
@ -324,6 +417,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="akre-doc-actions">
|
<div class="akre-doc-actions">
|
||||||
<div class="d-flex flex-column align-items-start">
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm"
|
class="btn btn-primary btn-sm"
|
||||||
@ -331,22 +425,29 @@
|
|||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
<i class="ti ti-download me-1"></i>
|
<i class="ti ti-download me-1"></i>
|
||||||
Download Terpilih
|
Download Terpilih (0)
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
id="btnClearSelection"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Reset Pilihan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<span
|
<span
|
||||||
id="selectedCount"
|
id="selectedCount"
|
||||||
class="small akre-selection-status mt-1"
|
class="small akre-selection-status mt-1"
|
||||||
>
|
>
|
||||||
0 dipilih
|
Belum ada file dipilih
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@if(Auth::guard('admin')->check())
|
@if(Auth::guard('admin')->check())
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-primary btn-sm"
|
class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
onclick="openAddAkreFromFolder('')"
|
||||||
data-bs-target="#modalAkreInstrumen"
|
|
||||||
>
|
>
|
||||||
<i class="ti ti-folder-plus me-1"></i>
|
<i class="ti ti-folder-plus me-1"></i>
|
||||||
Tambah Folder Akreditasi
|
Tambah Folder Akreditasi
|
||||||
@ -382,7 +483,7 @@
|
|||||||
<div class="akre-browser-head">
|
<div class="akre-browser-head">
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
<span>Tipe</span>
|
<span>Tipe</span>
|
||||||
<span>Struktur</span>
|
<span id="akreMetaHeader"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="akre-browser-body" id="akreSubtreeContainer">
|
<div class="akre-browser-body" id="akreSubtreeContainer">
|
||||||
<div class="akre-browser-empty">Pilih folder pada struktur kiri untuk melihat isinya.</div>
|
<div class="akre-browser-empty">Pilih folder pada struktur kiri untuk melihat isinya.</div>
|
||||||
@ -403,35 +504,37 @@
|
|||||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5">Tambah Folder/Instrumen Akreditasi</h1>
|
<h1 class="modal-title fs-5" id="akreInstrumenModalTitle">Tambah Folder Akreditasi</h1>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<form id="formAkreInstrumen" autocomplete="off">
|
<form id="formAkreInstrumen" autocomplete="off">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="alert alert-info small mb-3">
|
<input type="hidden" id="akreModeInput" value="type">
|
||||||
Isi <strong>Type</strong> untuk membuat folder utama. Isi <strong>Segment</strong> untuk membuat sub-folder di dalam type.
|
|
||||||
Isi <strong>Item</strong> untuk membuat item di dalam segment.
|
|
||||||
</div>
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label fw-semibold">Type <span class="text-danger">*</span></label>
|
<label class="form-label fw-semibold">Lokasi Parent</label>
|
||||||
<input type="text" class="form-control" name="type" id="akreTypeInput" placeholder="Contoh: Tata Kelola Rumah Sakit (TKRS)" required>
|
<div class="akre-parent-chip">
|
||||||
|
<small id="akreParentLabel">Akan ditambahkan ke</small>
|
||||||
|
<span id="akreParentDisplay">Root akreditasi</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-semibold">Segment</label>
|
<label class="form-label fw-semibold">Level Folder Baru</label>
|
||||||
<textarea class="form-control" name="segment" id="akreSegmentInput" rows="3" placeholder="Contoh: TKRS 7, TKRS 8, TKRS 9"></textarea>
|
<input type="text" class="form-control" id="akreLevelDisplay" value="Folder Utama" readonly>
|
||||||
<div class="form-text text-muted">Opsional. Bisa isi banyak (pisahkan dengan koma atau baris baru).</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label fw-semibold">Item</label>
|
<label class="form-label fw-semibold">Nama Folder Baru <span class="text-danger">*</span></label>
|
||||||
<textarea class="form-control" name="item" id="akreItemInput" rows="3" placeholder="Contoh: TKRS 7.a, TKRS 7.b, TKRS 7.c" disabled></textarea>
|
<input type="text" class="form-control" id="akreNameInput" placeholder="Contoh: PAP 1 atau Tata Kelola Rumah Sakit (TKRS)" required>
|
||||||
<div class="form-text text-muted">Aktif jika segment diisi. Bisa isi banyak (pisahkan dengan koma atau baris baru).</div>
|
<div class="form-text text-muted" id="akreNameHint">Masukkan satu nama folder sesuai level yang sedang dipilih.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<input type="hidden" name="type" id="akreTypeInput">
|
||||||
|
<input type="hidden" name="segment" id="akreSegmentInput">
|
||||||
|
<input type="hidden" name="item" id="akreItemInput">
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
|
||||||
<button type="submit" class="btn btn-primary" id="btnSaveAkreInstrumen">Simpan</button>
|
<button type="submit" class="btn btn-primary" id="btnSaveAkreInstrumen">Tambah Folder</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -445,8 +548,12 @@
|
|||||||
let selectedIds = [];
|
let selectedIds = [];
|
||||||
let searchTimer;
|
let searchTimer;
|
||||||
let activeFileSearch = '';
|
let activeFileSearch = '';
|
||||||
|
let isAkreDataLoading = false;
|
||||||
|
let currentAkreFetchController = null;
|
||||||
|
let latestAkreFetchToken = 0;
|
||||||
const isAdminUser = @json(Auth::guard('admin')->check());
|
const isAdminUser = @json(Auth::guard('admin')->check());
|
||||||
const btn = document.getElementById('btnDownloadMultiple');
|
const btn = document.getElementById('btnDownloadMultiple');
|
||||||
|
const clearSelectionBtn = document.getElementById('btnClearSelection');
|
||||||
// === INITIALIZATION ===
|
// === INITIALIZATION ===
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
loadAkreData().then(() => initAkreTree(true));
|
loadAkreData().then(() => initAkreTree(true));
|
||||||
@ -454,6 +561,7 @@
|
|||||||
renderAkreSubtree();
|
renderAkreSubtree();
|
||||||
initEventListeners();
|
initEventListeners();
|
||||||
initTooltips();
|
initTooltips();
|
||||||
|
configureAkreModal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
function initTooltips() {
|
function initTooltips() {
|
||||||
@ -475,7 +583,10 @@
|
|||||||
activeFileSearch = String(value || '').trim();
|
activeFileSearch = String(value || '').trim();
|
||||||
if (activeAkrePath) {
|
if (activeAkrePath) {
|
||||||
fetchData(activeAkrePath, 1);
|
fetchData(activeAkrePath, 1);
|
||||||
|
} else if (activeFileSearch) {
|
||||||
|
fetchData('', 1);
|
||||||
} else {
|
} else {
|
||||||
|
currentData = [];
|
||||||
renderAkreSubtree();
|
renderAkreSubtree();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -506,6 +617,12 @@
|
|||||||
});
|
});
|
||||||
updateDownloadButton();
|
updateDownloadButton();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (clearSelectionBtn) {
|
||||||
|
clearSelectionBtn.addEventListener('click', function() {
|
||||||
|
clearSelectedFiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === CORE FUNCTIONS ===
|
// === CORE FUNCTIONS ===
|
||||||
@ -515,11 +632,17 @@
|
|||||||
*/
|
*/
|
||||||
function fetchData(keyword = '', page = 1) {
|
function fetchData(keyword = '', page = 1) {
|
||||||
const perPage = 100;
|
const perPage = 100;
|
||||||
const subtreeEl = document.getElementById('akreSubtreeContainer');
|
const requestToken = ++latestAkreFetchToken;
|
||||||
if (subtreeEl) {
|
|
||||||
subtreeEl.innerHTML = '<div class="akre-browser-empty">Memuat isi folder...</div>';
|
if (currentAkreFetchController) {
|
||||||
|
currentAkreFetchController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentAkreFetchController = new AbortController();
|
||||||
|
isAkreDataLoading = true;
|
||||||
|
currentData = [];
|
||||||
|
renderAkreSubtree();
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
per_page: String(perPage),
|
per_page: String(perPage),
|
||||||
page: String(page),
|
page: String(page),
|
||||||
@ -532,21 +655,33 @@
|
|||||||
params.set('keyword', activeFileSearch);
|
params.set('keyword', activeFileSearch);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(`/datatable-akreditasi?${params.toString()}`)
|
return fetch(`/datatable-akreditasi?${params.toString()}`, {
|
||||||
|
signal: currentAkreFetchController.signal,
|
||||||
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
if (requestToken !== latestAkreFetchToken) return;
|
||||||
if (response.status) {
|
if (response.status) {
|
||||||
currentData = response.data || [];
|
currentData = response.data || [];
|
||||||
renderAkreSubtree();
|
|
||||||
} else {
|
} else {
|
||||||
currentData = [];
|
currentData = [];
|
||||||
renderAkreSubtree();
|
|
||||||
}
|
}
|
||||||
|
isAkreDataLoading = false;
|
||||||
|
renderAkreSubtree();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
if (err?.name === 'AbortError') return;
|
||||||
|
if (requestToken !== latestAkreFetchToken) return;
|
||||||
console.error('Error fetching data:', err);
|
console.error('Error fetching data:', err);
|
||||||
currentData = [];
|
currentData = [];
|
||||||
|
isAkreDataLoading = false;
|
||||||
renderAkreSubtree();
|
renderAkreSubtree();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (requestToken === latestAkreFetchToken) {
|
||||||
|
isAkreDataLoading = false;
|
||||||
|
currentAkreFetchController = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// === UTILITY FUNCTIONS ===
|
// === UTILITY FUNCTIONS ===
|
||||||
@ -557,27 +692,65 @@
|
|||||||
if (!selectedIds.includes(val)) selectedIds.push(val);
|
if (!selectedIds.includes(val)) selectedIds.push(val);
|
||||||
} else {
|
} else {
|
||||||
selectedIds = selectedIds.filter(id => id !== val);
|
selectedIds = selectedIds.filter(id => id !== val);
|
||||||
document.getElementById('checkAllRows').checked = false;
|
const checkAllRows = document.getElementById('checkAllRows');
|
||||||
|
if (checkAllRows) checkAllRows.checked = false;
|
||||||
}
|
}
|
||||||
updateDownloadButton();
|
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');
|
const countLabel = document.getElementById('selectedCount');
|
||||||
btn.disabled = selectedIds.length === 0;
|
const totalSelected = selectedIds.length;
|
||||||
countLabel.innerText = `${selectedIds.length} dipilih`;
|
const hasSelection = totalSelected > 0;
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = !hasSelection;
|
||||||
|
btn.innerHTML = `<i class="ti ti-download me-1"></i>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');
|
const downloadBtn = document.getElementById('btnDownloadMultiple');
|
||||||
if (downloadBtn) {
|
if (downloadBtn) {
|
||||||
downloadBtn.addEventListener('click', function(){
|
downloadBtn.addEventListener('click', async function(){
|
||||||
if(selectedIds.length === 0){
|
if(selectedIds.length === 0){
|
||||||
return;
|
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) };
|
const payload = { ids: Array.from(selectedIds) };
|
||||||
downloadBtn.disabled = true;
|
downloadBtn.disabled = true;
|
||||||
downloadBtn.textContent = 'Menyiapkan...';
|
if (clearSelectionBtn) clearSelectionBtn.disabled = true;
|
||||||
|
downloadBtn.innerHTML = '<i class="ti ti-loader me-1"></i>Menyiapkan ZIP...';
|
||||||
fetch('/download-multiple', {
|
fetch('/download-multiple', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -604,13 +777,20 @@
|
|||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
|
Swal.fire({
|
||||||
|
icon: 'success',
|
||||||
|
title: 'Berhasil',
|
||||||
|
text: `${selectedIds.length} file sedang diunduh.`,
|
||||||
|
timer: 1500,
|
||||||
|
showConfirmButton: false
|
||||||
|
});
|
||||||
|
clearSelectedFiles();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
Swal.fire({ icon: 'error', title: 'Gagal', text: err.message || 'Gagal download file' });
|
Swal.fire({ icon: 'error', title: 'Gagal', text: err.message || 'Gagal download file' });
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
downloadBtn.disabled = selectedIds.length === 0;
|
updateDownloadButton();
|
||||||
downloadBtn.textContent = 'Download Terpilih';
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -643,6 +823,7 @@
|
|||||||
let expandedAkreNodes = new Set();
|
let expandedAkreNodes = new Set();
|
||||||
let activeAkreNodeKey = null;
|
let activeAkreNodeKey = null;
|
||||||
let activeAkrePath = '';
|
let activeAkrePath = '';
|
||||||
|
let pendingAkreFocusPath = '';
|
||||||
|
|
||||||
function loadAkreData(){
|
function loadAkreData(){
|
||||||
return Promise.resolve(akreData);
|
return Promise.resolve(akreData);
|
||||||
@ -654,10 +835,24 @@
|
|||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
akreData = Array.isArray(data) ? data : [];
|
akreData = Array.isArray(data) ? data : [];
|
||||||
|
if (pendingAkreFocusPath) {
|
||||||
|
const nextNode = findAkreNodeMetaByPath(pendingAkreFocusPath);
|
||||||
|
if (nextNode) {
|
||||||
|
expandedAkreNodes = new Set(nextNode.ancestors || []);
|
||||||
|
activeAkreNodeKey = nextNode.key;
|
||||||
|
activeAkrePath = nextNode.path;
|
||||||
|
} else {
|
||||||
expandedAkreNodes = new Set();
|
expandedAkreNodes = new Set();
|
||||||
activeAkreNodeKey = null;
|
activeAkreNodeKey = null;
|
||||||
activeAkrePath = '';
|
activeAkrePath = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expandedAkreNodes = new Set();
|
||||||
|
activeAkreNodeKey = null;
|
||||||
|
activeAkrePath = '';
|
||||||
|
}
|
||||||
initAkreTree(true);
|
initAkreTree(true);
|
||||||
|
pendingAkreFocusPath = '';
|
||||||
return akreData;
|
return akreData;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -666,6 +861,7 @@
|
|||||||
activeAkreNodeKey = null;
|
activeAkreNodeKey = null;
|
||||||
activeAkrePath = '';
|
activeAkrePath = '';
|
||||||
initAkreTree(true);
|
initAkreTree(true);
|
||||||
|
pendingAkreFocusPath = '';
|
||||||
return akreData;
|
return akreData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -761,10 +957,30 @@
|
|||||||
data-node-key="${e(key)}"
|
data-node-key="${e(key)}"
|
||||||
data-path="${e(path || '')}"
|
data-path="${e(path || '')}"
|
||||||
data-is-leaf="${isLeaf ? 'true' : 'false'}">
|
data-is-leaf="${isLeaf ? 'true' : 'false'}">
|
||||||
|
<span class="akre-tree-row-main">
|
||||||
${isLeaf
|
${isLeaf
|
||||||
? '<span class="akre-tree-spacer"></span>'
|
? '<span class="akre-tree-spacer"></span>'
|
||||||
: `<i class="ti ti-chevron-right akre-tree-caret ${expanded ? 'expanded' : ''}"></i>`}
|
: `<i class="ti ti-chevron-right akre-tree-caret ${expanded ? 'expanded' : ''}"></i>`}
|
||||||
<span>${label}</span>
|
<span>${label}</span>
|
||||||
|
</span>
|
||||||
|
${isAdminUser ? `
|
||||||
|
<span class="akre-tree-actions">
|
||||||
|
${level < 2 ? `
|
||||||
|
<span class="akre-tree-action"
|
||||||
|
data-add-folder="true"
|
||||||
|
data-folder-path="${e(path || '')}"
|
||||||
|
title="Tambah folder di sini">
|
||||||
|
<i class="ti ti-plus"></i>
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
|
<span class="akre-tree-action delete"
|
||||||
|
data-delete-folder="true"
|
||||||
|
data-folder-path="${e(path || '')}"
|
||||||
|
title="Hapus folder ini">
|
||||||
|
<i class="ti ti-trash"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
` : ''}
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -796,6 +1012,22 @@
|
|||||||
if (!treeEl || treeEl.dataset.bound === 'true') return;
|
if (!treeEl || treeEl.dataset.bound === 'true') return;
|
||||||
|
|
||||||
treeEl.addEventListener('click', function(e) {
|
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');
|
const row = e.target.closest('.akre-tree-row');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
|
|
||||||
@ -810,7 +1042,6 @@
|
|||||||
if (!isLeaf) {
|
if (!isLeaf) {
|
||||||
toggleAkreNode(nodeKey, false, false);
|
toggleAkreNode(nodeKey, false, false);
|
||||||
}
|
}
|
||||||
renderAkreSubtree();
|
|
||||||
fetchData(path, 1);
|
fetchData(path, 1);
|
||||||
syncAkreTreeState();
|
syncAkreTreeState();
|
||||||
});
|
});
|
||||||
@ -847,7 +1078,6 @@
|
|||||||
activeAkreNodeKey = nodeKey;
|
activeAkreNodeKey = nodeKey;
|
||||||
activeAkrePath = path || '';
|
activeAkrePath = path || '';
|
||||||
fetchData(activeAkrePath, 1);
|
fetchData(activeAkrePath, 1);
|
||||||
renderAkreSubtree();
|
|
||||||
if (syncTree) syncAkreTreeState();
|
if (syncTree) syncAkreTreeState();
|
||||||
syncAkreSubtreeState();
|
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() {
|
function findAkreNodeByKey() {
|
||||||
if (!activeAkreNodeKey) return null;
|
if (!activeAkreNodeKey) return null;
|
||||||
|
|
||||||
@ -925,38 +1206,12 @@
|
|||||||
const subtreeEl = document.getElementById('akreSubtreeContainer');
|
const subtreeEl = document.getElementById('akreSubtreeContainer');
|
||||||
const hintEl = document.getElementById('akreSubtreeHint');
|
const hintEl = document.getElementById('akreSubtreeHint');
|
||||||
const pathLabelEl = document.getElementById('akrePathLabel');
|
const pathLabelEl = document.getElementById('akrePathLabel');
|
||||||
if (!subtreeEl || !hintEl) return;
|
const metaHeaderEl = document.getElementById('akreMetaHeader');
|
||||||
|
if (!subtreeEl) return;
|
||||||
|
|
||||||
const activeNode = findAkreNodeByKey();
|
const activeNode = findAkreNodeByKey();
|
||||||
const isSearchingFiles = activeFileSearch.length > 0;
|
const isSearchingFiles = activeFileSearch.length > 0;
|
||||||
|
const renderFileRows = () => (currentData || []).map(item => {
|
||||||
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 = '<div class="akre-browser-empty">Pilih folder pada struktur kiri untuk melihat isinya.</div>';
|
|
||||||
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 => `
|
|
||||||
<button type="button"
|
|
||||||
class="akre-browser-row ${activeAkreNodeKey === child.key ? 'active' : ''}"
|
|
||||||
data-browser-key="${e(child.key)}"
|
|
||||||
data-browser-path="${e(child.path)}">
|
|
||||||
<span class="akre-browser-name">
|
|
||||||
<i class="ti ti-folder akre-folder-icon-big"></i>
|
|
||||||
<span>${child.label}</span>
|
|
||||||
</span>
|
|
||||||
<span>Folder</span>
|
|
||||||
<span>Turunan</span>
|
|
||||||
</button>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
const fileRows = (currentData || []).map(item => {
|
|
||||||
const fileName = item.file ? item.file.split('/').pop() : 'Unknown';
|
const fileName = item.file ? item.file.split('/').pop() : 'Unknown';
|
||||||
const typeDok = fileName.split('.').pop().toUpperCase();
|
const typeDok = fileName.split('.').pop().toUpperCase();
|
||||||
return `
|
return `
|
||||||
@ -983,10 +1238,55 @@
|
|||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
if (!activeNode) {
|
||||||
|
if (metaHeaderEl) metaHeaderEl.textContent = isSearchingFiles ? 'Tanggal Unggah' : 'Struktur';
|
||||||
|
if (isAkreDataLoading && isSearchingFiles) {
|
||||||
|
if (pathLabelEl) pathLabelEl.textContent = 'Semua folder akreditasi';
|
||||||
|
subtreeEl.innerHTML = '<div class="akre-browser-empty">Mencari file...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSearchingFiles) {
|
||||||
|
if (pathLabelEl) pathLabelEl.textContent = 'Semua folder akreditasi';
|
||||||
|
const globalFileRows = renderFileRows();
|
||||||
|
subtreeEl.innerHTML = globalFileRows.trim().length
|
||||||
|
? globalFileRows
|
||||||
|
: '<div class="akre-browser-empty">Tidak ada file yang cocok di semua folder.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pathLabelEl) pathLabelEl.textContent = 'Belum ada folder dipilih';
|
||||||
|
subtreeEl.innerHTML = '<div class="akre-browser-empty">Pilih folder pada struktur kiri untuk melihat isinya.</div>';
|
||||||
|
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 = '<div class="akre-browser-empty">Memuat isi folder...</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childRows = isSearchingFiles ? '' : (Array.isArray(activeNode.children) ? activeNode.children : []).map(child => `
|
||||||
|
<button type="button"
|
||||||
|
class="akre-browser-row ${activeAkreNodeKey === child.key ? 'active' : ''}"
|
||||||
|
data-browser-key="${e(child.key)}"
|
||||||
|
data-browser-path="${e(child.path)}">
|
||||||
|
<span class="akre-browser-name">
|
||||||
|
<i class="ti ti-folder akre-folder-icon-big"></i>
|
||||||
|
<span>${child.label}</span>
|
||||||
|
</span>
|
||||||
|
<span>Folder</span>
|
||||||
|
<span>Turunan</span>
|
||||||
|
</button>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const fileRows = renderFileRows();
|
||||||
|
|
||||||
const explorerHtml = `${childRows}${fileRows}`;
|
const explorerHtml = `${childRows}${fileRows}`;
|
||||||
subtreeEl.innerHTML = explorerHtml.trim().length
|
subtreeEl.innerHTML = explorerHtml.trim().length
|
||||||
? explorerHtml
|
? explorerHtml
|
||||||
: '<div class="akre-browser-empty">Folder ini belum memiliki turunan atau file.</div>';
|
: '<div class="akre-browser-empty">Folder ini belum memiliki file.</div>';
|
||||||
|
|
||||||
subtreeEl.dataset.bound = '';
|
subtreeEl.dataset.bound = '';
|
||||||
bindAkreSubtreeEvents();
|
bindAkreSubtreeEvents();
|
||||||
@ -1363,33 +1663,195 @@
|
|||||||
|
|
||||||
// Admin: tambah folder/instrumen akreditasi (disimpan di public/json/akreditasi.jff)
|
// Admin: tambah folder/instrumen akreditasi (disimpan di public/json/akreditasi.jff)
|
||||||
const formAkreInstrumen = document.getElementById('formAkreInstrumen');
|
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 akreSegmentInput = document.getElementById('akreSegmentInput');
|
||||||
const akreItemInput = document.getElementById('akreItemInput');
|
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) {
|
function sanitizeAkreFolderName(value) {
|
||||||
const updateItemEnabled = () => {
|
return String(value || '').replace(/[\\\/:*?"<>|]/g, '').trim();
|
||||||
const hasSegment = String(akreSegmentInput.value || '').trim().length > 0;
|
}
|
||||||
akreItemInput.disabled = !hasSegment;
|
|
||||||
if (!hasSegment) akreItemInput.value = '';
|
function buildAkrePayload() {
|
||||||
};
|
const mode = String(akreModeInput?.value || 'type');
|
||||||
akreSegmentInput.addEventListener('input', updateItemEnabled);
|
const typeValue = String(akreTypeInput?.value || '').trim();
|
||||||
updateItemEnabled();
|
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) {
|
if (formAkreInstrumen) {
|
||||||
formAkreInstrumen.addEventListener('submit', (e) => {
|
formAkreInstrumen.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const btn = document.getElementById('btnSaveAkreInstrumen');
|
const saveBtn = document.getElementById('btnSaveAkreInstrumen');
|
||||||
if (btn) btn.disabled = true;
|
const rawName = sanitizeAkreFolderName(akreNameInput?.value || '');
|
||||||
if (btn) btn.textContent = 'menyimpan...';
|
|
||||||
|
|
||||||
const payload = {
|
if (akreNameInput) akreNameInput.value = rawName;
|
||||||
type: String(document.getElementById('akreTypeInput')?.value || '').trim(),
|
if (!rawName) {
|
||||||
segment: String(document.getElementById('akreSegmentInput')?.value || '').trim(),
|
Swal.fire({
|
||||||
item: String(document.getElementById('akreItemInput')?.value || '').trim(),
|
icon: 'warning',
|
||||||
};
|
title: 'Nama folder wajib diisi',
|
||||||
if (!payload.segment) delete payload.segment;
|
text: 'Isi satu nama folder yang valid tanpa karakter khusus seperti / atau \\.',
|
||||||
if (!payload.item) delete payload.item;
|
});
|
||||||
|
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', {
|
fetch('/akreditasi/instrumen', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -1416,15 +1878,24 @@
|
|||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createdPath = [payload.type, payload.segment, payload.item].filter(Boolean).join('/');
|
||||||
|
pendingAkreFocusPath = createdPath;
|
||||||
|
|
||||||
const modalEl = document.getElementById('modalAkreInstrumen');
|
const modalEl = document.getElementById('modalAkreInstrumen');
|
||||||
const modalInstance = modalEl ? bootstrap.Modal.getInstance(modalEl) : null;
|
const modalInstance = modalEl ? bootstrap.Modal.getInstance(modalEl) : null;
|
||||||
modalInstance?.hide();
|
modalInstance?.hide();
|
||||||
|
|
||||||
formAkreInstrumen.reset();
|
formAkreInstrumen.reset();
|
||||||
if (akreItemInput) akreItemInput.disabled = true;
|
configureAkreModal('');
|
||||||
|
|
||||||
// refresh dropdown instrumen di form upload
|
// refresh dropdown instrumen di form upload
|
||||||
return refreshAkreSelects();
|
return refreshAkreSelects().then(() => {
|
||||||
|
if (activeAkrePath) {
|
||||||
|
return fetchData(activeAkrePath, 1);
|
||||||
|
}
|
||||||
|
renderAkreSubtree();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
@ -1434,8 +1905,8 @@
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (btn) btn.disabled = false;
|
if (saveBtn) saveBtn.disabled = false;
|
||||||
if (btn) btn.textContent = 'Simpan';
|
configureAkreModal(getAkreCurrentParentPath());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1447,46 +1918,11 @@
|
|||||||
const modalEl = document.getElementById('modalAkreInstrumen');
|
const modalEl = document.getElementById('modalAkreInstrumen');
|
||||||
if (!modalEl) return;
|
if (!modalEl) return;
|
||||||
|
|
||||||
const typeInput = document.getElementById('akreTypeInput');
|
configureAkreModal(folderPath);
|
||||||
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 = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = bootstrap.Modal.getOrCreateInstance(modalEl);
|
const instance = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
instance.show();
|
instance.show();
|
||||||
|
setTimeout(() => akreNameInput?.focus(), 150);
|
||||||
// fokus ke field yang tepat
|
|
||||||
if (depth >= 2) {
|
|
||||||
itemInput.focus();
|
|
||||||
} else {
|
|
||||||
segmentInput.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('change', function(e){
|
document.addEventListener('change', function(e){
|
||||||
|
|||||||
@ -2,5 +2,5 @@
|
|||||||
<p class="mb-0 fs-4"> ©
|
<p class="mb-0 fs-4"> ©
|
||||||
<script>
|
<script>
|
||||||
document.write(new Date().getFullYear());
|
document.write(new Date().getFullYear());
|
||||||
</script> made with by <a href="/" target="_blank" >TIM SIMRS</a></p>
|
</script> made with by <a href="/" target="_blank" >TIM SIRS</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -23,6 +23,7 @@ Route::middleware(['auth:admin,web'])->group(function(){
|
|||||||
// Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database)
|
// Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database)
|
||||||
Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']);
|
Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']);
|
||||||
Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']);
|
Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']);
|
||||||
|
Route::delete('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'destroy']);
|
||||||
Route::get('/download-excel/data-umum', [DashboardController::class, 'downloadDataUmumExcel']);
|
Route::get('/download-excel/data-umum', [DashboardController::class, 'downloadDataUmumExcel']);
|
||||||
Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']);
|
Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']);
|
||||||
Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']);
|
Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user