feat: notifications

main
Yuri Dimas 2025-12-08 17:54:08 +07:00
parent 76a68b1645
commit e05b42a39f
No known key found for this signature in database
GPG Key ID: 9FD7E44BC294C68C
9 changed files with 833 additions and 6 deletions

View File

@ -0,0 +1,105 @@
using BankSampahApp.Models;
using BankSampahApp.Services;
using Microsoft.AspNetCore.Mvc;
namespace BankSampahApp.Controllers;
public class NotificationsController : Controller
{
private readonly INotificationService _notificationService;
public NotificationsController(INotificationService notificationService)
{
_notificationService = notificationService;
}
[HttpGet]
public IActionResult Index(string? tab)
{
NotificationCategory? categoryFilter = null;
if (!string.IsNullOrWhiteSpace(tab) && !tab.Equals("Semua", StringComparison.OrdinalIgnoreCase)
&& Enum.TryParse(tab, ignoreCase: true, out NotificationCategory parsedCategory))
{
categoryFilter = parsedCategory;
}
var viewModel = new NotificationListViewModel
{
ActiveCategory = categoryFilter
};
return View(viewModel);
}
[HttpGet]
public async Task<IActionResult> Show(int id)
{
var notification = _notificationService.GetById(id);
if (notification is null)
{
return NotFound();
}
if (!notification.IsRead)
{
await _notificationService.UpdateReadStateAsync(id, true);
notification.IsRead = true;
}
return View(notification);
}
[HttpGet]
public async Task<IActionResult> Table(string? category)
{
NotificationCategory? categoryFilter = null;
if (!string.IsNullOrWhiteSpace(category) &&
Enum.TryParse(category, ignoreCase: true, out NotificationCategory parsedCategory))
{
categoryFilter = parsedCategory;
}
var notifications = await _notificationService.GetNotificationsAsync(categoryFilter);
var rows = notifications.Select(n => new
{
id = n.Id,
title = n.Title,
category = n.Category.ToString(),
severity = n.Severity.ToString(),
isRead = n.IsRead,
summary = n.Summary,
createdAt = n.CreatedAt.ToString("O"),
detailUrl = Url.Action("Show", "Notifications", new { id = n.Id })
});
return Json(new { data = rows });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateReadStatus(int id, bool isRead)
{
var success = await _notificationService.UpdateReadStateAsync(id, isRead);
if (!success)
{
return NotFound();
}
return Json(new { success = true });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id)
{
var success = await _notificationService.DeleteAsync(id);
if (!success)
{
return NotFound();
}
return Json(new { success = true });
}
}

View File

@ -0,0 +1,28 @@
namespace BankSampahApp.Models;
public enum NotificationCategory
{
StatusAkun,
Transaksi,
Pengajuan
}
public enum NotificationSeverity
{
Info,
Success,
Warning,
Error
}
public class NotificationItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public bool IsRead { get; set; }
public NotificationCategory Category { get; set; }
public NotificationSeverity Severity { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public string Link { get; set; } = string.Empty;
}

View File

@ -0,0 +1,6 @@
namespace BankSampahApp.Models;
public class NotificationListViewModel
{
public NotificationCategory? ActiveCategory { get; set; }
}

View File

@ -31,6 +31,7 @@ builder.Services.AddControllersWithViews(options =>
// Register application services
builder.Services.AddScoped<IHomeService, HomeService>();
builder.Services.AddScoped<IStatisticsService, StatisticsService>();
builder.Services.AddSingleton<INotificationService, NotificationService>();
// Register new optimized services
builder.Services.AddSingleton<ICacheService, CacheService>();
@ -158,4 +159,4 @@ app.Lifetime.ApplicationStopping.Register(() =>
app.Logger.LogInformation("Application is shutting down gracefully...");
});
app.Run();
app.Run();

View File

@ -0,0 +1,11 @@
using BankSampahApp.Models;
namespace BankSampahApp.Services;
public interface INotificationService
{
Task<IReadOnlyList<NotificationItem>> GetNotificationsAsync(NotificationCategory? category = null);
NotificationItem? GetById(int id);
Task<bool> UpdateReadStateAsync(int id, bool isRead);
Task<bool> DeleteAsync(int id);
}

View File

@ -0,0 +1,207 @@
using BankSampahApp.Models;
namespace BankSampahApp.Services;
public class NotificationService : INotificationService
{
private readonly List<NotificationItem> _notifications =
[
new()
{
Id = 1,
Title = "User BSU - 1",
Category = NotificationCategory.StatusAkun,
Severity = NotificationSeverity.Warning,
Summary = "Status akun Anda saat ini belum disetujui oleh [Nama BSU]. Mohon menunggu proses verifikasi data oleh pihak BSU. Anda akan mendapatkan notifikasi kembali setelah data Anda diverifikasi.",
CreatedAt = DateTime.UtcNow.AddDays(-1),
IsRead = false
},
new()
{
Id = 2,
Title = "User BSU - 2",
Category = NotificationCategory.StatusAkun,
Severity = NotificationSeverity.Success,
Summary = "Selamat! Akun Anda telah disetujui dan aktif oleh [Nama Bank Sampah Unit (BSU)]. Anda kini dapat melakukan transaksi penyetoran dan penarikan sampah melalui aplikasi Bank Sampah.",
CreatedAt = DateTime.UtcNow.AddDays(-2),
IsRead = true
},
new()
{
Id = 3,
Title = "User BSU - 2",
Category = NotificationCategory.StatusAkun,
Severity = NotificationSeverity.Error,
Summary = "Akun Anda ditolak oleh [Nama Bank Sampah Unit (BSU)]. Alasan: i. Data yang Anda berikan tidak valid atau tidak sesuai. ii. Pelanggaran terhadap kebijakan atau aturan Bank Sampah. Silakan hubungi [Nama BSU] untuk informasi lebih lanjut atau proses pengaktifan kembali akun Anda.",
CreatedAt = DateTime.UtcNow.AddDays(-3),
IsRead = false
},
new()
{
Id = 4,
Title = "User BSU - 2",
Category = NotificationCategory.Transaksi,
Severity = NotificationSeverity.Success,
Summary = "Terima kasih telah melakukan pengumpulan sampah di [Nama BSU]! Anda telah menyetorkan sampah seberat {berat_sampah} kg dengan total nilai sebesar Rp {jumlah_uang}. Terus berkontribusi menjaga lingkungan!",
CreatedAt = DateTime.UtcNow.AddDays(-4),
IsRead = false
},
new()
{
Id = 5,
Title = "User BSU - 2",
Category = NotificationCategory.Transaksi,
Severity = NotificationSeverity.Success,
Summary = "Penarikan saldo Anda telah berhasil diproses di [Nama BSU]. Jumlah yang ditarik: Rp {jumlah_uang}. Sisa saldo tabungan Anda saat ini: Rp {sisa_saldo}. Terima kasih telah berpartisipasi!",
CreatedAt = DateTime.UtcNow.AddDays(-5),
IsRead = true
},
new()
{
Id = 6,
Title = "User BSU - 2",
Category = NotificationCategory.StatusAkun,
Severity = NotificationSeverity.Success,
Summary = "Selamat! Akun Bank Sampah Unit (BSU) Anda telah disetujui dan diaktifkan oleh pihak Dinas. Anda kini dapat melakukan pengelolaan data nasabah, transaksi, serta pelaporan melalui sistem Bank Sampah.",
CreatedAt = DateTime.UtcNow.AddDays(-6),
IsRead = true
},
new()
{
Id = 7,
Title = "User BSU - 2",
Category = NotificationCategory.StatusAkun,
Severity = NotificationSeverity.Warning,
Summary = "Status verifikasi Bank Sampah Unit (BSU) Anda saat ini belum disetujui oleh pihak Dinas. Mohon menunggu proses pemeriksaan data dan kelengkapan dokumen.",
CreatedAt = DateTime.UtcNow.AddDays(-7),
IsRead = false
},
new()
{
Id = 8,
Title = "User BSU - 2",
Category = NotificationCategory.StatusAkun,
Severity = NotificationSeverity.Error,
Summary = "Mohon maaf, akun Bank Sampah Unit (BSU) Anda tidak dapat diaktifkan oleh pihak Dinas. Silakan lengkapi data atau hubungi pihak Dinas untuk proses klarifikasi dan pengajuan ulang.",
CreatedAt = DateTime.UtcNow.AddDays(-8),
IsRead = false
},
new()
{
Id = 9,
Title = "Pengajuan BSU - 1",
Category = NotificationCategory.Pengajuan,
Severity = NotificationSeverity.Info,
Summary = "Pengajuan verifikasi Bank Sampah Unit (BSU) Anda telah berhasil dikirim ke Bank Sampah Induk (BSI).\nMohon menunggu proses verifikasi dan persetujuan dari pihak BSI.\nAnda akan mendapatkan notifikasi kembali setelah pengajuan diverifikasi. ♻️",
CreatedAt = DateTime.UtcNow.AddDays(-9),
IsRead = false
},
new()
{
Id = 10,
Title = "Pengajuan BSU - 1",
Category = NotificationCategory.Pengajuan,
Severity = NotificationSeverity.Success,
Summary = "Selamat! 🎉\nPengajuan verifikasi Bank Sampah Unit (BSU) Anda telah disetujui dan diaktifkan oleh Bank Sampah Induk (BSI).\nAnda kini dapat mulai melakukan pengelolaan data transaksi.",
CreatedAt = DateTime.UtcNow.AddDays(-10),
IsRead = false
},
new()
{
Id = 11,
Title = "Pengajuan BSU - 2",
Category = NotificationCategory.Pengajuan,
Severity = NotificationSeverity.Warning,
Summary = "Anda menerima pengajuan keanggotaan baru dari Nasabah yang ingin bergabung dengan Bank Sampah Unit (BSU) Anda.\nMohon lakukan verifikasi data dan kelengkapan identitas sebelum memberikan keputusan.\n🔹 Nama Nasabah: {nama_nasabah}\n🔹 Tanggal Pengajuan: {tanggal_pengajuan}\n🔹 Nomor HP / ID Nasabah: {id_nasabah}\nLakukan verifikasi agar nasabah dapat segera menjadi anggota aktif BSU Anda. ♻️",
CreatedAt = DateTime.UtcNow.AddDays(-11),
IsRead = true
},
new()
{
Id = 12,
Title = "Pengajuan BSU - 2",
Category = NotificationCategory.Pengajuan,
Severity = NotificationSeverity.Info,
Summary = "Anda menerima pengajuan keanggotaan baru dari Bank Sampah Unit (BSU).\nMohon tinjau dan verifikasi data sebelum menyetujui atau menolak pengajuan.\n🔹 Nama BSU: {nama_bsu}\n🔹 Tanggal Pengajuan: {tanggal_pengajuan}\n🔹 Diajukan oleh: {nama_pengaju / penanggung_jawab}\nSegera lakukan verifikasi agar BSU dapat menjadi bagian dari jaringan Bank Sampah Induk Anda. ♻️",
CreatedAt = DateTime.UtcNow.AddDays(-12),
IsRead = false
},
new()
{
Id = 13,
Title = "Pengajuan BSU - 3",
Category = NotificationCategory.Pengajuan,
Severity = NotificationSeverity.Warning,
Summary = "Terdapat pengajuan verifikasi baru dari Bank Sampah Unit (BSU) yang perlu ditinjau.\nSilakan lakukan pemeriksaan data dan dokumen BSU sebelum menentukan status persetujuan.\n🔹 Nama BSU: {nama_bsu}\n🔹 Tanggal Pengajuan: {tanggal_pengajuan}\n🔹 Diajukan oleh: {nama_pengaju / penanggung_jawab}\nSegera lakukan verifikasi agar BSU dapat melanjutkan proses operasionalnya. ♻️",
CreatedAt = DateTime.UtcNow.AddDays(-13),
IsRead = true
}
];
public Task<IReadOnlyList<NotificationItem>> GetNotificationsAsync(NotificationCategory? category = null)
{
IEnumerable<NotificationItem> query = _notifications;
if (category.HasValue)
{
query = query.Where(n => n.Category == category.Value);
}
return Task.FromResult<IReadOnlyList<NotificationItem>>(query
.OrderByDescending(n => n.CreatedAt)
.Select(n => new NotificationItem
{
Id = n.Id,
Title = n.Title,
Summary = n.Summary,
Category = n.Category,
Severity = n.Severity,
CreatedAt = n.CreatedAt,
Link = $"/Notifications/Show/{n.Id}",
IsRead = n.IsRead
})
.ToList());
}
public NotificationItem? GetById(int id)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
return notification is null
? null
: new NotificationItem
{
Id = notification.Id,
Title = notification.Title,
Summary = notification.Summary,
Category = notification.Category,
Severity = notification.Severity,
CreatedAt = notification.CreatedAt,
Link = $"/Notifications/Show/{notification.Id}",
IsRead = notification.IsRead
};
}
public Task<bool> UpdateReadStateAsync(int id, bool isRead)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
if (notification is null)
{
return Task.FromResult(false);
}
notification.IsRead = isRead;
return Task.FromResult(true);
}
public Task<bool> DeleteAsync(int id)
{
var notification = _notifications.FirstOrDefault(n => n.Id == id);
if (notification is null)
{
return Task.FromResult(false);
}
_notifications.Remove(notification);
return Task.FromResult(true);
}
}

View File

@ -0,0 +1,321 @@
@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>
}

View File

@ -0,0 +1,67 @@
@model NotificationItem
@{
ViewData["Title"] = "Detail Notifikasi";
}
<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 class="flex flex-col gap-2 sm:flex-row">
<a href="@Url.Action("Index", "Notifications")" class="btn btn-sm w-full sm:w-auto rounded-full bg-white border border-gray-300 hover:bg-gray-50" onclick="modal_download.showModal()">
<i class="ph ph-arrow-left"></i>
Kembali
</a>
</div>
</div>
<div class="h-6"></div>
<div class="card bg-white shadow-sm border border-gray-100">
<div class="card-body space-y-4">
<div class="flex flex-wrap items-center gap-2 text-sm">
<span class="badge badge-outline rounded">@GetCategoryLabel(Model.Category)</span>
<span class="badge badge-soft rounded-full @GetSeverityClass(Model.Severity)">@GetSeverityLabel(Model.Severity)</span>
<span class="badge @(Model.IsRead ? "badge-ghost text-gray-600" : "badge-primary")">
@(Model.IsRead ? "Sudah Dibaca" : "Belum Dibaca")
</span>
<span class="text-gray-500">
@Model.CreatedAt.ToString("dd MMMM yyyy HH:mm")
</span>
</div>
<article class="prose max-w-none text-gray-800 whitespace-pre-line leading-relaxed">
@Model.Summary
</article>
</div>
</div>
@functions {
private static string GetCategoryLabel(NotificationCategory category) => category switch
{
NotificationCategory.StatusAkun => "Status Akun",
NotificationCategory.Transaksi => "Transaksi",
NotificationCategory.Pengajuan => "Pengajuan",
_ => category.ToString()
};
private static string GetSeverityClass(NotificationSeverity severity) => severity switch
{
NotificationSeverity.Success => "badge-success",
NotificationSeverity.Warning => "badge-warning",
NotificationSeverity.Error => "badge-error",
_ => "badge-neutral"
};
private static string GetSeverityLabel(NotificationSeverity severity) => severity switch
{
NotificationSeverity.Success => "Sukses",
NotificationSeverity.Warning => "Peringatan",
NotificationSeverity.Error => "Gagal",
NotificationSeverity.Info => "Info",
_ => severity.ToString()
};
}

View File

@ -1,4 +1,15 @@
<div class="navbar bg-base-100 sticky top-0 z-20 w-full justify-between lg:justify-end">
@using System.Linq
@using BankSampahApp.Models
@inject BankSampahApp.Services.INotificationService NotificationService
@{
var notifications = (await NotificationService.GetNotificationsAsync())
.OrderBy(n => n.IsRead)
.ThenByDescending(n => n.CreatedAt)
.Take(5)
.ToList();
}
<div class="navbar bg-white shadow-sm border-b border-gray-100 sticky top-0 z-20 w-full px-4 justify-between lg:justify-end">
<div class="navbar-start">
<label for="my-drawer-2" class="btn bg-bpsrw-500 drawer-button btn-square lg:hidden">
<i class="ph ph-list text-lg"></i>
@ -8,9 +19,53 @@
<button class="btn btn-ghost btn-square rounded-lg border-gray-200">
<span class="icon icon-outline">help</span>
</button>
<button class="btn btn-ghost btn-square avatar avatar-online rounded-lg border-gray-200">
<span class="icon icon-outline">notifications</span>
</button>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-square avatar avatar-online rounded-lg border-gray-200">
<span class="icon icon-outline">notifications</span>
</div>
<div tabindex="0" class="dropdown-content card card-sm bg-base-100 z-1 w-lg shadow-md">
<div class="card-body">
<h2 class="card-title">
Notifikasi
</h2>
<ul class="divide-y border rounded border-gray-200">
@foreach (var notification in notifications)
{
<li>
<a class="flex gap-3 p-4 transition rounded hover:bg-gray-100 @(notification.IsRead ? string.Empty : "bg-gray-50")"
href="@Url.Action("Show", "Notifications", new { id = notification.Id })">
<div class="@GetSeverityWrapClasses(notification.Severity)">
<span class="icon icon-outline">@GetSeverityIcon(notification.Severity)</span>
</div>
<div class="flex flex-col gap-1 flex-1">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="font-semibold text-gray-800">@notification.Title</div>
<div class="flex items-center gap-2">
<span class="text-xs uppercase font-semibold text-gray-500">
@GetCategoryLabel(notification.Category)
</span>
@if (!notification.IsRead)
{
<span class="inline-flex size-2 rounded-full bg-bpsrw-500"></span>
<span class="text-[11px] font-semibold text-bpsrw-500 uppercase">baru</span>
}
</div>
</div>
<p class="text-xs text-gray-600 line-clamp-2">
@notification.Summary
</p>
<span class="text-[11px] text-gray-400">@notification.CreatedAt.ToString("dd MMM yyyy HH:mm")</span>
</div>
</a>
</li>
}
</ul>
<div class="card-actions">
<a href="@Url.Action("Index", "Notifications")" class="btn btn-primary btn-soft btn-sm">Lihat Semua</a>
</div>
</div>
</div>
</div>
<div class="divider divider-horizontal m-0"></div>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar border-bpsrw-500 border-2">
@ -26,4 +81,30 @@
</ul>
</div>
</div>
</div>
</div>
@functions {
private static string GetSeverityWrapClasses(NotificationSeverity severity) => severity switch
{
NotificationSeverity.Success => "bg-green-100 text-green-600 rounded size-10 flex items-center justify-center",
NotificationSeverity.Warning => "bg-yellow-100 text-yellow-600 rounded size-10 flex items-center justify-center",
NotificationSeverity.Error => "bg-red-100 text-red-600 rounded size-10 flex items-center justify-center",
_ => "bg-gray-100 text-gray-600 rounded size-10 flex items-center justify-center"
};
private static string GetSeverityIcon(NotificationSeverity severity) => severity switch
{
NotificationSeverity.Success => "check",
NotificationSeverity.Warning => "warning",
NotificationSeverity.Error => "error",
_ => "info"
};
private static string GetCategoryLabel(NotificationCategory category) => category switch
{
NotificationCategory.StatusAkun => "Status Akun",
NotificationCategory.Transaksi => "Transaksi",
NotificationCategory.Pengajuan => "Pengajuan",
_ => category.ToString()
};
}