This commit is contained in:
JokoPrasetio 2026-01-30 10:26:54 +07:00
parent 20d88e8823
commit 73d48d5499
6 changed files with 348 additions and 28 deletions

View File

@ -453,14 +453,45 @@ class DashboardController extends Controller
public function downloadDataMultiple(){
try {
$rows = request('ids', []); // [[unit_id=>u, sub_unit_id=>s], ...]
$rows = request('ids', []); // [[file_directory_id=>x] | [sub_unit_id=>y] | "file_directory_id", ...]
if (empty($rows)) {
return response()->json(['message' => 'Tidak ada data'], 422);
}
$paths = [];
foreach ($rows as $r) {
if(!empty($r['sub_unit_id'])){
$files = FileDirectory::where('id_sub_unit_kerja', $r['sub_unit_id'])->where('statusenabled', true)->where('status_action', 'approved')->pluck('file');
if (is_string($r) || is_numeric($r)) {
$file = FileDirectory::where('file_directory_id', $r)
->where('statusenabled', true)
->where('status_action', 'approved')
->value('file');
if ($file) {
$paths[] = $file;
}
continue;
}
if (!empty($r['file_directory_id'] ?? null)) {
$file = FileDirectory::where('file_directory_id', $r['file_directory_id'])
->where('statusenabled', true)
->where('status_action', 'approved')
->value('file');
if ($file) {
$paths[] = $file;
}
continue;
}
if (!empty($r['file'] ?? null)) {
$paths[] = $r['file'];
continue;
}
$subUnitId = $r['sub_unit_id'] ?? $r['id_sub_unit_kerja'] ?? null;
if (!empty($subUnitId)) {
$files = FileDirectory::where('id_sub_unit_kerja', $subUnitId)
->where('statusenabled', true)
->where('status_action', 'approved')
->pluck('file');
$paths = array_merge($paths, $files->toArray());
}
@ -632,13 +663,13 @@ class DashboardController extends Controller
$status = null;
if(auth()->user()->masterPersetujuan){
// if(auth()->user()->masterPersetujuan){
// $unitPegawaiIds = auth()->user()->masterPersetujuan->details->pluck('unit_pegawai_id')->unique()->toArray();
// $status = in_array($id_unit_kerja, $unitPegawaiIds)
// ? 'approved'
// : null;
$status = $isAtasan ? 'approved' : null;
}
// }
$payload = [
'id_unit_kerja' => $id_unit_kerja,
@ -653,7 +684,6 @@ class DashboardController extends Controller
'action_by' => $status && $status === "approved" ? auth()->user()->objectpegawaifk : null,
'action_at' => $status && $status === "approved" ? now() : null
];
$imageName = $uploadedFile->getClientOriginalName();
$path = "{$nama_unit_kerja}/{$nama_sub_unit_kerja}/{$nama_kategori}";
$uploadedFile->storeAs($path, $imageName, 'file_directory');
@ -669,8 +699,8 @@ class DashboardController extends Controller
'text_notifikasi' => "Dokumen {$docNumber} memerlukan persetujuan. Diunggah oleh {$uploaderName}.",
'url' => '/pending-file',
'is_read' => false,
// 'pegawai_id' => $mapping?->objectatasanlangsungfk,
'pegawai_id' => 23521,
'pegawai_id' => $mapping?->objectatasanlangsungfk,
// 'pegawai_id' => 23521,
];
Notifkasi::create($payloadNotification);
@ -974,15 +1004,17 @@ class DashboardController extends Controller
// ->orWhereNull('status_action');
// })->whereIn('id_unit_kerja', $authUnit)->orderBy('entry_at','desc');
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
// ->where('objectatasanlangsungfk', auth()->user()->dataUser->id)
->where('objectatasanlangsungfk', 22924)
->where('objectatasanlangsungfk', auth()->user()->dataUser->id)
// ->where('objectatasanlangsungfk', 22924)
->get(['objectpegawaifk']);
$objectpegawaifk = $mapping->pluck('objectpegawaifk')
->values()
->all();
$keyword = request('keyword');
$query = FileDirectory::where('statusenabled', true)
->where('status_action', '!=', 'approved')
->where(function($qsa){
$qsa->where('status_action', '!=', 'approved')->orWhereNull('status_action');
})
->whereIn('pegawai_id_entry', $objectpegawaifk)
->when($keyword, function ($q) use ($keyword) {
$q->where(function ($sub) use ($keyword) {
@ -1412,8 +1444,8 @@ class DashboardController extends Controller
'text_notifikasi' => "Dokumen {$docNumber} telah direvisi dan memerlukan persetujuan ulang. ". "Direvisi oleh {$uploaderName}.",
'url' => '/pending-file',
'is_read' => false,
// 'pegawai_id' => $mapping?->objectatasanlangsungfk,
'pegawai_id' => 23521,
'pegawai_id' => $mapping?->objectatasanlangsungfk,
// 'pegawai_id' => 23521,
];
Notifkasi::create($payloadNotification);

View File

@ -45,6 +45,13 @@ document.addEventListener('DOMContentLoaded', () => {
if (status === 'revised') return '<span class="badge bg-info">Revised</span>';
return '<span class="badge bg-warning text-dark">Pending</span>';
}
function aksesBadge(akses){
if (akses){
return '<span class="badge bg-success">Umum</span>';
} else{
return '<span class="badge bg-primary">Internal Unit</span>';
}
}
function safeText(val){
return val ? String(val) : '-';
@ -67,6 +74,7 @@ document.addEventListener('DOMContentLoaded', () => {
<td>${item?.status_action !== "rejected" ? aksi : ''}</td>
<td>${safeText(item.no_dokumen)}</td>
<td>${statusBadge(item?.status_action)}</td>
<td>${aksesBadge(item?.permission_file)}</td>
<td><a href="#" class="file-link"
data-file="${item.file}"
data-fileName="${item.fileName}"
@ -127,7 +135,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (pageData.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted py-4">
<td colspan="9" class="text-center text-muted py-4">
Tidak ada data
</td>
</tr>

View File

@ -120,9 +120,39 @@
<div class="card-body p-3">
<div class="tab-content">
<div class="tab-pane fade show active">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<h4 class="mb-0">Data Unit</h4>
<button type="button" class="btn btn-success ms-md-auto" data-bs-target="#modalCreateFile" data-bs-toggle="modal">Tambah File</button>
<div class="d-flex justify-content-between align-items-start mb-3">
<h4 class="mb-0">Data Umum</h4>
<div class="d-flex align-items-start gap-3">
<!-- DOWNLOAD + COUNT -->
<div class="d-flex flex-column align-items-start">
<button
type="button"
class="btn btn-primary btn-sm"
id="btnDownloadMultiple"
disabled
>
<i class="ti ti-download me-1"></i>
Download Terpilih
</button>
<span
id="selectedCount"
class="small text-muted mt-1"
>
0 dipilih
</span>
</div>
<!-- TAMBAH FILE -->
<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 File
</button>
</div>
</div>
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="input-group input-group-sm flex-grow-1">
@ -150,6 +180,9 @@
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead>
<tr>
<th class="text-center" style="width: 36px;">
<input type="checkbox" id="checkAllRows" class="form-check-input">
</th>
<th>Nomor Surat</th>
<th>File</th>
<th>Kategori</th>
@ -185,6 +218,10 @@
const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary');
const pageSizeSelect = document.getElementById('tablePageSize');
const downloadBtn = document.getElementById('btnDownloadMultiple');
const selectedCountEl = document.getElementById('selectedCount');
const checkAllEl = document.getElementById('checkAllRows');
const selectedIds = new Set();
if(pageSizeSelect){
const initialSize = parseInt(pageSizeSelect.value);
@ -232,8 +269,15 @@
statusLabel = 'Internal Unit';
statusClass = 'bg-secondary';
}
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
return `
<tr>
<td class="text-center">
<input type="checkbox"
class="form-check-input row-check"
data-id="${item.file_directory_id}"
${checked}>
</td>
<td class="text-nowrap">${item.no_dokumen || '-'}</td>
<td class="file-cell">
<div style="display:flex; flex-direction:column; gap:4px;">
@ -368,7 +412,7 @@
if(pageData.length === 0){
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center text-muted py-4">
<td colspan="7" class="text-center text-muted py-4">
Tidak ada data yang cocok
</td>
</tr>
@ -384,6 +428,8 @@
}
renderPagination(tableState.lastPage || 1);
syncCheckAllState();
updateSelectedCount();
}
function debouncedTableSearch(value){
@ -429,6 +475,99 @@
}
fetchData()
function updateSelectedCount(){
if(!selectedCountEl) return;
selectedCountEl.textContent = `${selectedIds.size} dipilih`;
if(downloadBtn){
downloadBtn.disabled = selectedIds.size === 0;
}
}
function syncCheckAllState(){
if(!checkAllEl) return;
const pageIds = (tableState.data || []).map(item => String(item.file_directory_id));
if(pageIds.length === 0){
checkAllEl.checked = false;
checkAllEl.indeterminate = false;
return;
}
const selectedOnPage = pageIds.filter(id => selectedIds.has(id)).length;
checkAllEl.checked = selectedOnPage === pageIds.length;
checkAllEl.indeterminate = selectedOnPage > 0 && selectedOnPage < pageIds.length;
}
if(checkAllEl){
checkAllEl.addEventListener('change', function(){
const pageIds = (tableState.data || []).map(item => String(item.file_directory_id));
if(this.checked){
pageIds.forEach(id => selectedIds.add(id));
}else{
pageIds.forEach(id => selectedIds.delete(id));
}
renderTable();
});
}
if(tbody){
tbody.addEventListener('change', function(e){
const checkbox = e.target.closest('.row-check');
if(!checkbox) return;
const id = String(checkbox.getAttribute('data-id'));
if(checkbox.checked){
selectedIds.add(id);
}else{
selectedIds.delete(id);
}
syncCheckAllState();
updateSelectedCount();
});
}
if(downloadBtn){
downloadBtn.addEventListener('click', function(){
if(selectedIds.size === 0){
return;
}
const payload = { ids: Array.from(selectedIds) };
downloadBtn.disabled = true;
downloadBtn.textContent = 'Menyiapkan...';
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);
})
.catch(err => {
Swal.fire({ icon: 'error', title: 'Gagal', text: err.message || 'Gagal download file' });
})
.finally(() => {
downloadBtn.disabled = selectedIds.size === 0;
downloadBtn.textContent = 'Download Terpilih';
});
});
}
document.addEventListener('click', function (e) {
const btn = e.target.closest('.folder-prefill');
if (!btn) return;

View File

@ -129,7 +129,7 @@
<li class="nav-item">
<a class="nav-link d-flex align-items-center gap-2" data-bs-toggle="tab" href="#tab-data-recap" role="tab" aria-controls="tab-data-recap" aria-selected="false">
<i class="ti ti-chart-bar"></i>
<span>Data Recap</span>
<span>Data Rekap</span>
</a>
</li>
</ul>
@ -138,9 +138,39 @@
<div class="card-body p-3">
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-data-unit" role="tabpanel">
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="d-flex justify-content-between align-items-start mb-3">
<h4 class="mb-0">Data Unit</h4>
<button type="button" class="btn btn-success ms-md-auto" data-bs-target="#modalCreateFile" data-bs-toggle="modal">Tambah File</button>
<div class="d-flex align-items-start gap-3">
<!-- DOWNLOAD + COUNT -->
<div class="d-flex flex-column align-items-start">
<button
type="button"
class="btn btn-primary btn-sm"
id="btnDownloadMultiple"
disabled
>
<i class="ti ti-download me-1"></i>
Download Terpilih
</button>
<span
id="selectedCount"
class="small text-muted mt-1"
>
0 dipilih
</span>
</div>
<!-- TAMBAH FILE -->
<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 File
</button>
</div>
</div>
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
<div class="input-group input-group-sm flex-grow-1">
@ -162,12 +192,16 @@
<option value="100">100</option>
</select>
</div>
<div class="small text-muted ms-md-auto" id="tableSummary">Memuat data...</div>
</div>
<div class="table-responsive" style="max-height: 70vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0 table-fixed" id="lastUpdatedTable">
<thead>
<tr>
<th class="text-center" style="width: 36px;">
<input type="checkbox" id="checkAllRows" class="form-check-input">
</th>
<th>Nomor Surat</th>
<th>File</th>
<th>Kategori</th>
@ -206,6 +240,10 @@
const paginationEl = document.getElementById('paginationControls');
const summaryEl = document.getElementById('tableSummary');
const pageSizeSelect = document.getElementById('tablePageSize');
const downloadBtn = document.getElementById('btnDownloadMultiple');
const selectedCountEl = document.getElementById('selectedCount');
const checkAllEl = document.getElementById('checkAllRows');
const selectedIds = new Set();
if(pageSizeSelect){
const initialSize = parseInt(pageSizeSelect.value);
@ -253,8 +291,15 @@
statusLabel = 'Internal Unit';
statusClass = 'bg-secondary';
}
const checked = selectedIds.has(String(item.file_directory_id)) ? 'checked' : '';
return `
<tr>
<td class="text-center">
<input type="checkbox"
class="form-check-input row-check"
data-id="${item.file_directory_id}"
${checked}>
</td>
<td class="text-nowrap">${item.no_dokumen || '-'}</td>
<td class="file-cell">
<div class="file-title">
@ -335,7 +380,7 @@
if(pageData.length === 0){
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center text-muted py-4">
<td colspan="7" class="text-center text-muted py-4">
Tidak ada data yang cocok
</td>
</tr>
@ -351,6 +396,8 @@
}
renderPagination(tableState.lastPage || 1);
syncCheckAllState();
updateSelectedCount();
}
function debouncedTableSearch(value){
@ -396,6 +443,99 @@
}
fetchData()
function updateSelectedCount(){
if(!selectedCountEl) return;
selectedCountEl.textContent = `${selectedIds.size} dipilih`;
if(downloadBtn){
downloadBtn.disabled = selectedIds.size === 0;
}
}
function syncCheckAllState(){
if(!checkAllEl) return;
const pageIds = (tableState.data || []).map(item => String(item.file_directory_id));
if(pageIds.length === 0){
checkAllEl.checked = false;
checkAllEl.indeterminate = false;
return;
}
const selectedOnPage = pageIds.filter(id => selectedIds.has(id)).length;
checkAllEl.checked = selectedOnPage === pageIds.length;
checkAllEl.indeterminate = selectedOnPage > 0 && selectedOnPage < pageIds.length;
}
if(checkAllEl){
checkAllEl.addEventListener('change', function(){
const pageIds = (tableState.data || []).map(item => String(item.file_directory_id));
if(this.checked){
pageIds.forEach(id => selectedIds.add(id));
}else{
pageIds.forEach(id => selectedIds.delete(id));
}
renderTable();
});
}
if(tbody){
tbody.addEventListener('change', function(e){
const checkbox = e.target.closest('.row-check');
if(!checkbox) return;
const id = String(checkbox.getAttribute('data-id'));
if(checkbox.checked){
selectedIds.add(id);
}else{
selectedIds.delete(id);
}
syncCheckAllState();
updateSelectedCount();
});
}
if(downloadBtn){
downloadBtn.addEventListener('click', function(){
if(selectedIds.size === 0){
return;
}
const payload = { ids: Array.from(selectedIds) };
downloadBtn.disabled = true;
downloadBtn.textContent = 'Menyiapkan...';
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);
})
.catch(err => {
Swal.fire({ icon: 'error', title: 'Gagal', text: err.message || 'Gagal download file' });
})
.finally(() => {
downloadBtn.disabled = selectedIds.size === 0;
downloadBtn.textContent = 'Download Terpilih';
});
});
}
document.addEventListener('click', function (e) {
const btn = e.target.closest('.folder-prefill');
if (!btn) return;

View File

@ -57,7 +57,7 @@
@php
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)->where('objectatasanlangsungfk', auth()->user()->objectpegawaifk)->exists();
@endphp
@if($isAtasan || auth()->user()->objectpegawaifk === 23521)
@if($isAtasan)
<li class="sidebar-item">
<a class="sidebar-link d-flex align-items-center justify-content-between"
href="{{ url('/pending-file') }}" aria-expanded="false">
@ -116,11 +116,11 @@
</a>
<ul class="collapse sidebar-submenu {{ $openMaster ? 'show' : '' }}" id="menu-master">
<li class="sidebar-item">
{{-- <li class="sidebar-item">
<a href="{{ url('/akses') }}" class="sidebar-link">
<span class="hide-menu">Akses</span>
</a>
</li>
</li> --}}
<li class="sidebar-item">
<a href="{{ url('/master-kategori') }}" class="sidebar-link">
@ -128,11 +128,11 @@
</a>
</li>
<li class="sidebar-item">
{{-- <li class="sidebar-item">
<a href="{{ url('/master-persetujuan') }}" class="sidebar-link">
<span class="hide-menu">Persetujuan</span>
</a>
</li>
</li> --}}
</ul>
</li>
@endif
@ -198,7 +198,7 @@
countData();
setInterval(countData, 60000);
// setInterval(countData, 60000);
const masterCollapse = document.getElementById('menu-master');
const masterItem = masterCollapse

View File

@ -47,6 +47,7 @@
<th>Aksi</th>
<th>No. Dokumen</th>
<th>Status</th>
<th>Akses</th>
<th>File</th>
<th>Folder</th>
<th>Unit / Sub Unit</th>