bank-sampah/Views/Notifications/Index.cshtml

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
};
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>
}