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)); } }