SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_ListByProject @ProjectID int, @UserID int AS BEGIN SET NOCOUNT ON; SELECT b.BookID AS BookId, b.BookTitle AS Title, b.Subtitle, b.SortOrder, b.UsesExplicitScenes FROM dbo.Books b INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = b.ProjectID INNER JOIN dbo.Projects p ON p.ProjectID = b.ProjectID WHERE b.ProjectID = @ProjectID AND pua.UserID = @UserID AND pua.IsActive = 1 AND p.IsArchived = 0 AND b.IsArchived = 0 ORDER BY b.SortOrder, b.BookNumber, b.BookTitle, b.BookID; END; GO CREATE OR ALTER PROCEDURE dbo.WordCompanion_Book_Create @ProjectID int, @UserID int, @Title nvarchar(200) AS BEGIN SET NOCOUNT ON; DECLARE @TrimmedTitle nvarchar(200) = LTRIM(RTRIM(COALESCE(@Title, N''))); IF NULLIF(@TrimmedTitle, N'') IS NULL RETURN; IF NOT EXISTS ( SELECT 1 FROM dbo.Projects p INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = p.ProjectID WHERE p.ProjectID = @ProjectID AND p.IsArchived = 0 AND pua.UserID = @UserID AND pua.IsActive = 1 ) RETURN; DECLARE @NextBookNumber int = ISNULL((SELECT MAX(BookNumber) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 1; DECLARE @NextBookOrder int = ISNULL((SELECT MAX(SortOrder) FROM dbo.Books WHERE ProjectID = @ProjectID), 0) + 10; DECLARE @BookID int; INSERT dbo.Books (ProjectID, BookTitle, Status, BookNumber, SortOrder, UsesExplicitScenes) VALUES (@ProjectID, @TrimmedTitle, N'Planning', @NextBookNumber, @NextBookOrder, 0); SET @BookID = CAST(SCOPE_IDENTITY() AS int); SELECT @BookID AS BookId, @TrimmedTitle AS Title, CAST(NULL AS nvarchar(200)) AS Subtitle, @NextBookOrder AS SortOrder, CAST(0 AS bit) AS UsesExplicitScenes, N'Book created.' AS Message; END; GO CREATE OR ALTER PROCEDURE dbo.WordCompanion_Manuscript_Analyse @UserID int, @ProjectID int, @BookID int, @DocumentGuid uniqueidentifier = NULL, @ChapterCount int, @SceneCount int, @WordCount int AS BEGIN SET NOCOUNT ON; DECLARE @ProjectTitle nvarchar(200); DECLARE @BookTitle nvarchar(200); DECLARE @BookSubtitle nvarchar(200); DECLARE @UsesExplicitScenes bit; SELECT @ProjectTitle = p.ProjectName, @BookTitle = b.BookTitle, @BookSubtitle = b.Subtitle, @UsesExplicitScenes = b.UsesExplicitScenes FROM dbo.Books b 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 b.ProjectID = @ProjectID AND b.IsArchived = 0 AND p.IsArchived = 0 AND pua.UserID = @UserID AND pua.IsActive = 1; IF @BookTitle IS NULL RETURN; DECLARE @ExistingChapterCount int = (SELECT COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0); DECLARE @ExistingSceneCount int = ( SELECT COUNT(*) FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID WHERE c.BookID = @BookID AND c.IsArchived = 0 AND s.IsArchived = 0 ); DECLARE @LinkedChapterCount int = ( SELECT COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0 AND IsLinkedToManuscript = 1 ); SELECT @ProjectID AS ProjectId, @BookID AS BookId, @ProjectTitle AS ProjectTitle, @BookTitle AS BookTitle, @BookSubtitle AS BookSubtitle, @UsesExplicitScenes AS UsesExplicitScenes, @ChapterCount AS ChapterCount, @SceneCount AS SceneCount, @WordCount AS WordCount, @ExistingChapterCount AS ExistingChapterCount, @ExistingSceneCount AS ExistingSceneCount, @LinkedChapterCount AS LinkedChapterCount, CAST(CASE WHEN @LinkedChapterCount = 0 AND @ChapterCount > 0 THEN 1 ELSE 0 END AS bit) AS CanImport, CAST(CASE WHEN @LinkedChapterCount = 0 AND @ExistingChapterCount > 0 THEN 1 ELSE 0 END AS bit) AS RequiresReplaceConfirmation, CASE WHEN @LinkedChapterCount > 0 THEN N'This Book already contains a linked manuscript.' WHEN @ChapterCount = 0 THEN N'No chapters were found. Use Heading 1 for chapter titles.' WHEN @ExistingChapterCount > 0 THEN N'This Book already contains planned chapters and scenes.' ELSE N'Ready to import manuscript structure.' END AS Message; END; GO CREATE OR ALTER PROCEDURE dbo.WordCompanion_Manuscript_Import @UserID int, @ProjectID int, @BookID int, @DocumentGuid uniqueidentifier, @BindingVersion int = 1, @ReplacePlanned bit = 0, @StructureJson nvarchar(max) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; IF @DocumentGuid IS NULL OR @DocumentGuid = '00000000-0000-0000-0000-000000000000' SET @DocumentGuid = NEWID(); DECLARE @BookTitle nvarchar(200); DECLARE @AccessProjectID int; SELECT @AccessProjectID = b.ProjectID, @BookTitle = b.BookTitle FROM dbo.Books b 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 b.ProjectID = @ProjectID AND b.IsArchived = 0 AND p.IsArchived = 0 AND pua.UserID = @UserID AND pua.IsActive = 1; IF @BookTitle IS NULL RETURN; DECLARE @LinkedChapterCount int = ( SELECT COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0 AND IsLinkedToManuscript = 1 ); IF @LinkedChapterCount > 0 BEGIN SELECT CAST(0 AS bit) AS Imported, CAST(0 AS bit) AS RequiresReplaceConfirmation, N'This Book already contains a linked manuscript.' AS Message, @ProjectID AS ProjectId, @BookID AS BookId, 0 AS ChaptersCreated, 0 AS ScenesCreated, 0 AS ArchivedChapters, 0 AS ArchivedScenes; RETURN; END; DECLARE @ChapterCount int = (SELECT COUNT(*) FROM dbo.Chapters WHERE BookID = @BookID AND IsArchived = 0); IF @ChapterCount > 0 AND @ReplacePlanned = 0 BEGIN SELECT CAST(0 AS bit) AS Imported, CAST(1 AS bit) AS RequiresReplaceConfirmation, N'This Book already contains planned chapters and scenes.' AS Message, @ProjectID AS ProjectId, @BookID AS BookId, 0 AS ChaptersCreated, 0 AS ScenesCreated, 0 AS ArchivedChapters, 0 AS ArchivedScenes; RETURN; END; DECLARE @Chapters table ( ImportChapterIndex int NOT NULL PRIMARY KEY, Title nvarchar(200) NOT NULL, SortOrder int NOT NULL, WordCount int NOT NULL ); DECLARE @Scenes table ( ImportChapterIndex int NOT NULL, ImportSceneIndex int NOT NULL, Title nvarchar(200) NOT NULL, SortOrder int NOT NULL, WordCount int NOT NULL, PRIMARY KEY (ImportChapterIndex, ImportSceneIndex) ); INSERT @Chapters (ImportChapterIndex, Title, SortOrder, WordCount) SELECT CONVERT(int, chapter.[key]) + 1, LEFT(LTRIM(RTRIM(JSON_VALUE(chapter.value, '$.Title'))), 200), TRY_CONVERT(int, JSON_VALUE(chapter.value, '$.SortOrder')), COALESCE(TRY_CONVERT(int, JSON_VALUE(chapter.value, '$.WordCount')), 0) FROM OPENJSON(@StructureJson) chapter WHERE NULLIF(LTRIM(RTRIM(JSON_VALUE(chapter.value, '$.Title'))), N'') IS NOT NULL; INSERT @Scenes (ImportChapterIndex, ImportSceneIndex, Title, SortOrder, WordCount) SELECT c.ImportChapterIndex, CONVERT(int, scene.[key]) + 1, LEFT(LTRIM(RTRIM(JSON_VALUE(scene.value, '$.Title'))), 200), TRY_CONVERT(int, JSON_VALUE(scene.value, '$.SortOrder')), COALESCE(TRY_CONVERT(int, JSON_VALUE(scene.value, '$.WordCount')), 0) FROM @Chapters c INNER JOIN OPENJSON(@StructureJson) chapter ON CONVERT(int, chapter.[key]) + 1 = c.ImportChapterIndex CROSS APPLY OPENJSON(chapter.value, '$.Scenes') scene WHERE NULLIF(LTRIM(RTRIM(JSON_VALUE(scene.value, '$.Title'))), N'') IS NOT NULL; IF NOT EXISTS (SELECT 1 FROM @Chapters) RETURN; IF EXISTS (SELECT 1 FROM @Chapters WHERE SortOrder IS NULL OR SortOrder <= 0 OR WordCount < 0) RETURN; IF EXISTS (SELECT 1 FROM @Scenes WHERE SortOrder IS NULL OR SortOrder <= 0 OR WordCount < 0) RETURN; IF EXISTS (SELECT 1 FROM @Chapters c WHERE NOT EXISTS (SELECT 1 FROM @Scenes s WHERE s.ImportChapterIndex = c.ImportChapterIndex)) RETURN; DECLARE @PlannedRevisionStatusID int = (SELECT TOP (1) RevisionStatusID FROM dbo.RevisionStatuses WHERE StatusName = N'Planned' ORDER BY SortOrder); DECLARE @DefaultTimeModeID int = (SELECT TOP (1) TimeModeID FROM dbo.TimeModes ORDER BY CASE WHEN TimeModeName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeModeID); DECLARE @DefaultTimeConfidenceID int = (SELECT TOP (1) TimeConfidenceID FROM dbo.TimeConfidences ORDER BY CASE WHEN TimeConfidenceName = N'Unknown' THEN 0 ELSE 1 END, SortOrder, TimeConfidenceID); IF @PlannedRevisionStatusID IS NULL OR @DefaultTimeModeID IS NULL OR @DefaultTimeConfidenceID IS NULL RETURN; DECLARE @ImportChapterMap table ( ImportChapterIndex int NOT NULL PRIMARY KEY, ChapterID int NOT NULL ); DECLARE @ImportSceneMap table ( ImportChapterIndex int NOT NULL, ImportSceneIndex int NOT NULL, SceneID int NOT NULL, PRIMARY KEY (ImportChapterIndex, ImportSceneIndex) ); DECLARE @ArchivedChapters int = 0; DECLARE @ArchivedScenes int = 0; DECLARE @ManuscriptDocumentID int; DECLARE @LinkedUtc datetime2 = SYSUTCDATETIME(); BEGIN TRANSACTION; IF @ChapterCount > 0 BEGIN UPDATE s SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = N'Replaced by Word Companion manuscript import', UpdatedDate = SYSUTCDATETIME() FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID WHERE c.BookID = @BookID AND c.IsArchived = 0 AND s.IsArchived = 0 AND c.IsLinkedToManuscript = 0 AND s.IsLinkedToManuscript = 0; SET @ArchivedScenes = @@ROWCOUNT; UPDATE dbo.Chapters SET IsArchived = 1, ArchivedDate = SYSUTCDATETIME(), ArchivedReason = N'Replaced by Word Companion manuscript import', UpdatedDate = SYSUTCDATETIME() WHERE BookID = @BookID AND IsArchived = 0 AND IsLinkedToManuscript = 0; SET @ArchivedChapters = @@ROWCOUNT; END; IF EXISTS ( SELECT 1 FROM dbo.ManuscriptDocuments WHERE DocumentGuid = @DocumentGuid AND IsActive = 1 AND BookID <> @BookID ) THROW 51000, 'This manuscript document is already linked to another book.', 1; SELECT @ManuscriptDocumentID = ManuscriptDocumentID FROM dbo.ManuscriptDocuments WHERE BookID = @BookID AND IsActive = 1; IF @ManuscriptDocumentID IS NULL BEGIN INSERT dbo.ManuscriptDocuments (BookID, ProjectID, DocumentGuid, BindingVersion, CreatedByUserID, LastOpenedUtc, LastSyncUtc) VALUES (@BookID, @ProjectID, @DocumentGuid, @BindingVersion, @UserID, @LinkedUtc, @LinkedUtc); SET @ManuscriptDocumentID = CAST(SCOPE_IDENTITY() AS int); END ELSE BEGIN UPDATE dbo.ManuscriptDocuments SET DocumentGuid = @DocumentGuid, ProjectID = @ProjectID, BindingVersion = @BindingVersion, LastOpenedUtc = @LinkedUtc, LastSyncUtc = @LinkedUtc WHERE ManuscriptDocumentID = @ManuscriptDocumentID; END; DECLARE @ImportChapterIndex int; DECLARE @ChapterTitle nvarchar(200); DECLARE @ChapterSortOrder int; DECLARE @ChapterID int; DECLARE @ChapterNumber decimal(9,2); DECLARE chapter_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT ImportChapterIndex, Title, SortOrder FROM @Chapters ORDER BY SortOrder, ImportChapterIndex; OPEN chapter_cursor; FETCH NEXT FROM chapter_cursor INTO @ImportChapterIndex, @ChapterTitle, @ChapterSortOrder; WHILE @@FETCH_STATUS = 0 BEGIN SET @ChapterNumber = @ImportChapterIndex; INSERT dbo.Chapters ( BookID, ChapterNumber, ChapterTitle, Summary, SortOrder, RevisionStatusID, IsLinkedToManuscript, ManuscriptDocumentID, ExternalReference, LinkedUtc ) VALUES ( @BookID, @ChapterNumber, @ChapterTitle, NULL, @ChapterSortOrder, @PlannedRevisionStatusID, 1, @ManuscriptDocumentID, CONCAT(N'PD_CHAPTER_IMPORT_', @ImportChapterIndex), @LinkedUtc ); SET @ChapterID = CAST(SCOPE_IDENTITY() AS int); INSERT @ImportChapterMap (ImportChapterIndex, ChapterID) VALUES (@ImportChapterIndex, @ChapterID); FETCH NEXT FROM chapter_cursor INTO @ImportChapterIndex, @ChapterTitle, @ChapterSortOrder; END; CLOSE chapter_cursor; DEALLOCATE chapter_cursor; DECLARE @ImportSceneIndex int; DECLARE @SceneTitle nvarchar(200); DECLARE @SceneSortOrder int; DECLARE @SceneWordCount int; DECLARE @SceneID int; DECLARE scene_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT s.ImportChapterIndex, s.ImportSceneIndex, s.Title, s.SortOrder, s.WordCount, m.ChapterID FROM @Scenes s INNER JOIN @ImportChapterMap m ON m.ImportChapterIndex = s.ImportChapterIndex ORDER BY s.ImportChapterIndex, s.SortOrder, s.ImportSceneIndex; OPEN scene_cursor; FETCH NEXT FROM scene_cursor INTO @ImportChapterIndex, @ImportSceneIndex, @SceneTitle, @SceneSortOrder, @SceneWordCount, @ChapterID; WHILE @@FETCH_STATUS = 0 BEGIN IF @ImportSceneIndex = 1 BEGIN SELECT TOP (1) @SceneID = SceneID FROM dbo.Scenes WHERE ChapterID = @ChapterID AND IsArchived = 0 ORDER BY SortOrder, SceneID; UPDATE dbo.Scenes SET SceneNumber = @ImportSceneIndex, SceneTitle = @SceneTitle, SortOrder = @SceneSortOrder, IsLinkedToManuscript = 1, ManuscriptDocumentID = @ManuscriptDocumentID, ExternalReference = CONCAT(N'PD_SCENE_IMPORT_', @ImportChapterIndex, N'_', @ImportSceneIndex), LinkedUtc = @LinkedUtc, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SceneID; END ELSE BEGIN INSERT dbo.Scenes ( ChapterID, SceneNumber, SceneTitle, Summary, SortOrder, TimeModeID, TimeConfidenceID, RevisionStatusID, IsLinkedToManuscript, ManuscriptDocumentID, ExternalReference, LinkedUtc ) VALUES ( @ChapterID, @ImportSceneIndex, @SceneTitle, NULL, @SceneSortOrder, @DefaultTimeModeID, @DefaultTimeConfidenceID, @PlannedRevisionStatusID, 1, @ManuscriptDocumentID, CONCAT(N'PD_SCENE_IMPORT_', @ImportChapterIndex, N'_', @ImportSceneIndex), @LinkedUtc ); SET @SceneID = CAST(SCOPE_IDENTITY() AS int); END; IF EXISTS (SELECT 1 FROM dbo.SceneWorkflow WHERE SceneID = @SceneID) BEGIN UPDATE dbo.SceneWorkflow SET ActualWordCount = @SceneWordCount, UpdatedDate = SYSUTCDATETIME() WHERE SceneID = @SceneID; END ELSE BEGIN INSERT dbo.SceneWorkflow (SceneID, ActualWordCount) VALUES (@SceneID, @SceneWordCount); END; INSERT @ImportSceneMap (ImportChapterIndex, ImportSceneIndex, SceneID) VALUES (@ImportChapterIndex, @ImportSceneIndex, @SceneID); FETCH NEXT FROM scene_cursor INTO @ImportChapterIndex, @ImportSceneIndex, @SceneTitle, @SceneSortOrder, @SceneWordCount, @ChapterID; END; CLOSE scene_cursor; DEALLOCATE scene_cursor; COMMIT TRANSACTION; SELECT CAST(1 AS bit) AS Imported, CAST(0 AS bit) AS RequiresReplaceConfirmation, N'Manuscript structure imported.' AS Message, @ProjectID AS ProjectId, @BookID AS BookId, (SELECT COUNT(*) FROM @ImportChapterMap) AS ChaptersCreated, (SELECT COUNT(*) FROM @ImportSceneMap) AS ScenesCreated, @ArchivedChapters AS ArchivedChapters, @ArchivedScenes AS ArchivedScenes; SELECT md.ManuscriptDocumentID AS ManuscriptDocumentId, md.BookID AS BookId, md.ProjectID AS ProjectId, md.DocumentGuid, md.BindingVersion, md.CreatedUtc, md.LastSyncUtc, md.LastOpenedUtc, md.IsActive FROM dbo.ManuscriptDocuments md WHERE md.ManuscriptDocumentID = @ManuscriptDocumentID; END; GO