From 36a4c76921614a3153c32c1f910b0663676f7501 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Tue, 30 Jun 2026 15:16:13 +0100 Subject: [PATCH] PlotDirector Phase 17H - Character Knowledge Editor Migration --- PlotLine/Controllers/ScenesController.cs | 56 ++++++ PlotLine/Services/CoreServices.cs | 9 +- PlotLine/Services/ProjectAccessFilter.cs | 1 + PlotLine/ViewModels/CoreViewModels.cs | 2 + .../_SceneContinuityCard.cshtml | 2 +- .../_SceneKnowledgeEditor.cshtml | 165 ++++++++++++++++++ .../SceneInspectorV2/_ScenePeopleCard.cshtml | 2 +- 7 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 PlotLine/Views/Shared/SceneInspectorV2/_SceneKnowledgeEditor.cshtml diff --git a/PlotLine/Controllers/ScenesController.cs b/PlotLine/Controllers/ScenesController.cs index 3683f7d..b8beff8 100644 --- a/PlotLine/Controllers/ScenesController.cs +++ b/PlotLine/Controllers/ScenesController.cs @@ -124,6 +124,15 @@ public sealed class ScenesController( : PartialView("~/Views/Shared/SceneInspectorV2/_SceneCharactersEditor.cshtml", model); } + [HttpGet] + public async Task SceneKnowledgeEditor(int sceneId) + { + var model = await scenes.GetEditAsync(sceneId); + return model is null + ? NotFound() + : PartialView("~/Views/Shared/SceneInspectorV2/_SceneKnowledgeEditor.cshtml", model); + } + [HttpGet] public async Task SceneLocationsEditor(int sceneId) { @@ -1123,6 +1132,24 @@ public sealed class ScenesController( public async Task AddCharacterKnowledge(CharacterKnowledgeCreateViewModel model) { await characters.AddKnowledgeAsync(model); + if (IsAjaxRequest()) + { + return await SceneKnowledgeRefreshJsonAsync(model.SceneID); + } + + return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task UpdateCharacterKnowledge(CharacterKnowledgeCreateViewModel model) + { + await characters.UpdateKnowledgeAsync(model); + if (IsAjaxRequest()) + { + return await SceneKnowledgeRefreshJsonAsync(model.SceneID); + } + return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); } @@ -1131,6 +1158,11 @@ public sealed class ScenesController( public async Task DeleteCharacterKnowledge(int id, int sceneId, int? projectId, int? bookId) { await characters.DeleteKnowledgeAsync(id); + if (IsAjaxRequest()) + { + return await SceneKnowledgeRefreshJsonAsync(sceneId); + } + return RedirectForScene(sceneId, projectId, bookId); } @@ -1359,6 +1391,30 @@ public sealed class ScenesController( }); } + private async Task SceneKnowledgeRefreshJsonAsync(int sceneId) + { + var updated = await scenes.GetEditAsync(sceneId); + if (updated is null) + { + return NotFound(); + } + + return Json(new + { + success = true, + html = new + { + health = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneInspectorHealthStrip.cshtml", updated), + people = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml", updated), + continuity = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml", updated) + }, + scene = new + { + updated.SceneID + } + }); + } + private IActionResult RedirectForScene(int sceneId, int? projectId, int? bookId) { if (TryGetLocalReturnUrl(out var returnUrl)) diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 235ae49..6f7d62e 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -261,6 +261,7 @@ public interface ICharacterService Task RejectSceneCharacterSuggestionAsync(int suggestionId); Task AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model); Task AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model); + Task UpdateKnowledgeAsync(CharacterKnowledgeCreateViewModel model); Task DeleteKnowledgeAsync(int characterKnowledgeId); Task AddRelationshipEventAsync(RelationshipEventCreateViewModel model); Task DeleteRelationshipEventAsync(int relationshipEventId); @@ -9521,13 +9522,19 @@ public sealed class CharacterService( Description = model.Description }); - public Task AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => characters.SaveKnowledgeAsync(new CharacterKnowledgeItem + public Task AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => SaveKnowledgeAsync(model); + + public Task UpdateKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => SaveKnowledgeAsync(model); + + private Task SaveKnowledgeAsync(CharacterKnowledgeCreateViewModel model) => characters.SaveKnowledgeAsync(new CharacterKnowledgeItem { + CharacterKnowledgeID = model.CharacterKnowledgeID, CharacterID = model.CharacterID, StoryAssetID = model.StoryAssetID, PlotThreadID = model.PlotThreadID, SceneID = model.SceneID, KnowledgeStateID = model.KnowledgeStateID, + SourceEventID = model.SourceEventID, Description = model.Description }); diff --git a/PlotLine/Services/ProjectAccessFilter.cs b/PlotLine/Services/ProjectAccessFilter.cs index 8bda11f..cb20cf5 100644 --- a/PlotLine/Services/ProjectAccessFilter.cs +++ b/PlotLine/Services/ProjectAccessFilter.cs @@ -59,6 +59,7 @@ public sealed class ProjectAccessFilter(IProjectAccessService access, ICurrentUs ["ParentPlotLineID"] = "PlotLine", ["PlotThreadID"] = "PlotThread", ["CharacterRelationshipID"] = "CharacterRelationship", + ["CharacterKnowledgeID"] = "CharacterKnowledge", ["TimelineViewPresetID"] = "TimelineViewPreset", ["ScenarioID"] = "Scenario" }; diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index b587089..bdabf2b 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -2025,11 +2025,13 @@ public sealed class CharacterAttributeEventCreateViewModel public sealed class CharacterKnowledgeCreateViewModel { + public int CharacterKnowledgeID { get; set; } public int SceneID { get; set; } public int CharacterID { get; set; } public int? StoryAssetID { get; set; } public int? PlotThreadID { get; set; } public int KnowledgeStateID { get; set; } + public int? SourceEventID { get; set; } public string? Description { get; set; } public int? ReturnProjectID { get; set; } public int? ReturnBookID { get; set; } diff --git a/PlotLine/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml b/PlotLine/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml index f401041..f44af2c 100644 --- a/PlotLine/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml +++ b/PlotLine/Views/Shared/SceneInspectorV2/_SceneContinuityCard.cshtml @@ -7,7 +7,7 @@ : $"{warningCount} warning{(warningCount == 1 ? "" : "s")} need review."; } -
+

Continuity

diff --git a/PlotLine/Views/Shared/SceneInspectorV2/_SceneKnowledgeEditor.cshtml b/PlotLine/Views/Shared/SceneInspectorV2/_SceneKnowledgeEditor.cshtml new file mode 100644 index 0000000..1369f22 --- /dev/null +++ b/PlotLine/Views/Shared/SceneInspectorV2/_SceneKnowledgeEditor.cshtml @@ -0,0 +1,165 @@ +@model SceneEditViewModel + +
+
+
+
+ Knowledge changes + @Model.CharacterKnowledge.Count +
+ + @if (!Model.CharacterKnowledge.Any()) + { +

No knowledge changes are recorded for this scene yet.

+ } + else + { +
+ @foreach (var item in Model.CharacterKnowledge) + { +
+
+
+

@item.CharacterName

+

@(item.AssetName ?? item.ThreadTitle ?? "Scene knowledge")

+
+ @item.KnowledgeStateName +
+ + @if (!string.IsNullOrWhiteSpace(item.Description)) + { +

@item.Description

+ } + +
+ @Html.AntiForgeryToken() + + + + + + + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ +
+ @Html.AntiForgeryToken() + + + + + + +
+
+ } +
+ } +
+ +
+
+ Add knowledge change +
+ +
+ @Html.AntiForgeryToken() + + + + + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ + +
diff --git a/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml b/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml index 6ad23d5..6db5d29 100644 --- a/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml +++ b/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml @@ -53,7 +53,7 @@
- +