2026-04-20 13:02:16 +07:00

428 lines
16 KiB
PHP

<style>
.message-body {
max-height: 360px;
overflow-y: auto;
}
.message-body .dropdown-item {
display: flex;
align-items: flex-start;
gap: 1px;
}
.message-body .dropdown-item div {
white-space: normal;
word-break: break-word;
}
.message-body .dropdown-item:hover {
background-color: #f0f2f5;
}
/* DOT */
.nav-link {
position: relative;
}
.notif-dot {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
background-color: red;
border-radius: 50%;
z-index: 10;
animation: pulse 1.5s infinite;
}
#expiredDot {
background-color: orange;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0.6; }
100% { transform: scale(1); opacity: 1; }
}
</style>
<header class="app-header">
<nav class="navbar navbar-expand-lg navbar-light">
<ul class="navbar-nav">
<li class="nav-item d-block d-xl-none">
<a class="nav-link sidebartoggler " id="headerCollapse" href="javascript:void(0)">
<i class="ti ti-menu-2"></i>
</a>
</li>
</ul>
<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">
<li class="nav-item dropdown">
<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;">
<i class="ti ti-alert-circle text-primary"></i>
</span>
<span id="expiredDot" class="notif-dot d-none">
</span>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="dropExpired" style="min-width: 320px;">
<div class="d-flex align-items-center justify-content-between px-3 border-bottom">
<span class="fw-semibold">Notifikasi Expired</span>
<button type="button" class="btn btn-sm btn-outline-primary my-2" id="expiredOpenDetailBtn">
Detail
</button>
</div>
<div class="message-body" id="expiredNotifList">
<div class="dropdown-item text-muted small">Memuat...</div>
</div>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link position-relative" href="javascript:void(0)" id="dropNotif" 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;">
<i class="ti ti-bell text-primary"></i>
</span>
<span id="notifCountBadge" class="position-absolute top-0 start-50 badge rounded-pill bg-danger d-none">
0
</span>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="dropNotif" style="min-width: 320px;">
<div class="d-flex align-items-center justify-content-between px-3 border-bottom">
<span class="fw-semibold">Notifikasi</span>
<span class="small text-muted" id="notifCountText">0 baru</span>
</div>
<div class="message-body" id="notifList">
<div class="dropdown-item text-muted small">Memuat...</div>
</div>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link " href="javascript:void(0)" id="drop2" data-bs-toggle="dropdown"
aria-expanded="false">
<img src="./assets/images/profile/user-1.jpg" alt="" width="35" height="35" class="rounded-circle">
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up" aria-labelledby="drop2">
<div class="message-body">
<a href="javascript:void(0)" class="d-flex align-items-center gap-2 dropdown-item">
<i class="ti ti-user fs-6"></i>
<p class="mb-0 fs-3">{{ auth()->user()->namauser ?? 'admin' }}</p>
</a>
<form action="/logout" method="POST">
@csrf
<button type="submit" class="btn btn-outline-primary mx-3 mt-2 d-block">Logout</button>
</form>
</div>
</div>
</li>
</ul>
</div>
</nav>
</header>
<!-- Modal: Detail Dokumen Akan Expired -->
<div class="modal fade" id="expiredDetailModal" tabindex="-1" aria-labelledby="expiredDetailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="expiredDetailModalLabel">Detail Notifikasi Dokumen Akan Expired</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-2 align-items-end mb-3">
<div class="col-12 col-md-4">
<label class="form-label mb-1">Akan expired (hari)</label>
<input type="number" class="form-control" id="expiredFilterDaysMax" min="0" step="1" value="30">
</div>
<div class="col-12 col-md-4 d-flex gap-2">
<button type="button" class="btn btn-primary flex-grow-1" id="expiredApplyFilterBtn">Terapkan</button>
<button type="button" class="btn btn-outline-secondary" id="expiredResetFilterBtn">Reset</button>
</div>
</div>
<div class="d-flex align-items-center justify-content-between mb-2">
<div class="small text-muted" id="expiredDetailInfo">-</div>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" id="expiredDetailPrevBtn">Prev</button>
<span class="small" id="expiredDetailPageText">1</span>
<button type="button" class="btn btn-sm btn-outline-secondary" id="expiredDetailNextBtn">Next</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead>
<tr>
<th style="width: 56px;">#</th>
<th>Unit</th>
<th>No Dokumen</th>
<th>Nama Dokumen</th>
<th style="width: 140px;">Tgl Expired</th>
<th style="width: 140px;">Sisa (hari)</th>
<th style="width: 140px;">Aksi</th>
</tr>
</thead>
<tbody id="expiredDetailTbody">
<tr>
<td colspan="7" class="text-muted small">Memuat...</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
function setList(listEl, items, itemClass = '') {
if (!listEl) return;
if (!items.length) {
listEl.innerHTML = '<div class="dropdown-item text-muted small">Tidak ada notifikasi</div>';
return;
}
listEl.innerHTML = items.map(item => `
<a href="${item.url || '#'}" class="dropdown-item d-flex align-items-start gap-2 ${itemClass}" data-notif-id="${item.id ?? ''}">
<span class="badge ${item.is_read ? 'bg-secondary' : 'bg-primary'} rounded-circle"
style="width:10px;height:10px;margin-top:6px;"></span>
<div>
<div class="fw-semibold">${item.text_notifikasi || '-'}</div>
<div class="small text-muted">${item.created_at || ''}</div>
</div>
</a>
`).join('');
}
function hydrateNotification(endpoint, els, errorText) {
fetch(endpoint)
.then(r => r.json())
.then(res => {
const unread = res?.status ? Number(res.unread || 0) : 0;
const items = res?.status ? (res.data || []) : [];
// 🔴 HANDLE DOT
if (els.dotEl) {
if (unread > 0) {
els.dotEl.classList.remove('d-none');
} else {
els.dotEl.classList.add('d-none');
}
}
setList(els.listEl, items, els.itemClass || '');
})
.catch(() => {
if (els.listEl) {
els.listEl.innerHTML = `<div class="dropdown-item text-muted small">${errorText}</div>`;
}
});
}
function attachMarkRead(toggleEl, els, endpoint) {
if (!toggleEl) return;
toggleEl.addEventListener('show.bs.dropdown', () => {
fetch(endpoint, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
}
}).then(() => {
if (els.dotEl) els.dotEl.classList.add('d-none');
}).catch(() => {});
});
}
// 🔴 EXPIRED
const expiredEls = {
listEl: document.getElementById('expiredNotifList'),
dotEl: document.getElementById('expiredDot'),
itemClass: 'js-expired-notif-item'
};
hydrateNotification('/data/expired-notifications', expiredEls, 'Gagal memuat notifikasi expired');
attachMarkRead(
document.getElementById('dropExpired'),
expiredEls,
'/data/expired-notifications/read'
);
// 🔔 NOTIF
const notifEls = {
listEl: document.getElementById('notifList'),
dotEl: document.getElementById('notifDot')
};
hydrateNotification('/data/notifications', notifEls, 'Gagal memuat notifikasi');
attachMarkRead(
document.getElementById('dropNotif'),
notifEls,
'/data/notifications/read'
);
// 📋 DETAIL MODAL (EXPIRED)
const expiredDetailModalEl = document.getElementById('expiredDetailModal');
const expiredDetailModal = (expiredDetailModalEl && window.bootstrap?.Modal)
? new bootstrap.Modal(expiredDetailModalEl)
: null;
const expiredState = {
docId: null,
page: 1,
perPage: 10,
};
const expiredDetailTbody = document.getElementById('expiredDetailTbody');
const expiredDetailInfo = document.getElementById('expiredDetailInfo');
const expiredDetailPageText = document.getElementById('expiredDetailPageText');
function setExpiredLoading(text = 'Memuat...') {
if (!expiredDetailTbody) return;
expiredDetailTbody.innerHTML = `
<tr>
<td colspan="7" class="text-muted small">${text}</td>
</tr>
`;
}
function buildExpiredDetailParams() {
const daysMax = document.getElementById('expiredFilterDaysMax')?.value ?? '';
const params = new URLSearchParams();
params.set('page', String(expiredState.page));
params.set('per_page', String(expiredState.perPage));
if (expiredState.docId) params.set('doc_id', String(expiredState.docId));
if (daysMax !== '' && daysMax !== null) params.set('days_left_max', String(daysMax));
return params;
}
function renderExpiredDetail(rows, meta) {
const total = Number(meta?.total || 0);
const perPage = Number(meta?.per_page || expiredState.perPage);
const page = Number(meta?.page || expiredState.page);
const maxPage = total ? Math.ceil(total / perPage) : 1;
if (expiredDetailPageText) expiredDetailPageText.textContent = String(page);
if (expiredDetailInfo) {
const start = total ? ((page - 1) * perPage + 1) : 0;
const end = Math.min(page * perPage, total);
expiredDetailInfo.textContent = total ? `Menampilkan ${start}-${end} dari ${total} data` : 'Tidak ada data';
}
const prevBtn = document.getElementById('expiredDetailPrevBtn');
const nextBtn = document.getElementById('expiredDetailNextBtn');
if (prevBtn) prevBtn.disabled = page <= 1;
if (nextBtn) nextBtn.disabled = page >= maxPage;
if (!expiredDetailTbody) return;
if (!rows?.length) {
expiredDetailTbody.innerHTML = `
<tr>
<td colspan="7" class="text-muted small">Tidak ada data</td>
</tr>
`;
return;
}
expiredDetailTbody.innerHTML = rows.map((row, idx) => {
const no = (page - 1) * perPage + (idx + 1);
const unit = row.unit_name || '-';
const noDok = row.no_dokumen || '-';
const nama = row.nama_dokumen || '-';
const tgl = row.tgl_expired_label || '-';
const daysLeft = (row.days_left === null || row.days_left === undefined) ? '-' : String(row.days_left);
const previewUrl = row.preview_url || '#';
return `
<tr>
<td>${no}</td>
<td>${unit}</td>
<td>${noDok}</td>
<td>${nama}</td>
<td>${tgl}</td>
<td>${daysLeft}</td>
<td>
<a class="btn btn-sm btn-outline-primary" href="${previewUrl}" target="_blank" rel="noopener">Preview</a>
</td>
</tr>
`;
}).join('');
}
async function loadExpiredDetail() {
setExpiredLoading();
const params = buildExpiredDetailParams();
try {
const r = await fetch(`/data/expired-notifications/detail?${params.toString()}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
const res = await r.json();
if (!res?.status) {
setExpiredLoading(res?.message || 'Gagal memuat data');
return;
}
renderExpiredDetail(res.data || [], res.meta || {});
} catch (e) {
setExpiredLoading('Gagal memuat data');
}
}
function openExpiredDetail(docId = null) {
expiredState.docId = docId ? Number(docId) : null;
expiredState.page = 1;
if (expiredState.docId) {
const daysMaxEl = document.getElementById('expiredFilterDaysMax');
if (daysMaxEl) daysMaxEl.value = '';
}
if (expiredDetailModal) expiredDetailModal.show();
loadExpiredDetail();
}
document.getElementById('expiredOpenDetailBtn')?.addEventListener('click', () => openExpiredDetail(null));
document.getElementById('expiredNotifList')?.addEventListener('click', (e) => {
const item = e.target.closest('a.js-expired-notif-item');
if (!item) return;
e.preventDefault();
openExpiredDetail(item.dataset.notifId || null);
});
document.getElementById('expiredApplyFilterBtn')?.addEventListener('click', () => {
expiredState.page = 1;
loadExpiredDetail();
});
document.getElementById('expiredResetFilterBtn')?.addEventListener('click', () => {
const daysMaxEl = document.getElementById('expiredFilterDaysMax');
if (daysMaxEl) daysMaxEl.value = '30';
expiredState.docId = null;
expiredState.page = 1;
loadExpiredDetail();
});
document.getElementById('expiredDetailPrevBtn')?.addEventListener('click', () => {
if (expiredState.page <= 1) return;
expiredState.page -= 1;
loadExpiredDetail();
});
document.getElementById('expiredDetailNextBtn')?.addEventListener('click', () => {
expiredState.page += 1;
loadExpiredDetail();
});
});
</script>