Merge pull request 'revisi2' (#7) from revisi2 into main

Reviewed-on: #7
This commit is contained in:
Joko 2026-06-26 09:27:06 +00:00
commit 7f4e6d1b20
18 changed files with 3012 additions and 385 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@ class EnsureMasterPersetujuan
{ {
$user = $request->user(); $user = $request->user();
$hasAccess = $user && $user->masterPersetujuan; $hasAccess = $user && $user->masterPersetujuan;
if (!$hasAccess) { if (!$hasAccess) {
abort(403, 'Tidak memiliki akses.'); abort(403, 'Tidak memiliki akses.');
} }

View File

@ -11,9 +11,9 @@ class SubUnitKerja extends Model
public $timestamps = false; public $timestamps = false;
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $guarded = ['id']; protected $guarded = ['id'];
protected $with = ['fileDirectory']; // protected $with = ['fileDirectory'];
public function fileDirectory(){ // public function fileDirectory(){
return $this->hasMany(FileDirectory::class, 'id_sub_unit_kerja', 'id')->where('statusenabled', true); // return $this->hasMany(FileDirectory::class, 'id_sub_unit_kerja', 'id')->where('statusenabled', true);
} // }
} }

View File

@ -11,10 +11,10 @@ class UnitKerja extends Model
public $timestamps = false; public $timestamps = false;
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $guarded = ['id']; protected $guarded = ['id'];
protected $with = ['subUnitKerja']; // protected $with = ['subUnitKerja'];
public function subUnitKerja(){ // public function subUnitKerja(){
return $this->hasMany(SubUnitKerja::class, 'objectunitkerjapegawaifk', 'id')->where('statusenabled', true)->select('id', 'objectunitkerjapegawaifk', 'name'); // return $this->hasMany(SubUnitKerja::class, 'objectunitkerjapegawaifk', 'id')->where('statusenabled', true)->select('id', 'objectunitkerjapegawaifk', 'name');
} // }
} }

View File

@ -45,7 +45,7 @@ class User extends Authenticatable
'katasandi' => 'hashed', 'katasandi' => 'hashed',
]; ];
} }
protected $with = ['dataUser', 'masterPersetujuan', 'akses']; protected $with = ['dataUser'];
public function dataUser(){ public function dataUser(){
return $this->belongsTo(DataUser::class, 'objectpegawaifk', 'id')->select('id', 'namalengkap'); return $this->belongsTo(DataUser::class, 'objectpegawaifk', 'id')->select('id', 'namalengkap');
} }

View File

@ -15904,14 +15904,14 @@ body {
border-radius: 20px; border-radius: 20px;
} }
.body-wrapper .body-wrapper-inner { .body-wrapper .body-wrapper-inner {
min-height: calc(100vh - 110px); min-height: calc(130vh - 180px);
} }
.body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl { .body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl {
max-width: 1300px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding: 24px; padding: 10px;
transition: 0.2s ease-in; transition: 0.2s ease-in;
padding-top: 120px; padding-top: 90px;
} }
@media (max-width: 991.98px) { @media (max-width: 991.98px) {
.body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl { .body-wrapper .container-fluid, .body-wrapper .container-sm, .body-wrapper .container-md, .body-wrapper .container-lg, .body-wrapper .container-xl, .body-wrapper .container-xxl {

View File

@ -19,6 +19,8 @@ document.addEventListener('DOMContentLoaded', () => {
const titleEl = document.getElementById('pendingTitle'); const titleEl = document.getElementById('pendingTitle');
const tabPendingEl = document.getElementById('tabPengajuan'); const tabPendingEl = document.getElementById('tabPengajuan');
const tabHistoryEl = document.getElementById('tabHistory'); const tabHistoryEl = document.getElementById('tabHistory');
const isKomiteMutu = !!window.isKomiteMutu;
const isTurt = !!window.isTurt;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const selectedIds = new Set(); const selectedIds = new Set();
@ -63,10 +65,54 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
function statusBadge(status){ function approvalBadge(status, label){
if (status === 'rejected') return '<span class="badge bg-danger">Rejected</span>'; const normalized = String(status || '').toLowerCase();
if (status === 'revised') return '<span class="badge bg-info">Revised</span>'; if (normalized === 'approved') return `<span class="badge bg-success">Approved ${label}</span>`;
return '<span class="badge bg-warning text-dark">Pending</span>'; if (normalized.startsWith('rejected')) return `<span class="badge bg-danger">Rejected ${label}</span>`;
if (normalized.startsWith('revised')) return `<span class="badge bg-info">Revised ${label}</span>`;
return `<span class="badge bg-warning text-dark">Pending ${label}</span>`;
}
function getWorkflowStatusBadges(item){
const actionStatus = String(item?.status_action || '').toLowerCase();
const mutuStatus = String(item?.status_mutu || '').toLowerCase();
const turtStatus = String(item?.status_turt || '').toLowerCase();
if (actionStatus.startsWith('revised')) {
return [`<span class="badge bg-info">Revised</span>`];
}
if (actionStatus.startsWith('rejected')) {
return [`<span class="badge bg-danger">Rejected Atasan</span>`];
}
if (actionStatus !== 'approved') {
return [`<span class="badge bg-warning text-dark">Pending Atasan</span>`];
}
const statuses = [];
if (item?.is_akre) {
if (mutuStatus.startsWith('rejected')) {
statuses.push(`<span class="badge bg-danger">Rejected Mutu</span>`);
} else if (mutuStatus !== 'approved') {
statuses.push(`<span class="badge bg-warning text-dark">Pending Mutu</span>`);
}
}
if (item?.master_kategori_directory_id) {
if (turtStatus.startsWith('rejected')) {
statuses.push(`<span class="badge bg-danger">Rejected TURT</span>`);
} else if (turtStatus !== 'approved') {
statuses.push(`<span class="badge bg-warning text-dark">Pending TURT</span>`);
}
}
if (statuses.length > 0) {
return statuses;
}
return [`<span class="badge bg-success">Approved</span>`];
} }
function aksesBadge(akses){ function aksesBadge(akses){
@ -81,12 +127,87 @@ document.addEventListener('DOMContentLoaded', () => {
} }
function isRejected(item){ function isRejected(item){
return item?.status_action === 'rejected'; return String(item?.status_action || '').toLowerCase().startsWith('rejected')
|| String(item?.status_mutu || '').toLowerCase().startsWith('rejected')
|| String(item?.status_turt || '').toLowerCase().startsWith('rejected');
}
function isAtasanApproved(item){
return String(item?.status_action || '').toLowerCase() === 'approved';
}
function canProcessRoleStatus(status){
const normalized = String(status || '').toLowerCase();
return normalized === '' || normalized.startsWith('revised');
}
function canApproveAsAtasan(item){
return item?.can_approve_atasan === true || item?.can_approve_atasan === 1 || item?.can_approve_atasan === '1';
}
function canEditSubmission(item){
return item?.can_edit_submission === true || item?.can_edit_submission === 1 || item?.can_edit_submission === '1';
}
function hasRevisionInfo(item){
return !!(item?.revision || item?.mutu_revision || item?.turt_revision);
}
function collectRevisionNotes(item){
const notes = [];
const genericRevision = String(item?.revision || '').trim();
const mutuRevision = String(item?.mutu_revision || '').trim();
const turtRevision = String(item?.turt_revision || '').trim();
if (genericRevision) {
const lowerGeneric = genericRevision.toLowerCase();
if (!lowerGeneric.startsWith('komite mutu:') && !lowerGeneric.startsWith('turt:')) {
notes.push({ label: 'Atasan', text: genericRevision });
}
}
if (mutuRevision) {
notes.push({ label: 'Komite Mutu', text: mutuRevision });
}
if (turtRevision) {
notes.push({ label: 'TURT', text: turtRevision });
}
return notes;
}
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildRevisionNotesHtml(notes){
if (!notes.length) {
return '<div class="text-muted">Tidak ada catatan revisi.</div>';
}
return `
<div class="text-start" style="line-height:1.6;">
${notes.map(note => `
<div class="mb-2">
<div><strong>${escapeHtml(note.label)}</strong></div>
<div>${escapeHtml(note.text)}</div>
</div>
`).join('')}
</div>
`;
}
function getItemById(id){
return (tableState.data || []).find((row) => String(row.file_directory_id) === String(id));
} }
function getSelectableIdsOnPage(){ function getSelectableIdsOnPage(){
return (tableState.data || []) return (tableState.data || [])
.filter((item) => !isRejected(item)) .filter((item) => !isRejected(item) && !isAtasanApproved(item) && canApproveAsAtasan(item))
.map((item) => String(item.file_directory_id)); .map((item) => String(item.file_directory_id));
} }
@ -114,9 +235,11 @@ document.addEventListener('DOMContentLoaded', () => {
function updateSelectionUI(){ function updateSelectionUI(){
const count = selectedIds.size; const count = selectedIds.size;
const selectableIds = getSelectableIdsOnPage();
const canBulkApprove = tableState.mode !== 'history' && selectableIds.length > 0;
if (selectedCountEl) selectedCountEl.textContent = String(count); if (selectedCountEl) selectedCountEl.textContent = String(count);
if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || tableState.mode === 'history'; if (bulkApproveBtn) bulkApproveBtn.disabled = count === 0 || !canBulkApprove;
if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || tableState.mode === 'history'; if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || !canBulkApprove;
updateSelectAllState(); updateSelectAllState();
} }
@ -126,26 +249,60 @@ document.addEventListener('DOMContentLoaded', () => {
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-'; const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
const id = String(item.file_directory_id); const id = String(item.file_directory_id);
const rejected = isRejected(item); const rejected = isRejected(item);
const atasanApproved = isAtasanApproved(item);
const canAtasanApprove = canApproveAsAtasan(item);
const atasanRejected = String(item?.status_action || '').toLowerCase().startsWith('rejected');
const canMutuAct = atasanApproved && isKomiteMutu && item.is_akre && canProcessRoleStatus(item.status_mutu);
const canTurtAct = atasanApproved && isTurt && item.master_kategori_directory_id && canProcessRoleStatus(item.status_turt);
const checked = selectedIds.has(id); const checked = selectedIds.has(id);
const aksi = ` const actions = [
<div class="d-flex gap-1"> `<button class="btn btn-sm btn-primary" onclick="infoDok(this)"
<button class="btn btn-sm btn-primary" onclick="infoDok(this)" data-file="${item.file}"
data-file="${item.file}" data-fileName="${item.nama_dokumen}"
data-fileName="${item.nama_dokumen}" data-id="${item.file_directory_id}"
data-id="${item.file_directory_id}" data-no_dokumen="${item.no_dokumen || '-'}"
data-no_dokumen="${item.no_dokumen || '-'}" data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}" data-permission_file="${item.permission_file || '-'}">
data-permission_file="${item.permission_file || '-'}"> <i class="fa-solid fa-eye"></i>
<i class="fa-solid fa-eye"></i> </button>`
</button> ];
<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')">
<i class="fa-solid fa-check"></i> if (isKomiteMutu || isTurt) {
</button> if (canAtasanApprove && !atasanApproved) {
<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"> actions.push(`<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
<i class="fa-solid fa-xmark"></i> actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
</button> }
</div> if (canMutuAct) {
`; actions.push(`<button class="btn btn-sm btn-success" onclick="approvePendingRole('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPendingRole('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
}
if (canTurtAct) {
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePendingRole('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPendingRole('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-ban"></i></button>`);
}
} else {
if (!atasanApproved) {
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPending('${item.file_directory_id}', '${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
}
}
if (rejected && atasanRejected && !canMutuAct && !canTurtAct) {
actions.length = 1;
}
if (rejected) {
if (hasRevisionInfo(item)) {
actions.push(`<button class="btn btn-sm btn-info" onclick="infoRejectPending('${item.file_directory_id}')"><i class="fa-solid fa-circle-info"></i></button>`);
}
if (canEditSubmission(item)) {
actions.push(`<button class="btn btn-sm btn-primary" onclick="editRejectedFromPending('${item.file_directory_id}')"><i class="fa-solid fa-pen-to-square"></i></button>`);
}
}
const statusContent = (isKomiteMutu || isTurt)
? getWorkflowStatusBadges(item).join(' ')
: getWorkflowStatusBadges(item).join(' ');
return ` return `
<tr class="${checked ? 'table-active' : ''}"> <tr class="${checked ? 'table-active' : ''}">
<td class="text-center"> <td class="text-center">
@ -153,11 +310,11 @@ document.addEventListener('DOMContentLoaded', () => {
class="form-check-input row-select" class="form-check-input row-select"
data-id="${id}" data-id="${id}"
${checked ? 'checked' : ''} ${checked ? 'checked' : ''}
${rejected ? 'disabled' : ''}> ${(rejected || atasanApproved || !canAtasanApprove) ? 'disabled' : ''}>
</td> </td>
<td class="text-center">${rejected ? '' : aksi}</td> <td class="text-center text-nowrap"><div class="d-flex justify-content-center align-items-center gap-1 flex-nowrap">${actions.join('')}</div></td>
<td>${safeText(item.no_dokumen)}</td> <td>${safeText(item.no_dokumen)}</td>
<td>${statusBadge(item?.status_action)}</td> <td>${statusContent}</td>
<td>${aksesBadge(item?.permission_file)}</td> <td>${aksesBadge(item?.permission_file)}</td>
<td><a href="#" class="file-link" <td><a href="#" class="file-link"
data-file="${item.file}" data-file="${item.file}"
@ -323,7 +480,7 @@ document.addEventListener('DOMContentLoaded', () => {
tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history'); tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history');
} }
if (pendingBulkActionsEl) { if (pendingBulkActionsEl) {
pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history'); pendingBulkActionsEl.classList.toggle('d-none', tableState.mode === 'history' || isKomiteMutu || isTurt);
} }
updateSelectionUI(); updateSelectionUI();
} }
@ -404,7 +561,7 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
selectedIds.delete(String(id)); selectedIds.delete(String(id));
countData(); if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -461,7 +618,127 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
selectedIds.delete(String(id)); selectedIds.delete(String(id));
countData(); if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData();
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
window.infoRejectPending = function(id){
const item = getItemById(id);
Swal.fire({
title: 'Catatan Revisi',
html: buildRevisionNotesHtml(collectRevisionNotes(item)),
icon: 'info',
confirmButtonText: 'Tutup'
});
}
window.editRejectedFromPending = function(id){
const targetUrl = new URL('/pengajuan-file', window.location.origin);
targetUrl.searchParams.set('edit', id);
window.location.href = targetUrl.toString();
}
window.approvePendingRole = function(type, id, fileName){
const endpoint = type === 'mutu'
? `/pending-file/${id}/approve-mutu`
: `/pending-file/${id}/approve-turt`;
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
Swal.fire({
title: `Approve ${label}?`,
text: fileName || 'Dokumen akan disetujui.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Approve',
cancelButtonText: 'Batal',
}).then((result) => {
if (!result.isConfirmed) return;
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.status) {
throw new Error(data?.message || 'Gagal approve.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen disetujui.',
timer: 1500,
showConfirmButton: false
});
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData();
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
window.rejectPendingRole = function(type, id, fileName){
const endpoint = type === 'mutu'
? `/pending-file/${id}/reject-mutu`
: `/pending-file/${id}/reject-turt`;
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
Swal.fire({
title: `Reject ${label}?`,
text: fileName || 'Dokumen akan ditolak.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Reject',
cancelButtonText: 'Batal',
input: 'textarea',
inputLabel: 'Catatan',
inputPlaceholder: 'Tulis alasan atau catatan revisi...',
inputAttributes: {
'aria-label': 'Catatan'
},
preConfirm: (value) => {
const revision = (value || '').trim();
if (!revision) {
Swal.showValidationMessage('Catatan revisi wajib diisi.');
}
return revision;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({ revision: result.value })
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.status) {
throw new Error(data?.message || 'Gagal reject.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen ditolak.',
timer: 1500,
showConfirmButton: false
});
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -572,7 +849,7 @@ document.addEventListener('DOMContentLoaded', () => {
timer: 1500, timer: 1500,
showConfirmButton: false showConfirmButton: false
}); });
countData(); if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({

View File

@ -13,6 +13,14 @@ document.addEventListener('DOMContentLoaded', () => {
const titleEl = document.getElementById('pengajuanTitle'); const titleEl = document.getElementById('pengajuanTitle');
const tabPengajuanEl = document.getElementById('tabPengajuan'); const tabPengajuanEl = document.getElementById('tabPengajuan');
const tabHistoryEl = document.getElementById('tabHistory'); const tabHistoryEl = document.getElementById('tabHistory');
const selectAllCheckbox = document.getElementById('selectAllPengajuan');
const bulkDeleteBtn = document.getElementById('bulkDeleteBtn');
const clearSelectionBtn = document.getElementById('clearSelectionBtn');
const selectedCountEl = document.getElementById('selectedCount');
const bulkActionsEl = document.getElementById('pengajuanBulkActions');
const deleteDataEl = document.getElementById('deleteData');
const pendingPersetujuanNotice = document.getElementById('pendingPersetujuanNotice');
const pendingPersetujuanNoticeCount = document.getElementById('pendingPersetujuanNoticeCount');
const formCreate = document.getElementById('formFile'); const formCreate = document.getElementById('formFile');
const modalCreate = document.getElementById('modalCreateFile'); const modalCreate = document.getElementById('modalCreateFile');
let colCount = 1; let colCount = 1;
@ -20,6 +28,11 @@ document.addEventListener('DOMContentLoaded', () => {
const editForm = document.getElementById('formEditPengajuanFile'); const editForm = document.getElementById('formEditPengajuanFile');
const editUnitSelect = $('#edit_id_unit_kerja'); const editUnitSelect = $('#edit_id_unit_kerja');
const editSubUnitSelect = $('#edit_id_sub_unit_kerja'); const editSubUnitSelect = $('#edit_id_sub_unit_kerja');
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) { if (pageSizeSelect) {
const initialSize = parseInt(pageSizeSelect.value); const initialSize = parseInt(pageSizeSelect.value);
@ -61,14 +74,175 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
function getSelectableIdsOnPage(){
return (tableState.data || []).map((row) => String(row.file_directory_id));
}
function updateSelectAllState(){
if (!selectAllCheckbox) return;
if (tableState.mode === 'history' || tableState.mode === 'persetujuan') {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.disabled = true;
return;
}
const pageIds = getSelectableIdsOnPage();
if (pageIds.length === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
selectAllCheckbox.disabled = true;
return;
}
selectAllCheckbox.disabled = false;
const selectedOnPage = pageIds.filter((id) => selectedIds.has(id)).length;
selectAllCheckbox.checked = selectedOnPage > 0 && selectedOnPage === pageIds.length;
selectAllCheckbox.indeterminate = selectedOnPage > 0 && selectedOnPage < pageIds.length;
}
function updateSelectionUI(){
const count = selectedIds.size;
if (selectedCountEl) selectedCountEl.textContent = String(count);
if (bulkDeleteBtn) bulkDeleteBtn.disabled = count === 0 || tableState.mode === 'history' || tableState.mode === 'persetujuan';
if (clearSelectionBtn) clearSelectionBtn.disabled = count === 0 || tableState.mode === 'history' || tableState.mode === 'persetujuan';
updateSelectAllState();
}
function approvalBadge(status, label){
const normalized = String(status || '').toLowerCase();
if (normalized === 'approved') return `<span class="badge bg-success">Approved ${label}</span>`;
if (normalized.startsWith('rejected')) return `<span class="badge bg-danger">Rejected ${label}</span>`;
if (normalized.startsWith('revised')) return `<span class="badge bg-info">Revised ${label}</span>`;
return `<span class="badge bg-warning text-dark">Pending ${label}</span>`;
}
function hasRevisionInfo(item){
return !!(item?.revision || item?.mutu_revision || item?.turt_revision);
}
function collectRevisionNotes(item){
const notes = [];
const genericRevision = String(item?.revision || '').trim();
const mutuRevision = String(item?.mutu_revision || '').trim();
const turtRevision = String(item?.turt_revision || '').trim();
if (genericRevision) {
const lowerGeneric = genericRevision.toLowerCase();
if (!lowerGeneric.startsWith('komite mutu:') && !lowerGeneric.startsWith('turt:')) {
notes.push({ label: 'Atasan/Bagian Terkait', text: genericRevision });
}
}
if (mutuRevision) {
notes.push({ label: 'Komite Mutu', text: mutuRevision });
}
if (turtRevision) {
notes.push({ label: 'TURT', text: turtRevision });
}
return notes;
}
function escapeHtml(value){
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function buildRevisionNotesHtml(notes){
if (!notes.length) {
return '<div class="text-muted">Tidak ada catatan revisi.</div>';
}
return `
<div class="text-start" style="line-height:1.6;">
${notes.map(note => `
<div class="mb-2">
<div><strong>${escapeHtml(note.label)}</strong></div>
<div>${escapeHtml(note.text)}</div>
</div>
`).join('')}
</div>
`;
}
function isRejectedStatus(item){
return String(item?.status_action || '').toLowerCase().startsWith('rejected')
|| String(item?.status_mutu || '').toLowerCase().startsWith('rejected')
|| String(item?.status_turt || '').toLowerCase().startsWith('rejected');
}
function isApprovedStatus(status){
return String(status || '').toLowerCase() === 'approved';
}
function canProcessRoleStatus(status){
const normalized = String(status || '').toLowerCase();
return normalized === '' || normalized.startsWith('revised');
}
function getPengajuanStatusBadges(item){
const actionStatus = String(item?.status_action || '').toLowerCase();
const mutuStatus = String(item?.status_mutu || '').toLowerCase();
const turtStatus = String(item?.status_turt || '').toLowerCase();
if (actionStatus.startsWith('revised')) {
return `<span class="badge bg-info">Revised</span>`;
}
if (actionStatus.startsWith('rejected')) {
return `<span class="badge bg-danger">Rejected Atasan</span>`;
}
if (actionStatus !== 'approved') {
return `<span class="badge bg-warning text-dark">Pending Atasan</span>`;
}
const statuses = [];
if (item?.is_akre) {
if (mutuStatus.startsWith('rejected mutu')) {
statuses.push(`<span class="badge bg-danger">Rejected Mutu</span>`);
} else if (mutuStatus === 'revised mutu') {
statuses.push(`<span class="badge bg-info text-white">Revisi Mutu</span>`);
} else if(mutuStatus === 'approved'){
statuses.push(`<span class="badge bg-success text-white">Approved Mutu</span>`);
}else{
statuses.push(`<span class="badge bg-warning text-dark">Pending Mutu</span>`);
}
}
if (item?.master_kategori_directory_id) {
if (turtStatus.startsWith('rejected turt')) {
statuses.push(`<span class="badge bg-danger">Rejected TURT</span>`);
} else if (turtStatus === 'revised turt') {
statuses.push(`<span class="badge bg-info text-white">Revisi TURT</span>`);
} else if(turtStatus === 'approved'){
statuses.push(`<span class="badge bg-success text-white">Approved TURT</span>`);
} else{
statuses.push(`<span class="badge bg-warning text-dark">Pending TURT</span>`);
}
}
if (statuses.length > 0) {
return statuses.join(' ');
}
return `<span class="badge bg-success">Approved</span>`;
}
function buildRow(item){ function buildRow(item){
let tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-'; let tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-';
let tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-'; let tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-';
let tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-'; let tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
const isApproved = item?.status_action === 'approved'; const isApproved = isApprovedStatus(item?.status_action);
const isRejected = item?.status_action === 'rejected'; const isRejected = isRejectedStatus(item);
const showEdit = !isApproved; const showEdit = isRejected;
const showInfo = isRejected; const showInfo = isRejected && hasRevisionInfo(item);
const id = String(item.file_directory_id);
const checked = selectedIds.has(id);
const aksi = ` const aksi = `
<div class="d-flex gap-1"> <div class="d-flex gap-1">
<button href="#" class="btn btn-sm btn-primary" onclick="infoDok(this)" <button href="#" class="btn btn-sm btn-primary" onclick="infoDok(this)"
@ -93,21 +267,16 @@ document.addEventListener('DOMContentLoaded', () => {
</div> </div>
`; `;
return ` return `
<tr> <tr class="${checked ? 'table-active' : ''}">
<td class="text-center">
<input type="checkbox"
class="form-check-input row-select"
data-id="${id}"
${checked ? 'checked' : ''}>
</td>
<td>${aksi}</td> <td>${aksi}</td>
<td>${item.no_dokumen || '-'}</td> <td>${item.no_dokumen || '-'}</td>
<td> <td><div class="d-flex gap-1 flex-wrap">${getPengajuanStatusBadges(item)}</div></td>
<button class="btn btn-sm
${item?.status_action === "approved" ? 'btn-success' :
item?.status_action === "rejected" ? 'btn-danger' :
item?.status_action === "revised" ? 'btn-info' :
'btn-warning'}">
${item?.status_action === "approved" ? 'Approved' :
item?.status_action === "rejected" ? 'Rejected' :
item?.status_action === "revised" ? 'Revised' :
'Pending'}
</button>
</td>
<td>${aksesBadge(item?.permission_file)}</td> <td>${aksesBadge(item?.permission_file)}</td>
<td><a href="#" class="file-link" <td><a href="#" class="file-link"
data-file="${item.file}" data-file="${item.file}"
@ -130,6 +299,64 @@ document.addEventListener('DOMContentLoaded', () => {
`; `;
} }
function buildRowPersetujuan(item){
const tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-';
const tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-';
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
const showMutu = isKomiteMutu && !!item.is_akre;
const showTurt = isTurt;
const actions = [
`<button type="button" class="btn btn-sm btn-primary" onclick="infoDok(this)"
data-file="${item.file}"
data-fileName="${item.nama_dokumen}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"><i class="fa-solid fa-eye"></i></button>`
];
if (showMutu && canProcessRoleStatus(item.status_mutu)) {
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePersetujuan('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPersetujuan('mutu','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-xmark"></i></button>`);
}
if (showTurt && canProcessRoleStatus(item.status_turt)) {
actions.push(`<button class="btn btn-sm btn-success" onclick="approvePersetujuan('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-check"></i></button>`);
actions.push(`<button class="btn btn-sm btn-danger" onclick="rejectPersetujuan('turt','${item.file_directory_id}','${item.nama_dokumen || ''}')"><i class="fa-solid fa-ban"></i></button>`);
}
if (hasRevisionInfo(item)) {
actions.push(`<button class="btn btn-sm btn-info" onclick="infoReject('${item.file_directory_id}')"><i class="fa-solid fa-circle-info"></i></button>`);
}
const statusParts = [];
if (item.is_akre) {
statusParts.push(approvalBadge(item.status_mutu, 'Mutu'));
}
if (item.master_kategori_directory_id) {
statusParts.push(approvalBadge(item.status_turt, 'TURT'));
}
return `
<tr>
<td class="text-center"></td>
<td class="text-center align-middle"><div class="d-flex justify-content-center gap-1 flex-nowrap">${actions.join('')}</div></td>
<td>${item.no_dokumen || '-'}</td>
<td><div class="d-flex gap-1 flex-wrap">${statusParts.join('')}</div></td>
<td>${aksesBadge(item?.permission_file)}</td>
<td><a href="#" class="file-link"
data-file="${item.file}"
data-fileName="${item.nama_dokumen}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}">${item.nama_dokumen}</a></td>
${item.is_akre ? `<td colspan="2" class="text-center">Dokumen akreditasi</td>` : `<td>${item.name_kategori || '-'}</td><td>${item.name_unit || '-'}</td>`}
<td class="text-nowrap">${tanggalTerbit}</td>
<td class="text-nowrap">${tanggalExp}</td>
<td class="text-nowrap">${tanggal}</td>
</tr>
`;
}
function buildHistoryRow(item){ function buildHistoryRow(item){
const tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-'; const tanggal = item.entry_at ? formatTanggal(item.entry_at) : '-';
const tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-'; const tanggalExp = item.tgl_expired ? formatTanggal(item.tgl_expired) : '-';
@ -149,8 +376,6 @@ document.addEventListener('DOMContentLoaded', () => {
<td>${item.folder || '-'}</td> <td>${item.folder || '-'}</td>
<td>${item.part || '-'}</td> <td>${item.part || '-'}</td>
<td><span class="badge bg-info">${item.action_type}</span></td> <td><span class="badge bg-info">${item.action_type}</span></td>
<td class="text-nowrap">${tanggalTerbit}</td>
<td class="text-nowrap">${tanggalExp}</td>
<td class="text-nowrap">${tanggal}</td> <td class="text-nowrap">${tanggal}</td>
</tr> </tr>
`; `;
@ -168,6 +393,41 @@ document.addEventListener('DOMContentLoaded', () => {
return (tableState.data || []).find((row) => String(row.file_directory_id) === String(id)); return (tableState.data || []).find((row) => String(row.file_directory_id) === String(id));
} }
function deleteDocuments(ids, successMessage){
return fetch('/delete-file/bulk', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ ids })
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.success) {
throw new Error(data?.message || 'Gagal menghapus dokumen.');
}
ids.forEach((id) => selectedIds.delete(String(id)));
if (window.idDirectory && ids.includes(String(window.idDirectory))) {
window.idDirectory = null;
window.currentFile = null;
if (deleteDataEl) deleteDataEl.classList.add('d-none');
const modalEl = document.getElementById('previewModal');
const modalInstance = modalEl ? bootstrap.Modal.getInstance(modalEl) : null;
modalInstance?.hide();
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || successMessage,
timer: 1500,
showConfirmButton: false
});
fetchData();
});
}
function renderPagination(totalPages){ function renderPagination(totalPages){
const activeState = tableState.mode === 'history' ? historyState : tableState; const activeState = tableState.mode === 'history' ? historyState : tableState;
const paginationEl = tableState.mode === 'history' ? paginationHistory : paginationPengajuan; const paginationEl = tableState.mode === 'history' ? paginationHistory : paginationPengajuan;
@ -215,11 +475,14 @@ document.addEventListener('DOMContentLoaded', () => {
function renderTable(){ function renderTable(){
const isHistoryMode = tableState.mode === 'history'; const isHistoryMode = tableState.mode === 'history';
const isPersetujuan = tableState.mode === 'persetujuan';
const activeState = isHistoryMode ? historyState : tableState; const activeState = isHistoryMode ? historyState : tableState;
const pageData = activeState.data || []; const pageData = activeState.data || [];
const targetBody = isHistoryMode ? tbodyHistory : tbodyPengajuan; const targetBody = isHistoryMode ? tbodyHistory : tbodyPengajuan;
const colSpan = isHistoryMode ? 9 : 10; const colSpan = isHistoryMode ? 9 : 11;
const rowBuilder = isHistoryMode ? buildHistoryRow : buildRow; const rowBuilder = isHistoryMode ? buildHistoryRow : (isPersetujuan ? buildRowPersetujuan : buildRow);
if (pageData.length === 0) { if (pageData.length === 0) {
targetBody.innerHTML = ` targetBody.innerHTML = `
<tr> <tr>
@ -239,6 +502,20 @@ document.addEventListener('DOMContentLoaded', () => {
} }
renderPagination(activeState.lastPage || 1); renderPagination(activeState.lastPage || 1);
updateSelectionUI();
maybeOpenEditFromQuery();
}
function maybeOpenEditFromQuery(){
if (!editFromQueryId || tableState.mode !== 'pengajuan') return;
const item = getItemById(editFromQueryId);
if (!item) return;
const cleanUrl = new URL(window.location.href);
cleanUrl.searchParams.delete('edit');
window.history.replaceState({}, '', cleanUrl.toString());
const pendingId = editFromQueryId;
editFromQueryId = null;
window.editFileReject(pendingId);
} }
let searchDebounce; let searchDebounce;
@ -264,6 +541,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('tableSearch').value = ''; document.getElementById('tableSearch').value = '';
startDateInput.value = ''; startDateInput.value = '';
endDateInput.value = ''; endDateInput.value = '';
selectedIds.clear();
tableState.page = 1; tableState.page = 1;
historyState.page = 1; historyState.page = 1;
fetchData(); fetchData();
@ -276,12 +554,51 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
if (titleEl) { if (titleEl) {
titleEl.textContent = tableState.mode === 'history' ? 'Log History' : 'Data Pengajuan'; titleEl.textContent = tableState.mode === 'history'
? 'Log History'
: (tableState.mode === 'persetujuan' ? 'Data Persetujuan' : 'Data Pengajuan');
} }
document.body.dataset.pengajuanMode = tableState.mode;
if (tabPengajuanEl && tabHistoryEl) { if (tabPengajuanEl && tabHistoryEl) {
tabPengajuanEl.classList.toggle('d-none', tableState.mode === 'history'); tabPengajuanEl.classList.toggle('d-none', tableState.mode === 'history');
tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history'); tabHistoryEl.classList.toggle('d-none', tableState.mode !== 'history');
} }
if (bulkActionsEl) {
bulkActionsEl.classList.toggle('d-none', tableState.mode === 'history' || tableState.mode === 'persetujuan');
}
if (deleteDataEl && (tableState.mode === 'history' || tableState.mode === 'persetujuan')) {
deleteDataEl.classList.add('d-none');
}
updateSelectionUI();
}
function updatePendingPersetujuanIndicators(total){
const count = Number(total || 0);
console.log(pendingPersetujuanNoticeCount);
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;
fetch('/data/count-pending', {
headers: {
'Accept': 'application/json'
}
})
.then(res => res.json())
.then(data => {
updatePendingPersetujuanIndicators(data?.count || 0);
})
.catch(() => {
updatePendingPersetujuanIndicators(0);
});
} }
if (tabsEl) { if (tabsEl) {
@ -297,6 +614,63 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
} }
if (tbodyPengajuan) {
tbodyPengajuan.addEventListener('change', (e) => {
const target = e.target;
if (!target.classList.contains('row-select')) return;
const id = target.getAttribute('data-id');
if (!id) return;
if (target.checked) selectedIds.add(id);
else selectedIds.delete(id);
const row = target.closest('tr');
if (row) row.classList.toggle('table-active', target.checked);
updateSelectionUI();
});
}
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', () => {
const pageIds = getSelectableIdsOnPage();
if (selectAllCheckbox.checked) {
pageIds.forEach((id) => selectedIds.add(id));
} else {
pageIds.forEach((id) => selectedIds.delete(id));
}
renderTable();
});
}
if (clearSelectionBtn) {
clearSelectionBtn.addEventListener('click', () => {
selectedIds.clear();
renderTable();
});
}
if (bulkDeleteBtn) {
bulkDeleteBtn.addEventListener('click', () => {
if (selectedIds.size === 0) return;
const ids = Array.from(selectedIds);
Swal.fire({
title: 'Hapus dokumen terpilih?',
text: `Total ${ids.length} dokumen akan dihapus.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Hapus',
cancelButtonText: 'Batal'
}).then((result) => {
if (!result.isConfirmed) return;
deleteDocuments(ids, 'Dokumen berhasil dihapus.').catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
});
}
function fetchData(){ function fetchData(){
if (summaryEl) summaryEl.textContent = 'Memuat data...'; if (summaryEl) summaryEl.textContent = 'Memuat data...';
const activeState = tableState.mode === 'history' ? historyState : tableState; const activeState = tableState.mode === 'history' ? historyState : tableState;
@ -305,7 +679,8 @@ document.addEventListener('DOMContentLoaded', () => {
per_page: activeState.pageSize, per_page: activeState.pageSize,
keyword: activeState.search || '', keyword: activeState.search || '',
start_date: activeState.startDate || '', start_date: activeState.startDate || '',
end_date: activeState.endDate || '' end_date: activeState.endDate || '',
mode: tableState.mode
}); });
const endpoint = tableState.mode === 'history' const endpoint = tableState.mode === 'history'
? `/data/log-dokumen?${params.toString()}` ? `/data/log-dokumen?${params.toString()}`
@ -317,6 +692,7 @@ document.addEventListener('DOMContentLoaded', () => {
activeState.lastPage = data?.pagination?.last_page || 1; activeState.lastPage = data?.pagination?.last_page || 1;
activeState.total = data?.pagination?.total || 0; activeState.total = data?.pagination?.total || 0;
renderTable(); renderTable();
fetchPendingPersetujuanCount();
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@ -326,14 +702,119 @@ document.addEventListener('DOMContentLoaded', () => {
window.infoReject = function(id){ window.infoReject = function(id){
const item = getItemById(id); const item = getItemById(id);
const revision = item?.revision ? String(item.revision) : 'Tidak ada catatan revisi.';
Swal.fire({ Swal.fire({
title: 'Catatan Revisi', title: 'Catatan Revisi',
text: revision, html: buildRevisionNotesHtml(collectRevisionNotes(item)),
icon: 'info', icon: 'info',
confirmButtonText: 'Tutup' confirmButtonText: 'Tutup'
}); });
} }
window.approvePersetujuan = function(type, id, fileName){
const endpoint = type === 'mutu'
? `/pengajuan-file/${id}/approve-mutu`
: `/pengajuan-file/${id}/approve-turt`;
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
Swal.fire({
title: `Approve ${label}?`,
text: fileName || 'Dokumen akan disetujui.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Approve',
cancelButtonText: 'Batal',
}).then((result) => {
if (!result.isConfirmed) return;
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.status) {
throw new Error(data?.message || 'Gagal approve.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen disetujui.',
timer: 1500,
showConfirmButton: false
});
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData();
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
window.rejectPersetujuan = function(type, id, fileName){
const endpoint = type === 'mutu'
? `/pengajuan-file/${id}/reject-mutu`
: `/pengajuan-file/${id}/reject-turt`;
const label = type === 'mutu' ? 'Komite Mutu' : 'TURT';
Swal.fire({
title: `Reject ${label}?`,
text: fileName || 'Dokumen akan ditolak.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Reject',
cancelButtonText: 'Batal',
input: 'textarea',
inputLabel: 'Catatan',
inputPlaceholder: 'Tulis alasan atau catatan revisi...',
inputAttributes: {
'aria-label': 'Catatan'
},
preConfirm: (value) => {
const revision = (value || '').trim();
if (!revision) {
Swal.showValidationMessage('Catatan revisi wajib diisi.');
}
return revision;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
revision: result.value
})
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.status) {
throw new Error(data?.message || 'Gagal reject.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen ditolak.',
timer: 1500,
showConfirmButton: false
});
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData();
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
window.infoDok = function(e){ window.infoDok = function(e){
let fileUrl = $(e).data('file'); let fileUrl = $(e).data('file');
let noDokumen = $(e).data('no_dokumen') let noDokumen = $(e).data('no_dokumen')
@ -360,6 +841,9 @@ document.addEventListener('DOMContentLoaded', () => {
permEl.textContent = publicDoc ? 'Umum' : 'Internal Unit'; permEl.textContent = publicDoc ? 'Umum' : 'Internal Unit';
permEl.className = 'badge ' + (publicDoc ? 'bg-success' : 'bg-secondary'); permEl.className = 'badge ' + (publicDoc ? 'bg-success' : 'bg-secondary');
} }
if (deleteDataEl) {
deleteDataEl.classList.remove('d-none');
}
let previewBox = document.getElementById('file-preview'); let previewBox = document.getElementById('file-preview');
previewBox.innerHTML = `<div id="pdfWrap" style="height:500px; overflow:auto; background:#f7f7f7; padding:8px;"> previewBox.innerHTML = `<div id="pdfWrap" style="height:500px; overflow:auto; background:#f7f7f7; padding:8px;">
<div id="pdfPages"></div>`; <div id="pdfPages"></div>`;
@ -404,6 +888,105 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.$ && $.fn.select2) $(selectEl).trigger('change'); if (window.$ && $.fn.select2) $(selectEl).trigger('change');
} }
function formatDateForInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateDisplay(dateValue) {
if (!dateValue) return '';
const date = new Date(`${dateValue}T00:00:00`);
if (Number.isNaN(date.getTime())) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
function addYearsToDate(dateValue, years) {
if (!dateValue || !years) return '';
const baseDate = new Date(`${dateValue}T00:00:00`);
if (Number.isNaN(baseDate.getTime())) return '';
const originalDay = baseDate.getDate();
baseDate.setFullYear(baseDate.getFullYear() + Number(years));
if (baseDate.getDate() !== originalDay) {
baseDate.setDate(0);
}
return formatDateForInput(baseDate);
}
function syncMasaBerlakuField(selectEl) {
if (!selectEl) return;
const fieldWrap = document.getElementById(selectEl.dataset.dateTarget);
const input = document.getElementById(selectEl.dataset.inputTarget);
const preview = document.getElementById(selectEl.dataset.previewTarget);
const baseInput = document.getElementById(selectEl.dataset.baseTarget);
if (!fieldWrap || !input) return;
if (selectEl.value === 'custom') {
fieldWrap.classList.remove('d-none');
input.disabled = false;
if (preview) preview.textContent = '';
return;
}
fieldWrap.classList.add('d-none');
input.disabled = true;
input.value = '';
if (!preview) return;
if (!selectEl.value) {
preview.textContent = '';
return;
}
if (!baseInput || !baseInput.value) {
preview.textContent = 'Pilih Tanggal Terbit untuk melihat hasil masa berlaku.';
return;
}
const expiredDate = addYearsToDate(baseInput.value, selectEl.value);
preview.textContent = expiredDate ? `Tanggal kedaluwarsa: ${formatDateDisplay(expiredDate)}` : '';
}
function prepareExpiryInputs(formEl) {
const selects = formEl.querySelectorAll('.masa-berlaku-select');
for (const selectEl of selects) {
const option = selectEl.value;
const dateInput = document.getElementById(selectEl.dataset.inputTarget);
const baseInput = document.getElementById(selectEl.dataset.baseTarget);
if (!dateInput) continue;
if (!option) {
dateInput.value = '';
dateInput.disabled = true;
continue;
}
if (option === 'custom') {
if (!dateInput.value) {
return 'Tanggal kedaluwarsa wajib diisi saat memilih opsi Lainnya.';
}
dateInput.disabled = false;
continue;
}
if (!baseInput || !baseInput.value) {
return 'Tanggal Terbit wajib diisi saat memilih masa berlaku 1, 2, atau 3 tahun.';
}
dateInput.value = addYearsToDate(baseInput.value, option);
dateInput.disabled = false;
}
return null;
}
function arrayFilterEmpty(arr){ function arrayFilterEmpty(arr){
return (arr || []).filter((val) => val !== null && val !== undefined && String(val).trim() !== ''); return (arr || []).filter((val) => val !== null && val !== undefined && String(val).trim() !== '');
} }
@ -563,9 +1146,11 @@ document.addEventListener('DOMContentLoaded', () => {
setChecked(byId('edit_perm_yes'), isPublic); setChecked(byId('edit_perm_yes'), isPublic);
setChecked(byId('edit_perm_no'), !isPublic); setChecked(byId('edit_perm_no'), !isPublic);
const hasExpired = !!item.tgl_expired; const editMasaBerlaku = byId('edit_masa_berlaku_option');
setChecked(byId('edit_has_expired'), hasExpired); if (editMasaBerlaku) {
syncEditExpiredField(); editMasaBerlaku.value = item.tgl_expired ? 'custom' : '';
syncMasaBerlakuField(editMasaBerlaku);
}
const displayName = item.fileName || (item.file ? String(item.file).split('/').pop() : ''); const displayName = item.fileName || (item.file ? String(item.file).split('/').pop() : '');
setTextValue(byId('edit_current_file'), displayName ? `File saat ini: ${displayName}` : ''); setTextValue(byId('edit_current_file'), displayName ? `File saat ini: ${displayName}` : '');
@ -592,19 +1177,18 @@ document.addEventListener('DOMContentLoaded', () => {
$("#modalEditPengajuanFile").modal('show'); $("#modalEditPengajuanFile").modal('show');
} }
function syncEditExpiredField() { const editMasaBerlaku = document.getElementById('edit_masa_berlaku_option');
const editHasExpired = document.getElementById('edit_has_expired'); if (editMasaBerlaku) {
const editExpiredInput = document.getElementById('edit_tgl_expired'); editMasaBerlaku.addEventListener('change', function() {
if (!editHasExpired || !editExpiredInput) return; syncMasaBerlakuField(editMasaBerlaku);
editExpiredInput.disabled = !editHasExpired.checked; });
if (!editHasExpired.checked) {
editExpiredInput.value = '';
}
} }
const editHasExpired = document.getElementById('edit_has_expired'); const editTanggalTerbit = document.getElementById('edit_tanggal_terbit');
if (editHasExpired) { if (editTanggalTerbit) {
editHasExpired.addEventListener('change', syncEditExpiredField); editTanggalTerbit.addEventListener('change', function() {
if (editMasaBerlaku) syncMasaBerlakuField(editMasaBerlaku);
});
} }
if (editForm) { if (editForm) {
@ -615,6 +1199,11 @@ document.addEventListener('DOMContentLoaded', () => {
Swal.fire({ icon: 'error', title: 'Gagal', text: 'ID dokumen tidak ditemukan.' }); Swal.fire({ icon: 'error', title: 'Gagal', text: 'ID dokumen tidak ditemukan.' });
return; return;
} }
const expiryError = prepareExpiryInputs(editForm);
if (expiryError) {
Swal.fire({ icon: 'warning', title: 'Validasi form', text: expiryError });
return;
}
const formData = new FormData(editForm); const formData = new FormData(editForm);
fetch(`/pengajuan-file/${id}/update`, { fetch(`/pengajuan-file/${id}/update`, {
@ -636,6 +1225,7 @@ document.addEventListener('DOMContentLoaded', () => {
showConfirmButton: false showConfirmButton: false
}); });
$("#modalEditPengajuanFile").modal('hide'); $("#modalEditPengajuanFile").modal('hide');
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
}).catch((err) => { }).catch((err) => {
Swal.fire({ Swal.fire({
@ -654,12 +1244,15 @@ document.addEventListener('DOMContentLoaded', () => {
// ===== Upload baru (seperti dataUnit) ===== // ===== Upload baru (seperti dataUnit) =====
document.addEventListener('change', function(e){ document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return; if(!e.target.classList.contains('masa-berlaku-select')) return;
const targetId = e.target.getAttribute('data-target'); syncMasaBerlakuField(e.target);
if(!targetId) return; });
const fieldWrap = document.getElementById(targetId);
const input = fieldWrap?.querySelector('input'); document.addEventListener('change', function(e){
if (input) input.disabled = !e.target.checked; if (!e.target.matches('input[type="date"][id^="dateActive_"]')) return;
const index = e.target.id.replace('dateActive_', '');
const selectEl = document.querySelector(`.masa-berlaku-select[data-index="${index}"]`);
if (selectEl) syncMasaBerlakuField(selectEl);
}); });
function resetCreateForm(){ function resetCreateForm(){
@ -672,6 +1265,7 @@ document.addEventListener('DOMContentLoaded', () => {
$(formCreate).find('input[type="file"]').val(''); $(formCreate).find('input[type="file"]').val('');
$(formCreate).find('.file-name').addClass('d-none').text(''); $(formCreate).find('.file-name').addClass('d-none').text('');
} }
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
resetAkreFields(0); resetAkreFields(0);
enableAkreFields(0); enableAkreFields(0);
} }
@ -901,22 +1495,33 @@ document.addEventListener('DOMContentLoaded', () => {
<label class="form-label fw-semibold">Tanggal Terbit</label> <label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control" <input class="form-control"
type="date" type="date"
name="data[${colCount}][date_active]"> name="data[${colCount}][date_active]"
id="dateActive_${colCount}">
</div> </div>
<div class="col-md-3"> <div class="col-md-4">
<div class="form-check"> <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
<input class="form-check-input toggle-expired" <select class="form-select masa-berlaku-select"
type="checkbox" name="data[${colCount}][masa_berlaku_option]"
id="hasExpired_${colCount}" data-index="${colCount}"
data-target="expiredField_${colCount}"> data-date-target="expiredField_${colCount}"
<label class="form-check-label" for="hasExpired_${colCount}">Masa Berlaku Dokumen??</label> data-input-target="expiredInput_${colCount}"
</div> data-base-target="dateActive_${colCount}"
data-preview-target="expiredPreview_${colCount}">
<option value="">Tidak ada</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="expiredPreview_${colCount}"></div>
</div> </div>
<div class="col-md-5" id="expiredField_${colCount}"> <div class="col-md-4 d-none" id="expiredField_${colCount}">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" <input class="form-control"
type="date" type="date"
id="expiredInput_${colCount}"
name="data[${colCount}][tgl_expired]" disabled> name="data[${colCount}][tgl_expired]" disabled>
</div> </div>
@ -997,6 +1602,8 @@ document.addEventListener('DOMContentLoaded', () => {
initKategoriSelect2(colCount); initKategoriSelect2(colCount);
enableAkreFields(colCount); enableAkreFields(colCount);
setKategoriRequired(colCount, false); setKategoriRequired(colCount, false);
const insertedSelect = document.querySelector(`.masa-berlaku-select[data-index="${colCount}"]`);
if (insertedSelect) syncMasaBerlakuField(insertedSelect);
colCount++; colCount++;
} }
@ -1010,12 +1617,22 @@ document.addEventListener('DOMContentLoaded', () => {
if (select0.length) selectOptionUnitKerjaV1(0); if (select0.length) selectOptionUnitKerjaV1(0);
initKategoriSelect2(0); initKategoriSelect2(0);
enableAkreFields(0); enableAkreFields(0);
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
formCreate.addEventListener('submit', (e) => { formCreate.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const submitBtn = formCreate.querySelector('button[type="submit"]'); const submitBtn = formCreate.querySelector('button[type="submit"]');
if (submitBtn) submitBtn.disabled = true; if (submitBtn) submitBtn.disabled = true;
if (submitBtn) submitBtn.textContent = 'menyimpan...'; if (submitBtn) submitBtn.textContent = 'menyimpan...';
const expiryError = prepareExpiryInputs(formCreate);
if (expiryError) {
Swal.fire({ icon: 'warning', title: 'Validasi form', text: expiryError });
if (submitBtn) submitBtn.disabled = false;
if (submitBtn) submitBtn.textContent = 'Simpan';
return;
}
const formData = new FormData(formCreate); const formData = new FormData(formCreate);
fetch(`/uploadv2`, { fetch(`/uploadv2`, {
@ -1037,6 +1654,7 @@ document.addEventListener('DOMContentLoaded', () => {
const modalInstance = bootstrap.Modal.getInstance(modalCreate); const modalInstance = bootstrap.Modal.getInstance(modalCreate);
modalInstance?.hide(); modalInstance?.hide();
resetCreateForm(); resetCreateForm();
if (typeof window.refreshPendingCount === 'function') window.refreshPendingCount();
fetchData(); fetchData();
if(responseData.status_action === null || responseData.status_action === undefined){ if(responseData.status_action === null || responseData.status_action === undefined){
Swal.fire({ Swal.fire({
@ -1076,6 +1694,7 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
}); });
}); });
document.addEventListener('click', function(e){ document.addEventListener('click', function(e){
if(e.target.matches('.file-link')){ if(e.target.matches('.file-link')){
@ -1105,6 +1724,10 @@ document.addEventListener('click', function(e){
const publicDoc = isPublic(permissionFile); const publicDoc = isPublic(permissionFile);
permEl.textContent = publicDoc ? 'Umum' : 'Internal Unit'; permEl.textContent = publicDoc ? 'Umum' : 'Internal Unit';
permEl.className = 'badge ' + (publicDoc ? 'bg-success' : 'bg-secondary'); permEl.className = 'badge ' + (publicDoc ? 'bg-success' : 'bg-secondary');
}
const deleteDataEl = document.getElementById('deleteData');
if (deleteDataEl) {
deleteDataEl.classList.toggle('d-none', document.body.dataset.pengajuanMode !== 'pengajuan');
} }
let previewBox = document.getElementById('file-preview'); let previewBox = document.getElementById('file-preview');
previewBox.innerHTML = `<div id="pdfWrap" style="height:500px; overflow:auto; background:#f7f7f7; padding:8px;"> previewBox.innerHTML = `<div id="pdfWrap" style="height:500px; overflow:auto; background:#f7f7f7; padding:8px;">
@ -1119,6 +1742,56 @@ document.addEventListener('click', function(e){
if(e.target.matches('#btn-view-full')){ if(e.target.matches('#btn-view-full')){
window.open(`/full-preview/${idDirectory}`, '_blank'); window.open(`/full-preview/${idDirectory}`, '_blank');
} }
if(e.target.matches('#delete-file')){
if(!idDirectory){
Swal.fire({
icon: 'warning',
title: 'Pilih dokumen',
text: 'Dokumen belum dipilih.'
});
return;
}
Swal.fire({
title: 'Hapus dokumen ini?',
text: 'Dokumen yang dihapus tidak akan tampil lagi.',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Hapus',
cancelButtonText: 'Batal'
}).then((result) => {
if (!result.isConfirmed) return;
fetch('/delete-file/bulk', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
},
body: JSON.stringify({ ids: [String(idDirectory)] })
}).then(async(res) => {
const data = await res.json();
if (!res.ok || !data?.success) {
throw new Error(data?.message || 'Gagal menghapus dokumen.');
}
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Dokumen berhasil dihapus.',
timer: 1500,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
}).catch((err) => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
}) })
function isPublic(permissionVal){ function isPublic(permissionVal){
if(permissionVal === null || permissionVal === undefined) return false; if(permissionVal === null || permissionVal === undefined) return false;

View File

@ -56,6 +56,49 @@
line-height: 1.2; line-height: 1.2;
border-radius: 999px; 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> </style>
@section('body_main') @section('body_main')
<div class="row"> <div class="row">
@ -167,6 +210,7 @@
</div> </div>
@include('dataUnit.modal.create') @include('dataUnit.modal.create')
@include('dataUnit.modal.view')
@if(Auth::guard('admin')->check()) @if(Auth::guard('admin')->check())
<div class="modal fade" id="modalAkreInstrumen" tabindex="-1" aria-hidden="true"> <div class="modal fade" id="modalAkreInstrumen" tabindex="-1" aria-hidden="true">
@ -214,6 +258,7 @@
let currentData = []; let currentData = [];
let selectedIds = []; let selectedIds = [];
let expandedFolders = new Set(); let expandedFolders = new Set();
let currentPreviewId = null;
let searchTimer; let searchTimer;
const isAdminUser = @json(Auth::guard('admin')->check()); const isAdminUser = @json(Auth::guard('admin')->check());
const btn = document.getElementById('btnDownloadMultiple'); const btn = document.getElementById('btnDownloadMultiple');
@ -349,6 +394,16 @@
</td> </td>
<td> <td>
<div class="btn-group"> <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" <a href="/file-download/${item.file_directory_id}" target="_blank"
class="btn btn-sm btn-outline-primary" download title="Download"> class="btn btn-sm btn-outline-primary" download title="Download">
<i class="ti ti-download"></i> <i class="ti ti-download"></i>
@ -356,9 +411,20 @@
</div> </div>
</td> </td>
<td> <td>
<div class="d-flex flex-column"> <div class="file-row-content">
<strong>${item.nama_dokumen}</strong> <i class="ti ti-file-type-pdf file-icon text-danger"></i>
<small class="text-muted" style="font-size: 10px;">Path: ${item.file}</small> <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> </div>
</td> </td>
<td><span class="badge bg-light text-primary border">${typeDok}</span></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)"> value="${item.file_directory_id}" ${isChecked} onchange="handleRowCheck(this)">
</td> </td>
<td> <td>
<a href="/file-download/${item.file_directory_id}" target="_blank" <div class="btn-group">
class="btn btn-sm btn-primary text-white border"> <button type="button"
<i class="ti ti-download"></i> class="btn btn-sm btn-outline-secondary btn-preview-file"
</a> 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>
<td> <td>
<div class="indent-container"> <div class="indent-container">
${lines} ${lines}
<span style="width: 24px;"></span> <i class="ti ti-file-text file-icon me-2"></i> <span style="width: 24px;"></span>
<div class="d-flex flex-column"> <div class="file-row-content">
<span class="text-dark fw-medium">${item.nama_dokumen}</span> <i class="ti ti-file-text file-icon text-danger"></i>
<small class="text-muted" style="font-size: 10px;">${fileName}</small> <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>
</div> </div>
</td> </td>
@ -651,6 +739,18 @@
} }
document.addEventListener('click', function(e){ 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'); const toggle = e.target.closest('.folder-toggle');
if (!toggle) return; if (!toggle) return;
const folderPath = toggle.getAttribute('data-folder'); const folderPath = toggle.getAttribute('data-folder');
@ -671,11 +771,84 @@
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ` return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} `
+ `${pad(d.getHours())}:${pad(d.getMinutes())}`; + `${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 katDok = @json($katDok);
const formCreate = document.getElementById('formFile'); const formCreate = document.getElementById('formFile');
const modalCreate = document.getElementById('modalCreateFile'); const modalCreate = document.getElementById('modalCreateFile');
let colCount = 1; 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){ document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return; if(!e.target.classList.contains('toggle-expired')) return;
const targetId = e.target.getAttribute('data-target'); const targetId = e.target.getAttribute('data-target');

View File

@ -200,8 +200,8 @@
<span class="small text-muted">Tampilkan</span> <span class="small text-muted">Tampilkan</span>
<select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;"> <select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;">
<option value="5">5</option> <option value="5">5</option>
<option value="10" selected>10</option> <option value="10" >10</option>
<option value="20">20</option> <option value="20" selected>20</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
</select> </select>
@ -226,7 +226,8 @@
<script> <script>
const katDok = @json($katDok); const katDok = @json($katDok);
const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja); const authUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->unitKerja);
const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
// const authSubUnitKerja = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]->sub_unit_kerja);
const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]); const mappingUnitKerjaPegawai = @json(auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]);
const authPegawai = @json(auth()->user()->objectpegawaifk); const authPegawai = @json(auth()->user()->objectpegawaifk);
const formCreate = $("#formFile") const formCreate = $("#formFile")
@ -407,6 +408,30 @@
return null; 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){ function buildRow(item){
const parts = (item.file || '').split('/'); const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-'; const fileName = parts.pop() || '-';
@ -472,6 +497,22 @@
> >
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</a> </a>
${authUnitKerja?.id === 22 ? `
<button
type="button"
class="btn btn-warning btn-sm"
onclick="rekomendasiDelete(this)"
title="Rekomendasi Hapus"
data-filename="${item.nama_dokumen || '-'}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"
data-pegawai_id_entry="${item.pegawai_id_entry || '-'}"
>
<i class="fa-solid fa-trash-can"></i>
</button>
` : ''}
</div> </div>
</td> </td>
@ -503,6 +544,7 @@
${expiryBadge} ${expiryBadge}
</div> </div>
${renderDeleteRecommendation(item)}
</div> </div>
</td> </td>
@ -1176,5 +1218,67 @@
} }
$("#previewModal").modal('show') $("#previewModal").modal('show')
} }
function rekomendasiDelete(button){
const id = $(button).data('id');
const fileName = $(button).data('filename') || 'dokumen';
const noDokumen = $(button).data('no_dokumen') || '-';
Swal.fire({
title: 'Rekomendasi hapus dokumen?',
text: `${fileName} (${noDokumen})`,
icon: 'warning',
input: 'textarea',
inputLabel: 'Catatan rekomendasi',
inputPlaceholder: 'Tulis alasan kenapa dokumen ini direkomendasikan untuk dihapus...',
inputAttributes: {
'aria-label': 'Catatan rekomendasi'
},
showCancelButton: true,
confirmButtonText: 'Kirim rekomendasi',
cancelButtonText: 'Batal',
preConfirm: (value) => {
const note = (value || '').trim();
if (!note) {
Swal.showValidationMessage('Catatan rekomendasi wajib diisi.');
}
return note;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(`/recommend-delete-file/${id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
note: result.value
})
})
.then(res => res.json())
.then(data => {
if (data.status) {
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Rekomendasi hapus berhasil dikirim.',
timer: 1800,
showConfirmButton: false
});
} else {
throw new Error(data.message || 'Gagal mengirim rekomendasi hapus.');
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
</script> </script>
@endsection @endsection

View File

@ -198,8 +198,8 @@
<span class="small text-muted">Tampilkan</span> <span class="small text-muted">Tampilkan</span>
<select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;"> <select id="tablePageSize" class="form-select form-select-sm" style="width: 80px;">
<option value="5">5</option> <option value="5">5</option>
<option value="10" selected>10</option> <option value="10">10</option>
<option value="20">20</option> <option value="20" selected>20</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
</select> </select>
@ -262,13 +262,117 @@
return [value]; return [value];
} }
function formatDateForInput(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateDisplay(dateValue) {
if (!dateValue) return '';
const date = new Date(`${dateValue}T00:00:00`);
if (Number.isNaN(date.getTime())) return '';
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
function addYearsToDate(dateValue, years) {
if (!dateValue || !years) return '';
const baseDate = new Date(`${dateValue}T00:00:00`);
if (Number.isNaN(baseDate.getTime())) return '';
const originalDay = baseDate.getDate();
baseDate.setFullYear(baseDate.getFullYear() + Number(years));
if (baseDate.getDate() !== originalDay) {
baseDate.setDate(0);
}
return formatDateForInput(baseDate);
}
function syncMasaBerlakuField(selectEl) {
if (!selectEl) return;
const fieldWrap = document.getElementById(selectEl.dataset.dateTarget);
const input = document.getElementById(selectEl.dataset.inputTarget);
const preview = document.getElementById(selectEl.dataset.previewTarget);
const baseInput = document.getElementById(selectEl.dataset.baseTarget);
if (!fieldWrap || !input) return;
if (selectEl.value === 'custom') {
fieldWrap.classList.remove('d-none');
input.disabled = false;
if (preview) preview.textContent = '';
return;
}
fieldWrap.classList.add('d-none');
input.disabled = true;
input.value = '';
if (!preview) return;
if (!selectEl.value) {
preview.textContent = '';
return;
}
if (!baseInput || !baseInput.value) {
preview.textContent = 'Pilih Tanggal Terbit untuk melihat hasil masa berlaku.';
return;
}
const expiredDate = addYearsToDate(baseInput.value, selectEl.value);
preview.textContent = expiredDate
? `Tanggal kedaluwarsa: ${formatDateDisplay(expiredDate)}`
: '';
}
function prepareExpiryInputs(formEl) {
const selects = formEl.querySelectorAll('.masa-berlaku-select');
for (const selectEl of selects) {
const option = selectEl.value;
const dateInput = document.getElementById(selectEl.dataset.inputTarget);
const baseInput = document.getElementById(selectEl.dataset.baseTarget);
if (!dateInput) continue;
if (!option) {
dateInput.value = '';
dateInput.disabled = true;
continue;
}
if (option === 'custom') {
if (!dateInput.value) {
return 'Tanggal kedaluwarsa wajib diisi saat memilih opsi Lainnya.';
}
dateInput.disabled = false;
continue;
}
if (!baseInput || !baseInput.value) {
return 'Tanggal Terbit wajib diisi saat memilih masa berlaku 1, 2, atau 3 tahun.';
}
dateInput.value = addYearsToDate(baseInput.value, option);
dateInput.disabled = false;
}
return null;
}
document.addEventListener('change', function(e){ document.addEventListener('change', function(e){
if(!e.target.classList.contains('toggle-expired')) return; if(!e.target.classList.contains('masa-berlaku-select')) return;
const targetId = e.target.getAttribute('data-target'); syncMasaBerlakuField(e.target);
if(!targetId) return; });
const fieldWrap = document.getElementById(targetId);
const input = fieldWrap.querySelector('input'); document.addEventListener('change', function(e){
input.disabled = !e.target.checked; if (!e.target.matches('input[type="date"][id^="dateActive_"]')) return;
const index = e.target.id.replace('dateActive_', '');
const selectEl = document.querySelector(`.masa-berlaku-select[data-index="${index}"]`);
if (selectEl) syncMasaBerlakuField(selectEl);
}); });
if(pageSizeSelect){ if(pageSizeSelect){
@ -349,6 +453,7 @@
formCreate.find('select').val(null).trigger('change'); formCreate.find('select').val(null).trigger('change');
formCreate.find('input[type="file"]').val(''); formCreate.find('input[type="file"]').val('');
formCreate.find('.file-name').addClass('d-none').text(''); formCreate.find('.file-name').addClass('d-none').text('');
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
resetAkreFields(0); resetAkreFields(0);
enableAkreFields(0); enableAkreFields(0);
} }
@ -544,6 +649,31 @@
return null; 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){ function buildRow(item){
const parts = (item.file || '').split('/'); const parts = (item.file || '').split('/');
const fileName = parts.pop() || '-'; const fileName = parts.pop() || '-';
@ -607,6 +737,23 @@
> >
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
</a> </a>
${authUnitKerja?.id === 22 ? `
<button
type="button"
class="btn btn-warning btn-sm"
onclick="rekomendasiDelete(this)"
title="Rekomendasi Hapus"
data-filename="${item.nama_dokumen || '-'}"
data-id="${item.file_directory_id}"
data-no_dokumen="${item.no_dokumen || '-'}"
data-tanggal_terbit="${item.tanggal_terbit || '-'}"
data-permission_file="${item.permission_file || '-'}"
data-pegawai_id_entry="${item.pegawai_id_entry || '-'}"
>
<i class="fa-solid fa-trash-can"></i>
</button>
` : ''}
</div> </div>
</td> </td>
<td class="text-nowrap">${item.no_dokumen || '-'}</td> <td class="text-nowrap">${item.no_dokumen || '-'}</td>
@ -626,6 +773,7 @@
</a> </a>
${akreBadge}${expiryBadge} ${akreBadge}${expiryBadge}
</div> </div>
${renderDeleteRecommendation(item)}
</td> </td>
<td> <td>
${item.nama_kategori || '-'} ${item.nama_kategori || '-'}
@ -1011,6 +1159,7 @@
selectOptionUnitKerjaV1(0); selectOptionUnitKerjaV1(0);
initKategoriSelect2(0); initKategoriSelect2(0);
enableAkreFields(0); enableAkreFields(0);
document.querySelectorAll('.masa-berlaku-select').forEach(syncMasaBerlakuField);
}); });
function loadSubUnitKerja(unitId){ function loadSubUnitKerja(unitId){
@ -1020,6 +1169,8 @@
url: `/select-sub-unit-kerja-mapping/${unitId}`, url: `/select-sub-unit-kerja-mapping/${unitId}`,
method: 'GET', method: 'GET',
success: function(response) { success: function(response) {
console.log(response);
if (response?.data) { if (response?.data) {
response.data.forEach(unit => { response.data.forEach(unit => {
let selected = (authSubUnitKerja && unit.id === authSubUnitKerja.objectsubunitkerjapegawaifk); let selected = (authSubUnitKerja && unit.id === authSubUnitKerja.objectsubunitkerjapegawaifk);
@ -1089,28 +1240,39 @@
placeholder="Contoh: Panduan Mencuci Tangan" required> placeholder="Contoh: Panduan Mencuci Tangan" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Tanggal Terbit</label> <label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control" <input class="form-control"
type="date" type="date"
name="data[${colCount}][date_active]"> name="data[${colCount}][date_active]"
</div> id="dateActive_${colCount}">
<div class="col-md-3"> </div>
<div class="form-check"> <div class="col-md-4">
<input class="form-check-input toggle-expired" <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
type="checkbox" <select class="form-select masa-berlaku-select"
id="hasExpired_${colCount}" name="data[${colCount}][masa_berlaku_option]"
data-target="expiredField_${colCount}"> data-index="${colCount}"
<label class="form-check-label" for="hasExpired_${colCount}">Masa Berlaku Dokumen??</label> data-date-target="expiredField_${colCount}"
</div> data-input-target="expiredInput_${colCount}"
</div> data-base-target="dateActive_${colCount}"
data-preview-target="expiredPreview_${colCount}">
<option value="">Selamanya</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="expiredPreview_${colCount}"></div>
</div>
<div class="col-md-5" id="expiredField_${colCount}"> <div class="col-md-4 d-none" id="expiredField_${colCount}">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" <input class="form-control"
type="date" type="date"
name="data[${colCount}][tgl_expired]" disabled> id="expiredInput_${colCount}"
</div> name="data[${colCount}][tgl_expired]" disabled>
</div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>
@ -1228,7 +1390,7 @@
processResults: function(data){ processResults: function(data){
return { return {
results : data?.data.map(item => ({ results : data?.data.map(item => ({
id: item.id+'/'+item.name, id: item.id,
text: item.name, text: item.name,
sub_units: item.sub_unit_kerja // kirim ke front sub_units: item.sub_unit_kerja // kirim ke front
})) }))
@ -1378,6 +1540,18 @@
e.preventDefault(); e.preventDefault();
const submitBtn = $(this).find('button[type="submit"]'); const submitBtn = $(this).find('button[type="submit"]');
submitBtn.prop('disabled', true).text('menyimpan...') submitBtn.prop('disabled', true).text('menyimpan...')
const expiryError = prepareExpiryInputs(this);
if (expiryError) {
Swal.fire({
icon: 'warning',
title: 'Validasi form',
text: expiryError
});
submitBtn.prop('disabled', false).text('Simpan');
return;
}
const formData = new FormData(this); const formData = new FormData(this);
console.log(formData); console.log(formData);
@ -1482,5 +1656,67 @@
} }
$("#previewModal").modal('show') $("#previewModal").modal('show')
} }
function rekomendasiDelete(button){
const id = $(button).data('id');
const fileName = $(button).data('filename') || 'dokumen';
const noDokumen = $(button).data('no_dokumen') || '-';
Swal.fire({
title: 'Rekomendasi hapus dokumen?',
text: `${fileName} (${noDokumen})`,
icon: 'warning',
input: 'textarea',
inputLabel: 'Catatan rekomendasi',
inputPlaceholder: 'Tulis alasan kenapa dokumen ini direkomendasikan untuk dihapus...',
inputAttributes: {
'aria-label': 'Catatan rekomendasi'
},
showCancelButton: true,
confirmButtonText: 'Kirim rekomendasi',
cancelButtonText: 'Batal',
preConfirm: (value) => {
const note = (value || '').trim();
if (!note) {
Swal.showValidationMessage('Catatan rekomendasi wajib diisi.');
}
return note;
}
}).then((result) => {
if (!result.isConfirmed) return;
fetch(`/recommend-delete-file/${id}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
note: result.value
})
})
.then(res => res.json())
.then(data => {
if (data.status) {
Swal.fire({
icon: 'success',
title: 'Berhasil',
text: data.message || 'Rekomendasi hapus berhasil dikirim.',
timer: 1800,
showConfirmButton: false
});
} else {
throw new Error(data.message || 'Gagal mengirim rekomendasi hapus.');
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: 'Gagal',
text: err.message || 'Terjadi kesalahan.'
});
});
});
}
</script> </script>
@endsection @endsection

View File

@ -46,17 +46,29 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Tanggal Terbit</label> <label class="form-label fw-semibold">Tanggal Terbit</label>
<input class="form-control" type="date" name="data[0][date_active]"> <input class="form-control" type="date" name="data[0][date_active]" id="dateActive_0">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
<input class="form-check-input toggle-expired" type="checkbox" id="hasExpired0" data-target="expiredField_0"> <select class="form-select masa-berlaku-select"
<label class="form-check-label" for="hasExpired0">Masa Berlaku Dokumen?</label> name="data[0][masa_berlaku_option]"
</div> data-index="0"
data-date-target="expiredField_0"
data-input-target="expiredInput_0"
data-base-target="dateActive_0"
data-preview-target="expiredPreview_0">
<option value="">Selamanya</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="expiredPreview_0"></div>
</div> </div>
<div class="col-md-4" id="expiredField_0"> <div class="col-md-4 d-none" id="expiredField_0">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" type="date" name="data[0][tgl_expired]" disabled> <input class="form-control" type="date" name="data[0][tgl_expired]" id="expiredInput_0" disabled>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label> <label class="form-label fw-semibold">Boleh dilihat unit lain? <span class="text-danger">*</span></label>

View File

@ -52,7 +52,19 @@
{{-- AKTIVITAS --}} {{-- AKTIVITAS --}}
<li class="nav-small-cap"><span class="hide-menu">Aktivitas</span></li> <li class="nav-small-cap"><span class="hide-menu">Aktivitas</span></li>
@php @php
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)->where('objectatasanlangsungfk', auth()->user()->objectpegawaifk)->exists(); $pegawaiId = auth()->user()->objectpegawaifk;
$userUnitIds = auth()->user()->dataUser->mappingUnitKerjaPegawai()
->where('statusenabled', true)
->pluck('objectunitkerjapegawaifk')
->unique()
->values()
->all();
$isAtasan = \App\Models\MappingUnitKerjaPegawai::where('statusenabled', true)
->where(function ($q) use ($pegawaiId) {
$q->where('objectatasanlangsungfk', $pegawaiId)
->orWhere('objectpejabatpenilaifk', $pegawaiId);
})->exists();
$isRoleApprover = in_array(51, $userUnitIds, true) || in_array(22, $userUnitIds, true);
@endphp @endphp
@if($isAtasan) @if($isAtasan)
@if(!Auth::guard('admin')->check()) @if(!Auth::guard('admin')->check())
@ -79,6 +91,10 @@
<i class="ti ti-clock"></i> <i class="ti ti-clock"></i>
<span class="hide-menu">Pengajuan</span> <span class="hide-menu">Pengajuan</span>
</div> </div>
@if($isRoleApprover)
<span class="badge bg-danger rounded-pill d-none" id="pengajuanPendingCountBadge">0</span>
@endif
</a> </a>
</li> </li>
@endif @endif
@ -114,7 +130,7 @@
</a> </a>
</li> </li>
@if(!Auth::guard('admin')->check()) @if(!Auth::guard('admin')->check())
@if(auth()->user()->dataUser->mappingUnitKerjaPegawai()->where('objectunitkerjapegawaifk', 43)->exists()) @if(auth()->user()->dataUser->mappingUnitKerjaPegawai()->whereIn('objectunitkerjapegawaifk', [43, 22])->exists())
<li class="nav-small-cap"><span class="hide-menu">Master</span></li> <li class="nav-small-cap"><span class="hide-menu">Master</span></li>
<li class="sidebar-item has-sub {{ $openMaster ? 'open' : '' }}"> <li class="sidebar-item has-sub {{ $openMaster ? 'open' : '' }}">
@ -192,10 +208,11 @@
</style> </style>
<script> <script>
const badge = document.getElementById('pendingCountBadge'); const pendingBadge = document.getElementById('pendingCountBadge');
const pengajuanBadge = document.getElementById('pengajuanPendingCountBadge');
async function countData() { async function countData() {
if (!badge) return; if (!pendingBadge && !pengajuanBadge) return;
try { try {
const res = await fetch('/data/count-pending', { const res = await fetch('/data/count-pending', {
@ -207,14 +224,24 @@
const data = await res.json(); const data = await res.json();
const count = Number(data?.count ?? 0); const count = Number(data?.count ?? 0);
badge.textContent = count; if (pendingBadge) {
badge.classList.toggle('d-none', count <= 0); pendingBadge.textContent = count;
pendingBadge.classList.toggle('d-none', count <= 0);
}
if (pengajuanBadge) {
pengajuanBadge.textContent = count;
pengajuanBadge.classList.toggle('d-none', count <= 0);
}
} catch (e) { } catch (e) {
badge.classList.add('d-none'); if (pendingBadge) pendingBadge.classList.add('d-none');
if (pengajuanBadge) pengajuanBadge.classList.add('d-none');
} }
} }
window.refreshPendingCount = countData;
countData(); countData();
// setInterval(countData, 60000); // setInterval(countData, 60000);

View File

@ -58,7 +58,7 @@
<div class="small text-muted ms-md-auto" id="tableSummary"></div> <div class="small text-muted ms-md-auto" id="tableSummary"></div>
</div> </div>
<div id="tabPengajuan"> <div id="tabPengajuan">
<div class="table-responsive" style="max-height: 55vh; overflow-y:auto; overflow-x:auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y:auto; overflow-x:auto;">
<table class="table table-sm table-hover table-striped align-middle mb-0 pending-table" id="lastUpdatedTable"> <table class="table table-sm table-hover table-striped align-middle mb-0 pending-table" id="lastUpdatedTable">
<thead> <thead>
<tr> <tr>
@ -114,5 +114,9 @@
</div> </div>
</div> </div>
@include('pendingFile.modal.view') @include('pendingFile.modal.view')
<script>
window.isKomiteMutu = @json($isKomiteMutu);
window.isTurt = @json($isTurt);
</script>
<script src="{{ ver('/js/pendingFile/index.js') }}"></script> <script src="{{ ver('/js/pendingFile/index.js') }}"></script>
@endsection @endsection

View File

@ -13,6 +13,9 @@
<!-- Body --> <!-- Body -->
<div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;"> <div class="modal-body p-2" style="min-height:250px; max-height:70vh; overflow:auto;">
<div class="d-flex justify-content-end align-items-center sticky-top bg-white z-10 d-none" id="deleteData">
<button type="button" class="btn btn-sm btn-outline-danger mb-2 me-2" id="delete-file">Hapus</button>
</div>
<div id="file-preview" <div id="file-preview"
class="text-center text-muted d-flex justify-content-center align-items-center" class="text-center text-muted d-flex justify-content-center align-items-center"
style="height:100%;"> style="height:100%;">

View File

@ -46,10 +46,26 @@
<li class="nav-item"> <li class="nav-item">
<button class="nav-link active" type="button" data-mode="pengajuan">Data Pengajuan</button> <button class="nav-link active" type="button" data-mode="pengajuan">Data Pengajuan</button>
</li> </li>
@if($isKomiteMutu || $isTurt || $isAtasan)
<li class="nav-item">
<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"> <li class="nav-item">
<button class="nav-link" type="button" data-mode="history">Log History</button> <button class="nav-link" type="button" data-mode="history">Log History</button>
</li> </li>
</ul> </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="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;"> <div class="input-group input-group-sm flex-grow-1" style="max-width:320px;">
<span class="input-group-text bg-white border-end-0"> <span class="input-group-text bg-white border-end-0">
@ -78,6 +94,14 @@
<option value="100">100</option> <option value="100">100</option>
</select> </select>
</div> </div>
<div class="d-flex align-items-center gap-2" id="pengajuanBulkActions">
<button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
Hapus dipilih (<span id="selectedCount">0</span>)
</button>
<button class="btn btn-outline-secondary btn-sm" id="clearSelectionBtn" disabled>
Reset pilihan
</button>
</div>
<button class="btn btn-outline-secondary btn-sm" onclick="refreshLog()"> <button class="btn btn-outline-secondary btn-sm" onclick="refreshLog()">
<i class="fa fa-rotate"></i> Refresh <i class="fa fa-rotate"></i> Refresh
</button> </button>
@ -88,6 +112,9 @@
<table class="table table-sm table-hover align-middle mb-0" id="tablePengajuan"> <table class="table table-sm table-hover align-middle mb-0" id="tablePengajuan">
<thead> <thead>
<tr> <tr>
<th class="text-center">
<input type="checkbox" class="form-check-input" id="selectAllPengajuan" title="Pilih semua di halaman">
</th>
<th>Aksi</th> <th>Aksi</th>
<th>No Dokumen</th> <th>No Dokumen</th>
<th>Status</th> <th>Status</th>
@ -108,7 +135,7 @@
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mt-3" id="paginationPengajuan"></div> <div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mt-3" id="paginationPengajuan"></div>
</div> </div>
<div id="tabHistory" class="d-none"> <div id="tabHistory" class="d-none">
<div class="table-responsive" style="max-height: 55vh; overflow-y:auto;"> <div class="table-responsive" style="max-height: 75vh; overflow-y:auto;">
<table class="table table-sm table-hover align-middle mb-0" id="tableHistory"> <table class="table table-sm table-hover align-middle mb-0" id="tableHistory">
<thead> <thead>
<tr> <tr>
@ -118,9 +145,7 @@
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Aksi</th> <th>Aksi</th>
<th>Tanggal Terbit</th> <th>Tanggal Aksi</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
<tbody id="tableHistoryFile"> <tbody id="tableHistoryFile">
@ -140,6 +165,9 @@
@include('dataUnit.modal.create') @include('dataUnit.modal.create')
<script> <script>
window.katDok = @json($katDok); window.katDok = @json($katDok);
window.isKomiteMutu = @json($isKomiteMutu);
window.isTurt = @json($isTurt);
window.isAtasan = @json($isAtasan);
</script> </script>
<script src="{{ ver('/js/pengajuanFile/index.js') }}"></script> <script src="{{ ver('/js/pengajuanFile/index.js') }}"></script>
@endsection @endsection

View File

@ -47,12 +47,25 @@
<input class="form-control" type="date" name="tanggal_terbit" id="edit_tanggal_terbit"> <input class="form-control" type="date" name="tanggal_terbit" id="edit_tanggal_terbit">
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-check"> <label class="form-label fw-semibold">Masa Berlaku Dokumen</label>
<input class="form-check-input toggle-expired" type="checkbox" id="edit_has_expired" data-target="edit_expired_field"> <select class="form-select masa-berlaku-select"
<label class="form-check-label" for="edit_has_expired">Masa Berlaku Dokumen?</label> name="masa_berlaku_option"
</div> id="edit_masa_berlaku_option"
data-index="edit"
data-date-target="edit_expired_field"
data-input-target="edit_tgl_expired"
data-base-target="edit_tanggal_terbit"
data-preview-target="edit_expired_preview">
<option value="">Selamanya</option>
<option value="1">1 Tahun</option>
<option value="2">2 Tahun</option>
<option value="3">3 Tahun</option>
<option value="custom">Lainnya</option>
</select>
<div class="form-text text-muted">Opsi 1, 2, dan 3 tahun dihitung dari Tanggal Terbit.</div>
<div class="form-text text-primary" id="edit_expired_preview"></div>
</div> </div>
<div class="col-md-4" id="edit_expired_field"> <div class="col-md-4 d-none" id="edit_expired_field">
<label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label> <label class="form-label fw-semibold">Tanggal Kedaluwarsa Dokumen</label>
<input class="form-control" type="date" name="tgl_expired" id="edit_tgl_expired" disabled> <input class="form-control" type="date" name="tgl_expired" id="edit_tgl_expired" disabled>
</div> </div>

View File

@ -51,7 +51,9 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::get('/select-sub-unit-kerja-mapping/{id}', [DashboardController::class, 'optionSubUnitKerjaByMapping']); Route::get('/select-sub-unit-kerja-mapping/{id}', [DashboardController::class, 'optionSubUnitKerjaByMapping']);
Route::delete('/delete-file/bulk', [DashboardController::class, 'deleteFiles']);
Route::delete('/delete-file/{id}', [DashboardController::class, 'deleteFile']); Route::delete('/delete-file/{id}', [DashboardController::class, 'deleteFile']);
Route::post('/recommend-delete-file/{id}', [DashboardController::class, 'recommendDeleteFile']);
// Route::get('/getFile/{id_unit_kerja}/{id_sub_unit_kerja}/{master_kategori_directory_id}', [DashboardController::class, 'getFile']); // Route::get('/getFile/{id_unit_kerja}/{id_sub_unit_kerja}/{master_kategori_directory_id}', [DashboardController::class, 'getFile']);
Route::post('/download-multiple', [DashboardController::class, 'downloadDataMultiple']); Route::post('/download-multiple', [DashboardController::class, 'downloadDataMultiple']);
@ -69,6 +71,10 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::get('/pengajuan-file', [DashboardController::class, 'pengajuanFile']); Route::get('/pengajuan-file', [DashboardController::class, 'pengajuanFile']);
Route::get('/datatable/pengajuan-file', [DashboardController::class, 'dataPengajuanFile']); Route::get('/datatable/pengajuan-file', [DashboardController::class, 'dataPengajuanFile']);
Route::post('/pengajuan-file/{id}/update', [DashboardController::class, 'updatePengajuanFile']); Route::post('/pengajuan-file/{id}/update', [DashboardController::class, 'updatePengajuanFile']);
Route::post('/pengajuan-file/{id}/approve-mutu', [DashboardController::class, 'approvePendingFileMutu']);
Route::post('/pengajuan-file/{id}/approve-turt', [DashboardController::class, 'approvePendingFileTurt']);
Route::post('/pengajuan-file/{id}/reject-mutu', [DashboardController::class, 'rejectPendingFileMutu']);
Route::post('/pengajuan-file/{id}/reject-turt', [DashboardController::class, 'rejectPendingFileTurt']);
// Route::middleware(['master.persetujuan'])->group(function () { // Route::middleware(['master.persetujuan'])->group(function () {
Route::get('/pending-file', [DashboardController::class, 'pendingFile']); Route::get('/pending-file', [DashboardController::class, 'pendingFile']);
@ -76,6 +82,10 @@ Route::middleware(['auth:admin,web'])->group(function(){
Route::post('/pending-file/{id}/approve', [DashboardController::class, 'approvePendingFile']); Route::post('/pending-file/{id}/approve', [DashboardController::class, 'approvePendingFile']);
Route::post('/pending-file/approve-multiple', [DashboardController::class, 'approvePendingFileMultiple']); Route::post('/pending-file/approve-multiple', [DashboardController::class, 'approvePendingFileMultiple']);
Route::post('/pending-file/{id}/reject', [DashboardController::class, 'rejectPendingFile']); Route::post('/pending-file/{id}/reject', [DashboardController::class, 'rejectPendingFile']);
Route::post('/pending-file/{id}/approve-mutu', [DashboardController::class, 'approvePendingFileMutu']);
Route::post('/pending-file/{id}/approve-turt', [DashboardController::class, 'approvePendingFileTurt']);
Route::post('/pending-file/{id}/reject-mutu', [DashboardController::class, 'rejectPendingFileMutu']);
Route::post('/pending-file/{id}/reject-turt', [DashboardController::class, 'rejectPendingFileTurt']);
Route::get('/data/count-pending', [DashboardController::class, 'countDataPending']); Route::get('/data/count-pending', [DashboardController::class, 'countDataPending']);
Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']); Route::get('/data/count-rejected', [DashboardController::class, 'countRejectedPengajuan']);
// }); // });