using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PowderCoating.Application.Interfaces; using PowderCoating.Core.Entities; using PowderCoating.Core.Interfaces; using PowderCoating.Shared.Constants; namespace PowderCoating.Web.Controllers; [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public class AnnouncementsController : Controller { private readonly IUnitOfWork _unitOfWork; private readonly IInAppNotificationService _inApp; public AnnouncementsController(IUnitOfWork unitOfWork, IInAppNotificationService inApp) { _unitOfWork = unitOfWork; _inApp = inApp; } /// /// Lists all platform announcements in reverse-chronological order. SuperAdmin-only (enforced at controller level by the SuperAdminOnly policy). /// public async Task Index() { var announcements = (await _unitOfWork.Announcements.GetAllAsync()) .OrderByDescending(a => a.CreatedAt) .ToList(); return View(announcements); } /// /// Shows the announcement creation form with sensible defaults: starts now, dismissible, and active. /// public async Task Create() { await PopulateDropdownsAsync(); return View(new Announcement { StartsAt = DateTime.Now, IsDismissible = true, IsActive = true }); } /// /// Persists a new announcement and immediately dispatches it as in-app notifications to all targeted companies. Dates are converted to UTC before storage; DispatchNotificationsAsync handles the fan-out. /// [HttpPost, ValidateAntiForgeryToken] public async Task Create(Announcement model) { if (!ModelState.IsValid) { await PopulateDropdownsAsync(); return View(model); } model.CreatedByUserId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? ""; model.CreatedByUserName = User.Identity?.Name ?? "SuperAdmin"; model.CreatedAt = DateTime.UtcNow; model.StartsAt = model.StartsAt.ToUniversalTime(); if (model.ExpiresAt.HasValue) model.ExpiresAt = model.ExpiresAt.Value.ToUniversalTime(); await _unitOfWork.Announcements.AddAsync(model); await _unitOfWork.CompleteAsync(); await DispatchNotificationsAsync(model); TempData["Success"] = "Announcement created and sent as notifications."; return RedirectToAction(nameof(Index)); } /// /// Shows the edit form for an existing announcement. Note: editing does NOT re-dispatch notifications; it only updates the stored announcement record. /// public async Task Edit(int id) { var announcement = await _unitOfWork.Announcements.GetByIdAsync(id); if (announcement == null) return NotFound(); await PopulateDropdownsAsync(); return View(announcement); } /// /// Saves changes to an existing announcement. TargetPlan and TargetCompanyId are cleared when the Target field changes, preventing stale filter values from persisting on the record. /// [HttpPost, ValidateAntiForgeryToken] public async Task Edit(int id, Announcement model) { if (!ModelState.IsValid) { await PopulateDropdownsAsync(); return View(model); } var existing = await _unitOfWork.Announcements.GetByIdAsync(id); if (existing == null) return NotFound(); existing.Title = model.Title; existing.Message = model.Message; existing.Type = model.Type; existing.Target = model.Target; existing.TargetPlan = model.Target == "Plan" ? model.TargetPlan : null; existing.TargetCompanyId = model.Target == "Company" ? model.TargetCompanyId : null; existing.StartsAt = model.StartsAt.ToUniversalTime(); existing.ExpiresAt = model.ExpiresAt.HasValue ? model.ExpiresAt.Value.ToUniversalTime() : null; existing.IsDismissible = model.IsDismissible; existing.IsActive = model.IsActive; existing.UpdatedAt = DateTime.UtcNow; await _unitOfWork.CompleteAsync(); TempData["Success"] = "Announcement updated."; return RedirectToAction(nameof(Index)); } /// /// Permanently deletes an announcement record. Hard delete is intentional here — announcements are platform content, not business data, and do not require audit-trail soft deletion. /// [HttpPost, ValidateAntiForgeryToken] public async Task Delete(int id) { var announcement = await _unitOfWork.Announcements.GetByIdAsync(id); if (announcement == null) return NotFound(); await _unitOfWork.Announcements.DeleteAsync(announcement); await _unitOfWork.CompleteAsync(); TempData["Success"] = "Announcement deleted."; return RedirectToAction(nameof(Index)); } /// /// Fans out the announcement as in-app notifications to each matching company. IgnoreQueryFilters is required to reach all active companies regardless of tenant context. Filtering by Target/Plan/Company happens after the fetch so only relevant tenants receive the notification. /// private async Task DispatchNotificationsAsync(Announcement model) { var companies = (await _unitOfWork.Companies.FindAsync( c => !c.IsDeleted && c.IsActive, ignoreQueryFilters: true)).ToList(); if (model.Target == "Plan" && model.TargetPlan.HasValue) companies = companies.Where(c => c.SubscriptionPlan == model.TargetPlan.Value).ToList(); else if (model.Target == "Company" && model.TargetCompanyId.HasValue) companies = companies.Where(c => c.Id == model.TargetCompanyId.Value).ToList(); foreach (var company in companies) await _inApp.CreateAsync(company.Id, model.Title, model.Message, "Announcement"); } /// /// Loads company and plan lists into ViewBag for the Create/Edit form dropdowns. Uses IgnoreQueryFilters to bypass soft-delete and tenant filters so all companies are available for targeting. /// private async Task PopulateDropdownsAsync() { ViewBag.Companies = (await _unitOfWork.Companies.FindAsync(c => !c.IsDeleted, ignoreQueryFilters: true)) .OrderBy(c => c.CompanyName) .Select(c => new { c.Id, c.CompanyName }) .ToList(); ViewBag.PlanConfigs = (await _unitOfWork.SubscriptionPlanConfigs.FindAsync(p => p.IsActive, ignoreQueryFilters: true)) .OrderBy(p => p.SortOrder) .ToList(); } }