Implemented the targeted Timeline performance pass and applied the SQL locally.

This commit is contained in:
Nick Beckley 2026-06-23 20:38:58 +01:00
parent a3ce37c66c
commit b5b353e80c
3 changed files with 530 additions and 41 deletions

View File

@ -102,6 +102,7 @@ public interface ILookupRepository
public interface ITimelineRepository
{
Task<TimelineData> GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable<int> metricTypeIds, bool includePlotLines);
Task<IReadOnlyList<TimelineSceneOverviewRow>> GetSceneOverviewsAsync(int projectId, int? bookId);
}
public interface ITimelinePresetRepository
@ -154,7 +155,7 @@ public interface IPlotRepository
public interface IAssetRepository
{
Task<AssetLookupData> GetLookupsAsync();
Task<AssetLookupData> GetLookupsAsync(int? projectId = null);
Task<IReadOnlyList<StoryAsset>> ListAssetsAsync(int projectId);
Task<StoryAsset?> GetAssetAsync(int storyAssetId);
Task<int> SaveAssetAsync(StoryAsset asset);
@ -178,7 +179,7 @@ public interface IAssetRepository
public interface ICharacterRepository
{
Task<CharacterLookupData> GetLookupsAsync();
Task<CharacterLookupData> GetLookupsAsync(int? projectId = null);
Task<IReadOnlyList<Character>> ListCharactersAsync(int projectId);
Task<Character?> GetCharacterAsync(int characterId);
Task<int> SaveCharacterAsync(Character character);
@ -419,6 +420,19 @@ public sealed class TimelineData
public IReadOnlyList<ThreadEvent> 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<LocationLookupData> GetLookupsAsync()
@ -2520,6 +2534,16 @@ public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory)
public int PlotEventID { get; set; }
public int PlotLineID { get; set; }
}
public async Task<IReadOnlyList<TimelineSceneOverviewRow>> GetSceneOverviewsAsync(int projectId, int? bookId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<TimelineSceneOverviewRow>(
"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<AssetLookupData> GetLookupsAsync()
public async Task<AssetLookupData> 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<AssetKind>()).ToList(),
@ -3148,10 +3172,10 @@ public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : I
public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) : ICharacterRepository
{
public async Task<CharacterLookupData> GetLookupsAsync()
public async Task<CharacterLookupData> 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<CharacterRoleInSceneType>()).ToList(),

View File

@ -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<ContinuityWarning>)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<ContinuityWarning>)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<int, TimelineSceneOverviewViewModel>();
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<int, TimelineSceneOverviewViewModel>(),
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<int, TimelineSceneOverviewViewModel> BuildSceneOverviews(
IReadOnlyList<Scene> orderedScenes,
IReadOnlyList<SceneCharacter> appearances,
IReadOnlyList<StoryAsset> assets,
IReadOnlyList<AssetEvent> assetEvents)
IReadOnlyList<TimelineSceneOverviewRow> 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);
}

View File

@ -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