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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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; } = [];
|
||||
}
|
||||
Reference in New Issue
Block a user