diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index 0088970..a81e87d 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -102,6 +102,7 @@ public interface ILookupRepository public interface ITimelineRepository { Task GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable metricTypeIds, bool includePlotLines); + Task> GetSceneOverviewsAsync(int projectId, int? bookId); } public interface ITimelinePresetRepository @@ -154,7 +155,7 @@ public interface IPlotRepository public interface IAssetRepository { - Task GetLookupsAsync(); + Task GetLookupsAsync(int? projectId = null); Task> ListAssetsAsync(int projectId); Task GetAssetAsync(int storyAssetId); Task SaveAssetAsync(StoryAsset asset); @@ -178,7 +179,7 @@ public interface IAssetRepository public interface ICharacterRepository { - Task GetLookupsAsync(); + Task GetLookupsAsync(int? projectId = null); Task> ListCharactersAsync(int projectId); Task GetCharacterAsync(int characterId); Task SaveCharacterAsync(Character character); @@ -419,6 +420,19 @@ public sealed class TimelineData public IReadOnlyList ThreadEvents { get; init; } = []; } +public sealed class TimelineSceneOverviewRow +{ + public int SceneID { get; set; } + public int? CharacterID { get; set; } + public string? CharacterName { get; set; } + public bool IsPov { get; set; } + public int? StoryAssetID { get; set; } + public string? AssetName { get; set; } + public int? AssetImportance { get; set; } + public string? AssetEventTypeName { get; set; } + public string? LocationName { get; set; } +} + public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository { public async Task GetLookupsAsync() @@ -2520,6 +2534,16 @@ public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) public int PlotEventID { get; set; } public int PlotLineID { get; set; } } + + public async Task> GetSceneOverviewsAsync(int projectId, int? bookId) + { + using var connection = connectionFactory.CreateConnection(); + var rows = await connection.QueryAsync( + "dbo.Timeline_SceneOverviews", + new { ProjectID = projectId, BookID = bookId }, + commandType: CommandType.StoredProcedure); + return rows.ToList(); + } } public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionFactory) : ITimelineSettingsRepository @@ -2902,10 +2926,10 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository { - public async Task GetLookupsAsync() + public async Task GetLookupsAsync(int? projectId = null) { using var connection = connectionFactory.CreateConnection(); - using var result = await connection.QueryMultipleAsync("dbo.AssetLookup_All", commandType: CommandType.StoredProcedure); + using var result = await connection.QueryMultipleAsync("dbo.AssetLookup_All", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new AssetLookupData { AssetKinds = (await result.ReadAsync()).ToList(), @@ -3148,10 +3172,10 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) : ICharacterRepository { - public async Task GetLookupsAsync() + public async Task GetLookupsAsync(int? projectId = null) { using var connection = connectionFactory.CreateConnection(); - using var result = await connection.QueryMultipleAsync("dbo.CharacterLookup_All", commandType: CommandType.StoredProcedure); + using var result = await connection.QueryMultipleAsync("dbo.CharacterLookup_All", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new CharacterLookupData { RoleTypes = (await result.ReadAsync()).ToList(), diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index a78473b..ffd4b41 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -2134,8 +2134,8 @@ public sealed class TimelineService( .ToList(); var needsPlotTimelineData = settings.ShowPlotLines || filter.PlotLineID.HasValue || filter.PlotThreadID.HasValue || filter.FocusType is "plotline" or "plotthread"; var needsMetricTimelineData = settings.ShowMetricShape && selectedMetricIds.Any(); - var needsAssetTimelineData = settings.ShowSceneCards || settings.ShowStoryAssets || filter.StoryAssetID.HasValue || filter.FocusType == "asset"; - var needsCharacterTimelineData = settings.ShowSceneCards || settings.ShowCharacterAppearances || filter.CharacterID.HasValue || filter.FocusType == "character"; + var needsAssetTimelineData = settings.ShowStoryAssets || filter.StoryAssetID.HasValue || filter.FocusType == "asset"; + var needsCharacterTimelineData = settings.ShowCharacterAppearances || filter.CharacterID.HasValue || filter.FocusType == "character"; var needsWarnings = settings.ShowWarnings || filter.WarningSeverityID.HasValue || filter.WarningTypeID.HasValue || filter.ShowWarningsOnly; var timeline = await TimeTimelineLoadAsync( @@ -2150,8 +2150,8 @@ public sealed class TimelineService( var allBooks = await TimeTimelineLoadAsync("dbo.Book_ListByProject", () => books.ListByProjectAsync(projectId), projectId, bookId); var lookupData = await TimeTimelineLoadAsync("dbo.Lookup_All", lookups.GetAllAsync, projectId, bookId); - var assetLookupData = await TimeTimelineLoadAsync("dbo.AssetLookup_All", assets.GetLookupsAsync, projectId, bookId); - var characterLookupData = await TimeTimelineLoadAsync("dbo.CharacterLookup_All", characters.GetLookupsAsync, projectId, bookId); + var assetLookupData = await TimeTimelineLoadAsync("dbo.AssetLookup_All", () => assets.GetLookupsAsync(projectId), projectId, bookId); + var characterLookupData = await TimeTimelineLoadAsync("dbo.CharacterLookup_All", () => characters.GetLookupsAsync(projectId), projectId, bookId); var locationLookupData = await TimeTimelineLoadAsync("dbo.LocationLookup_All", locations.GetLookupsAsync, projectId, bookId); var plotLines = await TimeTimelineLoadAsync("dbo.PlotLine_ListByProject", () => plots.ListPlotLinesAsync(projectId), projectId, bookId); var plotThreads = await TimeTimelineLoadAsync("dbo.PlotThread_ListByProject", () => plots.ListPlotThreadsByProjectAsync(projectId), projectId, bookId); @@ -2166,10 +2166,14 @@ public sealed class TimelineService( var timelineWarnings = needsWarnings ? await TimeTimelineLoadAsync("dbo.ContinuityWarning_List", () => warnings.ListAsync(projectId, bookId, null, filter.WarningTypeID, filter.WarningSeverityID, null, filter.IncludeDismissedWarnings, filter.IncludeIntentionalWarnings), projectId, bookId) : []; - var warningsByScene = timelineWarnings - .Where(x => x.SceneID.HasValue) - .GroupBy(x => x.SceneID!.Value) - .ToDictionary(x => x.Key, x => (IReadOnlyList)x.ToList()); + var warningsByScene = await TimeTimelineLoadAsync( + "TimelineService.PrepareWarnings", + () => Task.FromResult(timelineWarnings + .Where(x => x.SceneID.HasValue) + .GroupBy(x => x.SceneID!.Value) + .ToDictionary(x => x.Key, x => (IReadOnlyList)x.ToList())), + projectId, + bookId); var warningCounts = warningsByScene.ToDictionary(x => x.Key, x => x.Value.Count); var dependencyCounts = (await TimeTimelineLoadAsync("dbo.SceneDependency_Counts", () => sceneDependencies.GetCountsAsync(projectId, bookId), projectId, bookId)).ToDictionary(x => x.SceneID, x => x.DependencyCount); @@ -2187,6 +2191,9 @@ public sealed class TimelineService( ? await TimeTimelineLoadAsync("dbo.CharacterTimeline_GetByProject", () => characters.GetTimelineAsync(projectId, bookId), projectId, bookId) : ([], []); ApplyTimelineCharacterAvatars(characterTimeline, allCharacters); + var sceneOverviewRows = settings.ShowSceneCards + ? await TimeTimelineLoadAsync("dbo.Timeline_SceneOverviews", () => timelineRepository.GetSceneOverviewsAsync(projectId, bookId), projectId, bookId) + : []; var plotThreadDetailsById = plotThreads.ToDictionary(x => x.PlotThreadID); foreach (var threadEvent in timeline.ThreadEvents) { @@ -2202,7 +2209,22 @@ public sealed class TimelineService( .ToDictionary(x => x.Key, x => x.First().CharacterName); var matchingSceneIds = await TimeTimelineLoadAsync("TimelineService.BuildMatchingSceneIds", () => BuildMatchingSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId), projectId, bookId); var focusSceneIds = await TimeTimelineLoadAsync("TimelineService.BuildFocusSceneIds", () => BuildFocusSceneIdsAsync(filter, orderedScenes, timeline.ThreadEvents, assetTimeline.Events, characterTimeline.Appearances, projectId, bookId), projectId, bookId); - var focusLabel = BuildFocusLabel(filter, plotLines, plotThreads, allAssets, allCharacters, allLocations); + var focusLabel = await TimeTimelineLoadAsync("TimelineService.BuildFocusLabel", () => Task.FromResult(BuildFocusLabel(filter, plotLines, plotThreads, allAssets, allCharacters, allLocations)), projectId, bookId); + var sceneOverviews = settings.ShowSceneCards + ? await TimeTimelineLoadAsync("TimelineService.BuildSceneOverviews", () => Task.FromResult(BuildSceneOverviews(orderedScenes, sceneOverviewRows)), projectId, bookId) + : new Dictionary(); + var metricGraphs = settings.ShowMetricShape + ? await TimeTimelineLoadAsync("TimelineService.BuildMetricGraphs", () => Task.FromResult(BuildMetricGraphs(timeline, orderedScenes)), projectId, bookId) + : []; + var plotLanes = settings.ShowPlotLines + ? await TimeTimelineLoadAsync("TimelineService.BuildPlotLanes", () => Task.FromResult(BuildPlotLanes(timeline, orderedScenes)), projectId, bookId) + : []; + var assetLanes = settings.ShowStoryAssets + ? await TimeTimelineLoadAsync("TimelineService.BuildAssetLanes", () => Task.FromResult(BuildAssetLanes(assetTimeline.Assets, assetTimeline.Events, orderedScenes)), projectId, bookId) + : []; + var characterLanes = settings.ShowCharacterAppearances + ? await TimeTimelineLoadAsync("TimelineService.BuildCharacterLanes", () => Task.FromResult(BuildCharacterLanes(characterTimeline.Characters, characterTimeline.Appearances, orderedScenes)), projectId, bookId) + : []; var selectedScene = selectedSceneId.HasValue ? await TimeTimelineLoadAsync("SceneService.GetEditAsync", () => scenes.GetEditAsync(selectedSceneId.Value), projectId, bookId) : null; @@ -2277,11 +2299,11 @@ public sealed class TimelineService( .ToList() }).ToList(), OrderedScenes = orderedScenes, - MetricGraphs = settings.ShowMetricShape ? BuildMetricGraphs(timeline, orderedScenes) : [], - PlotLanes = settings.ShowPlotLines ? BuildPlotLanes(timeline, orderedScenes) : [], - AssetLanes = settings.ShowStoryAssets ? BuildAssetLanes(assetTimeline.Assets, assetTimeline.Events, orderedScenes) : [], - CharacterLanes = settings.ShowCharacterAppearances ? BuildCharacterLanes(characterTimeline.Characters, characterTimeline.Appearances, orderedScenes) : [], - SceneOverviews = settings.ShowSceneCards ? BuildSceneOverviews(orderedScenes, characterTimeline.Appearances, assetTimeline.Assets, assetTimeline.Events) : new Dictionary(), + MetricGraphs = metricGraphs, + PlotLanes = plotLanes, + AssetLanes = assetLanes, + CharacterLanes = characterLanes, + SceneOverviews = sceneOverviews, WarningsByScene = warningsByScene, MatchingSceneIDs = matchingSceneIds, FocusSceneIDs = focusSceneIds, @@ -2792,60 +2814,59 @@ public sealed class TimelineService( private static IReadOnlyDictionary BuildSceneOverviews( IReadOnlyList orderedScenes, - IReadOnlyList appearances, - IReadOnlyList assets, - IReadOnlyList assetEvents) + IReadOnlyList overviewRows) { - var assetImportanceById = assets.ToDictionary(x => x.StoryAssetID, x => x.Importance); - var charactersByScene = appearances + var charactersByScene = overviewRows + .Where(x => x.CharacterID.HasValue && !string.IsNullOrWhiteSpace(x.CharacterName)) .GroupBy(x => x.SceneID) .ToDictionary( sceneGroup => sceneGroup.Key, sceneGroup => sceneGroup - .GroupBy(x => x.CharacterID) + .GroupBy(x => x.CharacterID!.Value) .Select(characterGroup => { var first = characterGroup - .OrderByDescending(x => string.Equals(x.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase)) - .ThenBy(x => x.SceneCharacterID) + .OrderByDescending(x => x.IsPov) + .ThenBy(x => x.CharacterName) .First(); return new TimelineSceneCharacterOverviewViewModel { - CharacterID = first.CharacterID, - CharacterName = first.CharacterName, - IsPov = string.Equals(first.RoleInSceneTypeName, "POV Character", StringComparison.OrdinalIgnoreCase) + CharacterID = first.CharacterID!.Value, + CharacterName = first.CharacterName ?? string.Empty, + IsPov = first.IsPov }; }) .OrderByDescending(x => x.IsPov) .ThenBy(x => x.CharacterName) .ToList()); - var assetsByScene = assetEvents + var assetsByScene = overviewRows + .Where(x => x.StoryAssetID.HasValue && !string.IsNullOrWhiteSpace(x.AssetName)) .GroupBy(x => x.SceneID) .ToDictionary( sceneGroup => sceneGroup.Key, sceneGroup => sceneGroup - .GroupBy(x => x.StoryAssetID) + .GroupBy(x => x.StoryAssetID!.Value) .Select(assetGroup => { - var first = assetGroup.OrderByDescending(x => x.UpdatedDate).First(); + var first = assetGroup.First(); return new TimelineSceneAssetOverviewViewModel { - StoryAssetID = first.StoryAssetID, - AssetName = first.AssetName, - Importance = assetImportanceById.GetValueOrDefault(first.StoryAssetID), + StoryAssetID = first.StoryAssetID!.Value, + AssetName = first.AssetName ?? string.Empty, + Importance = first.AssetImportance.GetValueOrDefault(), EventTypeName = first.AssetEventTypeName }; }) .OrderByDescending(x => x.Importance) .ThenBy(x => x.AssetName) .ToList()); - var characterLocationsByScene = appearances - .Where(x => !string.IsNullOrWhiteSpace(x.LocationPath) || !string.IsNullOrWhiteSpace(x.LocationName)) + var locationsByScene = overviewRows + .Where(x => !string.IsNullOrWhiteSpace(x.LocationName)) .GroupBy(x => x.SceneID) .ToDictionary( sceneGroup => sceneGroup.Key, sceneGroup => sceneGroup - .Select(x => x.LocationPath ?? x.LocationName ?? string.Empty) + .Select(x => x.LocationName ?? string.Empty) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x) @@ -2862,7 +2883,7 @@ public sealed class TimelineService( { locations.Add(scene.PrimaryLocationName); } - if (characterLocationsByScene.TryGetValue(scene.SceneID, out var characterLocations)) + if (locationsByScene.TryGetValue(scene.SceneID, out var characterLocations)) { locations.AddRange(characterLocations); } diff --git a/PlotLine/Sql/089_TimelineLoadPerformance.sql b/PlotLine/Sql/089_TimelineLoadPerformance.sql new file mode 100644 index 0000000..ddf7ee2 --- /dev/null +++ b/PlotLine/Sql/089_TimelineLoadPerformance.sql @@ -0,0 +1,444 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +/* + Timeline-focused performance pass. + + Scope: + - Keep the initial Timeline page from loading full character/asset lane data + just to render scene-card summaries. + - Keep project/book filters as early as possible inside heavy Timeline queries. + - Add narrow supporting indexes for the Timeline warning, character, asset, + and scene-overview paths. +*/ + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ContinuityWarnings_TimelineActiveList' AND object_id = OBJECT_ID(N'dbo.ContinuityWarnings')) +BEGIN + CREATE INDEX IX_ContinuityWarnings_TimelineActiveList + ON dbo.ContinuityWarnings(ProjectID, BookID, WarningSeverityID, WarningTypeID, SceneID, LastDetectedDate DESC, ContinuityWarningID DESC) + INCLUDE (ValidationRunID, ChapterID, EntityType, EntityID, Message, Details, CreatedDate) + WHERE IsDismissed = 0 AND IsIntentional = 0; +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ContinuityWarnings_TimelineList' AND object_id = OBJECT_ID(N'dbo.ContinuityWarnings')) +BEGIN + CREATE INDEX IX_ContinuityWarnings_TimelineList + ON dbo.ContinuityWarnings(ProjectID, BookID, SceneID, EntityType, EntityID, WarningTypeID, WarningSeverityID) + INCLUDE (IsDismissed, IsIntentional, LastDetectedDate, ValidationRunID, ChapterID, Message, Details, CreatedDate); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_SceneCharacters_SceneID_TimelineOverview' AND object_id = OBJECT_ID(N'dbo.SceneCharacters')) +BEGIN + CREATE INDEX IX_SceneCharacters_SceneID_TimelineOverview + ON dbo.SceneCharacters(SceneID, CharacterID) + INCLUDE (SceneCharacterID, RoleInSceneTypeID, PresenceTypeID, LocationID, EntryLocationID, ExitLocationID, AppearanceNotes, OutfitDescription, PhysicalCondition, EmotionalState, KnowledgeNotes, CreatedDate, UpdatedDate); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Books_Project_Active_Timeline' AND object_id = OBJECT_ID(N'dbo.Books')) +BEGIN + CREATE INDEX IX_Books_Project_Active_Timeline + ON dbo.Books(ProjectID, IsArchived, BookID) + INCLUDE (BookTitle, Subtitle, SortOrder, BookNumber); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Chapters_Book_Active_Timeline' AND object_id = OBJECT_ID(N'dbo.Chapters')) +BEGIN + CREATE INDEX IX_Chapters_Book_Active_Timeline + ON dbo.Chapters(BookID, IsArchived, ChapterID) + INCLUDE (ChapterNumber, ChapterTitle, SortOrder); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_Scenes_Chapter_Active_Timeline' AND object_id = OBJECT_ID(N'dbo.Scenes')) +BEGIN + CREATE INDEX IX_Scenes_Chapter_Active_Timeline + ON dbo.Scenes(ChapterID, IsArchived, SceneID) + INCLUDE (SceneNumber, SceneTitle, SortOrder, POVCharacterID, PrimaryLocationID, RevisionStatusID); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_StoryAssets_Project_TimelineList' AND object_id = OBJECT_ID(N'dbo.StoryAssets')) +BEGIN + CREATE INDEX IX_StoryAssets_Project_TimelineList + ON dbo.StoryAssets(ProjectID, IsArchived, Importance DESC, AssetName) + INCLUDE (AssetKindID, CurrentStateID, CurrentLocationID, IsResolved, ShowInQuickAddBar, ImagePath, ThumbnailPath, CreatedDate, UpdatedDate); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetEvents_SceneID_TimelineOverview' AND object_id = OBJECT_ID(N'dbo.AssetEvents')) +BEGIN + CREATE INDEX IX_AssetEvents_SceneID_TimelineOverview + ON dbo.AssetEvents(SceneID, StoryAssetID, UpdatedDate DESC) + INCLUDE (AssetEventTypeID, EventTitle); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_AssetStates_Active_Project_Sort' AND object_id = OBJECT_ID(N'dbo.AssetStates')) +BEGIN + CREATE INDEX IX_AssetStates_Active_Project_Sort + ON dbo.AssetStates(IsActive, ProjectID, SortOrder) + INCLUDE (AssetKindID, StateName, Description, IsResolvedState); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_CharacterAttributeTypes_Active_Project_Sort' AND object_id = OBJECT_ID(N'dbo.CharacterAttributeTypes')) +BEGIN + CREATE INDEX IX_CharacterAttributeTypes_Active_Project_Sort + ON dbo.CharacterAttributeTypes(IsActive, ProjectID, SortOrder) + INCLUDE (AttributeName, DataType, IsNormallyStable); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ContinuityWarning_List + @ProjectID int, + @BookID int = NULL, + @SceneID int = NULL, + @EntityType nvarchar(80) = NULL, + @EntityID int = NULL, + @WarningTypeID int = NULL, + @WarningSeverityID int = NULL, + @IncludeDismissed bit = 0, + @IncludeIntentional bit = 0 +AS +BEGIN + SET NOCOUNT ON; + + IF @IncludeDismissed = 0 AND @IncludeIntentional = 0 + BEGIN + SELECT cw.ContinuityWarningID, cw.ValidationRunID, cw.ProjectID, cw.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, + cw.ChapterID, c.ChapterNumber, c.ChapterTitle, cw.SceneID, s.SceneNumber, s.SceneTitle, cw.EntityType, + cw.EntityID, cw.WarningTypeID, wt.TypeName AS WarningTypeName, cw.WarningSeverityID, ws.SeverityName, + cw.Message, cw.Details, cw.IsDismissed, cw.IsIntentional, cw.CreatedDate, cw.LastDetectedDate + FROM dbo.ContinuityWarnings cw + INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID + INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID + LEFT JOIN dbo.Books b ON b.BookID = cw.BookID + LEFT JOIN dbo.Chapters c ON c.ChapterID = cw.ChapterID + LEFT JOIN dbo.Scenes s ON s.SceneID = cw.SceneID + WHERE cw.ProjectID = @ProjectID + AND cw.IsDismissed = 0 + AND cw.IsIntentional = 0 + AND (@BookID IS NULL OR cw.BookID = @BookID) + AND (@SceneID IS NULL OR cw.SceneID = @SceneID) + AND (@EntityType IS NULL OR cw.EntityType = @EntityType) + AND (@EntityID IS NULL OR cw.EntityID = @EntityID) + AND (@WarningTypeID IS NULL OR cw.WarningTypeID = @WarningTypeID) + AND (@WarningSeverityID IS NULL OR cw.WarningSeverityID = @WarningSeverityID) + ORDER BY ws.SortOrder DESC, cw.LastDetectedDate DESC, cw.ContinuityWarningID DESC + OPTION (RECOMPILE); + + RETURN; + END; + + SELECT cw.ContinuityWarningID, cw.ValidationRunID, cw.ProjectID, cw.BookID, b.BookTitle, b.Subtitle AS BookSubtitle, + cw.ChapterID, c.ChapterNumber, c.ChapterTitle, cw.SceneID, s.SceneNumber, s.SceneTitle, cw.EntityType, + cw.EntityID, cw.WarningTypeID, wt.TypeName AS WarningTypeName, cw.WarningSeverityID, ws.SeverityName, + cw.Message, cw.Details, cw.IsDismissed, cw.IsIntentional, cw.CreatedDate, cw.LastDetectedDate + FROM dbo.ContinuityWarnings cw + INNER JOIN dbo.WarningTypes wt ON wt.WarningTypeID = cw.WarningTypeID + INNER JOIN dbo.WarningSeverities ws ON ws.WarningSeverityID = cw.WarningSeverityID + LEFT JOIN dbo.Books b ON b.BookID = cw.BookID + LEFT JOIN dbo.Chapters c ON c.ChapterID = cw.ChapterID + LEFT JOIN dbo.Scenes s ON s.SceneID = cw.SceneID + WHERE cw.ProjectID = @ProjectID + AND (@BookID IS NULL OR cw.BookID = @BookID) + AND (@SceneID IS NULL OR cw.SceneID = @SceneID) + AND (@EntityType IS NULL OR cw.EntityType = @EntityType) + AND (@EntityID IS NULL OR cw.EntityID = @EntityID) + AND (@WarningTypeID IS NULL OR cw.WarningTypeID = @WarningTypeID) + AND (@WarningSeverityID IS NULL OR cw.WarningSeverityID = @WarningSeverityID) + AND (@IncludeDismissed = 1 OR cw.IsDismissed = 0) + AND (@IncludeIntentional = 1 OR cw.IsIntentional = 0) + ORDER BY ws.SortOrder DESC, cw.LastDetectedDate DESC, cw.ContinuityWarningID DESC + OPTION (RECOMPILE); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.StoryAsset_ListByProject + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + CREATE TABLE #ProjectLocationPaths + ( + LocationID int NOT NULL PRIMARY KEY, + LocationName nvarchar(200) NOT NULL, + LocationPath nvarchar(max) NOT NULL + ); + + ;WITH ProjectLocationTree AS + ( + SELECT LocationID, LocationName, CAST(LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations + WHERE ProjectID = @ProjectID AND ParentLocationID IS NULL AND IsArchived = 0 + + UNION ALL + + SELECT child.LocationID, child.LocationName, CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations child + INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID + WHERE child.ProjectID = @ProjectID AND child.IsArchived = 0 + ) + INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath) + SELECT LocationID, LocationName, LocationPath + FROM ProjectLocationTree; + + SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, sa.Importance, + sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, + location.LocationName AS CurrentLocationName, location.LocationPath AS CurrentLocationPath, + sa.IsResolved, sa.ShowInQuickAddBar, sa.ImagePath, sa.ThumbnailPath, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived + FROM dbo.StoryAssets sa + INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID + LEFT JOIN dbo.AssetStates ast ON ast.AssetStateID = sa.CurrentStateID + LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sa.CurrentLocationID + WHERE sa.ProjectID = @ProjectID + AND sa.IsArchived = 0 + ORDER BY sa.Importance DESC, sa.AssetName; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.AssetLookup_All + @ProjectID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + SELECT AssetKindID, KindName, SortOrder, IsActive FROM dbo.AssetKinds WHERE IsActive = 1 ORDER BY SortOrder; + SELECT AssetStateID, ProjectID, AssetKindID, StateName, SortOrder, Description, IsResolvedState, IsActive + FROM dbo.AssetStates + WHERE IsActive = 1 + AND (@ProjectID IS NULL OR ProjectID IS NULL OR ProjectID = @ProjectID) + ORDER BY SortOrder; + SELECT AssetEventTypeID, TypeName, MarkerText, SortOrder, IsActive FROM dbo.AssetEventTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT AssetDependencyTypeID, TypeName, SortOrder, IsValidatingDependency, IsActive FROM dbo.AssetDependencyTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT AssetCustodyEventTypeID, TypeName, SortOrder, IsActive FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT CustodyRoleID, RoleName, SortOrder, IsActive FROM dbo.CustodyRoles WHERE IsActive = 1 ORDER BY SortOrder; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterLookup_All + @ProjectID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + SELECT CharacterRoleInSceneTypeID, TypeName, SortOrder, IsActive FROM dbo.CharacterRoleInSceneTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT PresenceTypeID, TypeName, SortOrder, IsActive FROM dbo.PresenceTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT CharacterAttributeTypeID, ProjectID, AttributeName, DataType, IsNormallyStable, SortOrder, IsActive + FROM dbo.CharacterAttributeTypes + WHERE IsActive = 1 + AND (@ProjectID IS NULL OR ProjectID IS NULL OR ProjectID = @ProjectID) + ORDER BY SortOrder; + SELECT KnowledgeStateID, StateName, SortOrder, IsActive FROM dbo.KnowledgeStates WHERE IsActive = 1 ORDER BY SortOrder; + SELECT rt.RelationshipTypeID, rt.TypeName, rt.IsPermanentDefault, rt.SortOrder, rt.IsActive, + rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, rc.SortOrder AS CategorySortOrder + FROM dbo.RelationshipTypes rt + LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID + WHERE rt.IsActive = 1 + ORDER BY COALESCE(rc.SortOrder, 900), rt.SortOrder, rt.TypeName; + SELECT RelationshipStateID, StateName, SortOrder, IsActive FROM dbo.RelationshipStates WHERE IsActive = 1 ORDER BY SortOrder; + SELECT RelationshipCategoryID, CategoryName, SortOrder, IsActive FROM dbo.RelationshipCategories WHERE IsActive = 1 ORDER BY SortOrder, CategoryName; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CharacterTimeline_GetByProject + @ProjectID int, + @BookID int = NULL +WITH RECOMPILE +AS +BEGIN + SET NOCOUNT ON; + + CREATE TABLE #TimelineScenes + ( + SceneID int NOT NULL PRIMARY KEY, + SceneNumber decimal(9,2) NOT NULL, + SceneTitle nvarchar(200) NOT NULL, + ChapterNumber decimal(9,2) NOT NULL, + BookTitle nvarchar(200) NOT NULL, + BookSubtitle nvarchar(200) NULL, + BookSortOrder int NOT NULL, + ChapterSortOrder int NOT NULL, + SceneSortOrder int NOT NULL + ); + + INSERT #TimelineScenes (SceneID, SceneNumber, SceneTitle, ChapterNumber, BookTitle, BookSubtitle, BookSortOrder, ChapterSortOrder, SceneSortOrder) + SELECT s.SceneID, s.SceneNumber, s.SceneTitle, ch.ChapterNumber, b.BookTitle, b.Subtitle, b.SortOrder, ch.SortOrder, s.SortOrder + FROM dbo.Books b + INNER JOIN dbo.Chapters ch ON ch.BookID = b.BookID + INNER JOIN dbo.Scenes s ON s.ChapterID = ch.ChapterID + WHERE b.ProjectID = @ProjectID + AND b.IsArchived = 0 + AND ch.IsArchived = 0 + AND s.IsArchived = 0 + AND (@BookID IS NULL OR b.BookID = @BookID); + + CREATE TABLE #TimelineCharacters + ( + CharacterID int NOT NULL PRIMARY KEY, + ProjectID int NOT NULL, + CharacterName nvarchar(200) NOT NULL, + ShortName nvarchar(100) NULL, + Sex nvarchar(50) NULL, + BirthDate date NULL, + AgeAtSeriesStart int NULL, + AgeReferenceSceneID int NULL, + Height nvarchar(50) NULL, + EyeColour nvarchar(50) NULL, + CharacterImportance int NULL, + ShowInQuickAddBar bit NOT NULL, + DefaultDescription nvarchar(max) NULL, + CreatedDate datetime2 NOT NULL, + UpdatedDate datetime2 NOT NULL, + IsArchived bit NOT NULL + ); + + INSERT #TimelineCharacters (CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, Height, EyeColour, CharacterImportance, ShowInQuickAddBar, DefaultDescription, CreatedDate, UpdatedDate, IsArchived) + SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, + Height, EyeColour, CharacterImportance, ShowInQuickAddBar, DefaultDescription, CreatedDate, UpdatedDate, IsArchived + FROM dbo.Characters + WHERE ProjectID = @ProjectID AND IsArchived = 0; + + CREATE TABLE #ProjectLocationPaths + ( + LocationID int NOT NULL PRIMARY KEY, + LocationName nvarchar(200) NOT NULL, + LocationPath nvarchar(max) NOT NULL + ); + + ;WITH ProjectLocationTree AS + ( + SELECT LocationID, LocationName, CAST(LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations + WHERE ProjectID = @ProjectID AND ParentLocationID IS NULL AND IsArchived = 0 + + UNION ALL + + SELECT child.LocationID, child.LocationName, CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations child + INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID + WHERE child.ProjectID = @ProjectID AND child.IsArchived = 0 + ) + INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath) + SELECT LocationID, LocationName, LocationPath + FROM ProjectLocationTree; + + SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, AgeReferenceSceneID, + Height, EyeColour, CharacterImportance, ShowInQuickAddBar, DefaultDescription, CreatedDate, UpdatedDate, IsArchived + FROM #TimelineCharacters + ORDER BY ISNULL(CharacterImportance, 0) DESC, CharacterName; + + SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, c.ProjectID, c.CharacterName, c.ShortName, c.BirthDate, c.AgeAtSeriesStart, + sc.RoleInSceneTypeID, role.TypeName AS RoleInSceneTypeName, sc.PresenceTypeID, presence.TypeName AS PresenceTypeName, + sc.LocationID, location.LocationName, location.LocationPath, sc.EntryLocationID, entry.LocationName AS EntryLocationName, + sc.ExitLocationID, exitLocation.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription, + sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, s.ChapterNumber, s.BookTitle, s.BookSubtitle, + sc.CreatedDate, sc.UpdatedDate + FROM #TimelineScenes s + INNER JOIN dbo.SceneCharacters sc ON sc.SceneID = s.SceneID + INNER JOIN #TimelineCharacters c ON c.CharacterID = sc.CharacterID + LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID + LEFT JOIN dbo.PresenceTypes presence ON presence.PresenceTypeID = sc.PresenceTypeID + LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sc.LocationID + LEFT JOIN #ProjectLocationPaths entry ON entry.LocationID = sc.EntryLocationID + LEFT JOIN #ProjectLocationPaths exitLocation ON exitLocation.LocationID = sc.ExitLocationID + ORDER BY s.BookSortOrder, s.ChapterSortOrder, s.SceneSortOrder, c.CharacterName; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.Timeline_SceneOverviews + @ProjectID int, + @BookID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + CREATE TABLE #TimelineScenes + ( + SceneID int NOT NULL PRIMARY KEY, + BookSortOrder int NOT NULL, + ChapterSortOrder int NOT NULL, + SceneSortOrder int NOT NULL + ); + + INSERT #TimelineScenes (SceneID, BookSortOrder, ChapterSortOrder, SceneSortOrder) + SELECT s.SceneID, b.SortOrder, c.SortOrder, s.SortOrder + FROM dbo.Books b + INNER JOIN dbo.Chapters c ON c.BookID = b.BookID + INNER JOIN dbo.Scenes s ON s.ChapterID = c.ChapterID + WHERE b.ProjectID = @ProjectID + AND b.IsArchived = 0 + AND c.IsArchived = 0 + AND s.IsArchived = 0 + AND (@BookID IS NULL OR b.BookID = @BookID); + + CREATE TABLE #ProjectLocationPaths + ( + LocationID int NOT NULL PRIMARY KEY, + LocationName nvarchar(200) NOT NULL, + LocationPath nvarchar(max) NOT NULL + ); + + ;WITH ProjectLocationTree AS + ( + SELECT LocationID, LocationName, CAST(LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations + WHERE ProjectID = @ProjectID AND ParentLocationID IS NULL AND IsArchived = 0 + + UNION ALL + + SELECT child.LocationID, child.LocationName, CAST(parent.LocationPath + N' > ' + child.LocationName AS nvarchar(max)) AS LocationPath + FROM dbo.Locations child + INNER JOIN ProjectLocationTree parent ON parent.LocationID = child.ParentLocationID + WHERE child.ProjectID = @ProjectID AND child.IsArchived = 0 + ) + INSERT #ProjectLocationPaths (LocationID, LocationName, LocationPath) + SELECT LocationID, LocationName, LocationPath + FROM ProjectLocationTree; + + SELECT ts.SceneID, + sc.CharacterID, + ch.CharacterName, + CONVERT(bit, CASE WHEN role.TypeName LIKE N'%POV%' THEN 1 ELSE 0 END) AS IsPov, + CONVERT(int, NULL) AS StoryAssetID, + CONVERT(nvarchar(200), NULL) AS AssetName, + CONVERT(int, NULL) AS AssetImportance, + CONVERT(nvarchar(100), NULL) AS AssetEventTypeName, + location.LocationPath AS LocationName + FROM #TimelineScenes ts + INNER JOIN dbo.SceneCharacters sc ON sc.SceneID = ts.SceneID + INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID AND ch.ProjectID = @ProjectID AND ch.IsArchived = 0 + LEFT JOIN dbo.CharacterRoleInSceneTypes role ON role.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID + LEFT JOIN #ProjectLocationPaths location ON location.LocationID = sc.LocationID + + UNION ALL + + SELECT SceneID, CharacterID, CharacterName, IsPov, StoryAssetID, AssetName, AssetImportance, AssetEventTypeName, LocationName + FROM + ( + SELECT ts.SceneID, + CONVERT(int, NULL) AS CharacterID, + CONVERT(nvarchar(200), NULL) AS CharacterName, + CONVERT(bit, 0) AS IsPov, + sa.StoryAssetID, + sa.AssetName, + sa.Importance AS AssetImportance, + aet.TypeName AS AssetEventTypeName, + CONVERT(nvarchar(max), NULL) AS LocationName, + ROW_NUMBER() OVER (PARTITION BY ts.SceneID, sa.StoryAssetID ORDER BY ae.UpdatedDate DESC, ae.AssetEventID DESC) AS rn + FROM #TimelineScenes ts + INNER JOIN dbo.AssetEvents ae ON ae.SceneID = ts.SceneID + INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID AND sa.ProjectID = @ProjectID AND sa.IsArchived = 0 + INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID + ) assetRows + WHERE rn = 1 + ORDER BY SceneID; +END; +GO