1943 lines
75 KiB
PHP

@extends('layout.main')
<style>
.akre-layout {
align-items: stretch;
}
.akre-column {
display: flex;
flex-direction: column;
}
.akre-tree-panel {
border: 1px solid #e1e7ef;
border-radius: 14px;
background: #fff;
overflow: hidden;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
height: 100%;
}
.akre-tree-header {
padding: 16px 18px;
border-bottom: 1px solid #edf2f7;
background: linear-gradient(180deg, #fbfdff 0%, #f4f8fc 100%);
}
.akre-tree-body {
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;
}
.akre-tree-node {
border-bottom: 1px solid #f1f5f9;
}
.akre-tree-node:last-child {
border-bottom: 0;
}
.akre-tree-row {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
border: 0;
background: transparent;
padding: 11px 16px;
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;
}
.akre-tree-row.active {
background: #e7f0fb;
color: #0f3d91;
font-weight: 600;
}
.akre-tree-row.level-1 {
padding-left: 34px;
}
.akre-tree-row.level-2 {
padding-left: 58px;
}
.akre-tree-row.level-3 {
padding-left: 82px;
}
.akre-tree-row.leaf {
cursor: default;
}
.akre-tree-caret {
width: 12px;
color: #6b7280;
transition: transform 0.2s ease;
flex: 0 0 12px;
}
.akre-tree-caret.expanded {
transform: rotate(90deg);
}
.akre-tree-spacer {
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;
}
.akre-tree-children[hidden] {
display: none !important;
}
.akre-doc-card {
border: 1px solid #e1e7ef;
border-radius: 14px;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
}
.akre-doc-toolbar {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 18px;
}
.akre-doc-actions {
display: flex;
align-items: flex-start;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.akre-doc-search {
margin-bottom: 14px;
}
.akre-doc-search .form-control {
min-height: 44px;
border-radius: 10px;
background: #fbfdff;
}
.akre-pathbar {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid #dbe5f0;
border-radius: 12px;
background: #f8fbff;
margin-bottom: 14px;
}
.akre-pathbar-label {
color: #64748b;
font-size: 12px;
text-transform: uppercase;
letter-spacing: .04em;
font-weight: 700;
}
.akre-pathbar-value {
color: #1e293b;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.akre-explorer-search {
margin-bottom: 14px;
}
.akre-explorer-search .form-control {
min-height: 42px;
border-radius: 10px;
background: #fbfdff;
}
.akre-browser-area {
border: 1px solid #edf2f7;
border-radius: 12px;
/* overflow: hidden; */
background: #fff;
margin-bottom: 14px;
}
.akre-browser-head {
display: grid;
grid-template-columns: minmax(0, 1.6fr) 120px 160px;
gap: 12px;
padding: 10px 14px;
background: #f8fafc;
border-bottom: 1px solid #e8eef5;
color: #64748b;
font-size: 12px;
font-weight: 700;
}
.akre-browser-body {
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;
grid-template-columns: minmax(0, 1.6fr) 120px 160px;
gap: 12px;
align-items: center;
width: 100%;
border: 0;
border-bottom: 1px solid #eef2f6;
background: #fff;
padding: 12px 14px;
text-align: left;
}
.akre-browser-row:last-child {
border-bottom: 0;
}
.akre-browser-row:hover {
background: #f8fbff;
}
.akre-browser-row.active {
background: #e7f0fb;
}
.akre-browser-row.file-row {
cursor: default;
}
.akre-browser-name {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
color: #1f2937;
}
.akre-browser-name span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.akre-browser-name-block {
display: flex;
flex-direction: column;
min-width: 0;
}
.akre-browser-subpath {
font-size: 11px;
color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.akre-folder-icon-big {
color: #f59e0b;
font-size: 18px;
}
.akre-browser-empty {
padding: 28px 16px;
color: #7b8aa0;
text-align: center;
}
.akre-file-icon-big {
color: #2563eb;
font-size: 18px;
}
.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;
max-height: 380px;
}
.akre-doc-toolbar {
flex-direction: column;
align-items: stretch;
}
.akre-doc-actions {
justify-content: flex-start;
}
}
/* Baris Folder */
.tree-folder {
background-color: #fcfcfc;
cursor: pointer;
transition: background 0.2s;
}
.tree-folder:hover {
background-color: #f4f7fb;
}
/* Container Indentasi */
.indent-container {
display: inline-flex;
align-items: center;
}
/* Garis Pandu Vertikal */
.tree-line {
display: inline-block;
width: 24px;
height: 40px; /* Sesuaikan dengan tinggi row */
border-left: 1px solid #e0e0e0;
margin-left: 12px;
position: relative;
}
/* Menghilangkan garis terakhir agar lebih rapi */
.tree-file:last-child .tree-line {
height: 20px;
}
/* Ikon dan Teks */
.folder-icon { color: #ffc107; font-size: 1.2rem; }
.file-icon { color: #6c757d; font-size: 1.1rem; }
.badge-pdf { background-color: #fff0f0; color: #e44d26; border: 1px solid #ffcccc; }
/* Aksi Folder (lebih user-friendly) */
.folder-actions {
display: inline-flex;
gap: 6px;
margin-left: 8px;
opacity: 0;
transform: translateY(-1px);
transition: opacity 0.15s ease, transform 0.15s ease;
}
.tree-folder:hover .folder-actions {
opacity: 1;
transform: translateY(0);
}
.btn-folder-action {
padding: 1px 2px;
font-size: 11px;
line-height: 1.2;
border-radius: 999px;
}
</style>
@section('body_main')
<div class="row g-3 akre-layout">
<div class="col-lg-4 akre-column">
<div class="akre-tree-panel">
<div class="akre-tree-header">
<h5 class="mb-1">Struktur Akreditasi</h5>
<p class="text-muted small mb-0">Pilih folder di kiri untuk memfilter dokumen di panel kanan.</p>
</div>
<div class="akre-tree-body" id="akreTreeContainer">
<div class="text-muted small p-3">Memuat instrumen...</div>
</div>
</div>
</div>
<div class="col-lg-8 akre-column">
<div class="card akre-doc-card">
<div class="card-body p-3">
<div class="tab-content">
<div class="tab-pane fade show active">
<div class="akre-doc-toolbar">
<div>
<h4 class="mb-1">Dokumen Akreditasi</h4>
<p class="text-muted small mb-0" id="akreSubtreeHint">Buka struktur di kiri sampai folder tujuan, lalu dokumen akan tampil di kanan.</p>
</div>
<div class="akre-doc-actions">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center gap-2 flex-wrap">
<button
type="button"
class="btn btn-primary btn-sm"
id="btnDownloadMultiple"
disabled
>
<i class="ti ti-download me-1"></i>
Download Terpilih (0)
</button>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
id="btnClearSelection"
disabled
>
Reset Pilihan
</button>
</div>
<span
id="selectedCount"
class="small akre-selection-status mt-1"
>
Belum ada file dipilih
</span>
</div>
@if(Auth::guard('admin')->check())
<button
type="button"
class="btn btn-outline-primary btn-sm"
onclick="openAddAkreFromFolder('')"
>
<i class="ti ti-folder-plus me-1"></i>
Tambah Folder Akreditasi
</button>
@endif
<!-- Tambah Dokumen -->
@if(!Auth::guard('admin')->check())
<button
type="button"
class="btn btn-success btn-sm"
data-bs-toggle="modal"
data-bs-target="#modalCreateFile"
>
<i class="ti ti-plus me-1"></i>
Tambah Dokumen
</button>
@endif
</div>
</div>
<div class="akre-pathbar">
<span class="akre-pathbar-label">Path</span>
<span class="akre-pathbar-value" id="akrePathLabel">Belum ada folder dipilih</span>
</div>
<div class="akre-explorer-search">
<input
type="search"
id="fileNameSearch"
class="form-control"
placeholder="Cari nama file / dokumen di folder aktif"
autocomplete="off">
</div>
<div class="akre-browser-area">
<div class="akre-browser-head">
<span>Name</span>
<span>Tipe</span>
<span id="akreMetaHeader"></span>
</div>
<div class="akre-browser-body" id="akreSubtreeContainer">
<div class="akre-browser-empty">Pilih folder pada struktur kiri untuk melihat isinya.</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@include('dataUnit.modal.create')
@if(Auth::guard('admin')->check())
<div class="modal fade" id="modalAkreInstrumen" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<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>
</div>
<form id="formAkreInstrumen" autocomplete="off">
<div class="modal-body">
<input type="hidden" id="akreModeInput" value="type">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-semibold">Lokasi Parent</label>
<div class="akre-parent-chip">
<small id="akreParentLabel">Akan ditambahkan ke</small>
<span id="akreParentDisplay">Root akreditasi</span>
</div>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Level Folder Baru</label>
<input type="text" class="form-control" id="akreLevelDisplay" value="Folder Utama" readonly>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Nama Folder Baru <span class="text-danger">*</span></label>
<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" id="akreNameHint">Masukkan satu nama folder sesuai level yang sedang dipilih.</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 class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Tutup</button>
<button type="submit" class="btn btn-primary" id="btnSaveAkreInstrumen">Tambah Folder</button>
</div>
</form>
</div>
</div>
</div>
@endif
<script>
// === STATE MANAGEMENT ===
let currentData = [];
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));
currentData = [];
renderAkreSubtree();
initEventListeners();
initTooltips();
configureAkreModal('');
});
function initTooltips() {
try {
if (!window.bootstrap?.Tooltip) return;
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
const existing = bootstrap.Tooltip.getInstance(el);
if (existing) existing.dispose();
new bootstrap.Tooltip(el);
});
} catch (_) {}
}
// === EVENT LISTENERS ===
function initEventListeners() {
const fileNameSearch = document.getElementById('fileNameSearch');
if (fileNameSearch) {
const runFileSearch = (value) => {
activeFileSearch = String(value || '').trim();
if (activeAkrePath) {
fetchData(activeAkrePath, 1);
} else if (activeFileSearch) {
fetchData('', 1);
} else {
currentData = [];
renderAkreSubtree();
}
};
fileNameSearch.addEventListener('input', function(e) {
clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
runFileSearch(e.target.value);
}, 150);
});
fileNameSearch.addEventListener('keydown', function(e) {
if (e.key !== 'Enter') return;
e.preventDefault();
clearTimeout(searchTimer);
runFileSearch(e.target.value);
});
}
// Check All Rows
const checkAllRows = document.getElementById('checkAllRows');
if (checkAllRows) checkAllRows.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.row-checkbox');
selectedIds = [];
checkboxes.forEach(cb => {
cb.checked = this.checked;
if (this.checked) selectedIds.push(cb.value);
});
updateDownloadButton();
});
if (clearSelectionBtn) {
clearSelectionBtn.addEventListener('click', function() {
clearSelectedFiles();
});
}
}
// === CORE FUNCTIONS ===
/**
* Mengambil data dari server
*/
function fetchData(keyword = '', page = 1) {
const perPage = 100;
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),
});
if (keyword) {
params.set('folder_path', keyword);
}
if (activeFileSearch) {
params.set('keyword', activeFileSearch);
}
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 || [];
} else {
currentData = [];
}
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 ===
function handleRowCheck(checkbox) {
const val = String(checkbox.value);
if (checkbox.checked) {
if (!selectedIds.includes(val)) selectedIds.push(val);
} else {
selectedIds = selectedIds.filter(id => id !== val);
const checkAllRows = document.getElementById('checkAllRows');
if (checkAllRows) checkAllRows.checked = false;
}
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 totalSelected = selectedIds.length;
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');
if (downloadBtn) {
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;
if (clearSelectionBtn) clearSelectionBtn.disabled = true;
downloadBtn.innerHTML = '<i class="ti ti-loader me-1"></i>Menyiapkan ZIP...';
fetch('/download-multiple', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(payload)
})
.then(async (res) => {
const contentType = res.headers.get('content-type') || '';
if(!res.ok || contentType.includes('application/json')){
const err = await res.json().catch(() => ({}));
throw new Error(err?.message || 'Gagal download file');
}
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
const disposition = res.headers.get('content-disposition') || '';
const match = disposition.match(/filename="?([^"]+)"?/);
a.href = url;
a.download = match?.[1] || 'files.zip';
document.body.appendChild(a);
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(() => {
updateDownloadButton();
});
});
}
function formatDateTime(value) {
if (!value) return '—';
const d = new Date(value);
if (isNaN(d)) return value;
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} `
+ `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
const katDok = @json($katDok);
const akreTreeSource = @json($akreditasiTree ?? []);
const formCreate = document.getElementById('formFile');
const modalCreate = document.getElementById('modalCreateFile');
let colCount = 1;
document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return;
const targetId = e.target.getAttribute('data-target');
if(!targetId) return;
const fieldWrap = document.getElementById(targetId);
const input = fieldWrap?.querySelector('input');
if (input) input.disabled = !e.target.checked;
});
let akreData = Array.isArray(akreTreeSource) ? akreTreeSource : [];
let akreFlat = [];
let expandedAkreNodes = new Set();
let activeAkreNodeKey = null;
let activeAkrePath = '';
let pendingAkreFocusPath = '';
function loadAkreData(){
return Promise.resolve(akreData);
}
function refreshAkreData(){
akreFlat = [];
return fetch('/json/akreditasi.jff')
.then(res => res.json())
.then(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();
activeAkreNodeKey = null;
activeAkrePath = '';
}
} else {
expandedAkreNodes = new Set();
activeAkreNodeKey = null;
activeAkrePath = '';
}
initAkreTree(true);
pendingAkreFocusPath = '';
return akreData;
})
.catch(() => {
akreData = [];
expandedAkreNodes = new Set();
activeAkreNodeKey = null;
activeAkrePath = '';
initAkreTree(true);
pendingAkreFocusPath = '';
return akreData;
});
}
function refreshAkreSelects(){
return refreshAkreData().then(() => {
document.querySelectorAll('select.akre-select').forEach(selectEl => {
const current = selectEl.value;
fillAkreSelect(selectEl);
if(current) {
// restore selection if still exists
selectEl.value = current;
}
if(window.$ && $.fn.select2){
$(selectEl).trigger('change');
}
});
});
}
function getAkreFlat(){
if(akreFlat.length) return akreFlat;
akreFlat = (akreData || []).flatMap(type => {
const segments = Array.isArray(type.segment) ? type.segment : [];
return segments.flatMap(seg => {
const children = Array.isArray(seg.turunan) ? seg.turunan : [];
return children.map(child => ({
value: `${type.name}/${seg.name}/${child.name}`,
label: `${type.name} / ${child.name}`,
type: type.name,
segment: seg.name,
item: child.name
}));
});
});
return akreFlat;
}
function fillAkreSelect(selectEl){
if(!selectEl) return;
selectEl.innerHTML = '<option value="">Pilih Instrumen</option>';
getAkreFlat().forEach(optData => {
const opt = document.createElement('option');
opt.value = optData.value;
opt.textContent = optData.label;
selectEl.appendChild(opt);
});
}
function initAkreTree(forceRender = false) {
const treeEl = document.getElementById('akreTreeContainer');
if (!treeEl) return;
if (!Array.isArray(akreData) || akreData.length === 0) {
treeEl.innerHTML = '<div class="text-muted small p-3">Data instrumen tidak tersedia.</div>';
return;
}
if (!forceRender && treeEl.dataset.rendered === 'true') {
syncAkreTreeState();
return;
}
treeEl.innerHTML = akreData.map((typeItem, typeIndex) => renderAkreTreeNode({
key: `type-${typeIndex}`,
label: `${String(typeIndex + 1).padStart(2, '0')}. ${typeItem.name || '-'}`,
path: typeItem.name || '',
level: 0,
children: (Array.isArray(typeItem.segment) ? typeItem.segment : []).map((segment, segmentIndex) => ({
key: `type-${typeIndex}-segment-${segmentIndex}`,
label: segment.name || '-',
path: `${typeItem.name || ''}/${segment.name || ''}`,
level: 1,
children: (Array.isArray(segment.turunan) ? segment.turunan : []).map((child, childIndex) => ({
key: `type-${typeIndex}-segment-${segmentIndex}-child-${childIndex}`,
label: child.name || '-',
path: `${typeItem.name || ''}/${segment.name || ''}/${child.name || ''}`,
level: 2,
children: []
}))
}))
})).join('');
treeEl.dataset.rendered = 'true';
bindAkreTreeEvents();
syncAkreTreeState();
}
function renderAkreTreeToggle({ key, label, path, level, expanded, isLeaf, active = false }) {
return `
<button type="button"
class="akre-tree-row level-${level} ${active ? 'active' : ''} ${isLeaf ? 'leaf' : ''}"
data-node-key="${e(key)}"
data-path="${e(path || '')}"
data-is-leaf="${isLeaf ? 'true' : 'false'}">
<span class="akre-tree-row-main">
${isLeaf
? '<span class="akre-tree-spacer"></span>'
: `<i class="ti ti-chevron-right akre-tree-caret ${expanded ? 'expanded' : ''}"></i>`}
<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>
`;
}
function renderAkreTreeNode(node) {
const hasChildren = Array.isArray(node.children) && node.children.length > 0;
return `
<div class="akre-tree-node" data-node-key="${e(node.key)}">
${renderAkreTreeToggle({
key: node.key,
label: node.label,
path: node.path,
level: node.level,
expanded: expandedAkreNodes.has(node.key),
isLeaf: !hasChildren,
active: activeAkreNodeKey === node.key
})}
${hasChildren ? `
<div class="akre-tree-children" data-children-of="${e(node.key)}" ${expandedAkreNodes.has(node.key) ? '' : 'hidden'}>
${node.children.map(child => renderAkreTreeNode(child)).join('')}
</div>
` : ''}
</div>
`;
}
function bindAkreTreeEvents() {
const treeEl = document.getElementById('akreTreeContainer');
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;
const nodeKey = row.dataset.nodeKey || '';
const path = row.dataset.path || '';
const isLeaf = row.dataset.isLeaf === 'true';
activeAkreNodeKey = nodeKey;
activeAkrePath = path;
activeFileSearch = '';
const fileNameSearch = document.getElementById('fileNameSearch');
if (fileNameSearch) fileNameSearch.value = '';
if (!isLeaf) {
toggleAkreNode(nodeKey, false, false);
}
fetchData(path, 1);
syncAkreTreeState();
});
treeEl.dataset.bound = 'true';
}
function syncAkreTreeState() {
const treeEl = document.getElementById('akreTreeContainer');
if (!treeEl) return;
treeEl.querySelectorAll('.akre-tree-row').forEach(row => {
const nodeKey = row.dataset.nodeKey || '';
row.classList.toggle('active', nodeKey === activeAkreNodeKey);
const caret = row.querySelector('.akre-tree-caret');
if (caret) caret.classList.toggle('expanded', expandedAkreNodes.has(nodeKey));
});
treeEl.querySelectorAll('.akre-tree-children').forEach(group => {
const parentKey = group.dataset.childrenOf || '';
group.hidden = !expandedAkreNodes.has(parentKey);
});
}
function e(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
window.applyAkreFilter = function(nodeKey, path, syncTree = true) {
activeAkreNodeKey = nodeKey;
activeAkrePath = path || '';
fetchData(activeAkrePath, 1);
if (syncTree) syncAkreTreeState();
syncAkreSubtreeState();
}
window.toggleAkreNode = function(nodeKey, rerender = true, syncTree = true) {
if (expandedAkreNodes.has(nodeKey)) {
expandedAkreNodes.delete(nodeKey);
} else {
expandedAkreNodes.add(nodeKey);
}
if (rerender) {
initAkreTree();
} else {
if (syncTree) syncAkreTreeState();
syncAkreSubtreeState();
}
}
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;
for (let typeIndex = 0; typeIndex < akreData.length; typeIndex++) {
const typeItem = akreData[typeIndex];
const typeKey = `type-${typeIndex}`;
if (typeKey === activeAkreNodeKey) {
return {
key: typeKey,
label: `${String(typeIndex + 1).padStart(2, '0')}. ${typeItem.name || '-'}`,
path: typeItem.name || '',
children: (Array.isArray(typeItem.segment) ? typeItem.segment : []).map((segment, segmentIndex) => ({
key: `${typeKey}-segment-${segmentIndex}`,
label: segment.name || '-',
path: `${typeItem.name || ''}/${segment.name || ''}`
}))
};
}
const segments = Array.isArray(typeItem.segment) ? typeItem.segment : [];
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
const segment = segments[segmentIndex];
const segmentKey = `${typeKey}-segment-${segmentIndex}`;
if (segmentKey === activeAkreNodeKey) {
return {
key: segmentKey,
label: segment.name || '-',
path: `${typeItem.name || ''}/${segment.name || ''}`,
children: (Array.isArray(segment.turunan) ? segment.turunan : []).map((child, childIndex) => ({
key: `${segmentKey}-child-${childIndex}`,
label: child.name || '-',
path: `${typeItem.name || ''}/${segment.name || ''}/${child.name || ''}`
}))
};
}
const children = Array.isArray(segment.turunan) ? segment.turunan : [];
for (let childIndex = 0; childIndex < children.length; childIndex++) {
const child = children[childIndex];
const childKey = `${segmentKey}-child-${childIndex}`;
if (childKey === activeAkreNodeKey) {
return {
key: childKey,
label: child.name || '-',
path: `${typeItem.name || ''}/${segment.name || ''}/${child.name || ''}`,
children: []
};
}
}
}
}
return null;
}
function renderAkreSubtree() {
const subtreeEl = document.getElementById('akreSubtreeContainer');
const hintEl = document.getElementById('akreSubtreeHint');
const pathLabelEl = document.getElementById('akrePathLabel');
const metaHeaderEl = document.getElementById('akreMetaHeader');
if (!subtreeEl) return;
const activeNode = findAkreNodeByKey();
const isSearchingFiles = activeFileSearch.length > 0;
const renderFileRows = () => (currentData || []).map(item => {
const fileName = item.file ? item.file.split('/').pop() : 'Unknown';
const typeDok = fileName.split('.').pop().toUpperCase();
return `
<div class="akre-browser-row file-row">
<span class="akre-browser-name">
<input type="checkbox" class="form-check-input row-checkbox"
value="${item.file_directory_id}" ${selectedIds.includes(String(item.file_directory_id)) ? 'checked' : ''}
onchange="handleRowCheck(this)">
<i class="ti ti-file-text akre-file-icon-big"></i>
<span class="akre-browser-name-block">
<span>${item.nama_dokumen || fileName}</span>
<small class="akre-browser-subpath">${item.file || '-'}</small>
</span>
</span>
<span>${typeDok}</span>
<span class="d-inline-flex align-items-center justify-content-between gap-2">
<span>${formatDateTime(item.entry_at)}</span>
<a href="/file-download/${item.file_directory_id}" target="_blank"
class="btn btn-sm btn-outline-primary" download title="Download">
<i class="ti ti-download"></i>
</a>
</span>
</div>
`;
}).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}`;
subtreeEl.innerHTML = explorerHtml.trim().length
? explorerHtml
: '<div class="akre-browser-empty">Folder ini belum memiliki file.</div>';
subtreeEl.dataset.bound = '';
bindAkreSubtreeEvents();
syncAkreSubtreeState();
}
function bindAkreSubtreeEvents() {
const subtreeEl = document.getElementById('akreSubtreeContainer');
if (!subtreeEl || subtreeEl.dataset.bound === 'true') return;
subtreeEl.addEventListener('click', function(e) {
const browserRow = e.target.closest('[data-browser-key]');
if (!browserRow) return;
applyAkreFilter(browserRow.dataset.browserKey || '', browserRow.dataset.browserPath || '');
});
subtreeEl.dataset.bound = 'true';
}
function syncAkreSubtreeState() {
const subtreeEl = document.getElementById('akreSubtreeContainer');
if (!subtreeEl) return;
const pathLabelEl = document.getElementById('akrePathLabel');
subtreeEl.querySelectorAll('[data-browser-key]').forEach(button => {
const key = button.dataset.browserKey || '';
button.classList.toggle('active', key === activeAkreNodeKey);
});
if (pathLabelEl) {
pathLabelEl.textContent = activeAkrePath || 'Belum ada folder dipilih';
}
}
function setKategoriRequired(index, isRequired){
const katSelect = document.getElementById(`select_kategori_${index}`);
if (!katSelect) return;
if (isRequired) {
katSelect.setAttribute('required', 'required');
} else {
katSelect.removeAttribute('required');
}
}
function resetAkreFields(index){
const selectEl = document.getElementById(`akre_select_${index}`);
const typeInput = document.getElementById(`akre_type_${index}`);
const segmentInput = document.getElementById(`akre_segment_${index}`);
const itemInput = document.getElementById(`akre_item_${index}`);
if(selectEl){
selectEl.value = '';
if(window.$ && $.fn.select2) $(selectEl).val(null).trigger('change');
}
if(typeInput) typeInput.value = '';
if(segmentInput) segmentInput.value = '';
if(itemInput) itemInput.value = '';
setKategoriRequired(index, false);
}
function enableAkreFields(index){
const selectEl = document.getElementById(`akre_select_${index}`);
setKategoriRequired(index, false);
loadAkreData().then(() => {
fillAkreSelect(selectEl);
if(window.$ && $.fn.select2){
$(selectEl).select2({
dropdownParent: $('#modalCreateFile'),
placeholder: 'Pilih Instrumen',
allowClear: true
});
}
});
}
function initKategoriSelect2(index){
if(!window.$ || !$.fn.select2) return;
const katSelect = $(`#select_kategori_${index}`);
const hukumSelect = $(`#select_kategori_hukum_${index}`);
if(katSelect.length){
katSelect.select2({
dropdownParent: $('#modalCreateFile'),
placeholder:'Pilih Kategori',
allowClear: true
});
}
if(hukumSelect.length){
hukumSelect.select2({
dropdownParent: $('#modalCreateFile'),
placeholder:'Pilih Kategori Hukum',
allowClear:true
});
}
}
function selectOptionUnitKerjaV1(localCol){
const selectUnit = $(`#select_id_unit_kerja_${localCol}`);
const selectSubUnit = $(`#select_id_sub_unit_kerja_${localCol}`);
selectUnit.select2({
placeholder: '-- Pilih Unit Kerja --',
allowClear:true,
width: '100%',
dropdownParent: selectUnit.parent(),
ajax:{
url : '/select-unit-kerja-mapping',
dataType: 'json',
delay: 250,
data: function(params){
return { q: params.term }
},
processResults: function(data){
return {
results : (data?.data || []).map(item => ({
id: item.id+'/'+item.name,
text: item.name,
sub_units: item.sub_unit_kerja
}))
}
},
cache: true,
},
minimumInputLength: 0,
});
selectSubUnit.select2({
placeholder: '-- Pilih Sub Unit Kerja --',
allowClear: true,
width: '100%',
dropdownParent: selectSubUnit.parent()
});
selectUnit.on('select2:select', function (e) {
const data = e.params.data;
selectSubUnit.empty().append('<option value="" disabled selected>-- Pilih Sub Unit Kerja --</option>');
if (data.sub_units && data.sub_units.length > 0) {
data.sub_units.forEach(sub => {
selectSubUnit.append(`<option value="${sub.id}/${sub.name}">${sub.name}</option>`);
});
}
});
}
window.addFormV2 = function(){
const col = document.getElementById('col_add_fileV2');
if (!col) return;
const katOptions = (Array.isArray(katDok) ? katDok : [])
.map(k => `<option value="${k.master_kategori_directory_id}/${k.nama_kategori_directory}">${k.nama_kategori_directory}</option>`)
.join('');
let html = `
<div class="row g-3 align-items-start" id="col-${colCount}">
<hr class="my-3" />
<div class="col-12 d-flex justify-content-end">
<button type="button"
class="btn btn-sm btn-danger"
onclick="removeCol(${colCount})">
<i class="fa-solid fa-trash"></i> Hapus
</button>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Unit <span class="text-danger">*</span></label>
<select class="form-select"
name="data[${colCount}][id_unit_kerja]"
id="select_id_unit_kerja_${colCount}"
required>
<option value="" disabled selected>Pilih Unit</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">Sub Unit <span class="text-danger">*</span></label>
<select class="form-select"
name="data[${colCount}][id_sub_unit_kerja]"
id="select_id_sub_unit_kerja_${colCount}"
required>
<option value="" disabled selected>Pilih Sub Unit</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Nomor Dokumen</label>
<div class="input-group">
<span class="input-group-text">#</span>
<input type="text"
class="form-control"
name="data[${colCount}][no_dokumen]"
placeholder="Contoh: 001/RS/IT/I/2026">
</div>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Nama Dokumen<span class="text-danger">*</span></label>
<input type="text"
class="form-control"
name="data[${colCount}][nama_dokumen]"
placeholder="Contoh: Panduan Mencuci Tangan" required>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control"
type="date"
name="data[${colCount}][date_active]">
</div>
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input toggle-expired"
type="checkbox"
id="hasExpired_${colCount}"
data-target="expiredField_${colCount}">
<label class="form-check-label" for="hasExpired_${colCount}">Masa Berlaku Dokumen?</label>
</div>
</div>
<div class="col-md-5" id="expiredField_${colCount}">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control"
type="date"
name="data[${colCount}][tgl_expired]" disabled>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
<div class="border rounded-3 p-2 bg-light">
<div class="form-check">
<input class="form-check-input"
type="radio"
name="data[${colCount}][is_permission]"
id="perm_yes_${colCount}"
value="1"
required>
<label class="form-check-label" for="perm_yes_${colCount}">Ya</label>
</div>
<div class="form-check mt-1">
<input class="form-check-input"
type="radio"
name="data[${colCount}][is_permission]"
id="perm_no_${colCount}"
value="2"
required>
<label class="form-check-label" for="perm_no_${colCount}">Tidak</label>
</div>
</div>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Instrumen Akreditasi </label>
<select class="form-select akre-select" id="akre_select_${colCount}" name="data[${colCount}][akre]" style="width: 350px;">
<option value="">Pilih Instrumen</option>
</select>
<div class="form-text text-muted">Isi form ini bila dokumen yang diunggah merupakan dokumen <strong>akreditasi</strong>.</div>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Hukum</label>
<select class="form-select select-kat-hukum" name="data[${colCount}][kategori_hukum]" id="select_kategori_hukum_${colCount}" style="width: 350px;">
<option value="">Pilih Kategori Hukum</option>
<option value="Kebijakan - Peraturan Direktur">Kebijakan - Peraturan Direktur</option>
<option value="Kebijakan - Keputusan Direktur Utama">Kebijakan - Keputusan Direktur Utama</option>
<option value="Kebijakan - Surat Edaran">Kebijakan - Surat Edaran</option>
<option value="Kebijakan - Pengumuman">Kebijakan - Pengumuman</option>
<option value="Kerjasama - Pelayanan Kesehatan">Kerjasama - Pelayanan Kesehatan</option>
<option value="Kerjasama - Management">Kerjasama - Management</option>
<option value="Kerjasama - Pemeliharan">Kerjasama - Pemeliharan</option>
<option value="Kerjasama - Diklat">Kerjasama - Diklat</option>
<option value="Kerjasama - Luar Negeri">Kerjasama - Luar Negeri</option>
<option value="Kerjasama - Area Bisnis">Kerjasama - Area Bisnis</option>
<option value="Kerjasama - Pendidikan">Kerjasama - Pendidikan</option>
<option value="Kerjasama - Pengampuan KIA">Kerjasama- Pengampuan KIA</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">Kategori Lainnya</label>
<select class="form-select"
name="data[${colCount}][master_kategori_directory_id]"
id="select_kategori_${colCount}" style="width: 350px;">
<option value="">Pilih Kategori</option>
${katOptions}
</select>
</div>
<div class="col-md-12">
<label for="fileUpload_${colCount}" class="form-label fw-semibold">📂 Upload Dokumen (PDF)</label>
<div class="border rounded-3 p-3 bg-white shadow-sm">
<input class="form-control"
type="file"
id="fileUpload_${colCount}"
accept=".pdf"
name="data[${colCount}][file]">
<div class="mt-2 text-success fw-semibold d-none file-name" id="fileName_${colCount}"></div>
</div>
<div class="form-text text-muted">Format yang didukung: <b>PDF</b>.</div>
</div>
</div>`;
col.insertAdjacentHTML('beforeend', html);
selectOptionUnitKerjaV1(colCount);
initKategoriSelect2(colCount);
enableAkreFields(colCount);
setKategoriRequired(colCount, false);
colCount++;
}
window.removeCol = function(count){
const el = document.getElementById(`col-${count}`);
if (el) el.remove();
}
function resetCreateForm(){
colCount = 1;
const colAdd = document.getElementById('col_add_fileV2');
if (colAdd) colAdd.innerHTML = '';
if (formCreate) {
formCreate.reset();
$(formCreate).find('select').val(null).trigger('change');
$(formCreate).find('input[type="file"]').val('');
$(formCreate).find('.file-name').addClass('d-none').text('');
}
resetAkreFields(0);
enableAkreFields(0);
}
if (formCreate) {
const select0 = $('#select_id_unit_kerja_0');
if (select0.length) selectOptionUnitKerjaV1(0);
initKategoriSelect2(0);
enableAkreFields(0);
formCreate.addEventListener('submit', (e) => {
e.preventDefault();
const submitBtn = formCreate.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.disabled = true;
if (submitBtn) submitBtn.textContent = 'menyimpan...';
const formData = new FormData(formCreate);
fetch(`/uploadv2`, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]')?.value || ''
},
body: formData
}).then(async(res) => {
const responseData = await res.json();
if(responseData.status){
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: responseData.message || 'Data berhasil disimpan.',
timer: 1500,
showConfirmButton: false
});
const modalInstance = bootstrap.Modal.getInstance(modalCreate);
modalInstance?.hide();
resetCreateForm();
if (activeAkrePath) {
fetchData(activeAkrePath, 1);
} else {
renderAkreSubtree();
}
} else {
throw new Error(responseData.message || 'Terjadi kesalahan saat menyimpan data.');
}
}).catch(err => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
}).finally(() => {
if (submitBtn) submitBtn.disabled = false;
if (submitBtn) submitBtn.textContent = 'Simpan';
});
});
}
// 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');
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 saveBtn = document.getElementById('btnSaveAkreInstrumen');
const rawName = sanitizeAkreFolderName(akreNameInput?.value || '');
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',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('input[name="_token"]')?.value || '',
},
body: JSON.stringify(payload),
})
.then(async (res) => {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const msg = data?.message || (data?.errors ? Object.values(data.errors).flat().join(' ') : '') || 'Terjadi kesalahan.';
throw new Error(msg);
}
if (data?.status === false) {
throw new Error(data?.message || 'Terjadi kesalahan.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data?.message || 'Folder akreditasi berhasil ditambahkan.',
timer: 1500,
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();
configureAkreModal('');
// refresh dropdown instrumen di form upload
return refreshAkreSelects().then(() => {
if (activeAkrePath) {
return fetchData(activeAkrePath, 1);
}
renderAkreSubtree();
return null;
});
})
.catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err?.message || 'Terjadi kesalahan.',
});
})
.finally(() => {
if (saveBtn) saveBtn.disabled = false;
configureAkreModal(getAkreCurrentParentPath());
});
});
}
// Klik tombol "+" di samping folder (tree view) untuk menambah sub-folder sesuai level:
// - depth 1 (Type) -> tambah Segment
// - depth 2 (Type/Segment) -> tambah Item
window.openAddAkreFromFolder = function(folderPath){
const modalEl = document.getElementById('modalAkreInstrumen');
if (!modalEl) return;
configureAkreModal(folderPath);
const instance = bootstrap.Modal.getOrCreateInstance(modalEl);
instance.show();
setTimeout(() => akreNameInput?.focus(), 150);
}
document.addEventListener('change', function(e){
if(e.target.classList.contains('akre-select')){
const id = e.target.id || '';
const idx = id.split('_').pop();
const [typeVal = '', segmentVal = '', itemVal = ''] = (e.target.value || '').split('/');
const typeInput = document.getElementById(`akre_type_${idx}`);
const segmentInput = document.getElementById(`akre_segment_${idx}`);
const itemInput = document.getElementById(`akre_item_${idx}`);
if(typeInput) typeInput.value = typeVal;
if(segmentInput) segmentInput.value = segmentVal;
if(itemInput) itemInput.value = itemVal;
}
});
</script>
@endsection