using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
namespace PowderCoating.Web.Controllers;
///
/// SuperAdmin-only CRUD interface for managing the rotating tip-of-the-day entries
/// displayed on the tenant dashboard. The system ships with 40 seed tips loaded by
/// SeedDataService.SeedSystemDataAsync(); this controller allows platform
/// operators to add, edit, deactivate, or remove tips without a code deployment.
/// The tip shown each day is selected by the dashboard controller using
/// DayOfYear % activeCount so it rotates predictably.
///
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
public class DashboardTipsController : Controller
{
private readonly IUnitOfWork _unitOfWork;
public DashboardTipsController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
///
/// Returns a paginated, optionally filtered list of all dashboard tips.
/// Active tips are sorted first (then by newest Id) to make the currently
/// live pool easy to review at a glance.
///
public async Task Index(string? search, bool? activeOnly, int page = 1)
{
const int pageSize = 25;
var all = (await _unitOfWork.DashboardTips.GetAllAsync()).ToList();
if (!string.IsNullOrWhiteSpace(search))
all = all.Where(t => t.TipText.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();
if (activeOnly == true)
all = all.Where(t => t.IsActive).ToList();
var total = all.Count;
var tips = all
.OrderByDescending(t => t.IsActive)
.ThenByDescending(t => t.Id)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
ViewBag.Search = search;
ViewBag.ActiveOnly = activeOnly ?? false;
ViewBag.Page = page;
ViewBag.PageSize = pageSize;
ViewBag.Total = total;
ViewBag.TotalPages = (int)Math.Ceiling(total / (double)pageSize);
ViewBag.ActiveCount = await _unitOfWork.DashboardTips.CountAsync(t => t.IsActive);
ViewBag.TotalCount = await _unitOfWork.DashboardTips.CountAsync();
return View(tips);
}
/// Returns the Create form with an empty model.
public IActionResult Create() => View(new DashboardTip());
///
/// Persists a new dashboard tip. Text is trimmed before saving to prevent
/// whitespace-only entries from appearing as blank tiles on the dashboard.
///
[HttpPost, ValidateAntiForgeryToken]
public async Task Create(DashboardTip model)
{
if (string.IsNullOrWhiteSpace(model.TipText))
{
ModelState.AddModelError(nameof(model.TipText), "Tip text is required.");
return View(model);
}
await _unitOfWork.DashboardTips.AddAsync(new DashboardTip
{
TipText = model.TipText.Trim(),
IsActive = model.IsActive,
CreatedAt = DateTime.UtcNow
});
await _unitOfWork.CompleteAsync();
TempData["Success"] = "Tip added successfully.";
return RedirectToAction(nameof(Index));
}
/// Returns the Edit form for an existing tip, or 404 if not found.
public async Task Edit(int id)
{
var tip = await _unitOfWork.DashboardTips.GetByIdAsync(id);
if (tip == null) return NotFound();
return View(tip);
}
///
/// Updates text and active flag for an existing tip.
///
[HttpPost, ValidateAntiForgeryToken]
public async Task Edit(int id, DashboardTip model)
{
var tip = await _unitOfWork.DashboardTips.GetByIdAsync(id);
if (tip == null) return NotFound();
if (string.IsNullOrWhiteSpace(model.TipText))
{
ModelState.AddModelError(nameof(model.TipText), "Tip text is required.");
return View(tip);
}
tip.TipText = model.TipText.Trim();
tip.IsActive = model.IsActive;
await _unitOfWork.CompleteAsync();
TempData["Success"] = "Tip updated.";
return RedirectToAction(nameof(Index));
}
///
/// Permanently (hard) deletes a dashboard tip. Tips are platform metadata so
/// they do not use soft delete — they have no foreign-key relationships to
/// tenant data and nothing references a deleted tip's Id.
///
[HttpPost, ValidateAntiForgeryToken]
public async Task Delete(int id)
{
var tip = await _unitOfWork.DashboardTips.GetByIdAsync(id);
if (tip != null)
{
await _unitOfWork.DashboardTips.DeleteAsync(tip);
await _unitOfWork.CompleteAsync();
TempData["Success"] = "Tip deleted.";
}
return RedirectToAction(nameof(Index));
}
///
/// Flips the IsActive flag on a tip without a full edit round-trip.
///
[HttpPost, ValidateAntiForgeryToken]
public async Task ToggleActive(int id)
{
var tip = await _unitOfWork.DashboardTips.GetByIdAsync(id);
if (tip != null)
{
tip.IsActive = !tip.IsActive;
await _unitOfWork.CompleteAsync();
}
return RedirectToAction(nameof(Index));
}
}