using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Infrastructure.Data; 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 ApplicationDbContext _db; public DashboardTipsController(ApplicationDbContext db) { _db = db; } /// /// 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. ViewBag includes both the filtered /// count and the global active/total counts for the header summary cards. /// // GET: /DashboardTips public async Task Index(string? search, bool? activeOnly, int page = 1) { const int pageSize = 25; var query = _db.DashboardTips.AsQueryable(); if (!string.IsNullOrWhiteSpace(search)) query = query.Where(t => t.TipText.Contains(search)); if (activeOnly == true) query = query.Where(t => t.IsActive); var total = await query.CountAsync(); var tips = await query .OrderByDescending(t => t.IsActive) .ThenByDescending(t => t.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); 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 _db.DashboardTips.CountAsync(t => t.IsActive); ViewBag.TotalCount = await _db.DashboardTips.CountAsync(); return View(tips); } /// Returns the Create form with an empty model. // GET: /DashboardTips/Create 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. /// Model validation is done manually (rather than relying solely on /// [Required] attributes) to ensure a meaningful error message is shown. /// // POST: /DashboardTips/Create [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); } _db.DashboardTips.Add(new DashboardTip { TipText = model.TipText.Trim(), IsActive = model.IsActive, CreatedAt = DateTime.UtcNow }); await _db.SaveChangesAsync(); TempData["Success"] = "Tip added successfully."; return RedirectToAction(nameof(Index)); } /// Returns the Edit form for an existing tip, or 404 if not found. // GET: /DashboardTips/Edit/5 public async Task Edit(int id) { var tip = await _db.DashboardTips.FindAsync(id); if (tip == null) return NotFound(); return View(tip); } /// /// Updates text and active flag for an existing tip. Returns the tracked entity /// (not the posted model) to the view on validation failure so the form shows /// the database version rather than potentially mangled posted data. /// // POST: /DashboardTips/Edit/5 [HttpPost, ValidateAntiForgeryToken] public async Task Edit(int id, DashboardTip model) { var tip = await _db.DashboardTips.FindAsync(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 _db.SaveChangesAsync(); 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. A missing Id is /// silently ignored to keep the action idempotent. /// // POST: /DashboardTips/Delete/5 [HttpPost, ValidateAntiForgeryToken] public async Task Delete(int id) { var tip = await _db.DashboardTips.FindAsync(id); if (tip != null) { _db.DashboardTips.Remove(tip); await _db.SaveChangesAsync(); TempData["Success"] = "Tip deleted."; } return RedirectToAction(nameof(Index)); } /// /// Flips the IsActive flag on a tip without a full edit round-trip. /// This lets operators quickly remove a tip from the rotation (deactivate) /// without deleting it, preserving the ability to reactivate it later. /// A missing Id is silently ignored to keep the action idempotent. /// // POST: /DashboardTips/ToggleActive/5 [HttpPost, ValidateAntiForgeryToken] public async Task ToggleActive(int id) { var tip = await _db.DashboardTips.FindAsync(id); if (tip != null) { tip.IsActive = !tip.IsActive; await _db.SaveChangesAsync(); } return RedirectToAction(nameof(Index)); } }