fixing download admin & menambahkan fitur tambah folder pada dokumen akreditasi
This commit is contained in:
parent
5786369973
commit
f6031e32fe
316
app/Http/Controllers/AkreditasiInstrumenController.php
Normal file
316
app/Http/Controllers/AkreditasiInstrumenController.php
Normal file
@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class AkreditasiInstrumenController extends Controller
|
||||
{
|
||||
private function akreditasiJsonPath(): string
|
||||
{
|
||||
return public_path('json/akreditasi.jff');
|
||||
}
|
||||
|
||||
private function akreditasiLockPath(): string
|
||||
{
|
||||
return public_path('json/akreditasi.jff.lock');
|
||||
}
|
||||
|
||||
private function ensureJsonDirExists(): void
|
||||
{
|
||||
$dir = public_path('json');
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
private function atomicWrite(string $path, string $contents): bool
|
||||
{
|
||||
$dir = dirname($path);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
|
||||
$tmp = $path . '.tmp';
|
||||
$bytes = @file_put_contents($tmp, $contents);
|
||||
if ($bytes === false || $bytes < strlen($contents)) {
|
||||
@unlink($tmp);
|
||||
return false;
|
||||
}
|
||||
|
||||
// best-effort backup
|
||||
if (file_exists($path)) {
|
||||
@copy($path, $path . '.bak');
|
||||
}
|
||||
|
||||
// replace
|
||||
@unlink($path);
|
||||
if (@rename($tmp, $path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fallback copy
|
||||
$copied = @copy($tmp, $path);
|
||||
@unlink($tmp);
|
||||
return (bool) $copied;
|
||||
}
|
||||
|
||||
private function stripUtf8Bom(string $raw): string
|
||||
{
|
||||
// Remove UTF-8 BOM if present
|
||||
if (strncmp($raw, "\xEF\xBB\xBF", 3) === 0) {
|
||||
return substr($raw, 3);
|
||||
}
|
||||
return $raw;
|
||||
}
|
||||
|
||||
private function decodeJsonArray(string $raw, bool $strict = false): ?array
|
||||
{
|
||||
$raw = $this->stripUtf8Bom($raw);
|
||||
$data = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return $strict ? null : [];
|
||||
}
|
||||
return is_array($data) ? $data : ($strict ? null : []);
|
||||
}
|
||||
|
||||
private function tryRepairJsonArray(string $raw): ?array
|
||||
{
|
||||
// Common corruption: trailing commas before ']' or '}'
|
||||
$raw = $this->stripUtf8Bom($raw);
|
||||
$repaired = preg_replace('/,(\s*[}\]])/m', '$1', $raw);
|
||||
if (!is_string($repaired) || $repaired === '') {
|
||||
return null;
|
||||
}
|
||||
$data = json_decode($repaired, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return null;
|
||||
}
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
private function splitList(?string $value): array
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') return [];
|
||||
|
||||
$parts = preg_split('/[,\r\n]+/', $value) ?: [];
|
||||
$out = [];
|
||||
foreach ($parts as $p) {
|
||||
$p = trim($p);
|
||||
if ($p !== '') $out[] = $p;
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
private function ensureTypeExists(array &$data, string $typeName): int
|
||||
{
|
||||
foreach ($data as $i => $t) {
|
||||
if (($t['name'] ?? null) === $typeName) {
|
||||
if (!isset($data[$i]['segment']) || !is_array($data[$i]['segment'])) {
|
||||
$data[$i]['segment'] = [];
|
||||
}
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
$data[] = [
|
||||
'name' => $typeName,
|
||||
'segment' => [],
|
||||
];
|
||||
return count($data) - 1;
|
||||
}
|
||||
|
||||
private function ensureSegmentExists(array &$typeSegments, string $segmentName): int
|
||||
{
|
||||
foreach ($typeSegments as $j => $s) {
|
||||
if (($s['name'] ?? null) === $segmentName) {
|
||||
if (!isset($typeSegments[$j]['turunan']) || !is_array($typeSegments[$j]['turunan'])) {
|
||||
$typeSegments[$j]['turunan'] = [];
|
||||
}
|
||||
return $j;
|
||||
}
|
||||
}
|
||||
$typeSegments[] = [
|
||||
'name' => $segmentName,
|
||||
'turunan' => [],
|
||||
];
|
||||
return count($typeSegments) - 1;
|
||||
}
|
||||
|
||||
private function ensureItemExists(array &$segmentChildren, string $itemName): bool
|
||||
{
|
||||
foreach ($segmentChildren as $c) {
|
||||
if (($c['name'] ?? null) === $itemName) {
|
||||
return false; // already exists
|
||||
}
|
||||
}
|
||||
$segmentChildren[] = ['name' => $itemName];
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read akreditasi.jff (array)
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$path = $this->akreditasiJsonPath();
|
||||
if (!file_exists($path)) {
|
||||
return response()->json([]);
|
||||
}
|
||||
|
||||
$raw = @file_get_contents($path);
|
||||
$data = $this->decodeJsonArray($raw === false ? '[]' : $raw, false) ?? [];
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "folder" structure to akreditasi.jff.
|
||||
*
|
||||
* Payload:
|
||||
* - type (required)
|
||||
* - segment (optional)
|
||||
* - item (optional)
|
||||
*
|
||||
* Behavior:
|
||||
* - type only: creates new top-level type {name, segment: []}
|
||||
* - type + segment: creates segment {name, turunan: []} under type
|
||||
* - type + segment + item: creates item {name} under segment.turunan
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'type' => ['required', 'string', 'max:255'],
|
||||
'segment' => ['nullable', 'string', 'max:255'],
|
||||
'item' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$typeName = trim($validated['type'] ?? '');
|
||||
$segmentRaw = isset($validated['segment']) ? (string) $validated['segment'] : '';
|
||||
$itemRaw = isset($validated['item']) ? (string) $validated['item'] : '';
|
||||
|
||||
$segments = $this->splitList($segmentRaw);
|
||||
$items = $this->splitList($itemRaw);
|
||||
|
||||
if ($typeName === '') {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Type wajib diisi.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
if (count($items) > 0 && count($segments) === 0) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Segment wajib diisi jika ingin menambah item.',
|
||||
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
||||
}
|
||||
|
||||
$this->ensureJsonDirExists();
|
||||
$path = $this->akreditasiJsonPath();
|
||||
$lockPath = $this->akreditasiLockPath();
|
||||
|
||||
// Use separate lock file so Windows can replace the target JSON file safely.
|
||||
$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) {
|
||||
// Try safe repair (e.g., remove trailing commas). If still invalid, abort to avoid data loss.
|
||||
$repaired = $this->tryRepairJsonArray($raw);
|
||||
if ($repaired === null) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak). Perubahan dibatalkan agar data tidak hilang. Silakan restore dari akreditasi.jff.bak lalu coba lagi.',
|
||||
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
$data = $repaired;
|
||||
}
|
||||
}
|
||||
|
||||
$typeIndex = $this->ensureTypeExists($data, $typeName);
|
||||
|
||||
$added = 0;
|
||||
$skipped = 0;
|
||||
|
||||
// Case: type only
|
||||
if (count($segments) === 0) {
|
||||
// type already ensured; treat as skipped if it existed, added if new.
|
||||
// We can't easily detect "new" without extra flag; keep message generic.
|
||||
} elseif (count($items) === 0) {
|
||||
// Case: add one or more segments
|
||||
$typeSegments = $data[$typeIndex]['segment'];
|
||||
foreach ($segments as $segName) {
|
||||
$beforeCount = count($typeSegments);
|
||||
$this->ensureSegmentExists($typeSegments, $segName);
|
||||
$afterCount = count($typeSegments);
|
||||
if ($afterCount > $beforeCount) $added++;
|
||||
else $skipped++;
|
||||
}
|
||||
$data[$typeIndex]['segment'] = $typeSegments;
|
||||
} else {
|
||||
// Case: add one or more items under the given segment(s)
|
||||
// If user provides multiple segments + items, add all items to each segment.
|
||||
$typeSegments = $data[$typeIndex]['segment'];
|
||||
foreach ($segments as $segName) {
|
||||
$segIndex = $this->ensureSegmentExists($typeSegments, $segName);
|
||||
$children = $typeSegments[$segIndex]['turunan'] ?? [];
|
||||
if (!is_array($children)) $children = [];
|
||||
|
||||
foreach ($items as $itemName) {
|
||||
$didAdd = $this->ensureItemExists($children, $itemName);
|
||||
if ($didAdd) $added++;
|
||||
else $skipped++;
|
||||
}
|
||||
$typeSegments[$segIndex]['turunan'] = $children;
|
||||
}
|
||||
$data[$typeIndex]['segment'] = $typeSegments;
|
||||
}
|
||||
|
||||
// write back
|
||||
$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' => ($added > 0 || $skipped > 0)
|
||||
? "Selesai. Ditambahkan: {$added}, dilewati (sudah ada): {$skipped}."
|
||||
: 'Selesai.',
|
||||
'data' => $data,
|
||||
]);
|
||||
} finally {
|
||||
@flock($lockFp, LOCK_UN);
|
||||
@fclose($lockFp);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
3215
public/json/akreditasi.jff.bak
Normal file
3215
public/json/akreditasi.jff.bak
Normal file
File diff suppressed because it is too large
Load Diff
0
public/json/akreditasi.jff.lock
Normal file
0
public/json/akreditasi.jff.lock
Normal file
@ -36,6 +36,26 @@
|
||||
.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">
|
||||
@ -65,6 +85,17 @@
|
||||
0 dipilih
|
||||
</span>
|
||||
</div>
|
||||
@if(Auth::guard('admin')->check())
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modalAkreInstrumen"
|
||||
>
|
||||
<i class="ti ti-folder-plus me-1"></i>
|
||||
Tambah Folder Akreditasi
|
||||
</button>
|
||||
@endif
|
||||
<!-- Tambah Dokumen -->
|
||||
@if(!Auth::guard('admin')->check())
|
||||
<button
|
||||
@ -137,12 +168,54 @@
|
||||
|
||||
@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">Tambah Folder/Instrumen 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">
|
||||
<div class="alert alert-info small mb-3">
|
||||
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="col-12">
|
||||
<label class="form-label fw-semibold">Type <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" name="type" id="akreTypeInput" placeholder="Contoh: Tata Kelola Rumah Sakit (TKRS)" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Segment</label>
|
||||
<textarea class="form-control" name="segment" id="akreSegmentInput" rows="3" placeholder="Contoh: TKRS 7, TKRS 8, TKRS 9"></textarea>
|
||||
<div class="form-text text-muted">Opsional. Bisa isi banyak (pisahkan dengan koma atau baris baru).</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold">Item</label>
|
||||
<textarea class="form-control" name="item" id="akreItemInput" rows="3" placeholder="Contoh: TKRS 7.a, TKRS 7.b, TKRS 7.c" disabled></textarea>
|
||||
<div class="form-text text-muted">Aktif jika segment diisi. Bisa isi banyak (pisahkan dengan koma atau baris baru).</div>
|
||||
</div>
|
||||
</div>
|
||||
</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">Simpan</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
// === STATE MANAGEMENT ===
|
||||
let currentData = [];
|
||||
let selectedIds = [];
|
||||
let expandedFolders = new Set();
|
||||
let searchTimer;
|
||||
const isAdminUser = @json(Auth::guard('admin')->check());
|
||||
const btn = document.getElementById('btnDownloadMultiple');
|
||||
const tableState = { page: 1, pageSize: 10, lastPage: 1, total: 0, from: 0, to: 0, search: '' };
|
||||
const paginationEl = document.getElementById('paginationControls');
|
||||
@ -150,8 +223,20 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchData();
|
||||
initEventListeners();
|
||||
initTooltips();
|
||||
});
|
||||
|
||||
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() {
|
||||
// Search dengan Debounce
|
||||
@ -244,6 +329,7 @@
|
||||
expandAllFolders(tree, '');
|
||||
}
|
||||
renderTree(tbody, tree, '', 0);
|
||||
initTooltips();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,6 +404,8 @@
|
||||
const folderPath = basePath ? `${basePath}/${folder}` : folder;
|
||||
const isExpanded = expandedFolders.has(folderPath);
|
||||
const folderNode = node.folders[folder];
|
||||
const depth = folderPath.split('/').filter(Boolean).length;
|
||||
const canAddChild = isAdminUser && (depth === 1 || depth === 2);
|
||||
|
||||
let lines = '';
|
||||
for (let i = 0; i < level; i++) {
|
||||
@ -334,6 +422,17 @@
|
||||
</span>
|
||||
<i class="ti ti-folder-filled folder-icon me-2"></i>
|
||||
<strong class="text-dark">${folder}</strong>
|
||||
${canAddChild ? `
|
||||
<span class="folder-actions">
|
||||
<button type="button"
|
||||
class="badge bg-primary"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-title="${depth === 1 ? 'Tambah Segment' : 'Tambah Item'}"
|
||||
onclick="event.stopPropagation(); openAddAkreFromFolder('${folderPath.replace(/'/g, "\\\\'")}')">
|
||||
<i class="ti ti-plus me-1"></i>${depth === 1 ? 'Segment' : 'Item'}
|
||||
</button>
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="badge bg-light text-muted border-0">FOLDER</span></td>
|
||||
@ -603,6 +702,29 @@
|
||||
});
|
||||
}
|
||||
|
||||
function refreshAkreData(){
|
||||
akreLoaded = false;
|
||||
akreData = [];
|
||||
akreFlat = [];
|
||||
return loadAkreData();
|
||||
}
|
||||
|
||||
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 => {
|
||||
@ -967,6 +1089,134 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Admin: tambah folder/instrumen akreditasi (disimpan di public/json/akreditasi.jff)
|
||||
const formAkreInstrumen = document.getElementById('formAkreInstrumen');
|
||||
const akreSegmentInput = document.getElementById('akreSegmentInput');
|
||||
const akreItemInput = document.getElementById('akreItemInput');
|
||||
|
||||
if (akreSegmentInput && akreItemInput) {
|
||||
const updateItemEnabled = () => {
|
||||
const hasSegment = String(akreSegmentInput.value || '').trim().length > 0;
|
||||
akreItemInput.disabled = !hasSegment;
|
||||
if (!hasSegment) akreItemInput.value = '';
|
||||
};
|
||||
akreSegmentInput.addEventListener('input', updateItemEnabled);
|
||||
updateItemEnabled();
|
||||
}
|
||||
|
||||
if (formAkreInstrumen) {
|
||||
formAkreInstrumen.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const btn = document.getElementById('btnSaveAkreInstrumen');
|
||||
if (btn) btn.disabled = true;
|
||||
if (btn) btn.textContent = 'menyimpan...';
|
||||
|
||||
const payload = {
|
||||
type: String(document.getElementById('akreTypeInput')?.value || '').trim(),
|
||||
segment: String(document.getElementById('akreSegmentInput')?.value || '').trim(),
|
||||
item: String(document.getElementById('akreItemInput')?.value || '').trim(),
|
||||
};
|
||||
if (!payload.segment) delete payload.segment;
|
||||
if (!payload.item) delete payload.item;
|
||||
|
||||
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 modalEl = document.getElementById('modalAkreInstrumen');
|
||||
const modalInstance = modalEl ? bootstrap.Modal.getInstance(modalEl) : null;
|
||||
modalInstance?.hide();
|
||||
|
||||
formAkreInstrumen.reset();
|
||||
if (akreItemInput) akreItemInput.disabled = true;
|
||||
|
||||
// refresh dropdown instrumen di form upload
|
||||
return refreshAkreSelects();
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire({
|
||||
icon: 'error',
|
||||
title: 'Gagal',
|
||||
text: err?.message || 'Terjadi kesalahan.',
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
if (btn) btn.disabled = false;
|
||||
if (btn) btn.textContent = 'Simpan';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
const typeInput = document.getElementById('akreTypeInput');
|
||||
const segmentInput = document.getElementById('akreSegmentInput');
|
||||
const itemInput = document.getElementById('akreItemInput');
|
||||
if (!typeInput || !segmentInput || !itemInput) return;
|
||||
|
||||
const parts = String(folderPath || '').split('/').filter(Boolean);
|
||||
const depth = parts.length;
|
||||
|
||||
// reset state
|
||||
typeInput.readOnly = false;
|
||||
segmentInput.readOnly = false;
|
||||
itemInput.disabled = true;
|
||||
itemInput.readOnly = false;
|
||||
|
||||
if (depth >= 1) {
|
||||
typeInput.value = parts[0] || '';
|
||||
typeInput.readOnly = true;
|
||||
} else {
|
||||
typeInput.value = '';
|
||||
}
|
||||
|
||||
if (depth >= 2) {
|
||||
segmentInput.value = parts[1] || '';
|
||||
segmentInput.readOnly = true;
|
||||
itemInput.disabled = false;
|
||||
itemInput.value = '';
|
||||
} else {
|
||||
segmentInput.value = '';
|
||||
itemInput.value = '';
|
||||
}
|
||||
|
||||
const instance = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
instance.show();
|
||||
|
||||
// fokus ke field yang tepat
|
||||
if (depth >= 2) {
|
||||
itemInput.focus();
|
||||
} else {
|
||||
segmentInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(e){
|
||||
if(e.target.classList.contains('akre-select')){
|
||||
const id = e.target.id || '';
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AksesFileController;
|
||||
use App\Http\Controllers\AkreditasiInstrumenController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\MasterKategoriController;
|
||||
@ -18,6 +19,10 @@ Route::middleware(['auth:admin,web'])->group(function(){
|
||||
Route::get('/datatable-umum', [DashboardController::class, 'datatableDataUmum']);
|
||||
Route::get('/data-akreditasi', [DashboardController::class, 'dataAkreditasi']);
|
||||
Route::get('/datatable-akreditasi', [DashboardController::class, 'dataTableAkreditasi']);
|
||||
|
||||
// Kelola "folder/instrumen" akreditasi via file: public/json/akreditasi.jff (tanpa database)
|
||||
Route::get('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'index']);
|
||||
Route::post('/akreditasi/instrumen', [AkreditasiInstrumenController::class, 'store']);
|
||||
Route::get('/download-excel/data-umum', [DashboardController::class, 'downloadDataUmumExcel']);
|
||||
Route::post('/uploadv2', [DashboardController::class, 'storeVersion2']);
|
||||
Route::get('/file-preview/{id}', [DashboardController::class, 'dataPdf']);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user