using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PlotLine.Services; using PlotLine.ViewModels; namespace PlotLine.Controllers; [Authorize] public sealed class FeatureRequestsController( ICurrentUserService currentUser, IFeatureRequestService featureRequests, IEmailService emails) : Controller { [HttpGet] public async Task Index() { var userId = RequireUserId(); var requests = await featureRequests.ListForUserAsync(userId); return View(new FeatureRequestIndexViewModel { Requests = requests }); } [HttpGet] public IActionResult Create() { return View(new FeatureRequestCreateViewModel()); } [HttpGet] public async Task Roadmap() { return View(new FeatureRequestRoadmapViewModel { Sections = await featureRequests.ListRoadmapAsync() }); } [HttpPost] [ValidateAntiForgeryToken] public async Task Create(FeatureRequestCreateViewModel model) { if (!ModelState.IsValid) { return View(WithOptions(model)); } var created = await featureRequests.CreateAsync(RequireUserId(), model); TempData["FeatureRequestMessage"] = "Your feature request has been submitted."; return RedirectToAction(nameof(Details), new { id = created.FeatureRequestID }); } [HttpGet] public async Task Details(int id) { var request = await featureRequests.GetForUserAsync(id, RequireUserId()); if (request is null) { return NotFound(); } await featureRequests.MarkViewedForUserAsync(request.FeatureRequestID, request.UserID); request.UserNeedsAttention = false; request.UserAttentionUtc = null; return View(new FeatureRequestDetailsViewModel { Request = request, Messages = await featureRequests.ListMessagesAsync(request.FeatureRequestID), Reply = new FeatureRequestReplyViewModel { FeatureRequestID = request.FeatureRequestID } }); } [HttpPost] [ValidateAntiForgeryToken] public async Task Reply([Bind(Prefix = "Reply")] FeatureRequestReplyViewModel model) { var userId = RequireUserId(); var request = await featureRequests.GetForUserAsync(model.FeatureRequestID, userId); if (request is null) { return NotFound(); } if (!ModelState.IsValid) { return View(nameof(Details), new FeatureRequestDetailsViewModel { Request = request, Messages = await featureRequests.ListMessagesAsync(request.FeatureRequestID), Reply = model }); } await featureRequests.AddUserReplyAsync(model.FeatureRequestID, userId, model.MessageBody); await emails.SendFeatureRequestUserReplyAsync( request, Preview(model.MessageBody), BuildAbsoluteUrl("FeatureRequestDetails", "Admin", new { id = request.FeatureRequestID }), currentUser.Email ?? string.Empty); return RedirectToAction(nameof(Details), new { id = model.FeatureRequestID }); } private int RequireUserId() { return currentUser.UserId ?? throw new InvalidOperationException("Authenticated user id was not available."); } private static FeatureRequestCreateViewModel WithOptions(FeatureRequestCreateViewModel model) { model.AppAreas = PlotLine.Models.FeatureRequestOptions.AppAreas; model.ImportanceOptions = PlotLine.Models.FeatureRequestOptions.ImportanceOptions; return model; } private string BuildAbsoluteUrl(string action, string controller, object routeValues) { return Url.Action(action, controller, routeValues, Request.Scheme) ?? $"{Request.Scheme}://{Request.Host}"; } private static string Preview(string value) { var cleaned = value.ReplaceLineEndings(" ").Trim(); return cleaned.Length <= 240 ? cleaned : $"{cleaned[..240]}..."; } }