Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,207 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
using System.ComponentModel.DataAnnotations;
namespace PowderCoating.Web.Controllers;
[Authorize]
public class ContactController : Controller
{
private readonly IAdminNotificationService _adminNotification;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ITenantContext _tenantContext;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ContactController> _logger;
public ContactController(
IAdminNotificationService adminNotification,
UserManager<ApplicationUser> userManager,
ITenantContext tenantContext,
IUnitOfWork unitOfWork,
ILogger<ContactController> logger)
{
_adminNotification = adminNotification;
_userManager = userManager;
_tenantContext = tenantContext;
_unitOfWork = unitOfWork;
_logger = logger;
}
/// <summary>
/// Renders the Contact Us page pre-filled with the current user's name, email, and company.
/// </summary>
[HttpGet]
public async Task<IActionResult> Index()
{
var model = await BuildViewModelAsync();
return View(model);
}
/// <summary>
/// Handles the contact form submission. Saves to the database and sends an email notification
/// to configured admin addresses with reply-to set to the submitter.
/// </summary>
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> Submit(ContactFormModel form)
{
if (!ModelState.IsValid)
{
var vm = await BuildViewModelAsync();
vm.Form = form;
return View("Index", vm);
}
var user = await _userManager.GetUserAsync(User);
var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
// Persist first — email is best-effort, record must survive even if email fails
var submission = new ContactSubmission
{
CompanyId = companyId,
SenderName = form.Name,
SenderEmail = form.Email,
CompanyName = form.CompanyName,
Category = form.Category,
Subject = form.Subject,
Message = form.Message,
CreatedBy = user?.Id,
};
await _unitOfWork.ContactSubmissions.AddAsync(submission);
await _unitOfWork.CompleteAsync();
_logger.LogInformation("Contact form saved (id {Id}) from {Email} ({Company}): [{Category}] {Subject}",
submission.Id, form.Email, form.CompanyName, form.Category, form.Subject);
// Email notification is best-effort — a failure doesn't prevent the success message
try
{
await _adminNotification.NotifyContactFormSubmittedAsync(
senderName: form.Name,
senderEmail: form.Email,
companyName: form.CompanyName,
category: form.Category,
subject: form.Subject,
message: form.Message);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Contact form email notification failed for submission {Id} — record was saved", submission.Id);
}
TempData["Success"] = "Your message has been sent. We'll get back to you as soon as possible.";
return RedirectToAction(nameof(Index));
}
/// <summary>
/// Admin-only list of all contact form submissions, newest first.
/// Unread submissions are highlighted. SuperAdmin sees all companies; company admins see their own.
/// </summary>
[HttpGet]
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
public async Task<IActionResult> Submissions()
{
var all = await _unitOfWork.ContactSubmissions.GetAllAsync();
var list = all.OrderByDescending(s => s.CreatedAt).ToList();
return View(list);
}
/// <summary>
/// Marks a submission as read and optionally saves an admin note. SuperAdmin only.
/// </summary>
[HttpPost, ValidateAntiForgeryToken]
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
public async Task<IActionResult> MarkRead(int id, string? adminNotes)
{
var submission = await _unitOfWork.ContactSubmissions.GetByIdAsync(id, ignoreQueryFilters: true);
if (submission == null) return NotFound();
var user = await _userManager.GetUserAsync(User);
submission.IsRead = true;
submission.ReadAt = DateTime.UtcNow;
submission.ReadByUserId = user?.Id;
submission.ReadByUserName = user != null ? $"{user.FirstName} {user.LastName}".Trim() : null;
if (adminNotes != null)
submission.AdminNotes = adminNotes.Trim();
await _unitOfWork.CompleteAsync();
return RedirectToAction(nameof(Submissions));
}
// ─── Helpers ─────────────────────────────────────────────────────────────
/// <summary>
/// Builds the ContactViewModel pre-filled with the current user's identity and company name.
/// </summary>
private async Task<ContactViewModel> BuildViewModelAsync()
{
var user = await _userManager.GetUserAsync(User);
var companyId = _tenantContext.GetCurrentCompanyId();
var company = companyId.HasValue
? await _unitOfWork.Companies.GetByIdAsync(companyId.Value)
: null;
return new ContactViewModel
{
Form = new ContactFormModel
{
Name = user != null ? $"{user.FirstName} {user.LastName}".Trim() : string.Empty,
Email = user?.Email ?? string.Empty,
CompanyName = company?.CompanyName ?? string.Empty,
}
};
}
}
// ─── View models ─────────────────────────────────────────────────────────────
public class ContactViewModel
{
public ContactFormModel Form { get; set; } = new();
}
public class ContactFormModel
{
[Required, MaxLength(150)]
[Display(Name = "Your Name")]
public string Name { get; set; } = string.Empty;
[Required, EmailAddress, MaxLength(200)]
[Display(Name = "Your Email")]
public string Email { get; set; } = string.Empty;
[Required, MaxLength(200)]
[Display(Name = "Company")]
public string CompanyName { get; set; } = string.Empty;
[Required]
[Display(Name = "Category")]
public string Category { get; set; } = string.Empty;
[Required, MaxLength(200)]
[Display(Name = "Subject")]
public string Subject { get; set; } = string.Empty;
[Required, MaxLength(4000)]
[Display(Name = "Message")]
public string Message { get; set; } = string.Empty;
/// <summary>Standard contact reason categories shown in the dropdown.</summary>
public static readonly string[] Categories =
[
"General Question",
"Technical Issue / Bug",
"Billing & Subscription",
"Feature Request",
"Account Access Issue",
"Data Import / Migration Help",
"Training & Onboarding",
"Other",
];
}