diff --git a/PlotLine/Models/WordCompanionApiModels.cs b/PlotLine/Models/WordCompanionApiModels.cs index a9889cd..6ad3cba 100644 --- a/PlotLine/Models/WordCompanionApiModels.cs +++ b/PlotLine/Models/WordCompanionApiModels.cs @@ -56,6 +56,7 @@ public sealed class WordCompanionResolveSceneRequest public sealed class WordCompanionResolveSceneResponse { public bool Matched { get; init; } + public bool ChapterMatched { get; init; } public int? ChapterId { get; init; } public int? SceneId { get; init; } public string? MatchType { get; init; } diff --git a/PlotLine/Sql/063_Phase10J_UnmatchedSceneChapterDiagnostics.sql b/PlotLine/Sql/063_Phase10J_UnmatchedSceneChapterDiagnostics.sql new file mode 100644 index 0000000..c334eeb --- /dev/null +++ b/PlotLine/Sql/063_Phase10J_UnmatchedSceneChapterDiagnostics.sql @@ -0,0 +1,102 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.WordCompanion_Scene_Resolve + @BookID int, + @UserID int, + @ChapterTitle nvarchar(200), + @SceneTitle nvarchar(200) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @ChapterTitleSearched nvarchar(200) = + LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(@ChapterTitle, N''), NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))); + DECLARE @SceneTitleSearched nvarchar(200) = + LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(@SceneTitle, N''), NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))); + + IF NOT EXISTS + ( + SELECT 1 + 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.IsArchived = 0 + AND p.IsArchived = 0 + AND pua.UserID = @UserID + AND pua.IsActive = 1 + ) + BEGIN + SELECT CAST(0 AS bit) AS Matched, + CAST(0 AS bit) AS ChapterMatched, + CAST(NULL AS int) AS ChapterId, + CAST(NULL AS int) AS SceneId, + CAST(NULL AS nvarchar(50)) AS MatchType, + @BookID AS BookId, + @ChapterTitleSearched AS ChapterTitleSearched, + @SceneTitleSearched AS SceneTitleSearched, + CAST(0 AS int) AS AvailableChapterCount, + CAST(0 AS int) AS AvailableSceneCount, + CAST(0 AS bit) AS BookAccessible; + RETURN; + END; + + DECLARE @AvailableChapterCount int; + DECLARE @AvailableSceneCount int; + + SELECT @AvailableChapterCount = COUNT(*) + FROM dbo.Chapters c + WHERE c.BookID = @BookID + AND c.IsArchived = 0; + + SELECT @AvailableSceneCount = COUNT(*) + FROM dbo.Chapters c + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID + WHERE c.BookID = @BookID + AND c.IsArchived = 0 + AND s.IsArchived = 0; + + DECLARE @ChapterID int; + DECLARE @SceneID int; + + SELECT TOP (1) @ChapterID = c.ChapterID + FROM dbo.Chapters c + CROSS APPLY + ( + SELECT LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(c.ChapterTitle, NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))) AS NormalizedChapterTitle + ) chapterMatch + WHERE c.BookID = @BookID + AND c.IsArchived = 0 + AND chapterMatch.NormalizedChapterTitle COLLATE SQL_Latin1_General_CP1_CI_AS = @ChapterTitleSearched COLLATE SQL_Latin1_General_CP1_CI_AS + ORDER BY c.SortOrder, c.ChapterID; + + IF @ChapterID IS NOT NULL + BEGIN + SELECT TOP (1) @SceneID = s.SceneID + FROM dbo.Scenes s + CROSS APPLY + ( + SELECT LTRIM(RTRIM(REPLACE(REPLACE(REPLACE(REPLACE(s.SceneTitle, NCHAR(160), N' '), CHAR(9), N' '), CHAR(13), N' '), CHAR(10), N' '))) AS NormalizedSceneTitle + ) sceneMatch + WHERE s.ChapterID = @ChapterID + AND s.IsArchived = 0 + AND sceneMatch.NormalizedSceneTitle COLLATE SQL_Latin1_General_CP1_CI_AS = @SceneTitleSearched COLLATE SQL_Latin1_General_CP1_CI_AS + ORDER BY s.SortOrder, s.SceneID; + END; + + SELECT CAST(CASE WHEN @SceneID IS NULL THEN 0 ELSE 1 END AS bit) AS Matched, + CAST(CASE WHEN @ChapterID IS NULL THEN 0 ELSE 1 END AS bit) AS ChapterMatched, + @ChapterID AS ChapterId, + @SceneID AS SceneId, + CASE WHEN @SceneID IS NULL THEN CAST(NULL AS nvarchar(50)) ELSE N'ExactTitle' END AS MatchType, + @BookID AS BookId, + @ChapterTitleSearched AS ChapterTitleSearched, + @SceneTitleSearched AS SceneTitleSearched, + @AvailableChapterCount AS AvailableChapterCount, + @AvailableSceneCount AS AvailableSceneCount, + CAST(1 AS bit) AS BookAccessible; +END; +GO diff --git a/PlotLine/wwwroot/js/word-companion-host.js b/PlotLine/wwwroot/js/word-companion-host.js index 85599d9..8f6fca2 100644 --- a/PlotLine/wwwroot/js/word-companion-host.js +++ b/PlotLine/wwwroot/js/word-companion-host.js @@ -268,7 +268,10 @@ setText(createLinkStatus, message); }; - const canUseCreateLinkActions = () => !!selectedBook() && !!detectedSceneTitle; + const canUseCreateLinkActions = () => !!selectedBook() + && !!detectedSceneTitle + && Number.isInteger(resolvedChapterId) + && resolvedChapterId > 0; const hideSceneCreateLink = () => { setHidden(sceneCreateLink, true); @@ -504,6 +507,13 @@ }; const findCurrentChapterInChapters = (chapters) => { + if (Number.isInteger(resolvedChapterId) && resolvedChapterId > 0) { + const anchoredChapter = chapters.find((chapter) => chapter.chapterId === resolvedChapterId); + if (anchoredChapter) { + return anchoredChapter; + } + } + const requestChapterTitle = normalizeTitleForResolve(detectedChapterTitle, "chapter").toLocaleLowerCase(); if (!requestChapterTitle) { return null; @@ -1527,12 +1537,10 @@ } if (!detectedSceneAnchorId) { - storedAnchorInvalid = true; currentResolutionAnchorType = "ChapterID Anchor"; - setPlotDirectorSceneStatus("Not found in PlotDirector"); - setDocumentMessage("Stored PlotDirector ID is invalid."); + setResolvedScene(null, detectedChapterAnchorId, "Chapter anchor"); setResolveDiagnostics({ - result: "Error", + result: "Chapter matched", chapterId: detectedChapterAnchorId, sceneId: null }); @@ -1546,16 +1554,28 @@ if (!resolveResponse?.matched || !resolveResponse.sceneId) { const hadInvalidAnchor = storedAnchorInvalid; + const matchedChapterId = Number.parseInt(resolveResponse?.chapterId || "", 10); + const chapterMatched = !!resolveResponse?.chapterMatched || (Number.isInteger(matchedChapterId) && matchedChapterId > 0); resetPlotDirectorScene("Not found in PlotDirector"); storedAnchorInvalid = hadInvalidAnchor; - setDocumentMessage(hadInvalidAnchor ? "Stored PlotDirector ID is invalid." : "Scene not found in PlotDirector."); + if (chapterMatched) { + setResolvedScene(null, matchedChapterId, "Chapter title"); + setPlotDirectorSceneStatus("Scene not found in PlotDirector."); + } + setDocumentMessage(hadInvalidAnchor + ? "Stored PlotDirector ID is invalid." + : chapterMatched + ? "Scene not found in PlotDirector." + : "This chapter does not exist in PlotDirector. Create or link the chapter first."); setResolveDiagnostics({ result: "Not matched", - chapterId: resolveResponse?.chapterId, + chapterId: chapterMatched ? matchedChapterId : resolveResponse?.chapterId, sceneId: resolveResponse?.sceneId }); if (!hadInvalidAnchor) { - showSceneCreateLink("Scene not found in PlotDirector."); + showSceneCreateLink(chapterMatched + ? "Scene not found in PlotDirector." + : "This chapter does not exist in PlotDirector. Create or link the chapter first."); } restorePlotDirectorSceneButton(); return false;