322 lines
14 KiB
Plaintext
322 lines
14 KiB
Plaintext
@model NotificationListViewModel
|
|
@{
|
|
ViewData["Title"] = "Notifikasi";
|
|
var activeTabKey = Model.ActiveCategory?.ToString() ?? "Semua";
|
|
var statusKey = NotificationCategory.StatusAkun.ToString();
|
|
var transaksiKey = NotificationCategory.Transaksi.ToString();
|
|
var pengajuanKey = NotificationCategory.Pengajuan.ToString();
|
|
var isSemuaActive = activeTabKey.Equals("Semua", StringComparison.OrdinalIgnoreCase);
|
|
var isStatusActive = activeTabKey.Equals(statusKey, StringComparison.OrdinalIgnoreCase);
|
|
var isTransaksiActive = activeTabKey.Equals(transaksiKey, StringComparison.OrdinalIgnoreCase);
|
|
var isPengajuanActive = activeTabKey.Equals(pengajuanKey, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
<form id="notificationsActionsForm" method="post" class="hidden">
|
|
@Html.AntiForgeryToken()
|
|
</form>
|
|
|
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
|
|
<div class="prose">
|
|
<span class="text-xl font-semibold text-gray-900 font-['Plus_Jakarta_Sans']">
|
|
Notifikasi
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-6"></div>
|
|
|
|
<div class="card bg-white">
|
|
<div class="card-body">
|
|
<div class="grid grid-cols-1 gap-3 lg:grid-cols-4">
|
|
<fieldset class="fieldset">
|
|
<legend class="fieldset-legend">Status Notifikasi</legend>
|
|
<select id="filterStatusNotifikasi" class="select">
|
|
<option value="">Semua Status</option>
|
|
<option value="Sukses">Sukses</option>
|
|
<option value="Peringatan">Peringatan</option>
|
|
<option value="Gagal">Gagal</option>
|
|
<option value="Info">Info</option>
|
|
</select>
|
|
</fieldset>
|
|
<fieldset class="fieldset">
|
|
<legend class="fieldset-legend">Status Baca</legend>
|
|
<select id="filterStatusBaca" class="select">
|
|
<option value="">Semua</option>
|
|
<option value="Dibaca">Sudah Dibaca</option>
|
|
<option value="Belum Dibaca">Belum Dibaca</option>
|
|
</select>
|
|
</fieldset>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-6"></div>
|
|
|
|
<div class="card bg-white">
|
|
<div class="tabs tabs-border" id="notificationTabs">
|
|
<input type="radio" name="notifications_tabs" class="tab font-semibold" aria-label="Semua" data-tab-value="Semua" @(isSemuaActive ? "checked=\"checked\"" : string.Empty) />
|
|
<div class="tab-content border-base-300 bg-base-100 p-0 rounded-none rounded-b-lg" data-tab-panel="Semua">
|
|
<div id="notificationsTableContainer">
|
|
<div class="overflow-x-auto">
|
|
<table id="notificationsTable" class="display stripe hover w-full text-sm text-left">
|
|
<thead>
|
|
<tr>
|
|
<th>Kategori</th>
|
|
<th>Status Notifikasi</th>
|
|
<th>Status Baca</th>
|
|
<th>Ringkasan</th>
|
|
<th>Diterima Pada</th>
|
|
<th>Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="radio" name="notifications_tabs" class="tab font-semibold" aria-label="Status Akun" data-tab-value="@statusKey" @(isStatusActive ? "checked=\"checked\"" : string.Empty) />
|
|
<div class="tab-content border-base-300 bg-base-100 p-0 rounded-none rounded-b-lg" data-tab-panel="@statusKey">
|
|
</div>
|
|
|
|
<input type="radio" name="notifications_tabs" class="tab font-semibold" aria-label="Transaksi" data-tab-value="@transaksiKey" @(isTransaksiActive ? "checked=\"checked\"" : string.Empty) />
|
|
<div class="tab-content border-base-300 bg-base-100 p-0 rounded-none rounded-b-lg" data-tab-panel="@transaksiKey">
|
|
</div>
|
|
|
|
<input type="radio" name="notifications_tabs" class="tab font-semibold" aria-label="Pengajuan" data-tab-value="@pengajuanKey" @(isPengajuanActive ? "checked=\"checked\"" : string.Empty) />
|
|
<div class="tab-content border-base-300 bg-base-100 p-0 rounded-none rounded-b-lg" data-tab-panel="@pengajuanKey">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@section Scripts {
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const tableElement = document.querySelector('#notificationsTable');
|
|
if (!tableElement) {
|
|
return;
|
|
}
|
|
|
|
const initialTabKey = '@activeTabKey';
|
|
const resolvedInitialTab = initialTabKey && initialTabKey !== '' ? initialTabKey : 'Semua';
|
|
let currentCategory = resolvedInitialTab;
|
|
|
|
const categoryLabelMap = {
|
|
StatusAkun: 'Status Akun',
|
|
Transaksi: 'Transaksi',
|
|
Pengajuan: 'Pengajuan'
|
|
};
|
|
|
|
const severityLabelMap = {
|
|
Success: 'Sukses',
|
|
Warning: 'Peringatan',
|
|
Error: 'Gagal',
|
|
Info: 'Info'
|
|
};
|
|
|
|
const severityClassMap = {
|
|
Success: 'badge-success',
|
|
Warning: 'badge-warning',
|
|
Error: 'badge-error',
|
|
Info: 'badge-neutral'
|
|
};
|
|
|
|
const escapeHtml = (value) => {
|
|
if (!value) return '';
|
|
return value
|
|
.toString()
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
};
|
|
|
|
const escapeRegex = (value) => {
|
|
if (!value) return '';
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
};
|
|
|
|
const formatDate = (value) => {
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) {
|
|
return '-';
|
|
}
|
|
return date.toLocaleString('id-ID', {
|
|
day: '2-digit',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
};
|
|
|
|
const moveTableToPanel = (tabKey) => {
|
|
const panel = document.querySelector(`[data-tab-panel="${tabKey}"]`) || document.querySelector('[data-tab-panel="Semua"]');
|
|
if (!panel) return;
|
|
const container = document.getElementById('notificationsTableContainer');
|
|
if (container && panel !== container.parentElement) {
|
|
panel.appendChild(container);
|
|
}
|
|
};
|
|
|
|
const filterSeveritySelect = document.getElementById('filterStatusNotifikasi');
|
|
const filterReadSelect = document.getElementById('filterStatusBaca');
|
|
|
|
const table = new DataTable(tableElement, {
|
|
ajax: {
|
|
url: '@Url.Action("Table", "Notifications")',
|
|
dataSrc: 'data',
|
|
data: function (params) {
|
|
if (currentCategory && currentCategory !== 'Semua') {
|
|
params.category = currentCategory;
|
|
} else {
|
|
delete params.category;
|
|
}
|
|
}
|
|
},
|
|
order: [[5, 'desc']],
|
|
autoWidth: false,
|
|
columns: [
|
|
{
|
|
data: 'category',
|
|
render: (data) => `<span class="badge badge-sm rounded badge-outline">${categoryLabelMap[data] ?? data}</span>`
|
|
},
|
|
{
|
|
data: 'severity',
|
|
render: (data) => `<span class="badge badge-sm badge-soft rounded-full ${severityClassMap[data] ?? 'badge-neutral'}">${severityLabelMap[data] ?? escapeHtml(data)}</span>`
|
|
},
|
|
{
|
|
data: 'isRead',
|
|
render: (data) => `<span class="badge badge-sm badge-soft rounded-full ${data ? 'badge-ghost text-gray-500' : 'badge-primary'}" data-role="read-status">${data ? 'Dibaca' : 'Belum Dibaca'}</span>`
|
|
},
|
|
{
|
|
data: 'summary',
|
|
render: (data) => `<p class="text-gray-600 line-clamp-2 max-w-xl">${escapeHtml(data)}</p>`
|
|
},
|
|
{
|
|
data: 'createdAt',
|
|
render: (data) => formatDate(data)
|
|
},
|
|
{
|
|
data: null,
|
|
orderable: false,
|
|
searchable: false,
|
|
render: (_, __, row) => `
|
|
<div class="flex flex-wrap items-center gap-1">
|
|
<a class="btn btn-circle btn-ghost btn-sm tooltip tooltip-left" href="${row.detailUrl}" data-tip="Detail">
|
|
<i class="ph ph-eye text-lg"></i>
|
|
</a>
|
|
<button type="button" class="btn btn-circle btn-soft btn-sm tooltip tooltip-left tooltip-info btn-info btn-toggle-read" data-id="${row.id}" data-is-read="${row.isRead}" data-tip="${row.isRead ? 'Tandai sebagai belum dibaca' : 'Tandai sebagai dibaca'}">
|
|
<i class="ph ${row.isRead ? 'ph-envelope-open' : 'ph-envelope'} text-lg"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-circle btn-soft btn-sm tooltip tooltip-left tooltip-error btn-error btn-delete" data-id="${row.id}" data-tip="Hapus">
|
|
<i class="ph ph-trash text-lg"></i>
|
|
</button>
|
|
</div>`
|
|
}
|
|
]
|
|
});
|
|
|
|
const actionsForm = document.getElementById('notificationsActionsForm');
|
|
const antiForgeryToken = actionsForm?.querySelector('input[name="__RequestVerificationToken"]').value;
|
|
const updateReadUrl = '@Url.Action("UpdateReadStatus", "Notifications")';
|
|
const deleteUrl = '@Url.Action("Delete", "Notifications")';
|
|
|
|
const postForm = async (url, payload) => {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
'X-CSRF-TOKEN': antiForgeryToken
|
|
},
|
|
body: new URLSearchParams(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Permintaan gagal diproses');
|
|
}
|
|
|
|
return response.json();
|
|
};
|
|
|
|
tableElement.addEventListener('click', async (event) => {
|
|
const toggleReadButton = event.target.closest('.btn-toggle-read');
|
|
if (toggleReadButton) {
|
|
const id = toggleReadButton.dataset.id;
|
|
const isRead = toggleReadButton.dataset.isRead === 'true';
|
|
const nextState = !isRead;
|
|
try {
|
|
await postForm(updateReadUrl, { id, isRead: nextState });
|
|
table.ajax.reload(null, false);
|
|
Swal.fire('Berhasil', nextState ? 'Notifikasi ditandai sebagai dibaca' : 'Notifikasi ditandai belum dibaca', 'success');
|
|
} catch (error) {
|
|
console.error(error);
|
|
Swal.fire('Gagal', error.message, 'error');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const deleteButton = event.target.closest('.btn-delete');
|
|
if (deleteButton) {
|
|
const id = deleteButton.dataset.id;
|
|
const confirmation = await Swal.fire({
|
|
title: 'Hapus notifikasi?',
|
|
text: 'Notifikasi akan dihapus dari daftar.',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Hapus',
|
|
cancelButtonText: 'Batal'
|
|
});
|
|
|
|
if (!confirmation.isConfirmed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await postForm(deleteUrl, { id });
|
|
table.ajax.reload(null, false);
|
|
Swal.fire('Terhapus', 'Notifikasi berhasil dihapus.', 'success');
|
|
} catch (error) {
|
|
console.error(error);
|
|
Swal.fire('Gagal', error.message, 'error');
|
|
}
|
|
}
|
|
});
|
|
|
|
const tabRadios = document.querySelectorAll('#notificationTabs .tab');
|
|
tabRadios.forEach((radio) => {
|
|
radio.addEventListener('change', () => {
|
|
if (!radio.checked) return;
|
|
const tabKey = radio.dataset.tabValue || 'Semua';
|
|
currentCategory = tabKey;
|
|
table.ajax.reload();
|
|
moveTableToPanel(tabKey);
|
|
});
|
|
});
|
|
|
|
const applySeverityFilter = () => {
|
|
const value = filterSeveritySelect?.value ?? '';
|
|
table.column(1).search(value, false, false).draw();
|
|
};
|
|
|
|
const applyReadFilter = () => {
|
|
const value = filterReadSelect?.value ?? '';
|
|
if (!value) {
|
|
table.column(2).search('', false, false).draw();
|
|
return;
|
|
}
|
|
const escapedValue = escapeRegex(value);
|
|
table.column(2).search(`^${escapedValue}$`, true, false).draw();
|
|
};
|
|
|
|
filterSeveritySelect?.addEventListener('change', applySeverityFilter);
|
|
filterReadSelect?.addEventListener('change', applyReadFilter);
|
|
|
|
applySeverityFilter();
|
|
applyReadFilter();
|
|
moveTableToPanel(resolvedInitialTab);
|
|
});
|
|
</script>
|
|
}
|