PlotDirector/PlotLine/Controllers/AdminController.cs
2026-07-04 21:04:40 +01:00

231 lines
8.5 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
[Authorize(Policy = "AdminOnly")]
[Route("admin")]
public sealed class AdminController(
ICurrentUserService currentUser,
IFeatureRequestService featureRequests,
IEmailService emails,
IStoryIntelligenceDiagnosticsService storyIntelligenceDiagnostics,
IStoryIntelligenceDryRunService storyIntelligenceDryRun,
IChapterStructureDryRunService chapterStructureDryRun,
IChapterStoryIntelligenceDryRunService chapterStoryIntelligenceDryRun,
IStoryIntelligenceResultPersistenceService storyIntelligenceResults) : Controller
{
[HttpGet("")]
[HttpGet("index")]
public async Task<IActionResult> Index()
{
return View(new AdminDashboardViewModel
{
FeatureRequestSummary = await featureRequests.GetAdminSummaryAsync()
});
}
[HttpGet("theme-audit")]
public IActionResult ThemeAudit()
{
return View();
}
[HttpGet("story-intelligence-diagnostics")]
public async Task<IActionResult> StoryIntelligenceDiagnostics(CancellationToken cancellationToken)
{
return View(await storyIntelligenceDiagnostics.GetDiagnosticsAsync(cancellationToken));
}
[HttpGet("story-intelligence-test")]
public IActionResult StoryIntelligenceTest()
{
return View(storyIntelligenceDryRun.GetDefault());
}
[HttpPost("story-intelligence-test")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> StoryIntelligenceTest([Bind(Prefix = "Form")] StoryIntelligenceDryRunForm form, CancellationToken cancellationToken)
{
return View(await storyIntelligenceDryRun.ExecuteAsync(form, cancellationToken));
}
[HttpGet("chapter-structure-test")]
public IActionResult ChapterStructureTest()
{
return View(chapterStructureDryRun.GetDefault());
}
[HttpPost("chapter-structure-test")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChapterStructureTest([Bind(Prefix = "Form")] ChapterStructureDryRunForm form, CancellationToken cancellationToken)
{
return View(await chapterStructureDryRun.ExecuteAsync(form, cancellationToken));
}
[HttpGet("chapter-story-intelligence-test")]
public IActionResult ChapterStoryIntelligenceTest()
{
return View(chapterStoryIntelligenceDryRun.GetDefault());
}
[HttpPost("chapter-story-intelligence-test")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChapterStoryIntelligenceTest([Bind(Prefix = "Form")] ChapterStoryIntelligenceDryRunForm form, CancellationToken cancellationToken)
{
return View(await chapterStoryIntelligenceDryRun.ExecuteAsync(form, cancellationToken));
}
[HttpPost("chapter-story-intelligence-test/save")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveChapterStoryIntelligenceRun(StoryIntelligenceSaveRunForm form)
{
if (currentUser.UserId is not int userId)
{
return Forbid();
}
var runId = await storyIntelligenceResults.SaveDisplayedRunAsync(userId, form.Payload);
TempData["AdminMessage"] = $"Story Intelligence run {runId:N0} saved. Saving the same displayed dry run again creates a separate audit record.";
return RedirectToAction(nameof(StoryIntelligenceRunDetails), new { id = runId });
}
[HttpGet("story-intelligence-runs")]
public async Task<IActionResult> StoryIntelligenceRuns()
{
return View(await storyIntelligenceResults.ListRunsAsync());
}
[HttpGet("story-intelligence-runs/{id:int}")]
public async Task<IActionResult> StoryIntelligenceRunDetails(int id)
{
var model = await storyIntelligenceResults.GetRunDetailAsync(id);
return model is null ? NotFound() : View(model);
}
[HttpGet("feature-requests")]
public async Task<IActionResult> FeatureRequests(string? status, string? appArea, string? importance, string? search, string? sort)
{
var filter = new AdminFeatureRequestFilter
{
Status = FeatureRequestStatuses.IsValid(status) ? status : null,
AppArea = FeatureRequestOptions.AppAreas.Contains(appArea ?? string.Empty, StringComparer.OrdinalIgnoreCase) ? appArea : null,
Importance = FeatureRequestOptions.ImportanceOptions.Contains(importance ?? string.Empty, StringComparer.OrdinalIgnoreCase) ? importance : null,
Search = string.IsNullOrWhiteSpace(search) ? null : search.Trim(),
Sort = FeatureRequestSortOptions.IsValid(sort) ? sort! : FeatureRequestSortOptions.LatestActivity
};
return View(new AdminFeatureRequestIndexViewModel
{
Status = filter.Status,
AppArea = filter.AppArea,
Importance = filter.Importance,
Search = filter.Search,
Sort = filter.Sort,
Requests = await featureRequests.ListForAdminAsync(filter)
});
}
[HttpGet("feature-requests/{id:int}")]
public async Task<IActionResult> FeatureRequestDetails(int id)
{
var request = await featureRequests.GetForAdminAsync(id);
if (request is null)
{
return NotFound();
}
return View(new FeatureRequestDetailsViewModel
{
IsAdminView = true,
Request = request,
Messages = await featureRequests.ListMessagesAsync(request.FeatureRequestID),
Reply = new FeatureRequestReplyViewModel { FeatureRequestID = request.FeatureRequestID }
});
}
[HttpPost("feature-requests/update")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateFeatureRequest(AdminFeatureRequestUpdateViewModel model)
{
if (!FeatureRequestStatuses.IsValid(model.Status))
{
return BadRequest();
}
var before = await featureRequests.GetForAdminAsync(model.FeatureRequestID);
if (before is null)
{
return NotFound();
}
var updated = await featureRequests.UpdateAdminFieldsAsync(model);
if (!updated)
{
return NotFound();
}
var after = await featureRequests.GetForAdminAsync(model.FeatureRequestID);
if (after is not null && !string.Equals(before.Status, after.Status, StringComparison.OrdinalIgnoreCase))
{
await emails.SendFeatureRequestStatusChangedAsync(
after,
BuildAbsoluteUrl("Details", "FeatureRequests", new { id = after.FeatureRequestID }),
currentUser.Email ?? string.Empty);
}
TempData["AdminMessage"] = "Feature request updated.";
return RedirectToAction(nameof(FeatureRequestDetails), new { id = model.FeatureRequestID });
}
[HttpPost("feature-requests/reply")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> FeatureRequestReply([Bind(Prefix = "Reply")] FeatureRequestReplyViewModel model)
{
if (currentUser.UserId is not int userId)
{
return Forbid();
}
var request = await featureRequests.GetForAdminAsync(model.FeatureRequestID);
if (request is null)
{
return NotFound();
}
if (!ModelState.IsValid)
{
return View(nameof(FeatureRequestDetails), new FeatureRequestDetailsViewModel
{
IsAdminView = true,
Request = request,
Messages = await featureRequests.ListMessagesAsync(request.FeatureRequestID),
Reply = model
});
}
await featureRequests.AddAdminReplyAsync(model.FeatureRequestID, userId, model.MessageBody);
await emails.SendFeatureRequestAdminReplyAsync(
request,
Preview(model.MessageBody),
BuildAbsoluteUrl("Details", "FeatureRequests", new { id = request.FeatureRequestID }),
currentUser.Email ?? string.Empty);
return RedirectToAction(nameof(FeatureRequestDetails), new { id = model.FeatureRequestID });
}
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]}...";
}
}