add expired dokumen
This commit is contained in:
parent
1b07699476
commit
8c0aecd4aa
@ -144,16 +144,40 @@ class DashboardController extends Controller
|
||||
|
||||
public function dataUnitInternal(){
|
||||
$perPage = (int) request('per_page', 10);
|
||||
// $authUnitId = auth()->user()->dataUser?->mappingUnitKerjaPegawai[0]?->objectunitkerjapegawaifk;
|
||||
$userId = auth()->user()->dataUser->id ?? 937;
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where('objectpegawaifk', $userId)
|
||||
$user = auth()->user();
|
||||
$isAdmin = auth('admin')->check();
|
||||
$pegawaiId = $user?->dataUser?->id;
|
||||
|
||||
$unitIds = [];
|
||||
if (!$isAdmin) {
|
||||
if (!$pegawaiId) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Pegawai tidak ditemukan',
|
||||
'data' => [],
|
||||
'kategori_list' => [],
|
||||
'pagination' => [
|
||||
'current_page' => 1,
|
||||
'next_page' => null,
|
||||
'has_more' => false,
|
||||
'last_page' => 1,
|
||||
'per_page' => $perPage,
|
||||
'total' => 0,
|
||||
],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where('objectpegawaifk', $pegawaiId)
|
||||
->get(['objectunitkerjapegawaifk']);
|
||||
$unitIds = $mapping->pluck('objectunitkerjapegawaifk')
|
||||
->filter() // buang null
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
|
||||
$unitIds = $mapping->pluck('objectunitkerjapegawaifk')
|
||||
->filter(fn ($v) => $v !== null && $v !== '')
|
||||
->map(fn ($v) => (int) $v)
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
$keyword = request('keyword');
|
||||
$kategori = request('kategori');
|
||||
$kategoriHeader = request('kategori_header');
|
||||
@ -202,11 +226,14 @@ class DashboardController extends Controller
|
||||
->where('statusenabled', true)
|
||||
->where('status_action', 'approved');
|
||||
|
||||
if(in_array(22, $unitIds)){
|
||||
}elseif(auth()->user()->username === "admin.turt"){
|
||||
|
||||
}else{
|
||||
$baseQuery = $baseQuery->whereIn('id_unit_kerja', $unitIds);
|
||||
if (!$isAdmin) {
|
||||
if (in_array(22, $unitIds, true)) {
|
||||
// akses semua unit
|
||||
} elseif (($user?->username === 'admin.turt')) {
|
||||
// akses semua unit (exception user)
|
||||
} else {
|
||||
$baseQuery = $baseQuery->whereIn('id_unit_kerja', $unitIds);
|
||||
}
|
||||
}
|
||||
|
||||
$query = (clone $baseQuery)
|
||||
@ -235,6 +262,7 @@ class DashboardController extends Controller
|
||||
});
|
||||
})
|
||||
->when($keyword, function ($q) use ($keyword) {
|
||||
|
||||
$q->where(function ($sub) use ($keyword) {
|
||||
$sub->where('nama_dokumen', 'ILIKE', "%{$keyword}%")
|
||||
->orWhere('no_dokumen', 'ILIKE', "%{$keyword}%");
|
||||
@ -1312,10 +1340,17 @@ class DashboardController extends Controller
|
||||
'data' => $file
|
||||
]);
|
||||
}
|
||||
$userUnitIds = auth()->user()
|
||||
->dataUser
|
||||
->mappingUnitKerjaPegawai
|
||||
->pluck('objectunitkerjapegawaifk');
|
||||
|
||||
$user = auth()->user();
|
||||
$isAdmin = auth('admin')->check();
|
||||
if ($isAdmin || ($user?->username === 'admin.turt')) {
|
||||
return view('pdf.index', [
|
||||
'id' => $id,
|
||||
'data' => $file
|
||||
]);
|
||||
}
|
||||
|
||||
$userUnitIds = $user?->dataUser?->mappingUnitKerjaPegawai?->pluck('objectunitkerjapegawaifk') ?? collect();
|
||||
|
||||
// cek apakah unit file termasuk unit user
|
||||
if ($userUnitIds->contains($file->id_unit_kerja)) {
|
||||
@ -1962,6 +1997,14 @@ class DashboardController extends Controller
|
||||
public function notifkasiList()
|
||||
{
|
||||
try {
|
||||
if (auth('admin')->check()) {
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'data' => [],
|
||||
'unread' => 0
|
||||
]);
|
||||
}
|
||||
|
||||
$pegawaiId = auth()->user()?->dataUser?->id;
|
||||
if (!$pegawaiId) {
|
||||
return response()->json([
|
||||
@ -1996,6 +2039,13 @@ class DashboardController extends Controller
|
||||
public function notifkasiMarkRead()
|
||||
{
|
||||
try {
|
||||
if (auth('admin')->check()) {
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'Notifikasi ditandai dibaca'
|
||||
]);
|
||||
}
|
||||
|
||||
$pegawaiId = auth()->user()?->dataUser?->id;
|
||||
if (!$pegawaiId) {
|
||||
return response()->json([
|
||||
@ -2020,6 +2070,228 @@ class DashboardController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function expiredNotifkasiList()
|
||||
{
|
||||
try {
|
||||
$user = auth()->user();
|
||||
$pegawaiId = $user?->dataUser?->id;
|
||||
$isAdmin = auth('admin')->check();
|
||||
$unitIds = [];
|
||||
if (!$isAdmin) {
|
||||
// if (!$pegawaiId) {
|
||||
// return response()->json([
|
||||
// 'status' => false,
|
||||
// 'message' => 'Pegawai tidak ditemukan',
|
||||
// 'data' => [],
|
||||
// 'unread' => 0
|
||||
// ], 404);
|
||||
// }
|
||||
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where('objectpegawaifk', $pegawaiId)
|
||||
->get(['objectunitkerjapegawaifk']);
|
||||
|
||||
$unitIds = $mapping->pluck('objectunitkerjapegawaifk')
|
||||
->filter(fn ($v) => $v !== null && $v !== '')
|
||||
->map(fn ($v) => (int) $v)
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$baseQuery = FileDirectory::query()
|
||||
->where('statusenabled', true)
|
||||
->where('status_action', 'approved')
|
||||
->whereNotNull('tgl_expired')
|
||||
->whereBetween('tgl_expired', [
|
||||
now()->startOfDay(),
|
||||
now()->addMonth()->endOfDay()
|
||||
]);
|
||||
|
||||
if (!$isAdmin && !in_array(22, $unitIds, true) && ($user?->username !== 'admin.turt')) {
|
||||
$baseQuery->whereIn('id_unit_kerja', $unitIds);
|
||||
}
|
||||
|
||||
$seenAtRaw = session('expired_notif_seen_at');
|
||||
$seenAt = $seenAtRaw ? Carbon::parse($seenAtRaw) : null;
|
||||
$unreadQuery = (clone $baseQuery);
|
||||
if ($seenAt) {
|
||||
$unreadQuery->whereDate('tgl_expired', '>=', $seenAt->toDateString());
|
||||
}
|
||||
$unread = (int) $unreadQuery->count();
|
||||
|
||||
$rows = (clone $baseQuery)
|
||||
->orderBy('tgl_expired', 'desc')
|
||||
->orderBy('entry_at', 'desc')
|
||||
->limit(20)
|
||||
->get([
|
||||
'file_directory_id',
|
||||
'nama_dokumen',
|
||||
'no_dokumen',
|
||||
'tgl_expired',
|
||||
'file',
|
||||
]);
|
||||
$items = $rows->map(function ($row) use ($seenAt) {
|
||||
$expiredAt = $row->tgl_expired ? Carbon::parse($row->tgl_expired)->format('d/m/Y') : '-';
|
||||
$namaDokumen = $row->nama_dokumen ?: ($row->no_dokumen ?: 'Dokumen');
|
||||
$text = "Dokumen '{$namaDokumen}' expired pada {$expiredAt}";
|
||||
$isRead = false;
|
||||
if ($seenAt && $row->tgl_expired) {
|
||||
$isRead = Carbon::parse($row->tgl_expired)->toDateString() < $seenAt->toDateString();
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $row->file_directory_id,
|
||||
'text_notifikasi' => $text,
|
||||
'url' => url("/file-preview/{$row->file_directory_id}"),
|
||||
'is_read' => $isRead,
|
||||
'created_at' => $expiredAt,
|
||||
'meta' => [
|
||||
'no_dokumen' => $row->no_dokumen,
|
||||
'nama_dokumen' => $row->nama_dokumen,
|
||||
'tgl_expired' => $row->tgl_expired,
|
||||
'file' => $row->file,
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'data' => $items,
|
||||
'unread' => $unread,
|
||||
]);
|
||||
} catch (\Throwable $th) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Terdapat kesalahan!',
|
||||
'data' => [],
|
||||
'unread' => 0
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function expiredNotifkasiDetail(Request $request)
|
||||
{
|
||||
try {
|
||||
$user = auth()->user();
|
||||
$pegawaiId = $user?->dataUser?->id;
|
||||
$isAdmin = auth('admin')->check();
|
||||
|
||||
$unitIds = [];
|
||||
if (!$isAdmin) {
|
||||
$mapping = MappingUnitKerjaPegawai::where('statusenabled', true)
|
||||
->where('objectpegawaifk', $pegawaiId)
|
||||
->get(['objectunitkerjapegawaifk']);
|
||||
|
||||
$unitIds = $mapping->pluck('objectunitkerjapegawaifk')
|
||||
->filter(fn ($v) => $v !== null && $v !== '')
|
||||
->map(fn ($v) => (int) $v)
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$docId = (int) $request->query('doc_id', 0);
|
||||
$daysLeftMinRaw = $request->query('days_left_min');
|
||||
$daysLeftMaxRaw = $request->query('days_left_max');
|
||||
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$perPage = (int) $request->query('per_page', 10);
|
||||
$perPage = max(5, min(50, $perPage));
|
||||
|
||||
$today = Carbon::now()->startOfDay();
|
||||
$query = FileDirectory::query()
|
||||
->where('statusenabled', true)
|
||||
->where('status_action', 'approved')
|
||||
->whereNotNull('tgl_expired')
|
||||
->whereDate('tgl_expired', '>=', $today->toDateString());
|
||||
|
||||
if (!$isAdmin && !in_array(22, $unitIds, true) && ($user?->username !== 'admin.turt')) {
|
||||
$query->whereIn('id_unit_kerja', $unitIds);
|
||||
}
|
||||
|
||||
if ($docId > 0) {
|
||||
$query->where('file_directory_id', $docId);
|
||||
}
|
||||
|
||||
$daysLeftMin = null;
|
||||
if ($daysLeftMinRaw !== null && $daysLeftMinRaw !== '') {
|
||||
$daysLeftMin = max(0, (int) $daysLeftMinRaw);
|
||||
$query->whereDate('tgl_expired', '>=', (clone $today)->addDays($daysLeftMin)->toDateString());
|
||||
}
|
||||
|
||||
$daysLeftMax = null;
|
||||
if ($daysLeftMaxRaw !== null && $daysLeftMaxRaw !== '') {
|
||||
$daysLeftMax = max(0, (int) $daysLeftMaxRaw);
|
||||
$query->whereDate('tgl_expired', '<=', (clone $today)->addDays($daysLeftMax)->toDateString());
|
||||
}
|
||||
|
||||
if ($daysLeftMin === null && $daysLeftMax === null && $docId <= 0) {
|
||||
$query->whereDate('tgl_expired', '<=', (clone $today)->addMonth()->endOfDay()->toDateString());
|
||||
}
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query
|
||||
->orderBy('tgl_expired', 'asc')
|
||||
->orderBy('entry_at', 'desc')
|
||||
->forPage($page, $perPage)
|
||||
->get([
|
||||
'file_directory_id',
|
||||
'nama_dokumen',
|
||||
'no_dokumen',
|
||||
'tgl_expired',
|
||||
'id_unit_kerja'
|
||||
]);
|
||||
$data = $rows->map(function ($row) use ($today) {
|
||||
$expiredAt = $row->tgl_expired ? Carbon::parse($row->tgl_expired)->startOfDay() : null;
|
||||
$daysLeft = $expiredAt ? $today->diffInDays($expiredAt, false) : null;
|
||||
|
||||
return [
|
||||
'id' => $row->file_directory_id,
|
||||
'no_dokumen' => $row->no_dokumen,
|
||||
'nama_dokumen' => $row->nama_dokumen,
|
||||
'unit_name' => $row->unit?->name,
|
||||
'tgl_expired' => $expiredAt ? $expiredAt->toDateString() : null,
|
||||
'tgl_expired_label' => $expiredAt ? $expiredAt->format('d/m/Y') : '-',
|
||||
'days_left' => $daysLeft,
|
||||
'preview_url' => url("/full-preview/{$row->file_directory_id}"),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'data' => $data,
|
||||
'meta' => [
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => $total,
|
||||
],
|
||||
]);
|
||||
} catch (\Throwable $th) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Terdapat kesalahan!',
|
||||
'data' => [],
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function expiredNotifkasiMarkRead()
|
||||
{
|
||||
try {
|
||||
session(['expired_notif_seen_at' => now()->toISOString()]);
|
||||
|
||||
return response()->json([
|
||||
'status' => true,
|
||||
'message' => 'Notifikasi expired ditandai dibaca'
|
||||
]);
|
||||
} catch (\Throwable $th) {
|
||||
return response()->json([
|
||||
'status' => false,
|
||||
'message' => 'Terdapat kesalahan!'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function pengajuanFile(){
|
||||
$katDok = MasterKategori::where('statusenabled', true)
|
||||
->select('master_kategori_directory_id', 'nama_kategori_directory')
|
||||
|
||||
@ -1,41 +1,50 @@
|
||||
<style>
|
||||
/* ===== NOTIFIKASI STYLE FACEBOOK ===== */
|
||||
<style>
|
||||
|
||||
.message-body {
|
||||
max-height: 360px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* item notif */
|
||||
|
||||
.message-body .dropdown-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
.message-body .dropdown-item div {
|
||||
white-space: normal;
|
||||
border-radius: 6px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* jarak antar notif */
|
||||
.message-body .dropdown-item + .dropdown-item {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* judul teks */
|
||||
.message-body .notif-text {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* waktu */
|
||||
.message-body .notif-time {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* hover ala Facebook */
|
||||
.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">
|
||||
@ -50,7 +59,27 @@
|
||||
<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="drop1" 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;">
|
||||
<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>
|
||||
@ -58,7 +87,7 @@
|
||||
0
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end dropdown-menu-animate-up shadow" aria-labelledby="drop1" style="min-width: 320px;">
|
||||
<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>
|
||||
@ -91,68 +120,308 @@
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const listEl = document.getElementById('notifList');
|
||||
const countTextEl = document.getElementById('notifCountText');
|
||||
const badgeEl = document.getElementById('notifCountBadge');
|
||||
|
||||
function setList(items) {
|
||||
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">
|
||||
<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>
|
||||
<!-- 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>
|
||||
</a>
|
||||
`).join('');
|
||||
}
|
||||
<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>
|
||||
|
||||
fetch('/data/notifications')
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
const unread = res?.status ? Number(res.unread || 0) : 0;
|
||||
const items = res?.status ? (res.data || []) : [];
|
||||
<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>
|
||||
|
||||
if (countTextEl) countTextEl.textContent = `${unread} baru`;
|
||||
if (badgeEl) {
|
||||
if (unread > 0) {
|
||||
badgeEl.classList.remove('d-none');
|
||||
badgeEl.textContent = unread;
|
||||
} else {
|
||||
badgeEl.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
<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 || '';
|
||||
|
||||
setList(items);
|
||||
})
|
||||
.catch(() => {
|
||||
if (listEl) listEl.innerHTML = '<div class="dropdown-item text-muted small">Gagal memuat notifikasi</div>';
|
||||
});
|
||||
function setList(listEl, items, itemClass = '') {
|
||||
if (!listEl) return;
|
||||
|
||||
const notifToggle = document.getElementById('drop1');
|
||||
if (notifToggle) {
|
||||
notifToggle.addEventListener('show.bs.dropdown', () => {
|
||||
const unread = Number(badgeEl?.textContent || 0);
|
||||
if (unread > 0) {
|
||||
fetch('/data/notifications/read', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(() => {
|
||||
if (countTextEl) countTextEl.textContent = '0 baru';
|
||||
if (badgeEl) badgeEl.classList.add('d-none');
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
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>`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
@ -76,6 +76,9 @@ Route::middleware(['auth:admin,web'])->group(function(){
|
||||
// });
|
||||
Route::get('/data/notifications', [DashboardController::class, 'notifkasiList']);
|
||||
Route::post('/data/notifications/read', [DashboardController::class, 'notifkasiMarkRead']);
|
||||
Route::get('/data/expired-notifications', [DashboardController::class, 'expiredNotifkasiList']);
|
||||
Route::post('/data/expired-notifications/read', [DashboardController::class, 'expiredNotifkasiMarkRead']);
|
||||
Route::get('/data/expired-notifications/detail', [DashboardController::class, 'expiredNotifkasiDetail']);
|
||||
|
||||
Route::get('/data/log-dokumen', [DashboardController::class, 'logDokumen']);
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user