Initial commit
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PowderCoating.Core.Enums;
|
||||
using PowderCoating.Infrastructure.Data;
|
||||
using PowderCoating.Shared.Constants;
|
||||
|
||||
namespace PowderCoating.Web.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// SuperAdmin-only cross-company notification log viewer.
|
||||
/// The company-scoped version lives in NotificationLogsController (CanManageJobs policy).
|
||||
/// </summary>
|
||||
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
|
||||
public class PlatformNotificationsController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
|
||||
public PlatformNotificationsController(ApplicationDbContext db) => _db = db;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a paginated, filterable cross-company notification log view visible
|
||||
/// only to SuperAdmins. Unlike the tenant-scoped
|
||||
/// <see cref="NotificationLogsController.Index"/>, this action uses
|
||||
/// <c>IgnoreQueryFilters()</c> to bypass the multi-tenancy global filter and
|
||||
/// see logs from all companies simultaneously.
|
||||
/// <para>
|
||||
/// Company names are resolved in a single follow-up query after paging (not via
|
||||
/// a JOIN) because company data lives outside the notification-log table's usual
|
||||
/// query scope, and a separate query keeps the main sort/filter logic clean.
|
||||
/// Summary counts (total, failed, last 24 h) are computed as independent queries
|
||||
/// against the full unfiltered dataset so the summary reflects platform-wide
|
||||
/// health regardless of the active filters.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Index(
|
||||
int? companyId,
|
||||
string? type,
|
||||
string? status,
|
||||
string? channel,
|
||||
string? search,
|
||||
DateTime? from,
|
||||
DateTime? to,
|
||||
int page = 1,
|
||||
int pageSize = 50)
|
||||
{
|
||||
pageSize = pageSize is 25 or 50 or 100 ? pageSize : 50;
|
||||
page = Math.Max(1, page);
|
||||
|
||||
var query = _db.NotificationLogs
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(n => !n.IsDeleted)
|
||||
.AsQueryable();
|
||||
|
||||
if (companyId.HasValue)
|
||||
query = query.Where(n => n.CompanyId == companyId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(type) && Enum.TryParse<NotificationType>(type, out var typeEnum))
|
||||
query = query.Where(n => n.NotificationType == typeEnum);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<NotificationStatus>(status, out var statusEnum))
|
||||
query = query.Where(n => n.Status == statusEnum);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(channel) && Enum.TryParse<NotificationChannel>(channel, out var channelEnum))
|
||||
query = query.Where(n => n.Channel == channelEnum);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
query = query.Where(n =>
|
||||
n.RecipientName.Contains(search) ||
|
||||
n.Recipient.Contains(search) ||
|
||||
(n.Subject != null && n.Subject.Contains(search)));
|
||||
|
||||
if (from.HasValue)
|
||||
query = query.Where(n => n.SentAt >= from.Value.Date);
|
||||
|
||||
if (to.HasValue)
|
||||
query = query.Where(n => n.SentAt < to.Value.Date.AddDays(1));
|
||||
|
||||
query = query.OrderByDescending(n => n.SentAt);
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(n => new PlatformNotificationRow
|
||||
{
|
||||
Id = n.Id,
|
||||
CompanyId = (int)n.CompanyId,
|
||||
Channel = n.Channel,
|
||||
NotificationType = n.NotificationType,
|
||||
Status = n.Status,
|
||||
RecipientName = n.RecipientName,
|
||||
Recipient = n.Recipient,
|
||||
Subject = n.Subject,
|
||||
ErrorMessage = n.ErrorMessage,
|
||||
SentAt = n.SentAt
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// Resolve company names for the rows in one query
|
||||
var cids = items.Select(i => i.CompanyId).Distinct().ToList();
|
||||
var companyNames = await _db.Companies.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(c => cids.Contains(c.Id))
|
||||
.ToDictionaryAsync(c => c.Id, c => c.CompanyName);
|
||||
|
||||
foreach (var item in items)
|
||||
item.CompanyName = companyNames.GetValueOrDefault(item.CompanyId);
|
||||
|
||||
// Sidebar company list for filter dropdown
|
||||
var companies = await _db.Companies.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(c => !c.IsDeleted)
|
||||
.OrderBy(c => c.CompanyName)
|
||||
.Select(c => new { c.Id, c.CompanyName })
|
||||
.ToListAsync();
|
||||
|
||||
// Summary counts
|
||||
ViewBag.TotalCount = await _db.NotificationLogs.IgnoreQueryFilters().CountAsync(n => !n.IsDeleted);
|
||||
ViewBag.FailedCount = await _db.NotificationLogs.IgnoreQueryFilters()
|
||||
.CountAsync(n => !n.IsDeleted && n.Status == NotificationStatus.Failed);
|
||||
ViewBag.Last24hCount = await _db.NotificationLogs.IgnoreQueryFilters()
|
||||
.CountAsync(n => !n.IsDeleted && n.SentAt >= DateTime.UtcNow.AddHours(-24));
|
||||
|
||||
ViewBag.Companies = companies;
|
||||
ViewBag.CompanyIdFilter = companyId;
|
||||
ViewBag.TypeFilter = type;
|
||||
ViewBag.StatusFilter = status;
|
||||
ViewBag.ChannelFilter = channel;
|
||||
ViewBag.Search = search;
|
||||
ViewBag.From = from?.ToString("yyyy-MM-dd");
|
||||
ViewBag.To = to?.ToString("yyyy-MM-dd");
|
||||
ViewBag.Page = page;
|
||||
ViewBag.PageSize = pageSize;
|
||||
ViewBag.FilteredCount = totalCount;
|
||||
ViewBag.TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
|
||||
return View(items);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the full notification log entry for the given <paramref name="id"/>,
|
||||
/// including the complete message body and error details. The owning company name
|
||||
/// is resolved separately (rather than via navigation property) because company
|
||||
/// records are in a different query-filter context.
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Details(int id)
|
||||
{
|
||||
var log = await _db.NotificationLogs.AsNoTracking().IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(n => n.Id == id);
|
||||
if (log == null) return NotFound();
|
||||
|
||||
string? companyName = null;
|
||||
if (log.CompanyId > 0)
|
||||
companyName = await _db.Companies.AsNoTracking().IgnoreQueryFilters()
|
||||
.Where(c => c.Id == log.CompanyId)
|
||||
.Select(c => c.CompanyName)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
ViewBag.CompanyName = companyName;
|
||||
return View(log);
|
||||
}
|
||||
}
|
||||
|
||||
public class PlatformNotificationRow
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int CompanyId { get; set; }
|
||||
public string? CompanyName { get; set; }
|
||||
public NotificationChannel Channel { get; set; }
|
||||
public NotificationType NotificationType { get; set; }
|
||||
public NotificationStatus Status { get; set; }
|
||||
public string RecipientName { get; set; } = string.Empty;
|
||||
public string Recipient { get; set; } = string.Empty;
|
||||
public string? Subject { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public DateTime SentAt { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user