Compare commits

...

15 Commits
akrev2 ... main

22 changed files with 3401 additions and 419 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

@ -1,8 +1,10 @@
<?php <?php
use Illuminate\Http\Request;
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Session\TokenMismatchException;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -17,5 +19,24 @@ return Application::configure(basePath: dirname(__DIR__))
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// $exceptions->render(function (TokenMismatchException $exception, Request $request) {
$message = 'Sesi halaman sudah habis. Silakan login ulang.';
if ($request->is('login')) {
return redirect()
->route('login')
->withInput($request->only('namauser'))
->with('alertError', 'expired');
}
if (!$request->expectsJson()) {
return redirect()
->guest(route('login'))
->with('alertError', 'expired-session');
}
return response()->json([
'message' => $message,
], 419);
});
})->create(); })->create();

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}"
@ -166,8 +323,8 @@ document.addEventListener('DOMContentLoaded', () => {
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 || '-'}">${item.nama_dokumen}</a></td> data-permission_file="${item.permission_file || '-'}">${item.nama_dokumen}</a></td>
<td class="col-kategori"><div class="cell-wrap">${safeText(item.folder)}</div></td> <td class="col-kategori"><div class="cell-wrap">${safeText(item.kategori)}</div></td>
<td class="col-unit"><div class="cell-wrap">${safeText(item.part)}</div></td> <td class="col-unit"><div class="cell-wrap">${safeText(item.nama_unit)}</div></td>
<td class="text-nowrap">${tanggalTerbit}</td> <td class="text-nowrap">${tanggalTerbit}</td>
<td class="text-nowrap">${tanggalExp}</td> <td class="text-nowrap">${tanggalExp}</td>
<td class="text-nowrap">${tanggal}</td> <td class="text-nowrap">${tanggal}</td>
@ -177,6 +334,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
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) : '-';
const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-'; const tanggalTerbit = item.tanggal_terbit ? formatTanggal(item.tanggal_terbit) : '-';
@ -192,11 +350,9 @@ document.addEventListener('DOMContentLoaded', () => {
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 || '-'}">${safeText(item.nama_dokumen)}</a></td> data-permission_file="${item.permission_file || '-'}">${safeText(item.nama_dokumen)}</a></td>
<td>${safeText(item.folder)}</td> <td>${safeText(item.kategori)}</td>
<td>${safeText(item.part)}</td> <td>${safeText(item.nama_unit)}</td>
<td><span class="badge bg-info">${actionType}</span></td> <td><span class="badge bg-info">${actionType}</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>
`; `;
@ -323,7 +479,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 +560,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 +617,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 +848,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({

File diff suppressed because it is too large Load Diff

View File

@ -159,7 +159,11 @@
@csrf @csrf
@if (session()->has('alertError')) @if (session()->has('alertError'))
<div class="alert alert-danger fw-bold" role="alert"> <div class="alert alert-danger fw-bold" role="alert">
@if(session('alertError') === 'captcha') @if(session('alertError') === 'expired')
Sesi login sudah habis karena halaman terlalu lama terbuka. Silakan coba lagi.
@elseif(session('alertError') === 'expired-session')
Sesi Anda sudah habis. Silakan login kembali.
@elseif(session('alertError') === 'captcha')
Captcha salah! Captcha salah!
@elseif(session('alertError') === 'rate') @elseif(session('alertError') === 'rate')
Terlalu banyak percobaan login. Coba lagi dalam 1 menit. Terlalu banyak percobaan login. Coba lagi dalam 1 menit.
@ -177,7 +181,7 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="exampleInputEmail1" class="form-label">Username</label> <label for="exampleInputEmail1" class="form-label">Username</label>
<input type="text" name="namauser" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required> <input type="text" name="namauser" value="{{ old('namauser') }}" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" required>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="exampleInputPassword1" class="form-label">Password</label> <label for="exampleInputPassword1" class="form-label">Password</label>

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');
@ -221,7 +266,7 @@
const paginationEl = document.getElementById('paginationControls'); const paginationEl = document.getElementById('paginationControls');
// === INITIALIZATION === // === INITIALIZATION ===
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
fetchData(); loadAkreData().finally(() => fetchData());
initEventListeners(); initEventListeners();
initTooltips(); initTooltips();
}); });
@ -319,12 +364,13 @@
// MODE 1: SEARCH VIEW (Flat View - Langsung tampilkan semua file) // MODE 1: SEARCH VIEW (Flat View - Langsung tampilkan semua file)
if (keyword.length > 0) { if (keyword.length > 0) {
currentData.forEach(item => renderFileRow(tbody, item)); sortAkreItemsByJff(currentData).forEach(item => renderFileRow(tbody, item));
return; return;
} }
// MODE 2: TREE VIEW (All folders & files) // MODE 2: TREE VIEW (All folders & files)
const tree = buildTree(currentData); const orderedItems = sortAkreItemsByJff(currentData);
const tree = buildTree(orderedItems);
if (expandedFolders.size === 0) { if (expandedFolders.size === 0) {
expandAllFolders(tree, ''); expandAllFolders(tree, '');
} }
@ -348,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>
@ -355,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>
@ -398,7 +465,11 @@
} }
function renderTree(tbody, node, basePath, level) { function renderTree(tbody, node, basePath, level) {
const folderNames = Object.keys(node.folders || {}).sort(); const folderNames = Object.keys(node.folders || {}).sort((left, right) => {
const leftPath = basePath ? `${basePath}/${left}` : left;
const rightPath = basePath ? `${basePath}/${right}` : right;
return compareAkrePaths(leftPath, rightPath);
});
folderNames.forEach(folder => { folderNames.forEach(folder => {
const folderPath = basePath ? `${basePath}/${folder}` : folder; const folderPath = basePath ? `${basePath}/${folder}` : folder;
@ -490,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>
@ -646,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');
@ -666,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');
@ -683,6 +861,64 @@
let akreData = []; let akreData = [];
let akreLoaded = false; let akreLoaded = false;
let akreFlat = []; let akreFlat = [];
let akreOrderMap = new Map();
function buildAkreOrderMap() {
akreOrderMap = new Map();
(akreData || []).forEach((typeItem, typeIndex) => {
const typeName = String(typeItem?.name || '').trim();
if (!typeName) return;
akreOrderMap.set(typeName, [typeIndex, -1, -1]);
const segments = Array.isArray(typeItem?.segment) ? typeItem.segment : [];
segments.forEach((segment, segmentIndex) => {
const segmentName = String(segment?.name || '').trim();
if (!segmentName) return;
const segmentPath = `${typeName}/${segmentName}`;
akreOrderMap.set(segmentPath, [typeIndex, segmentIndex, -1]);
const children = Array.isArray(segment?.turunan) ? segment.turunan : [];
children.forEach((child, childIndex) => {
const childName = String(child?.name || '').trim();
if (!childName) return;
akreOrderMap.set(`${segmentPath}/${childName}`, [typeIndex, segmentIndex, childIndex]);
});
});
});
}
function getAkreFolderPathFromFile(filePath) {
const parts = String(filePath || '').split('/').filter(Boolean);
if (parts.length <= 1) return '';
parts.pop();
return parts.join('/');
}
function compareAkrePaths(leftPath, rightPath) {
const leftOrder = akreOrderMap.get(leftPath) || [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
const rightOrder = akreOrderMap.get(rightPath) || [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
for (let i = 0; i < 3; i++) {
if (leftOrder[i] !== rightOrder[i]) {
return leftOrder[i] - rightOrder[i];
}
}
return leftPath.localeCompare(rightPath, undefined, { numeric: true, sensitivity: 'base' });
}
function sortAkreItemsByJff(items = []) {
return [...items].sort((left, right) => {
const leftFolderPath = getAkreFolderPathFromFile(left?.file || '');
const rightFolderPath = getAkreFolderPathFromFile(right?.file || '');
const pathCompare = compareAkrePaths(leftFolderPath, rightFolderPath);
if (pathCompare !== 0) return pathCompare;
const leftName = String(left?.nama_dokumen || left?.file || '').toLowerCase();
const rightName = String(right?.nama_dokumen || right?.file || '').toLowerCase();
return leftName.localeCompare(rightName, undefined, { numeric: true, sensitivity: 'base' });
});
}
function loadAkreData(){ function loadAkreData(){
if(akreLoaded) return Promise.resolve(akreData); if(akreLoaded) return Promise.resolve(akreData);
@ -691,12 +927,14 @@
.then(data => { .then(data => {
akreData = Array.isArray(data) ? data : []; akreData = Array.isArray(data) ? data : [];
akreFlat = []; akreFlat = [];
buildAkreOrderMap();
akreLoaded = true; akreLoaded = true;
return akreData; return akreData;
}) })
.catch(() => { .catch(() => {
akreData = []; akreData = [];
akreFlat = []; akreFlat = [];
akreOrderMap = new Map();
akreLoaded = true; akreLoaded = true;
return akreData; return akreData;
}); });

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){
@ -308,7 +412,7 @@
cache: true cache: true
} }
}); });
} }
if (kategoriSelect) { if (kategoriSelect) {
$('#tableKategori').select2({ $('#tableKategori').select2({
@ -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

@ -21,6 +21,55 @@
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<style> <style>
html,
body,
.body-wrapper,
.body-wrapper-inner,
.container-fluid {
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: rgba(148, 163, 184, 0.5) transparent;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar,
.body-wrapper::-webkit-scrollbar,
.body-wrapper-inner::-webkit-scrollbar,
.container-fluid::-webkit-scrollbar {
width: 6px;
height: 6px;
}
html::-webkit-scrollbar-track,
body::-webkit-scrollbar-track,
.body-wrapper::-webkit-scrollbar-track,
.body-wrapper-inner::-webkit-scrollbar-track,
.container-fluid::-webkit-scrollbar-track {
background: transparent;
border-radius: 999px;
}
html::-webkit-scrollbar-thumb,
body::-webkit-scrollbar-thumb,
.body-wrapper::-webkit-scrollbar-thumb,
.body-wrapper-inner::-webkit-scrollbar-thumb,
.container-fluid::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.5);
border-radius: 999px;
border: 1px solid transparent;
background-clip: padding-box;
}
html::-webkit-scrollbar-thumb:hover,
body::-webkit-scrollbar-thumb:hover,
.body-wrapper::-webkit-scrollbar-thumb:hover,
.body-wrapper-inner::-webkit-scrollbar-thumb:hover,
.container-fluid::-webkit-scrollbar-thumb:hover {
background: rgba(100, 116, 139, 0.68);
border: 1px solid transparent;
background-clip: padding-box;
}
.modal, .modal,
.modal-content, .modal-content,
.modal-body, .modal-body,

View File

@ -52,7 +52,25 @@
{{-- 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();
if($userUnitIds){
$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);
}else{
$isAtasan = null;
}
@endphp @endphp
@if($isAtasan) @if($isAtasan)
@if(!Auth::guard('admin')->check()) @if(!Auth::guard('admin')->check())
@ -79,6 +97,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 +136,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 +214,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 +230,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

@ -1,3 +1,22 @@
{{-- @php
$globalUpdateItems = [
[
'date' => '29 Juni 2026',
'title' => 'Metadata preview dokumen diperluas',
'description' => 'Modal preview sekarang menampilkan nomor dokumen, tanggal terbit, tanggal kedaluwarsa, tanggal upload, akses dokumen, jenis dokumen akreditasi/non-akreditasi, dan kategori.',
],
[
'date' => '29 Juni 2026',
'title' => 'Nama unit ditampilkan lebih jelas',
'description' => 'Data persetujuan dan pengajuan file sekarang memakai nama unit langsung dari tabel unit agar lebih mudah dibaca.',
],
[
'date' => '29 Juni 2026',
'title' => 'Perbaikan query persetujuan',
'description' => 'Query halaman persetujuan dan pengajuan file dirapikan supaya aman saat join tabel dan tidak gagal karena kolom ambigu.',
],
];
@endphp --}}
<style> <style>
.message-body { .message-body {
@ -57,7 +76,12 @@
</li> </li>
</ul> </ul>
<div class="navbar-collapse justify-content-end px-0" id="navbarNav"> <div class="navbar-collapse justify-content-end px-0" id="navbarNav">
<ul class="navbar-nav flex-row ms-auto align-items-center justify-content-end"> <ul class="navbar-nav flex-row ms-auto align-items-center justify-content-end">
{{-- <li class="nav-item me-2">
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#updatesInfoModal">
<i class="fa-solid fa-bullhorn me-1"></i> Info Update
</button>
</li> --}}
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link position-relative" href="javascript:void(0)" id="dropExpired" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link position-relative" href="javascript:void(0)" id="dropExpired" data-bs-toggle="dropdown" aria-expanded="false">
<span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;"> <span class="d-inline-flex align-items-center justify-content-center rounded-circle bg-light" style="width:46px;height:46px;">
@ -177,6 +201,33 @@
</div> </div>
</div> </div>
</div> </div>
{{-- <div class="modal fade" id="updatesInfoModal" tabindex="-1" aria-labelledby="updatesInfoModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="updatesInfoModalLabel">Info Update Terbaru</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="d-flex flex-column gap-3">
@foreach ($globalUpdateItems as $update)
<div class="border rounded p-3">
<div class="d-flex flex-column flex-md-row align-items-md-center justify-content-between gap-2 mb-2">
<h6 class="mb-0">{{ $update['title'] }}</h6>
<span class="badge bg-light text-dark border">{{ $update['date'] }}</span>
</div>
<div class="text-muted small">{{ $update['description'] }}</div>
</div>
@endforeach
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div> --}}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';

View File

@ -1,6 +1,5 @@
@extends('layout.main') @extends('layout.main')
@section('body_main') @section('body_main')
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="card"> <div class="card">
@ -58,7 +57,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>
@ -96,9 +95,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">
@ -114,5 +111,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>
@ -95,8 +122,6 @@
<th>Nama</th> <th>Nama</th>
<th>Kategori</th> <th>Kategori</th>
<th>Unit</th> <th>Unit</th>
<th>Tanggal Terbit</th>
<th>Tanggal Kedaluwarsa Dokumen</th>
<th>Tanggal Upload</th> <th>Tanggal Upload</th>
</tr> </tr>
</thead> </thead>
@ -108,7 +133,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 +143,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 +163,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

@ -1,7 +1,7 @@
<?php <?php
use App\Http\Controllers\AksesFileController; use App\Http\Controllers\AksesFileController;
use App\Http\Controllers\AkreditasiInstrumenController; use App\Http\Controllers\AkreditasiInstrumenController;
use App\Http\Controllers\AuthController; use App\Http\Controllers\AuthController;
use App\Http\Controllers\DashboardController; use App\Http\Controllers\DashboardController;
use App\Http\Controllers\MasterKategoriController; use App\Http\Controllers\MasterKategoriController;
@ -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']);
// }); // });