done approval add mutu & turt -> review

This commit is contained in:
JokoPrasetio 2026-06-19 17:26:33 +07:00
parent 353efd16b0
commit f8c3816ca7
7 changed files with 309 additions and 19 deletions

View File

@ -1991,7 +1991,7 @@ class DashboardController extends Controller
}
$paginated = $query->paginate($perPage);
$data = $paginated->getCollection()->map(function($item) use($objectpegawaifk){
$data = $paginated->getCollection()->map(function($item) use($objectpegawaifk, $pegawaiId){
$dataSlice = array_values(array_filter(explode('/', $item->file)));
return [
'file_directory_id' => $item->file_directory_id,
@ -3369,6 +3369,9 @@ class DashboardController extends Controller
'no_dokumen',
'file',
'entry_at',
'tanggal_terbit',
'permission_file',
'pegawai_id_entry',
'id_unit_kerja',
'is_akre',
])

View File

@ -19,6 +19,9 @@ document.addEventListener('DOMContentLoaded', () => {
const selectedCountEl = document.getElementById('selectedCount');
const bulkActionsEl = document.getElementById('pengajuanBulkActions');
const deleteDataEl = document.getElementById('deleteData');
const pendingPersetujuanTabBadge = document.getElementById('pendingPersetujuanTabBadge');
const pendingPersetujuanNotice = document.getElementById('pendingPersetujuanNotice');
const pendingPersetujuanNoticeCount = document.getElementById('pendingPersetujuanNoticeCount');
const formCreate = document.getElementById('formFile');
const modalCreate = document.getElementById('modalCreateFile');
let colCount = 1;
@ -29,6 +32,7 @@ document.addEventListener('DOMContentLoaded', () => {
const selectedIds = new Set();
const isKomiteMutu = !!window.isKomiteMutu;
const isTurt = !!window.isTurt;
const isAtasan = !!window.isAtasan;
let editFromQueryId = new URLSearchParams(window.location.search).get('edit');
if (pageSizeSelect) {
@ -493,6 +497,41 @@ document.addEventListener('DOMContentLoaded', () => {
updateSelectionUI();
}
function updatePendingPersetujuanIndicators(total){
const count = Number(total || 0);
if (pendingPersetujuanTabBadge) {
pendingPersetujuanTabBadge.textContent = String(count);
pendingPersetujuanTabBadge.classList.toggle('d-none', count <= 0);
}
if (pendingPersetujuanNotice && pendingPersetujuanNoticeCount) {
pendingPersetujuanNoticeCount.textContent = String(count);
pendingPersetujuanNotice.classList.toggle('d-none', count <= 0);
pendingPersetujuanNotice.classList.toggle('d-flex', count > 0);
}
}
function fetchPendingPersetujuanCount(){
if (!(isKomiteMutu || isTurt || isAtasan)) return;
const params = new URLSearchParams({
page: 1,
per_page: 1,
keyword: '',
start_date: '',
end_date: '',
mode: 'persetujuan'
});
fetch(`/datatable/pengajuan-file?${params.toString()}`)
.then(res => res.json())
.then(data => {
updatePendingPersetujuanIndicators(data?.pagination?.total || 0);
})
.catch(() => {
updatePendingPersetujuanIndicators(0);
});
}
if (tabsEl) {
tabsEl.addEventListener('click', (e) => {
const btn = e.target.closest('[data-mode]');
@ -584,6 +623,7 @@ document.addEventListener('DOMContentLoaded', () => {
activeState.lastPage = data?.pagination?.last_page || 1;
activeState.total = data?.pagination?.total || 0;
renderTable();
fetchPendingPersetujuanCount();
})
.catch(err => {
console.error(err);

View File

@ -56,6 +56,49 @@
line-height: 1.2;
border-radius: 999px;
}
#lastUpdatedTable {
table-layout: fixed;
}
#lastUpdatedTable td,
#lastUpdatedTable th {
vertical-align: middle;
}
.file-row-content {
display: flex;
align-items: flex-start;
gap: 8px;
min-width: 0;
width: 100%;
}
.file-text-wrap {
min-width: 0;
flex: 1 1 auto;
}
.file-name-trigger {
display: block;
width: 100%;
overflow: hidden;
line-height: 1.3;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.file-name-trigger strong {
display: block;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.file-meta-path {
display: none;
}
</style>
@section('body_main')
<div class="row">
@ -167,6 +210,7 @@
</div>
@include('dataUnit.modal.create')
@include('dataUnit.modal.view')
@if(Auth::guard('admin')->check())
<div class="modal fade" id="modalAkreInstrumen" tabindex="-1" aria-hidden="true">
@ -214,6 +258,7 @@
let currentData = [];
let selectedIds = [];
let expandedFolders = new Set();
let currentPreviewId = null;
let searchTimer;
const isAdminUser = @json(Auth::guard('admin')->check());
const btn = document.getElementById('btnDownloadMultiple');
@ -349,6 +394,16 @@
</td>
<td>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-secondary btn-preview-file"
data-id="${item.file_directory_id}"
data-name="${escapeAttribute(item.nama_dokumen || fileName)}"
data-no="${escapeAttribute(item.no_dokumen || '-')}"
data-date="${escapeAttribute(formatPreviewDate(item.tanggal_terbit || item.entry_at))}"
data-permission="${item.permission_file === true ? '1' : '0'}"
title="Preview">
<i class="ti ti-eye"></i>
</button>
<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>
@ -356,9 +411,20 @@
</div>
</td>
<td>
<div class="d-flex flex-column">
<strong>${item.nama_dokumen}</strong>
<small class="text-muted" style="font-size: 10px;">Path: ${item.file}</small>
<div class="file-row-content">
<i class="ti ti-file-type-pdf file-icon text-danger"></i>
<div class="d-flex flex-column file-text-wrap">
<button type="button"
class="btn btn-link btn-sm p-0 text-start text-decoration-none btn-preview-file file-name-trigger"
data-id="${item.file_directory_id}"
data-name="${escapeAttribute(item.nama_dokumen || fileName)}"
data-no="${escapeAttribute(item.no_dokumen || '-')}"
data-date="${escapeAttribute(formatPreviewDate(item.tanggal_terbit || item.entry_at))}"
data-permission="${item.permission_file === true ? '1' : '0'}">
<strong>${escapeHtml(item.nama_dokumen || fileName)}</strong>
</button>
<small class="text-muted file-meta-path" style="font-size: 10px;">Path: ${item.file}</small>
</div>
</div>
</td>
<td><span class="badge bg-light text-primary border">${typeDok}</span></td>
@ -495,18 +561,40 @@
value="${item.file_directory_id}" ${isChecked} onchange="handleRowCheck(this)">
</td>
<td>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-outline-secondary btn-preview-file"
data-id="${item.file_directory_id}"
data-name="${escapeAttribute(item.nama_dokumen || fileName)}"
data-no="${escapeAttribute(item.no_dokumen || '-')}"
data-date="${escapeAttribute(formatPreviewDate(item.tanggal_terbit || item.entry_at))}"
data-permission="${item.permission_file === true ? '1' : '0'}"
title="Preview">
<i class="ti ti-eye"></i>
</button>
<a href="/file-download/${item.file_directory_id}" target="_blank"
class="btn btn-sm btn-primary text-white border">
<i class="ti ti-download"></i>
</a>
</div>
</td>
<td>
<div class="indent-container">
${lines}
<span style="width: 24px;"></span> <i class="ti ti-file-text file-icon me-2"></i>
<div class="d-flex flex-column">
<span class="text-dark fw-medium">${item.nama_dokumen}</span>
<small class="text-muted" style="font-size: 10px;">${fileName}</small>
<span style="width: 24px;"></span>
<div class="file-row-content">
<i class="ti ti-file-text file-icon text-danger"></i>
<div class="d-flex flex-column file-text-wrap">
<button type="button"
class="btn btn-link btn-sm p-0 text-start text-decoration-none text-dark fw-medium btn-preview-file file-name-trigger"
data-id="${item.file_directory_id}"
data-name="${escapeAttribute(item.nama_dokumen || fileName)}"
data-no="${escapeAttribute(item.no_dokumen || '-')}"
data-date="${escapeAttribute(formatPreviewDate(item.tanggal_terbit || item.entry_at))}"
data-permission="${item.permission_file === true ? '1' : '0'}">
${escapeHtml(item.nama_dokumen || fileName)}
</button>
</div>
</div>
</div>
</td>
@ -651,6 +739,18 @@
}
document.addEventListener('click', function(e){
const previewTrigger = e.target.closest('.btn-preview-file');
if (previewTrigger) {
openDocumentPreview({
id: previewTrigger.getAttribute('data-id'),
name: previewTrigger.getAttribute('data-name'),
noDokumen: previewTrigger.getAttribute('data-no'),
tanggalTerbit: previewTrigger.getAttribute('data-date'),
permissionFile: previewTrigger.getAttribute('data-permission')
});
return;
}
const toggle = e.target.closest('.folder-toggle');
if (!toggle) return;
const folderPath = toggle.getAttribute('data-folder');
@ -671,11 +771,84 @@
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} `
+ `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
function formatPreviewDate(value) {
if (!value) return '-';
const d = new Date(value);
if (isNaN(d)) return value;
const pad = n => String(n).padStart(2, '0');
return `${pad(d.getDate())}/${pad(d.getMonth()+1)}/${d.getFullYear()}`;
}
function isPublic(permissionVal){
return permissionVal === true || permissionVal === 'true' || permissionVal === 1 || permissionVal === '1';
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function escapeAttribute(value) {
return escapeHtml(value);
}
function openDocumentPreview({ id, name, noDokumen, tanggalTerbit, permissionFile }) {
currentPreviewId = id;
const titleEl = document.getElementById('confirm_preview_file');
if (titleEl) titleEl.textContent = name || '-';
const noEl = document.getElementById('confirm-upload-dokumen');
if (noEl) noEl.textContent = noDokumen || '-';
const tglEl = document.getElementById('confirm-time-dokumen');
if (tglEl) tglEl.textContent = tanggalTerbit || '-';
const permEl = document.getElementById('confirm-permission');
if (permEl) {
const publicDoc = isPublic(permissionFile);
permEl.textContent = publicDoc ? 'Umum' : 'Internal Unit';
permEl.className = 'badge ' + (publicDoc ? 'bg-success' : 'bg-secondary');
}
const downloadBtn = document.getElementById('btn-download');
if (downloadBtn) {
downloadBtn.setAttribute('href', `/file-download/${id}`);
}
const deleteWrapper = document.getElementById('deleteData');
if (deleteWrapper) {
deleteWrapper.classList.add('d-none');
}
const previewBox = document.getElementById('file-preview');
if (previewBox) {
previewBox.innerHTML = `<div id="pdfWrap" style="height:500px; overflow:auto; background:#f7f7f7; padding:8px;">
<div id="pdfPages"></div>
</div>`;
}
openPreview(id);
$("#previewModal").modal('show');
}
const katDok = @json($katDok);
const formCreate = document.getElementById('formFile');
const modalCreate = document.getElementById('modalCreateFile');
let colCount = 1;
document.addEventListener('click', function(e){
if(e.target.closest('#btn-view-full')){
if (!currentPreviewId) return;
window.open(`/full-preview/${currentPreviewId}`, '_blank');
}
});
document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return;
const targetId = e.target.getAttribute('data-target');

View File

@ -408,6 +408,30 @@
return null;
}
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderDeleteRecommendation(item){
const recommendation = item?.delete_recommendation;
if (!recommendation?.visible || !recommendation?.note) return '';
const actorRole = recommendation.actor_role || 'TURT';
const actorName = recommendation.actor_name ? ` (${escapeHtml(recommendation.actor_name)})` : '';
const note = escapeHtml(recommendation.note);
return `
<div class="mt-1 px-2 py-1 rounded border border-warning-subtle bg-warning-subtle text-warning-emphasis" style="font-size:12px; line-height:1.35;">
Dokumen ini direkomendasikan dihapus oleh ${escapeHtml(actorRole)}${actorName}. Catatan: ${note}
</div>
`;
}
function buildRow(item){
const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-';
@ -520,6 +544,7 @@
${expiryBadge}
</div>
${renderDeleteRecommendation(item)}
</div>
</td>

View File

@ -649,6 +649,30 @@
return null;
}
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function renderDeleteRecommendation(item){
const recommendation = item?.delete_recommendation;
if (!recommendation?.visible || !recommendation?.note) return '';
const actorRole = recommendation.actor_role || 'TURT';
const actorName = recommendation.actor_name ? ` (${escapeHtml(recommendation.actor_name)})` : '';
const note = escapeHtml(recommendation.note);
return `
<div class="mt-1 px-2 py-1 rounded border border-warning-subtle bg-warning-subtle text-warning-emphasis" style="font-size:12px; line-height:1.35;">
Dokumen ini direkomendasikan dihapus oleh ${escapeHtml(actorRole)}${actorName}. Catatan: ${note}
</div>
`;
}
function buildRow(item){
const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-';
@ -748,6 +772,7 @@
</a>
${akreBadge}${expiryBadge}
</div>
${renderDeleteRecommendation(item)}
</td>
<td>
${item.nama_kategori || '-'}

View File

@ -66,7 +66,7 @@
})->exists();
$isRoleApprover = in_array(51, $userUnitIds, true) || in_array(22, $userUnitIds, true);
@endphp
@if($isAtasan || $isRoleApprover)
@if($isAtasan)
@if(!Auth::guard('admin')->check())
<li class="sidebar-item">
<a class="sidebar-link d-flex align-items-center justify-content-between"
@ -91,6 +91,10 @@
<i class="ti ti-clock"></i>
<span class="hide-menu">Pengajuan</span>
</div>
@if($isRoleApprover)
<span class="badge bg-danger rounded-pill d-none" id="pengajuanPendingCountBadge">0</span>
@endif
</a>
</li>
@endif
@ -204,10 +208,11 @@
</style>
<script>
const badge = document.getElementById('pendingCountBadge');
const pendingBadge = document.getElementById('pendingCountBadge');
const pengajuanBadge = document.getElementById('pengajuanPendingCountBadge');
async function countData() {
if (!badge) return;
if (!pendingBadge && !pengajuanBadge) return;
try {
const res = await fetch('/data/count-pending', {
@ -219,11 +224,19 @@
const data = await res.json();
const count = Number(data?.count ?? 0);
badge.textContent = count;
badge.classList.toggle('d-none', count <= 0);
if (pendingBadge) {
pendingBadge.textContent = count;
pendingBadge.classList.toggle('d-none', count <= 0);
}
if (pengajuanBadge) {
pengajuanBadge.textContent = count;
pengajuanBadge.classList.toggle('d-none', count <= 0);
}
} catch (e) {
badge.classList.add('d-none');
if (pendingBadge) pendingBadge.classList.add('d-none');
if (pengajuanBadge) pengajuanBadge.classList.add('d-none');
}
}

View File

@ -48,13 +48,24 @@
</li>
@if($isKomiteMutu || $isTurt || $isAtasan)
<li class="nav-item">
<button class="nav-link" type="button" data-mode="persetujuan">Data Persetujuan</button>
<button class="nav-link d-flex align-items-center gap-2" type="button" data-mode="persetujuan">
<span>Data Persetujuan</span>
<span class="badge bg-danger rounded-pill d-none" id="pendingPersetujuanTabBadge">0</span>
</button>
</li>
@endif
<li class="nav-item">
<button class="nav-link" type="button" data-mode="history">Log History</button>
</li>
</ul>
@if(($isKomiteMutu || $isTurt) && !$isAtasan)
<div class="alert alert-warning d-none align-items-center justify-content-between gap-3 py-2" id="pendingPersetujuanNotice" role="alert">
<div>
Ada <strong id="pendingPersetujuanNoticeCount">0</strong> data yang menunggu persetujuan Anda.
</div>
<a href="{{ url('/pending-file') }}" class="btn btn-sm btn-outline-warning">Lihat Persetujuan</a>
</div>
@endif
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3 flex-wrap">
<div class="input-group input-group-sm flex-grow-1" style="max-width:320px;">
<span class="input-group-text bg-white border-end-0">