feat: notifications
parent
76a68b1645
commit
e05b42a39f
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace BankSampahApp.Models;
|
||||
|
||||
public class NotificationListViewModel
|
||||
{
|
||||
public NotificationCategory? ActiveCategory { get; set; }
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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, '&')
|
||||
.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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue