231 lines
8.5 KiB
C#
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]}...";
|
|
}
|
|
}
|