From c54da61524db44370a1f2c414ad01f5816c82eab Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Tue, 30 Jun 2026 16:04:14 +0100 Subject: [PATCH] PlotDirector Phase 17K - Character Attributes Editor Migration --- PlotLine/Controllers/ScenesController.cs | 87 ++++++++++++ PlotLine/Services/CoreServices.cs | 8 +- PlotLine/ViewModels/CoreViewModels.cs | 1 + .../_SceneCharacterAttributesEditor.cshtml | 126 ++++++++++++++++++ .../SceneInspectorV2/_ScenePeopleCard.cshtml | 5 + PlotLine/wwwroot/js/scene-inspector-v2.js | 1 + 6 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 PlotLine/Views/Shared/SceneInspectorV2/_SceneCharacterAttributesEditor.cshtml diff --git a/PlotLine/Controllers/ScenesController.cs b/PlotLine/Controllers/ScenesController.cs index ffdf7eb..7469411 100644 --- a/PlotLine/Controllers/ScenesController.cs +++ b/PlotLine/Controllers/ScenesController.cs @@ -142,6 +142,15 @@ public sealed class ScenesController( : PartialView("~/Views/Shared/SceneInspectorV2/_SceneRelationshipsEditor.cshtml", model); } + [HttpGet] + public async Task SceneCharacterAttributesEditor(int sceneId) + { + var model = await scenes.GetEditAsync(sceneId); + return model is null + ? NotFound() + : PartialView("~/Views/Shared/SceneInspectorV2/_SceneCharacterAttributesEditor.cshtml", model); + } + [HttpGet] public async Task SceneLocationsEditor(int sceneId) { @@ -1116,23 +1125,76 @@ public sealed class ScenesController( if (model.CharacterID <= 0) { + if (IsAjaxRequest()) + { + return BadRequest(new { success = false, message = "Choose a character before adding an attribute." }); + } + TempData["CharacterAttributeAddError"] = "Choose a character before adding an attribute."; return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); } if (model.CharacterAttributeTypeID <= 0) { + if (IsAjaxRequest()) + { + return BadRequest(new { success = false, message = "Choose an attribute type before saving." }); + } + TempData["CharacterAttributeAddError"] = "Choose an attribute type before saving."; return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); } if (string.IsNullOrWhiteSpace(model.AttributeValue)) { + if (IsAjaxRequest()) + { + return BadRequest(new { success = false, message = "Enter an attribute value before saving." }); + } + TempData["CharacterAttributeAddError"] = "Enter an attribute value before saving."; return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); } await characters.AddAttributeEventAsync(model); + if (IsAjaxRequest()) + { + return await SceneCharacterAttributesRefreshJsonAsync(model.SceneID); + } + + return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task UpdateCharacterAttributeEvent(CharacterAttributeEventCreateViewModel model) + { + if (model.CharacterAttributeEventID <= 0 || model.SceneID <= 0) + { + return BadRequest(); + } + + if (model.CharacterID <= 0) + { + return BadRequest(new { success = false, message = "Choose a character before saving this attribute." }); + } + + if (model.CharacterAttributeTypeID <= 0) + { + return BadRequest(new { success = false, message = "Choose an attribute type before saving." }); + } + + if (string.IsNullOrWhiteSpace(model.AttributeValue)) + { + return BadRequest(new { success = false, message = "Enter an attribute value before saving." }); + } + + await characters.UpdateAttributeEventAsync(model); + if (IsAjaxRequest()) + { + return await SceneCharacterAttributesRefreshJsonAsync(model.SceneID); + } + return RedirectForScene(model.SceneID, model.ReturnProjectID, model.ReturnBookID); } @@ -1441,6 +1503,31 @@ public sealed class ScenesController( return await ScenePeopleContinuityRefreshJsonAsync(sceneId); } + private async Task SceneCharacterAttributesRefreshJsonAsync(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) + }, + editorHtml = await RenderPartialViewToStringAsync("~/Views/Shared/SceneInspectorV2/_SceneCharacterAttributesEditor.cshtml", updated), + scene = new + { + updated.SceneID + } + }); + } + private async Task ScenePeopleContinuityRefreshJsonAsync(int sceneId) { var updated = await scenes.GetEditAsync(sceneId); diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index dfb72a6..e7ff2c0 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -260,6 +260,7 @@ public interface ICharacterService Task AcceptSceneCharacterSuggestionAsync(int suggestionId); Task RejectSceneCharacterSuggestionAsync(int suggestionId); Task AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model); + Task UpdateAttributeEventAsync(CharacterAttributeEventCreateViewModel model); Task AddKnowledgeAsync(CharacterKnowledgeCreateViewModel model); Task UpdateKnowledgeAsync(CharacterKnowledgeCreateViewModel model); Task DeleteKnowledgeAsync(int characterKnowledgeId); @@ -9514,8 +9515,13 @@ public sealed class CharacterService( } } - public Task AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model) => characters.SaveAttributeEventAsync(new CharacterAttributeEvent + public Task AddAttributeEventAsync(CharacterAttributeEventCreateViewModel model) => SaveAttributeEventAsync(model); + + public Task UpdateAttributeEventAsync(CharacterAttributeEventCreateViewModel model) => SaveAttributeEventAsync(model); + + private Task SaveAttributeEventAsync(CharacterAttributeEventCreateViewModel model) => characters.SaveAttributeEventAsync(new CharacterAttributeEvent { + CharacterAttributeEventID = model.CharacterAttributeEventID, CharacterID = model.CharacterID, CharacterAttributeTypeID = model.CharacterAttributeTypeID, SceneID = model.SceneID, diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index dbd1364..62e75b4 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -2014,6 +2014,7 @@ public sealed class SceneCharacterEditViewModel public sealed class CharacterAttributeEventCreateViewModel { + public int CharacterAttributeEventID { get; set; } public int SceneID { get; set; } public int CharacterID { get; set; } public int CharacterAttributeTypeID { get; set; } diff --git a/PlotLine/Views/Shared/SceneInspectorV2/_SceneCharacterAttributesEditor.cshtml b/PlotLine/Views/Shared/SceneInspectorV2/_SceneCharacterAttributesEditor.cshtml new file mode 100644 index 0000000..74a4df1 --- /dev/null +++ b/PlotLine/Views/Shared/SceneInspectorV2/_SceneCharacterAttributesEditor.cshtml @@ -0,0 +1,126 @@ +@model SceneEditViewModel + +
+
+
+
+ Character attribute events + @Model.CharacterAttributeEvents.Count +
+ + @if (!Model.CharacterAttributeEvents.Any()) + { +

No character attribute events are recorded for this scene yet.

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

@item.CharacterName

+

@item.AttributeName

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

@item.Description

+ } + +
+ @Html.AntiForgeryToken() + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ } +
+ } +
+ +
+
+ Add attribute event +
+ +
+ @Html.AntiForgeryToken() + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ + +
diff --git a/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml b/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml index 8461cb0..cf764ba 100644 --- a/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml +++ b/PlotLine/Views/Shared/SceneInspectorV2/_ScenePeopleCard.cshtml @@ -49,10 +49,15 @@ Relationship changes @Model.RelationshipEvents.Count +
+ Attribute changes + @Model.CharacterAttributeEvents.Count +
+
diff --git a/PlotLine/wwwroot/js/scene-inspector-v2.js b/PlotLine/wwwroot/js/scene-inspector-v2.js index fba3a12..7ce4941 100644 --- a/PlotLine/wwwroot/js/scene-inspector-v2.js +++ b/PlotLine/wwwroot/js/scene-inspector-v2.js @@ -6,6 +6,7 @@ threads: "Manage threads", dependencies: "Manage dependencies", characters: "Manage characters", + attributes: "Manage attributes", knowledge: "Manage knowledge", relationships: "Manage relationships", locations: "Manage locations",