Files
PowderCoatingLogix/src/PowderCoating.Web/Controllers/SmsAgreementsController.cs
T
spouliot c18b580ec9 Add SMS Agreements admin page and update help docs
- Add /SmsAgreements SuperAdmin page listing per-company SMS terms acceptance
  status, with stats cards, filter/search, and a full acceptance history modal
  (terms version, accepted by, timestamp, IP, user agent)
- Add SMS Agreements nav link under Tenants & Billing in the platform sidebar
- Update HelpKnowledgeBase and Help docs (Quotes, Settings) to document
  quote approval via SMS and the reuse of existing approval tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 10:17:11 -04:00

100 lines
4.1 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
namespace PowderCoating.Web.Controllers;
/// <summary>
/// SuperAdmin view of per-company SMS terms agreement history.
/// Shows which companies have accepted the current SMS terms, who accepted them,
/// and the full acceptance log for each company.
/// </summary>
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
public class SmsAgreementsController : Controller
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<SmsAgreementsController> _logger;
public SmsAgreementsController(IUnitOfWork unitOfWork, ILogger<SmsAgreementsController> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
/// <summary>
/// Lists every company with its current SMS agreement status and full acceptance history.
/// Uses IgnoreQueryFilters on both queries so deleted/inactive companies and all historical
/// agreement records are included in the audit view.
/// </summary>
public async Task<IActionResult> Index(string? search = null, string? filter = null)
{
var companies = await _unitOfWork.Companies.GetAllAsync(ignoreQueryFilters: true);
var allAgreements = await _unitOfWork.CompanySmsAgreements.GetAllAsync(ignoreQueryFilters: true);
var agreementsByCompany = allAgreements
.GroupBy(a => a.CompanyId)
.ToDictionary(g => g.Key, g => g.OrderByDescending(a => a.AgreedAt).ToList());
var rows = companies
.OrderBy(c => c.CompanyName)
.Select(c =>
{
var agreements = agreementsByCompany.TryGetValue(c.Id, out var list) ? list : [];
var current = agreements.FirstOrDefault(a => a.TermsVersion == AppConstants.SmsTermsVersion);
return new CompanySmsRow
{
CompanyId = c.Id,
CompanyName = c.CompanyName ?? "(unnamed)",
SmsEnabled = c.SmsEnabled,
SmsDisabledByAdmin = c.SmsDisabledByAdmin,
CurrentAgreement = current,
LatestAgreement = agreements.FirstOrDefault(),
AllAgreements = agreements,
IsDeleted = c.IsDeleted
};
})
.ToList();
// Filter
rows = filter switch
{
"accepted" => rows.Where(r => r.CurrentAgreement != null).ToList(),
"pending" => rows.Where(r => r.CurrentAgreement == null).ToList(),
"enabled" => rows.Where(r => r.SmsEnabled).ToList(),
"disabled" => rows.Where(r => r.SmsDisabledByAdmin).ToList(),
_ => rows
};
if (!string.IsNullOrWhiteSpace(search))
rows = rows.Where(r => r.CompanyName.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();
ViewBag.Search = search;
ViewBag.Filter = filter ?? "all";
ViewBag.CurrentTermsVersion = AppConstants.SmsTermsVersion;
// Stats (pre-filter totals)
var all = companies.ToList();
ViewBag.TotalCompanies = all.Count(c => !c.IsDeleted);
ViewBag.AcceptedCount = agreementsByCompany.Count(kvp =>
kvp.Value.Any(a => a.TermsVersion == AppConstants.SmsTermsVersion) &&
!all.FirstOrDefault(c => c.Id == kvp.Key)?.IsDeleted == true);
ViewBag.SmsEnabledCount = all.Count(c => !c.IsDeleted && c.SmsEnabled);
return View(rows);
}
}
/// <summary>View model for one company row on the SMS agreements page.</summary>
public class CompanySmsRow
{
public int CompanyId { get; set; }
public string CompanyName { get; set; } = string.Empty;
public bool SmsEnabled { get; set; }
public bool SmsDisabledByAdmin { get; set; }
public bool IsDeleted { get; set; }
public PowderCoating.Core.Entities.CompanySmsAgreement? CurrentAgreement { get; set; }
public PowderCoating.Core.Entities.CompanySmsAgreement? LatestAgreement { get; set; }
public List<PowderCoating.Core.Entities.CompanySmsAgreement> AllAgreements { get; set; } = [];
}