diff --git a/PlotDirector.WordCompanion/manifest.xml b/PlotDirector.WordCompanion/manifest.xml index 99e6324..2553ca1 100644 --- a/PlotDirector.WordCompanion/manifest.xml +++ b/PlotDirector.WordCompanion/manifest.xml @@ -22,7 +22,9 @@ - ReadWriteDocument + + ReadDocument diff --git a/PlotLine/Controllers/WordCompanionController.cs b/PlotLine/Controllers/WordCompanionController.cs index ba59d5e..acbfab7 100644 --- a/PlotLine/Controllers/WordCompanionController.cs +++ b/PlotLine/Controllers/WordCompanionController.cs @@ -46,6 +46,13 @@ public sealed class WordCompanionController(IWordCompanionService wordCompanion) return response is null ? NotFound() : Ok(response); } + [HttpPost("books/{bookId:int}/chapters/{chapterId:int}/scenes")] + public async Task CreateScene(int bookId, int chapterId, WordCompanionCreateSceneRequest request) + { + var response = await wordCompanion.CreateSceneAsync(bookId, chapterId, request); + return response is null ? BadRequest() : Ok(response); + } + [HttpPost("books/{bookId:int}/sync-scene")] public async Task SyncScene(int bookId, WordCompanionSyncSceneRequest request) { diff --git a/PlotLine/Data/WordCompanionRepository.cs b/PlotLine/Data/WordCompanionRepository.cs index dc1e9b3..60e1338 100644 --- a/PlotLine/Data/WordCompanionRepository.cs +++ b/PlotLine/Data/WordCompanionRepository.cs @@ -11,6 +11,7 @@ public interface IWordCompanionRepository Task GetBookStructureAsync(int bookId, int userId); Task ResolveSceneAsync(int bookId, int userId, string chapterTitle, string sceneTitle); Task GetSceneCompanionAsync(int sceneId, int userId); + Task CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request); Task SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request); Task SyncManuscriptAsync(int bookId, int userId, WordCompanionSyncManuscriptRequest request); Task UpdateSceneLinksAsync(int sceneId, int userId, WordCompanionUpdateSceneLinksRequest request); @@ -115,6 +116,15 @@ public sealed class WordCompanionRepository(ISqlConnectionFactory connectionFact }; } + public async Task CreateSceneAsync(int bookId, int chapterId, int userId, WordCompanionCreateSceneRequest request) + { + using var connection = connectionFactory.CreateConnection(); + return await connection.QuerySingleOrDefaultAsync( + "dbo.WordCompanion_Scene_Create", + new { BookID = bookId, ChapterID = chapterId, UserID = userId, request.SceneTitle }, + commandType: CommandType.StoredProcedure); + } + public async Task SyncSceneAsync(int bookId, int userId, WordCompanionSyncSceneRequest request) { using var connection = connectionFactory.CreateConnection(); diff --git a/PlotLine/Models/WordCompanionApiModels.cs b/PlotLine/Models/WordCompanionApiModels.cs index c3a05d3..a9889cd 100644 --- a/PlotLine/Models/WordCompanionApiModels.cs +++ b/PlotLine/Models/WordCompanionApiModels.cs @@ -114,6 +114,18 @@ public sealed class WordCompanionSyncSceneResponse public string Message { get; init; } = "Scene synced successfully."; } +public sealed class WordCompanionCreateSceneRequest +{ + public string? SceneTitle { get; set; } +} + +public sealed class WordCompanionCreateSceneResponse +{ + public int ChapterId { get; init; } + public int SceneId { get; init; } + public string Message { get; init; } = "Scene created."; +} + public sealed class WordCompanionSyncManuscriptRequest { public string? DocumentId { get; set; } diff --git a/PlotLine/Services/WordCompanionService.cs b/PlotLine/Services/WordCompanionService.cs index 42717a9..d032ebf 100644 --- a/PlotLine/Services/WordCompanionService.cs +++ b/PlotLine/Services/WordCompanionService.cs @@ -10,6 +10,7 @@ public interface IWordCompanionService Task GetBookStructureAsync(int bookId); Task ResolveSceneAsync(int bookId, WordCompanionResolveSceneRequest request); Task GetSceneCompanionAsync(int sceneId); + Task CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request); Task SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request); Task SyncManuscriptAsync(int bookId, WordCompanionSyncManuscriptRequest request); Task UpdateSceneLinksAsync(int sceneId, WordCompanionUpdateSceneLinksRequest request); @@ -55,6 +56,17 @@ public sealed class WordCompanionService(IWordCompanionRepository repository, IC return sceneId <= 0 ? Task.FromResult(null) : repository.GetSceneCompanionAsync(sceneId, RequireUserId()); } + public Task CreateSceneAsync(int bookId, int chapterId, WordCompanionCreateSceneRequest request) + { + if (bookId <= 0 || chapterId <= 0 || string.IsNullOrWhiteSpace(request.SceneTitle)) + { + return Task.FromResult(null); + } + + request.SceneTitle = request.SceneTitle.Trim(); + return repository.CreateSceneAsync(bookId, chapterId, RequireUserId(), request); + } + public Task SyncSceneAsync(int bookId, WordCompanionSyncSceneRequest request) { if (bookId <= 0 diff --git a/PlotLine/Sql/062_Phase10J_WordCompanionSceneCreateLink.sql b/PlotLine/Sql/062_Phase10J_WordCompanionSceneCreateLink.sql new file mode 100644 index 0000000..38e883b --- /dev/null +++ b/PlotLine/Sql/062_Phase10J_WordCompanionSceneCreateLink.sql @@ -0,0 +1,85 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Create + @BookID int, + @ChapterID int, + @UserID int, + @SceneTitle nvarchar(200) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @TrimmedSceneTitle nvarchar(200) = LTRIM(RTRIM(COALESCE(@SceneTitle, N''))); + IF NULLIF(@TrimmedSceneTitle, N'') IS NULL + RETURN; + + IF NOT EXISTS + ( + SELECT 1 + FROM dbo.Chapters c + INNER JOIN dbo.Books b ON b.BookID = c.BookID + INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID + INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID + WHERE b.BookID = @BookID + AND c.ChapterID = @ChapterID + AND c.IsArchived = 0 + AND b.IsArchived = 0 + AND p.IsArchived = 0 + AND pua.UserID = @UserID + AND pua.IsActive = 1 + ) + RETURN; + + DECLARE @PlannedRevisionStatusID int = (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder); + DECLARE @UnknownTimeModeID int = (SELECT TOP (1) TimeModeID FROM dbo.TimeModes WHERE TimeModeName = N'Unknown' ORDER BY SortOrder); + DECLARE @UnknownTimeConfidenceID int = (SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences WHERE TimeConfidenceName = N'Unknown' ORDER BY SortOrder); + + IF @PlannedRevisionStatusID IS NULL OR @UnknownTimeModeID IS NULL OR @UnknownTimeConfidenceID IS NULL + RETURN; + + DECLARE @NextSceneNumber decimal(9,2) = ISNULL((SELECT MAX(SceneNumber) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 1; + DECLARE @NextSceneOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Scenes WHERE ChapterID = @ChapterID), 0) + 10; + DECLARE @SceneID int; + + INSERT dbo.Scenes + ( + ChapterID, + SceneNumber, + SceneTitle, + Summary, + SortOrder, + TimeModeID, + TimeConfidenceID, + ScenePurposeNotes, + SceneOutcomeNotes, + RevisionStatusID, + PrimaryLocationID + ) + VALUES + ( + @ChapterID, + @NextSceneNumber, + @TrimmedSceneTitle, + NULL, + @NextSceneOrder, + @UnknownTimeModeID, + @UnknownTimeConfidenceID, + NULL, + NULL, + @PlannedRevisionStatusID, + NULL + ); + + SET @SceneID = CAST(SCOPE_IDENTITY() AS int); + + INSERT dbo.SceneWorkflow (SceneID, DraftStatus, EstimatedWordCount, ActualWordCount, IsBlocked) + VALUES (@SceneID, N'Planned', 0, 0, 0); + + SELECT @ChapterID AS ChapterId, + @SceneID AS SceneId, + N'Scene created.' AS Message; +END; +GO diff --git a/PlotLine/Views/WordCompanionHost/Index.cshtml b/PlotLine/Views/WordCompanionHost/Index.cshtml index 43f5e32..3beeed4 100644 --- a/PlotLine/Views/WordCompanionHost/Index.cshtml +++ b/PlotLine/Views/WordCompanionHost/Index.cshtml @@ -121,6 +121,27 @@

+ +