From fb5352ab8f60b69599633a170f551e94a450af7e Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Thu, 11 Jun 2026 19:41:03 +0100 Subject: [PATCH] Phase 6A Plot Lines --- PlotLine/Data/ImportRepository.cs | 21 +- PlotLine/Data/Repositories.cs | 9 +- PlotLine/Models/CoreModels.cs | 34 +- PlotLine/Services/CoreServices.cs | 10 +- .../Sql/053_Phase6A_PlotLineDataModel.sql | 1065 +++++++++++++++++ PlotLine/ViewModels/CoreViewModels.cs | 10 +- PlotLine/Views/PlotLines/Edit.cshtml | 16 +- PlotLine/Views/PlotLines/Index.cshtml | 2 + 8 files changed, 1152 insertions(+), 15 deletions(-) create mode 100644 PlotLine/Sql/053_Phase6A_PlotLineDataModel.sql diff --git a/PlotLine/Data/ImportRepository.cs b/PlotLine/Data/ImportRepository.cs index 40dcbfd..6432449 100644 --- a/PlotLine/Data/ImportRepository.cs +++ b/PlotLine/Data/ImportRepository.cs @@ -381,6 +381,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : BookID = (int?)null, PlotLineName = Clean(DisplayName(plotLine.DisplayName, plotLine.Name)), PlotLineTypeID = ResolvePlotLineTypeId(lookupIds, plotLine.Type), + PlotImportanceID = ResolvePlotImportanceId(lookupIds, plotLine), Description = CombineText( plotLine.Description, MetadataLine("Type", plotLine.Type), @@ -388,9 +389,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : plotLine.Notes, ImportMarker), ParentPlotLineID = (int?)null, + EmergesFromPlotLineID = (int?)null, SortOrder = (int?)null, Colour = "#2f6f63", - IsMainPlot = string.Equals(plotLine.Type?.Trim(), "Main Plot", StringComparison.OrdinalIgnoreCase), IsVisibleOnTimeline = true }, transaction, @@ -685,6 +686,9 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : var plotLineTypes = (await connection.QueryAsync( "SELECT PlotLineTypeID AS LookupID, TypeName FROM dbo.PlotLineTypes WHERE IsActive = 1;", transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); + var plotImportance = (await connection.QueryAsync( + "SELECT PlotImportanceID AS LookupID, ImportanceName AS TypeName FROM dbo.PlotImportance WHERE IsActive = 1;", + transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); var threadTypes = (await connection.QueryAsync( "SELECT ThreadTypeID AS LookupID, TypeName FROM dbo.ThreadTypes WHERE IsActive = 1;", transaction: transaction)).ToDictionary(x => x.TypeName, x => x.LookupID, StringComparer.OrdinalIgnoreCase); @@ -726,6 +730,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : roleTypes, presenceTypes, plotLineTypes, + plotImportance, threadTypes, threadStatuses, threadEventTypes, @@ -1111,6 +1116,19 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : : lookupIds.PlotLineTypeIds.Values.First(); } + private static int ResolvePlotImportanceId(ImportLookupIds lookupIds, ImportPlotLineDto plotLine) + { + if (string.Equals(plotLine.Type?.Trim(), "Main Plot", StringComparison.OrdinalIgnoreCase) + && lookupIds.PlotImportanceIds.TryGetValue("Primary", out var primaryId)) + { + return primaryId; + } + + return lookupIds.PlotImportanceIds.TryGetValue("Secondary", out var secondaryId) + ? secondaryId + : lookupIds.PlotImportanceIds.Values.First(); + } + private static int ResolveThreadTypeId(ImportLookupIds lookupIds, string? type) { if (!string.IsNullOrWhiteSpace(type) && lookupIds.ThreadTypeIds.TryGetValue(type.Trim(), out var id)) @@ -1387,6 +1405,7 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : IReadOnlyDictionary RoleTypeIds, IReadOnlyDictionary PresenceTypeIds, IReadOnlyDictionary PlotLineTypeIds, + IReadOnlyDictionary PlotImportanceIds, IReadOnlyDictionary ThreadTypeIds, IReadOnlyDictionary ThreadStatusIds, IReadOnlyDictionary ThreadEventTypeIds, diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index 8a21be7..bab729a 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -1850,9 +1850,11 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP return new PlotLookupData { PlotLineTypes = (await result.ReadAsync()).ToList(), + PlotImportance = (await result.ReadAsync()).ToList(), ThreadTypes = (await result.ReadAsync()).ToList(), ThreadStatuses = (await result.ReadAsync()).ToList(), - ThreadEventTypes = (await result.ReadAsync()).ToList() + ThreadEventTypes = (await result.ReadAsync()).ToList(), + PlotEventTypes = (await result.ReadAsync()).ToList() }; } @@ -1881,11 +1883,12 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP plotLine.BookID, plotLine.PlotLineName, plotLine.PlotLineTypeID, + plotLine.PlotImportanceID, plotLine.Description, plotLine.ParentPlotLineID, + plotLine.EmergesFromPlotLineID, plotLine.SortOrder, plotLine.Colour, - plotLine.IsMainPlot, plotLine.IsVisibleOnTimeline }, commandType: CommandType.StoredProcedure); @@ -1969,6 +1972,8 @@ public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IP threadEvent.PlotThreadID, threadEvent.SceneID, threadEvent.EventTypeID, + threadEvent.PlotEventTypeID, + threadEvent.TargetPlotLineID, threadEvent.EventTitle, threadEvent.EventDescription }, diff --git a/PlotLine/Models/CoreModels.cs b/PlotLine/Models/CoreModels.cs index 1c62908..2e0a580 100644 --- a/PlotLine/Models/CoreModels.cs +++ b/PlotLine/Models/CoreModels.cs @@ -361,6 +361,14 @@ public sealed class PlotLineType public bool IsActive { get; set; } } +public sealed class PlotImportance +{ + public int PlotImportanceID { get; set; } + public string ImportanceName { get; set; } = string.Empty; + public int SortOrder { get; set; } + public bool IsActive { get; set; } +} + public sealed class PlotLineItem { public int PlotLineID { get; set; } @@ -370,12 +378,15 @@ public sealed class PlotLineItem public string PlotLineName { get; set; } = string.Empty; public int PlotLineTypeID { get; set; } public string PlotLineTypeName { get; set; } = string.Empty; + public int PlotImportanceID { get; set; } = 2; + public string PlotImportanceName { get; set; } = "Secondary"; public string? Description { get; set; } public int? ParentPlotLineID { get; set; } public string? ParentPlotLineName { get; set; } + public int? EmergesFromPlotLineID { get; set; } + public string? EmergesFromPlotLineName { get; set; } public int SortOrder { get; set; } public string Colour { get; set; } = "#2f6f63"; - public bool IsMainPlot { get; set; } public bool IsVisibleOnTimeline { get; set; } = true; public DateTime CreatedDate { get; set; } public DateTime UpdatedDate { get; set; } @@ -434,6 +445,14 @@ public sealed class ThreadEventType public bool IsActive { get; set; } } +public sealed class PlotEventType +{ + public int PlotEventTypeID { get; set; } + public string TypeName { get; set; } = string.Empty; + public int SortOrder { get; set; } + public bool IsActive { get; set; } +} + public sealed class ThreadEvent { public int ThreadEventID { get; set; } @@ -446,12 +465,23 @@ public sealed class ThreadEvent public int EventTypeID { get; set; } public string EventTypeName { get; set; } = string.Empty; public string MarkerText { get; set; } = string.Empty; + public int PlotEventTypeID { get; set; } = 2; + public string PlotEventTypeName { get; set; } = "Progress"; + public int? TargetPlotLineID { get; set; } + public string? TargetPlotLineName { get; set; } public string EventTitle { get; set; } = string.Empty; public string? EventDescription { get; set; } public DateTime CreatedDate { get; set; } public DateTime UpdatedDate { get; set; } } +public sealed class PlotEventTargetPlotLine +{ + public int PlotEventID { get; set; } + public int PlotLineID { get; set; } + public DateTime CreatedDate { get; set; } +} + public sealed class SceneOption { public int SceneID { get; set; } @@ -465,9 +495,11 @@ public sealed class SceneOption public sealed class PlotLookupData { public IReadOnlyList PlotLineTypes { get; init; } = []; + public IReadOnlyList PlotImportance { get; init; } = []; public IReadOnlyList ThreadTypes { get; init; } = []; public IReadOnlyList ThreadStatuses { get; init; } = []; public IReadOnlyList ThreadEventTypes { get; init; } = []; + public IReadOnlyList PlotEventTypes { get; init; } = []; } public sealed class AssetKind diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index 0e6a7dc..c8f167b 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -2058,6 +2058,8 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo SortOrder = (plotLines.Count + 1) * 10, Colour = "#2f6f63", IsVisibleOnTimeline = true, + PlotImportanceID = lookupData.PlotImportance.FirstOrDefault(x => x.ImportanceName == "Secondary")?.PlotImportanceID + ?? lookupData.PlotImportance.First().PlotImportanceID, PlotLineTypeID = lookupData.PlotLineTypes.FirstOrDefault(x => x.TypeName == "Side Plot")?.PlotLineTypeID ?? lookupData.PlotLineTypes.First().PlotLineTypeID }, lookupData, plotLines); @@ -2086,11 +2088,12 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo BookID = plotLine.BookID, PlotLineName = plotLine.PlotLineName, PlotLineTypeID = plotLine.PlotLineTypeID, + PlotImportanceID = plotLine.PlotImportanceID, Description = plotLine.Description, ParentPlotLineID = plotLine.ParentPlotLineID, + EmergesFromPlotLineID = plotLine.EmergesFromPlotLineID, SortOrder = plotLine.SortOrder, Colour = plotLine.Colour, - IsMainPlot = plotLine.IsMainPlot, IsVisibleOnTimeline = plotLine.IsVisibleOnTimeline, Project = project }, lookupData, plotLines.Where(x => x.PlotLineID != plotLine.PlotLineID)); @@ -2106,11 +2109,12 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo BookID = model.BookID, PlotLineName = model.PlotLineName, PlotLineTypeID = model.PlotLineTypeID, + PlotImportanceID = model.PlotImportanceID, Description = model.Description, ParentPlotLineID = model.ParentPlotLineID, + EmergesFromPlotLineID = model.EmergesFromPlotLineID, SortOrder = model.SortOrder, Colour = string.IsNullOrWhiteSpace(model.Colour) ? "#2f6f63" : model.Colour, - IsMainPlot = model.IsMainPlot, IsVisibleOnTimeline = model.IsVisibleOnTimeline }); await activity.RecordAsync(model.ProjectID, isNew ? "Created" : "Updated", "Plot Line", plotLineId, model.PlotLineName); @@ -2264,6 +2268,7 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo PlotThreadID = plotThreadId, SceneID = model.SceneID, EventTypeID = eventType.ThreadEventTypeID, + PlotEventTypeID = lookupData.PlotEventTypes.FirstOrDefault(x => x.TypeName == "Progress")?.PlotEventTypeID ?? 2, EventTitle = title, EventDescription = model.EventDescription }); @@ -2297,6 +2302,7 @@ public sealed class PlotService(IProjectRepository projects, IBookRepository boo private async Task PopulatePlotLineListsAsync(PlotLineEditViewModel model, PlotLookupData lookupData, IEnumerable parentCandidates) { model.PlotLineTypes = ChapterService.ToSelectList(lookupData.PlotLineTypes, x => x.PlotLineTypeID, x => x.TypeName); + model.PlotImportance = ChapterService.ToSelectList(lookupData.PlotImportance, x => x.PlotImportanceID, x => x.ImportanceName); model.BookOptions = (await books.ListByProjectAsync(model.ProjectID)) .Select(x => new SelectListItem($"Book {x.BookNumber}: {x.BookTitle}", x.BookID.ToString())) .ToList(); diff --git a/PlotLine/Sql/053_Phase6A_PlotLineDataModel.sql b/PlotLine/Sql/053_Phase6A_PlotLineDataModel.sql new file mode 100644 index 0000000..2fab05e --- /dev/null +++ b/PlotLine/Sql/053_Phase6A_PlotLineDataModel.sql @@ -0,0 +1,1065 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +IF OBJECT_ID(N'dbo.PlotImportance', N'U') IS NULL +BEGIN + CREATE TABLE dbo.PlotImportance + ( + PlotImportanceID int NOT NULL CONSTRAINT PK_PlotImportance PRIMARY KEY, + ImportanceName nvarchar(100) NOT NULL, + SortOrder int NOT NULL, + IsActive bit NOT NULL CONSTRAINT DF_PlotImportance_IsActive DEFAULT 1, + CONSTRAINT UQ_PlotImportance_ImportanceName UNIQUE (ImportanceName) + ); +END; +GO + +MERGE dbo.PlotImportance AS target +USING (VALUES + (1, N'Primary', 10), + (2, N'Secondary', 20), + (3, N'Minor', 30) +) AS source (PlotImportanceID, ImportanceName, SortOrder) +ON target.PlotImportanceID = source.PlotImportanceID +WHEN MATCHED THEN + UPDATE SET ImportanceName = source.ImportanceName, SortOrder = source.SortOrder, IsActive = 1 +WHEN NOT MATCHED THEN + INSERT (PlotImportanceID, ImportanceName, SortOrder, IsActive) + VALUES (source.PlotImportanceID, source.ImportanceName, source.SortOrder, 1); +GO + +IF OBJECT_ID(N'dbo.PlotEventTypes', N'U') IS NULL +BEGIN + CREATE TABLE dbo.PlotEventTypes + ( + PlotEventTypeID int NOT NULL CONSTRAINT PK_PlotEventTypes PRIMARY KEY, + TypeName nvarchar(100) NOT NULL, + SortOrder int NOT NULL, + IsActive bit NOT NULL CONSTRAINT DF_PlotEventTypes_IsActive DEFAULT 1, + CONSTRAINT UQ_PlotEventTypes_TypeName UNIQUE (TypeName) + ); +END; +GO + +MERGE dbo.PlotEventTypes AS target +USING (VALUES + (1, N'Start', 10), + (2, N'Progress', 20), + (3, N'Reveal', 30), + (4, N'Twist', 40), + (5, N'Branch', 50), + (6, N'Split', 60), + (7, N'Merge', 70), + (8, N'Resolve', 80), + (9, N'Abandon', 90) +) AS source (PlotEventTypeID, TypeName, SortOrder) +ON target.PlotEventTypeID = source.PlotEventTypeID +WHEN MATCHED THEN + UPDATE SET TypeName = source.TypeName, SortOrder = source.SortOrder, IsActive = 1 +WHEN NOT MATCHED THEN + INSERT (PlotEventTypeID, TypeName, SortOrder, IsActive) + VALUES (source.PlotEventTypeID, source.TypeName, source.SortOrder, 1); +GO + +IF COL_LENGTH(N'dbo.PlotLines', N'PlotImportanceID') IS NULL +BEGIN + ALTER TABLE dbo.PlotLines ADD PlotImportanceID int NULL; +END; +GO + +IF COL_LENGTH(N'dbo.PlotLines', N'EmergesFromPlotLineID') IS NULL +BEGIN + ALTER TABLE dbo.PlotLines ADD EmergesFromPlotLineID int NULL; +END; +GO + +IF COL_LENGTH(N'dbo.PlotLines', N'IsMainPlot') IS NOT NULL +BEGIN + EXEC (N'UPDATE dbo.PlotLines SET PlotImportanceID = CASE WHEN IsMainPlot = 1 THEN 1 ELSE 2 END WHERE PlotImportanceID IS NULL;'); +END +ELSE +BEGIN + UPDATE dbo.PlotLines SET PlotImportanceID = 2 WHERE PlotImportanceID IS NULL; +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = N'DF_PlotLines_PlotImportanceID' AND parent_object_id = OBJECT_ID(N'dbo.PlotLines')) +BEGIN + ALTER TABLE dbo.PlotLines ADD CONSTRAINT DF_PlotLines_PlotImportanceID DEFAULT 2 FOR PlotImportanceID; +END; +GO + +ALTER TABLE dbo.PlotLines ALTER COLUMN PlotImportanceID int NOT NULL; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_PlotLines_PlotImportance') +BEGIN + ALTER TABLE dbo.PlotLines WITH CHECK ADD CONSTRAINT FK_PlotLines_PlotImportance + FOREIGN KEY (PlotImportanceID) REFERENCES dbo.PlotImportance(PlotImportanceID); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_PlotLines_EmergesFromPlotLine') +BEGIN + ALTER TABLE dbo.PlotLines WITH CHECK ADD CONSTRAINT FK_PlotLines_EmergesFromPlotLine + FOREIGN KEY (EmergesFromPlotLineID) REFERENCES dbo.PlotLines(PlotLineID); +END; +GO + +IF COL_LENGTH(N'dbo.PlotLines', N'IsMainPlot') IS NOT NULL +BEGIN + DECLARE @DropIsMainPlotDefault nvarchar(max); + SELECT @DropIsMainPlotDefault = N'ALTER TABLE dbo.PlotLines DROP CONSTRAINT ' + QUOTENAME(dc.name) + FROM sys.default_constraints dc + INNER JOIN sys.columns c ON c.default_object_id = dc.object_id + WHERE dc.parent_object_id = OBJECT_ID(N'dbo.PlotLines') AND c.name = N'IsMainPlot'; + + IF @DropIsMainPlotDefault IS NOT NULL EXEC (@DropIsMainPlotDefault); + ALTER TABLE dbo.PlotLines DROP COLUMN IsMainPlot; +END; +GO + +IF COL_LENGTH(N'dbo.ThreadEvents', N'PlotEventTypeID') IS NULL +BEGIN + ALTER TABLE dbo.ThreadEvents ADD PlotEventTypeID int NULL; +END; +GO + +IF COL_LENGTH(N'dbo.ThreadEvents', N'TargetPlotLineID') IS NULL +BEGIN + ALTER TABLE dbo.ThreadEvents ADD TargetPlotLineID int NULL; +END; +GO + +UPDATE dbo.ThreadEvents +SET PlotEventTypeID = 2 +WHERE PlotEventTypeID IS NULL; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.default_constraints WHERE name = N'DF_ThreadEvents_PlotEventTypeID' AND parent_object_id = OBJECT_ID(N'dbo.ThreadEvents')) +BEGIN + ALTER TABLE dbo.ThreadEvents ADD CONSTRAINT DF_ThreadEvents_PlotEventTypeID DEFAULT 2 FOR PlotEventTypeID; +END; +GO + +ALTER TABLE dbo.ThreadEvents ALTER COLUMN PlotEventTypeID int NOT NULL; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_ThreadEvents_PlotEventTypes') +BEGIN + ALTER TABLE dbo.ThreadEvents WITH CHECK ADD CONSTRAINT FK_ThreadEvents_PlotEventTypes + FOREIGN KEY (PlotEventTypeID) REFERENCES dbo.PlotEventTypes(PlotEventTypeID); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_ThreadEvents_TargetPlotLines') +BEGIN + ALTER TABLE dbo.ThreadEvents WITH CHECK ADD CONSTRAINT FK_ThreadEvents_TargetPlotLines + FOREIGN KEY (TargetPlotLineID) REFERENCES dbo.PlotLines(PlotLineID); +END; +GO + +IF OBJECT_ID(N'dbo.PlotEventTargetPlotLines', N'U') IS NULL +BEGIN + CREATE TABLE dbo.PlotEventTargetPlotLines + ( + PlotEventID int NOT NULL, + PlotLineID int NOT NULL, + CreatedDate datetime2 NOT NULL CONSTRAINT DF_PlotEventTargetPlotLines_CreatedDate DEFAULT SYSUTCDATETIME(), + CONSTRAINT PK_PlotEventTargetPlotLines PRIMARY KEY (PlotEventID, PlotLineID), + CONSTRAINT FK_PlotEventTargetPlotLines_ThreadEvents FOREIGN KEY (PlotEventID) REFERENCES dbo.ThreadEvents(ThreadEventID) ON DELETE CASCADE, + CONSTRAINT FK_PlotEventTargetPlotLines_PlotLines FOREIGN KEY (PlotLineID) REFERENCES dbo.PlotLines(PlotLineID) + ); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_PlotLines_PlotImportanceID' AND object_id = OBJECT_ID(N'dbo.PlotLines')) + CREATE INDEX IX_PlotLines_PlotImportanceID ON dbo.PlotLines(ProjectID, PlotImportanceID, IsArchived); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_ThreadEvents_PlotEventTypeID' AND object_id = OBJECT_ID(N'dbo.ThreadEvents')) + CREATE INDEX IX_ThreadEvents_PlotEventTypeID ON dbo.ThreadEvents(PlotEventTypeID, TargetPlotLineID); +GO + +CREATE OR ALTER PROCEDURE dbo.PlotLookup_All +AS +BEGIN + SET NOCOUNT ON; + SELECT PlotLineTypeID, TypeName, SortOrder, IsActive FROM dbo.PlotLineTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT PlotImportanceID, ImportanceName, SortOrder, IsActive FROM dbo.PlotImportance WHERE IsActive = 1 ORDER BY SortOrder; + SELECT ThreadTypeID, TypeName, SortOrder, IsActive FROM dbo.ThreadTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT ThreadStatusID, StatusName, SortOrder, IsOpenStatus, IsResolvedStatus, IsActive FROM dbo.ThreadStatuses WHERE IsActive = 1 ORDER BY SortOrder; + SELECT ThreadEventTypeID, TypeName, MarkerText, SortOrder, IsActive FROM dbo.ThreadEventTypes WHERE IsActive = 1 ORDER BY SortOrder; + SELECT PlotEventTypeID, TypeName, SortOrder, IsActive FROM dbo.PlotEventTypes WHERE IsActive = 1 ORDER BY SortOrder; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.PlotLine_ListByProject + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID, + plt.TypeName AS PlotLineTypeName, pl.PlotImportanceID, pi.ImportanceName AS PlotImportanceName, + pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName, + pl.EmergesFromPlotLineID, emerges.PlotLineName AS EmergesFromPlotLineName, + pl.SortOrder, pl.Colour, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived + FROM dbo.PlotLines pl + INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID + INNER JOIN dbo.PlotImportance pi ON pi.PlotImportanceID = pl.PlotImportanceID + LEFT JOIN dbo.Books b ON b.BookID = pl.BookID + LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID + LEFT JOIN dbo.PlotLines emerges ON emerges.PlotLineID = pl.EmergesFromPlotLineID + WHERE pl.ProjectID = @ProjectID AND pl.IsArchived = 0 + ORDER BY pl.SortOrder, pl.PlotLineName; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.PlotLine_Get + @PlotLineID int +AS +BEGIN + SET NOCOUNT ON; + SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID, + plt.TypeName AS PlotLineTypeName, pl.PlotImportanceID, pi.ImportanceName AS PlotImportanceName, + pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName, + pl.EmergesFromPlotLineID, emerges.PlotLineName AS EmergesFromPlotLineName, + pl.SortOrder, pl.Colour, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived + FROM dbo.PlotLines pl + INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID + INNER JOIN dbo.PlotImportance pi ON pi.PlotImportanceID = pl.PlotImportanceID + LEFT JOIN dbo.Books b ON b.BookID = pl.BookID + LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID + LEFT JOIN dbo.PlotLines emerges ON emerges.PlotLineID = pl.EmergesFromPlotLineID + WHERE pl.PlotLineID = @PlotLineID AND pl.IsArchived = 0; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.PlotLine_Save + @PlotLineID int = NULL, + @ProjectID int, + @BookID int = NULL, + @PlotLineName nvarchar(200), + @PlotLineTypeID int, + @PlotImportanceID int = 2, + @Description nvarchar(max) = NULL, + @ParentPlotLineID int = NULL, + @EmergesFromPlotLineID int = NULL, + @SortOrder int = NULL, + @Colour nvarchar(20), + @IsVisibleOnTimeline bit +AS +BEGIN + SET NOCOUNT ON; + IF @PlotLineID IS NULL OR @PlotLineID = 0 + BEGIN + DECLARE @NextSortOrder int = COALESCE(@SortOrder, ISNULL((SELECT MAX(SortOrder) FROM dbo.PlotLines WHERE ProjectID = @ProjectID), 0) + 10); + INSERT dbo.PlotLines (ProjectID, BookID, PlotLineName, PlotLineTypeID, PlotImportanceID, Description, ParentPlotLineID, EmergesFromPlotLineID, SortOrder, Colour, IsVisibleOnTimeline) + VALUES (@ProjectID, @BookID, @PlotLineName, @PlotLineTypeID, @PlotImportanceID, @Description, @ParentPlotLineID, @EmergesFromPlotLineID, @NextSortOrder, @Colour, @IsVisibleOnTimeline); + SELECT CAST(SCOPE_IDENTITY() AS int); + RETURN; + END; + + UPDATE dbo.PlotLines + SET BookID = @BookID, + PlotLineName = @PlotLineName, + PlotLineTypeID = @PlotLineTypeID, + PlotImportanceID = @PlotImportanceID, + Description = @Description, + ParentPlotLineID = @ParentPlotLineID, + EmergesFromPlotLineID = @EmergesFromPlotLineID, + SortOrder = COALESCE(@SortOrder, SortOrder), + Colour = @Colour, + IsVisibleOnTimeline = @IsVisibleOnTimeline, + UpdatedDate = SYSUTCDATETIME() + WHERE PlotLineID = @PlotLineID; + + SELECT @PlotLineID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ThreadEvent_ListByScene + @SceneID int +AS +BEGIN + SET NOCOUNT ON; + SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour, + te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, + te.PlotEventTypeID, pet.TypeName AS PlotEventTypeName, te.TargetPlotLineID, target.PlotLineName AS TargetPlotLineName, + te.EventTitle, te.EventDescription, te.CreatedDate, te.UpdatedDate + FROM dbo.ThreadEvents te + INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID + INNER JOIN dbo.PlotEventTypes pet ON pet.PlotEventTypeID = te.PlotEventTypeID + LEFT JOIN dbo.PlotLines target ON target.PlotLineID = te.TargetPlotLineID + WHERE te.SceneID = @SceneID AND pt.IsArchived = 0 AND pl.IsArchived = 0 + ORDER BY pl.SortOrder, pt.Importance DESC, te.CreatedDate; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ThreadEvent_ListByPlotThread + @PlotThreadID int +AS +BEGIN + SET NOCOUNT ON; + SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour, + te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, + te.PlotEventTypeID, pet.TypeName AS PlotEventTypeName, te.TargetPlotLineID, target.PlotLineName AS TargetPlotLineName, + te.EventTitle, te.EventDescription, te.CreatedDate, te.UpdatedDate + FROM dbo.ThreadEvents te + INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID + INNER JOIN dbo.PlotEventTypes pet ON pet.PlotEventTypeID = te.PlotEventTypeID + LEFT JOIN dbo.PlotLines target ON target.PlotLineID = te.TargetPlotLineID + WHERE te.PlotThreadID = @PlotThreadID + ORDER BY te.CreatedDate; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ThreadEvent_Save + @ThreadEventID int = NULL, + @PlotThreadID int, + @SceneID int, + @EventTypeID int, + @PlotEventTypeID int = 2, + @TargetPlotLineID int = NULL, + @EventTitle nvarchar(200), + @EventDescription nvarchar(max) = NULL +AS +BEGIN + SET NOCOUNT ON; + + IF @ThreadEventID IS NULL OR @ThreadEventID = 0 + BEGIN + INSERT dbo.ThreadEvents (PlotThreadID, SceneID, EventTypeID, PlotEventTypeID, TargetPlotLineID, EventTitle, EventDescription) + VALUES (@PlotThreadID, @SceneID, @EventTypeID, @PlotEventTypeID, @TargetPlotLineID, @EventTitle, @EventDescription); + SET @ThreadEventID = CAST(SCOPE_IDENTITY() AS int); + END + ELSE + BEGIN + UPDATE dbo.ThreadEvents + SET PlotThreadID = @PlotThreadID, + SceneID = @SceneID, + EventTypeID = @EventTypeID, + PlotEventTypeID = @PlotEventTypeID, + TargetPlotLineID = @TargetPlotLineID, + EventTitle = @EventTitle, + EventDescription = @EventDescription, + UpdatedDate = SYSUTCDATETIME() + WHERE ThreadEventID = @ThreadEventID; + END; + + DECLARE @EventTypeName nvarchar(100); + SELECT @EventTypeName = TypeName FROM dbo.ThreadEventTypes WHERE ThreadEventTypeID = @EventTypeID; + + IF @EventTypeName = N'Introduced' + BEGIN + UPDATE pt + SET IntroducedSceneID = COALESCE(IntroducedSceneID, @SceneID), + ThreadStatusID = CASE WHEN ts.StatusName = N'Planned' THEN introduced.ThreadStatusID ELSE pt.ThreadStatusID END, + UpdatedDate = SYSUTCDATETIME() + FROM dbo.PlotThreads pt + INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID + CROSS APPLY (SELECT ThreadStatusID FROM dbo.ThreadStatuses WHERE StatusName = N'Introduced') introduced + WHERE pt.PlotThreadID = @PlotThreadID; + END; + + IF @EventTypeName IN (N'Resolved', N'Payoff') + BEGIN + UPDATE pt + SET ActualResolutionSceneID = COALESCE(ActualResolutionSceneID, @SceneID), + ThreadStatusID = CASE WHEN @EventTypeName = N'Resolved' THEN resolved.ThreadStatusID ELSE pt.ThreadStatusID END, + UpdatedDate = SYSUTCDATETIME() + FROM dbo.PlotThreads pt + CROSS APPLY (SELECT ThreadStatusID FROM dbo.ThreadStatuses WHERE StatusName = N'Resolved') resolved + WHERE pt.PlotThreadID = @PlotThreadID; + END; + + SELECT @ThreadEventID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.Timeline_GetByProject + @ProjectID int, + @BookID int = NULL, + @IncludeMetrics bit = 1, + @SelectedMetricTypeIDs nvarchar(max) = NULL, + @IncludePlotLines bit = 1 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @MetricSelection TABLE (MetricTypeID int NOT NULL PRIMARY KEY); + INSERT @MetricSelection (MetricTypeID) + SELECT DISTINCT TRY_CONVERT(int, value) + FROM string_split(COALESCE(@SelectedMetricTypeIDs, N''), N',') + WHERE TRY_CONVERT(int, value) IS NOT NULL; + + SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived + FROM dbo.Projects + WHERE ProjectID = @ProjectID AND IsArchived = 0; + + SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived + FROM dbo.Books + WHERE ProjectID = @ProjectID AND IsArchived = 0 AND (@BookID IS NULL OR BookID = @BookID) + ORDER BY SortOrder, BookNumber; + + SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary, + c.SortOrder, c.RevisionStatusID, rs.StatusName AS RevisionStatusName, + c.CreatedDate, c.UpdatedDate, c.IsArchived + FROM dbo.Chapters c + INNER JOIN dbo.Books b ON b.BookID = c.BookID + INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID + WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID) + ORDER BY b.SortOrder, c.SortOrder, c.ChapterNumber; + + SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID, + s.PrimaryLocationID, location.LocationName AS PrimaryLocationName, location.LocationPath AS PrimaryLocationPath, + s.SortOrder, s.TimeModeID, tm.TimeModeName, s.StartDateTime, + s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, s.RelativeTimeText, + s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes, + s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, s.IsArchived + FROM dbo.Scenes s + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID + LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID + INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID + INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID + LEFT JOIN dbo.LocationPaths location ON location.LocationID = s.PrimaryLocationID + WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID) + ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, s.SceneNumber; + + SELECT sp.SceneID, spt.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder + FROM dbo.ScenePurposes sp + INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID AND b.IsArchived = 0 AND c.IsArchived = 0 AND s.IsArchived = 0 AND (@BookID IS NULL OR b.BookID = @BookID) + ORDER BY sp.SceneID, spt.SortOrder; + + SELECT MetricTypeID, ProjectID, MetricName, Description, MinValue, MaxValue, DefaultValue, SortOrder, IsActive + FROM dbo.SceneMetricTypes + WHERE @IncludeMetrics = 1 + AND IsActive = 1 + AND (ProjectID IS NULL OR ProjectID = @ProjectID) + AND (NOT EXISTS (SELECT 1 FROM @MetricSelection) OR MetricTypeID IN (SELECT MetricTypeID FROM @MetricSelection)) + ORDER BY SortOrder, MetricName; + + SELECT s.SceneID, mt.MetricTypeID, mt.MetricName, COALESCE(smv.Value, mt.DefaultValue) AS Value, smv.Notes + FROM dbo.Scenes s + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + CROSS JOIN dbo.SceneMetricTypes mt + LEFT JOIN dbo.SceneMetricValues smv ON smv.SceneID = s.SceneID AND smv.MetricTypeID = mt.MetricTypeID + WHERE b.ProjectID = @ProjectID + AND @IncludeMetrics = 1 + AND b.IsArchived = 0 + AND c.IsArchived = 0 + AND s.IsArchived = 0 + AND mt.IsActive = 1 + AND (mt.ProjectID IS NULL OR mt.ProjectID = @ProjectID) + AND (@BookID IS NULL OR b.BookID = @BookID) + AND (NOT EXISTS (SELECT 1 FROM @MetricSelection) OR mt.MetricTypeID IN (SELECT MetricTypeID FROM @MetricSelection)) + ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, mt.SortOrder; + + SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID, + plt.TypeName AS PlotLineTypeName, pl.PlotImportanceID, pi.ImportanceName AS PlotImportanceName, + pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName, + pl.EmergesFromPlotLineID, emerges.PlotLineName AS EmergesFromPlotLineName, + pl.SortOrder, pl.Colour, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived + FROM dbo.PlotLines pl + INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID + INNER JOIN dbo.PlotImportance pi ON pi.PlotImportanceID = pl.PlotImportanceID + LEFT JOIN dbo.Books b ON b.BookID = pl.BookID + LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID + LEFT JOIN dbo.PlotLines emerges ON emerges.PlotLineID = pl.EmergesFromPlotLineID + WHERE @IncludePlotLines = 1 + AND pl.ProjectID = @ProjectID + AND pl.IsArchived = 0 + AND pl.IsVisibleOnTimeline = 1 + AND (@BookID IS NULL OR pl.BookID IS NULL OR pl.BookID = @BookID) + ORDER BY pl.SortOrder, pl.PlotLineName; + + SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pt.PlotLineID, pl.PlotLineName, pl.Colour, + te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, + te.PlotEventTypeID, pet.TypeName AS PlotEventTypeName, te.TargetPlotLineID, target.PlotLineName AS TargetPlotLineName, + te.EventTitle, te.EventDescription, te.CreatedDate, te.UpdatedDate + FROM dbo.ThreadEvents te + INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID + INNER JOIN dbo.PlotEventTypes pet ON pet.PlotEventTypeID = te.PlotEventTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = te.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.PlotLines target ON target.PlotLineID = te.TargetPlotLineID + WHERE @IncludePlotLines = 1 + AND b.ProjectID = @ProjectID + AND b.IsArchived = 0 + AND c.IsArchived = 0 + AND s.IsArchived = 0 + AND pl.IsArchived = 0 + AND pl.IsVisibleOnTimeline = 1 + AND pt.IsArchived = 0 + AND (@BookID IS NULL OR b.BookID = @BookID) + ORDER BY pl.SortOrder, b.SortOrder, c.SortOrder, s.SortOrder, te.CreatedDate; +END; +GO + + +CREATE OR ALTER PROCEDURE dbo.ProjectBackup_Export + @ProjectID int, + @UserID int = NULL +AS +BEGIN + SET NOCOUNT ON; + + IF @UserID IS NOT NULL + AND NOT EXISTS + ( + SELECT 1 + FROM dbo.ProjectUserAccess + WHERE ProjectID = @ProjectID + AND UserID = @UserID + AND AccessRole = N'Owner' + AND IsActive = 1 + ) + SET @ProjectID = -1; + + SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived, ArchivedDate, ArchivedReason + FROM dbo.Projects + WHERE ProjectID = @ProjectID; + + SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, + IsArchived, ArchivedDate, ArchivedReason + FROM dbo.Books + WHERE ProjectID = @ProjectID + ORDER BY SortOrder, BookNumber, BookID; + + SELECT c.ChapterID, c.BookID, c.ChapterNumber, c.ChapterTitle, c.POVCharacterID, c.Summary, c.SortOrder, + c.RevisionStatusID, rs.StatusName AS RevisionStatusName, c.CreatedDate, c.UpdatedDate, + c.IsArchived, c.ArchivedDate, c.ArchivedReason + FROM dbo.Chapters c + INNER JOIN dbo.Books b ON b.BookID = c.BookID + INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = c.RevisionStatusID + WHERE b.ProjectID = @ProjectID + ORDER BY b.SortOrder, c.SortOrder, c.ChapterID; + + SELECT s.SceneID, s.ChapterID, s.SceneNumber, s.SceneTitle, s.Summary, s.POVCharacterID, s.PrimaryLocationID, + l.LocationName AS PrimaryLocationName, l.LocationName AS PrimaryLocationPath, s.SortOrder, s.TimeModeID, tm.TimeModeName, + s.StartDateTime, s.EndDateTime, s.DurationAmount, s.DurationUnitID, du.DurationUnitName, + s.RelativeTimeText, s.TimeConfidenceID, tc.TimeConfidenceName, s.ScenePurposeNotes, s.SceneOutcomeNotes, + s.RevisionStatusID, rs.StatusName AS RevisionStatusName, s.CreatedDate, s.UpdatedDate, + s.IsArchived, s.ArchivedDate, s.ArchivedReason + FROM dbo.Scenes s + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + INNER JOIN dbo.TimeModes tm ON tm.TimeModeID = s.TimeModeID + INNER JOIN dbo.TimeConfidences tc ON tc.TimeConfidenceID = s.TimeConfidenceID + INNER JOIN dbo.RevisionStatuses rs ON rs.RevisionStatusID = s.RevisionStatusID + LEFT JOIN dbo.DurationUnits du ON du.DurationUnitID = s.DurationUnitID + LEFT JOIN dbo.Locations l ON l.LocationID = s.PrimaryLocationID + WHERE b.ProjectID = @ProjectID + ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, s.SceneID; + + SELECT sp.SceneID, sp.ScenePurposeTypeID, spt.PurposeName, spt.SortOrder + FROM dbo.ScenePurposes sp + INNER JOIN dbo.ScenePurposeTypes spt ON spt.ScenePurposeTypeID = sp.ScenePurposeTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = sp.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY sp.SceneID, spt.SortOrder; + + SELECT so.SceneOutcomeID, so.SceneID, so.OutcomeDescription, so.OutcomeTypeID, so.CreatedDate, so.UpdatedDate + FROM dbo.SceneOutcomes so + INNER JOIN dbo.Scenes s ON s.SceneID = so.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY so.SceneID, so.SceneOutcomeID; + + SELECT mt.MetricTypeID, mt.ProjectID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, + mt.DefaultValue, COALESCE(ptms.SortOrder, mt.SortOrder) AS SortOrder, mt.IsActive, + CAST(CASE WHEN mt.IsActive = 1 AND ptms.MetricTypeID IS NOT NULL THEN 1 ELSE 0 END AS bit) AS IsEnabledForProject, + CAST(CASE WHEN mt.ProjectID = @ProjectID THEN 1 ELSE 0 END AS bit) AS IsCustom, + STUFF(( + SELECT N',' + gmp.PresetKey + FROM dbo.GenreMetricPresetMetrics gpmm + INNER JOIN dbo.GenreMetricPresets gmp ON gmp.GenreMetricPresetID = gpmm.GenreMetricPresetID + WHERE gpmm.MetricTypeID = mt.MetricTypeID + ORDER BY gmp.SortOrder, gmp.PresetName + FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 1, N'') AS GenrePresetKeys, + STUFF(( + SELECT N', ' + gmp.PresetName + FROM dbo.GenreMetricPresetMetrics gpmm + INNER JOIN dbo.GenreMetricPresets gmp ON gmp.GenreMetricPresetID = gpmm.GenreMetricPresetID + WHERE gpmm.MetricTypeID = mt.MetricTypeID + ORDER BY gmp.SortOrder, gmp.PresetName + FOR XML PATH(N''), TYPE).value(N'.', N'nvarchar(max)'), 1, 2, N'') AS GenrePresetNames, + ( + SELECT COUNT(1) + FROM dbo.SceneMetricValues smv + INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE smv.MetricTypeID = mt.MetricTypeID AND b.ProjectID = @ProjectID + ) AS ValueCount + FROM dbo.SceneMetricTypes mt + LEFT JOIN dbo.ProjectTimelineMetricSettings ptms ON ptms.ProjectID = @ProjectID AND ptms.MetricTypeID = mt.MetricTypeID + WHERE mt.ProjectID IS NULL + OR mt.ProjectID = @ProjectID + OR EXISTS + ( + SELECT 1 + FROM dbo.SceneMetricValues smv + INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE smv.MetricTypeID = mt.MetricTypeID AND b.ProjectID = @ProjectID + ) + ORDER BY COALESCE(ptms.SortOrder, mt.SortOrder), mt.MetricName; + + SELECT smv.SceneID, smv.MetricTypeID, mt.MetricName, mt.Description, mt.MinValue, mt.MaxValue, + mt.DefaultValue, mt.SortOrder, mt.IsActive, smv.Value, smv.Notes + FROM dbo.SceneMetricValues smv + INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = smv.MetricTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = smv.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY smv.SceneID, mt.SortOrder; + + SELECT ProjectTimelineSettingsID, ProjectID, ShowSceneCards, ShowMetricShape, ShowPlotLines, + ShowStoryAssets, ShowCharacterAppearances, ShowWarnings, CreatedDate, UpdatedDate + FROM dbo.ProjectTimelineSettings + WHERE ProjectID = @ProjectID; + + SELECT ptms.ProjectID, ptms.MetricTypeID, mt.MetricName, ptms.SortOrder, ptms.CreatedDate + FROM dbo.ProjectTimelineMetricSettings ptms + INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = ptms.MetricTypeID + WHERE ptms.ProjectID = @ProjectID + ORDER BY ptms.SortOrder; + + SELECT TimelineViewPresetID, ProjectID, BookID, PresetName, FilterJson, VisibilityJson, FocusType, FocusID, + CreatedDate, UpdatedDate, IsArchived + FROM dbo.TimelineViewPresets + WHERE ProjectID = @ProjectID + ORDER BY PresetName; + + SELECT pl.PlotLineID, pl.ProjectID, pl.BookID, b.BookTitle, pl.PlotLineName, pl.PlotLineTypeID, + plt.TypeName AS PlotLineTypeName, pl.PlotImportanceID, pi.ImportanceName AS PlotImportanceName, + pl.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName, + pl.EmergesFromPlotLineID, emerges.PlotLineName AS EmergesFromPlotLineName, + pl.SortOrder, pl.Colour, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, + pl.IsArchived, pl.ArchivedDate, pl.ArchivedReason + FROM dbo.PlotLines pl + INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID + INNER JOIN dbo.PlotImportance pi ON pi.PlotImportanceID = pl.PlotImportanceID + LEFT JOIN dbo.Books b ON b.BookID = pl.BookID + LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID + LEFT JOIN dbo.PlotLines emerges ON emerges.PlotLineID = pl.EmergesFromPlotLineID + WHERE pl.ProjectID = @ProjectID + ORDER BY pl.SortOrder, pl.PlotLineName; + + SELECT pt.PlotThreadID, pt.PlotLineID, pl.ProjectID, pl.PlotLineName, pt.ThreadTitle, pt.ThreadTypeID, + tt.TypeName AS ThreadTypeName, pt.ThreadStatusID, ts.StatusName AS ThreadStatusName, pt.Importance, + pt.Summary, pt.IntroducedSceneID, pt.PlannedResolutionSceneID, pt.ActualResolutionSceneID, + pt.CreatedDate, pt.UpdatedDate, pt.IsArchived, pt.ArchivedDate, pt.ArchivedReason + FROM dbo.PlotThreads pt + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadTypes tt ON tt.ThreadTypeID = pt.ThreadTypeID + INNER JOIN dbo.ThreadStatuses ts ON ts.ThreadStatusID = pt.ThreadStatusID + WHERE pl.ProjectID = @ProjectID + ORDER BY pl.SortOrder, pt.Importance DESC, pt.ThreadTitle; + + SELECT te.ThreadEventID, te.PlotThreadID, pt.ThreadTitle, pl.PlotLineID, pl.PlotLineName, pl.Colour, + te.SceneID, te.EventTypeID, tet.TypeName AS EventTypeName, tet.MarkerText, + te.PlotEventTypeID, pet.TypeName AS PlotEventTypeName, te.TargetPlotLineID, target.PlotLineName AS TargetPlotLineName, + te.EventTitle, te.EventDescription, te.CreatedDate, te.UpdatedDate + FROM dbo.ThreadEvents te + INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID + INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID + INNER JOIN dbo.ThreadEventTypes tet ON tet.ThreadEventTypeID = te.EventTypeID + INNER JOIN dbo.PlotEventTypes pet ON pet.PlotEventTypeID = te.PlotEventTypeID + LEFT JOIN dbo.PlotLines target ON target.PlotLineID = te.TargetPlotLineID + WHERE pl.ProjectID = @ProjectID + ORDER BY te.SceneID, te.ThreadEventID; + + SELECT sa.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, sa.Description, + sa.Importance, sa.CurrentStateID, ast.StateName AS CurrentStateName, sa.CurrentLocationID, + l.LocationName AS CurrentLocationName, l.LocationName AS CurrentLocationPath, sa.IsResolved, + sa.ShowInQuickAddBar, sa.CreatedDate, sa.UpdatedDate, sa.IsArchived, sa.ArchivedDate, sa.ArchivedReason + 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 dbo.Locations l ON l.LocationID = sa.CurrentLocationID + WHERE sa.ProjectID = @ProjectID + ORDER BY sa.Importance DESC, sa.AssetName; + + SELECT ast.AssetStateID, ast.ProjectID, ast.AssetKindID, ak.KindName, ast.StateName, ast.SortOrder, ast.Description, + ast.IsResolvedState, ast.IsActive + FROM dbo.AssetStates ast + LEFT JOIN dbo.AssetKinds ak ON ak.AssetKindID = ast.AssetKindID + WHERE ast.ProjectID IS NULL + OR ast.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM dbo.StoryAssets sa WHERE sa.ProjectID = @ProjectID AND sa.CurrentStateID = ast.AssetStateID) + OR EXISTS (SELECT 1 FROM dbo.AssetEvents ae INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID WHERE sa.ProjectID = @ProjectID AND (ae.FromStateID = ast.AssetStateID OR ae.ToStateID = ast.AssetStateID)) + OR EXISTS (SELECT 1 FROM dbo.AssetDependencies ad INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ad.SourceAssetID WHERE sa.ProjectID = @ProjectID AND (ad.SourceRequiredStateID = ast.AssetStateID OR ad.TargetBlockedStateID = ast.AssetStateID)) + ORDER BY ast.SortOrder, ast.StateName; + + SELECT ae.AssetEventID, ae.StoryAssetID, sa.ProjectID, sa.AssetName, sa.AssetKindID, ak.KindName, + ae.SceneID, ae.AssetEventTypeID, aet.TypeName AS AssetEventTypeName, aet.MarkerText, + ae.FromStateID, fs.StateName AS FromStateName, ae.ToStateID, ts.StateName AS ToStateName, + ae.EventTitle, ae.EventDescription, b.BookTitle, c.ChapterNumber, s.SceneNumber, s.SceneTitle, + ae.CreatedDate, ae.UpdatedDate + FROM dbo.AssetEvents ae + INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID + INNER JOIN dbo.AssetKinds ak ON ak.AssetKindID = sa.AssetKindID + INNER JOIN dbo.AssetEventTypes aet ON aet.AssetEventTypeID = ae.AssetEventTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = ae.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.AssetStates fs ON fs.AssetStateID = ae.FromStateID + LEFT JOIN dbo.AssetStates ts ON ts.AssetStateID = ae.ToStateID + WHERE sa.ProjectID = @ProjectID + ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, ae.AssetEventID; + + SELECT ad.AssetDependencyID, ad.SourceAssetID, source.AssetName AS SourceAssetName, ad.TargetAssetID, + target.AssetName AS TargetAssetName, ad.AssetDependencyTypeID, adt.TypeName AS DependencyTypeName, + adt.IsValidatingDependency, ad.SourceRequiredStateID, srs.StateName AS SourceRequiredStateName, + ad.TargetBlockedStateID, tbs.StateName AS TargetBlockedStateName, ad.Description, + ad.CreatedDate, ad.UpdatedDate, ad.IsArchived + FROM dbo.AssetDependencies ad + INNER JOIN dbo.StoryAssets source ON source.StoryAssetID = ad.SourceAssetID + INNER JOIN dbo.StoryAssets target ON target.StoryAssetID = ad.TargetAssetID + INNER JOIN dbo.AssetDependencyTypes adt ON adt.AssetDependencyTypeID = ad.AssetDependencyTypeID + LEFT JOIN dbo.AssetStates srs ON srs.AssetStateID = ad.SourceRequiredStateID + LEFT JOIN dbo.AssetStates tbs ON tbs.AssetStateID = ad.TargetBlockedStateID + WHERE source.ProjectID = @ProjectID OR target.ProjectID = @ProjectID + ORDER BY source.AssetName, target.AssetName; + + SELECT ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, + acet.TypeName AS CustodyEventTypeName, ace.Description, + STRING_AGG(COALESCE(ch.CharacterName, acec.CharacterNameText), ', ') AS CustodianSummary, + b.BookTitle, c.ChapterNumber, s.SceneNumber, s.SceneTitle, ace.CreatedDate, ace.UpdatedDate + FROM dbo.AssetCustodyEvents ace + INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID + INNER JOIN dbo.AssetCustodyEventTypes acet ON acet.AssetCustodyEventTypeID = ace.AssetCustodyEventTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = ace.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.AssetCustodyEventCharacters acec ON acec.AssetCustodyEventID = ace.AssetCustodyEventID + LEFT JOIN dbo.Characters ch ON ch.CharacterID = acec.CharacterID + WHERE sa.ProjectID = @ProjectID + GROUP BY ace.AssetCustodyEventID, ace.StoryAssetID, sa.AssetName, ace.SceneID, ace.AssetCustodyEventTypeID, + acet.TypeName, ace.Description, b.BookTitle, c.ChapterNumber, s.SceneNumber, s.SceneTitle, ace.CreatedDate, ace.UpdatedDate + ORDER BY MIN(b.SortOrder), MIN(c.SortOrder), MIN(s.SortOrder), ace.AssetCustodyEventID; + + SELECT acec.AssetCustodyEventCharacterID, acec.AssetCustodyEventID, acec.CharacterID, ch.CharacterName, + acec.CharacterNameText, acec.CustodyRoleID, cr.RoleName AS CustodyRoleName + FROM dbo.AssetCustodyEventCharacters acec + INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID + INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID + INNER JOIN dbo.CustodyRoles cr ON cr.CustodyRoleID = acec.CustodyRoleID + LEFT JOIN dbo.Characters ch ON ch.CharacterID = acec.CharacterID + WHERE sa.ProjectID = @ProjectID + ORDER BY acec.AssetCustodyEventID, acec.AssetCustodyEventCharacterID; + + SELECT CharacterID, ProjectID, CharacterName, ShortName, Sex, BirthDate, AgeAtSeriesStart, + AgeReferenceSceneID, Height, EyeColour, CharacterImportance, ShowInQuickAddBar, + DefaultDescription, CreatedDate, UpdatedDate, IsArchived, ArchivedDate, ArchivedReason + FROM dbo.Characters + WHERE ProjectID = @ProjectID + ORDER BY CharacterImportance DESC, CharacterName; + + SELECT sc.SceneCharacterID, sc.SceneID, sc.CharacterID, ch.ProjectID, ch.CharacterName, ch.ShortName, + ch.BirthDate, ch.AgeAtSeriesStart, sc.RoleInSceneTypeID, rist.TypeName AS RoleInSceneTypeName, + sc.PresenceTypeID, pt.TypeName AS PresenceTypeName, sc.LocationID, loc.LocationName, + loc.LocationName AS LocationPath, sc.EntryLocationID, entryLoc.LocationName AS EntryLocationName, + sc.ExitLocationID, exitLoc.LocationName AS ExitLocationName, sc.AppearanceNotes, sc.OutfitDescription, + sc.PhysicalCondition, sc.EmotionalState, sc.KnowledgeNotes, s.SceneNumber, s.SceneTitle, + c.ChapterNumber, b.BookTitle, sc.CreatedDate, sc.UpdatedDate + FROM dbo.SceneCharacters sc + INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID + INNER JOIN dbo.Scenes s ON s.SceneID = sc.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.CharacterRoleInSceneTypes rist ON rist.CharacterRoleInSceneTypeID = sc.RoleInSceneTypeID + LEFT JOIN dbo.PresenceTypes pt ON pt.PresenceTypeID = sc.PresenceTypeID + LEFT JOIN dbo.Locations loc ON loc.LocationID = sc.LocationID + LEFT JOIN dbo.Locations entryLoc ON entryLoc.LocationID = sc.EntryLocationID + LEFT JOIN dbo.Locations exitLoc ON exitLoc.LocationID = sc.ExitLocationID + WHERE ch.ProjectID = @ProjectID + ORDER BY b.SortOrder, c.SortOrder, s.SortOrder, ch.CharacterName; + + SELECT cat.CharacterAttributeTypeID, cat.ProjectID, cat.AttributeName, cat.DataType, cat.IsNormallyStable, + cat.SortOrder, cat.IsActive + FROM dbo.CharacterAttributeTypes cat + WHERE cat.ProjectID IS NULL + OR cat.ProjectID = @ProjectID + OR EXISTS (SELECT 1 FROM dbo.CharacterAttributeEvents cae INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID WHERE ch.ProjectID = @ProjectID AND cae.CharacterAttributeTypeID = cat.CharacterAttributeTypeID) + ORDER BY cat.SortOrder, cat.AttributeName; + + SELECT cae.CharacterAttributeEventID, cae.CharacterID, ch.CharacterName, cae.CharacterAttributeTypeID, + cat.AttributeName, cae.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle, + cae.AttributeValue, cae.Description, cae.CreatedDate, cae.UpdatedDate + FROM dbo.CharacterAttributeEvents cae + INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID + INNER JOIN dbo.CharacterAttributeTypes cat ON cat.CharacterAttributeTypeID = cae.CharacterAttributeTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = cae.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE ch.ProjectID = @ProjectID + ORDER BY ch.CharacterName, b.SortOrder, c.SortOrder, s.SortOrder; + + SELECT ck.CharacterKnowledgeID, ck.CharacterID, ch.CharacterName, ck.StoryAssetID, sa.AssetName, + ck.PlotThreadID, pt.ThreadTitle, ck.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, b.BookTitle, + ck.KnowledgeStateID, ks.StateName AS KnowledgeStateName, ck.SourceEventID, ck.Description, + ck.CreatedDate, ck.UpdatedDate + FROM dbo.CharacterKnowledge ck + INNER JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID + INNER JOIN dbo.KnowledgeStates ks ON ks.KnowledgeStateID = ck.KnowledgeStateID + INNER JOIN dbo.Scenes s ON s.SceneID = ck.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + LEFT JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ck.StoryAssetID + LEFT JOIN dbo.PlotThreads pt ON pt.PlotThreadID = ck.PlotThreadID + WHERE ch.ProjectID = @ProjectID + ORDER BY ch.CharacterName, b.SortOrder, c.SortOrder, s.SortOrder; + + SELECT cr.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, a.CharacterName AS CharacterAName, + cr.CharacterBID, bch.CharacterName AS CharacterBName, cr.RelationshipTypeID, rt.TypeName AS RelationshipTypeName, + rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, rc.SortOrder AS CategorySortOrder, + rt.SortOrder AS RelationshipTypeSortOrder, cr.IsPermanent, cr.StartSceneID, cr.EndSceneID, + cr.IsKnownToReader, cr.Notes, cr.IsInitialRelationship, cr.InitialBookID, ib.BookTitle AS InitialBookTitle, + cr.ReaderInitiallyKnows, cr.InitialRelationshipStateID, irs.StateName AS InitialRelationshipStateName, + cr.InitialIntensity, cr.IsReciprocal, cr.CreatedDate, cr.UpdatedDate, + cr.IsArchived, cr.ArchivedDate, cr.ArchivedReason + FROM dbo.CharacterRelationships cr + INNER JOIN dbo.Characters a ON a.CharacterID = cr.CharacterAID + INNER JOIN dbo.Characters bch ON bch.CharacterID = cr.CharacterBID + INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID + INNER JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID + LEFT JOIN dbo.Books ib ON ib.BookID = cr.InitialBookID + LEFT JOIN dbo.RelationshipStates irs ON irs.RelationshipStateID = cr.InitialRelationshipStateID + WHERE cr.ProjectID = @ProjectID + ORDER BY a.CharacterName, bch.CharacterName, cr.CharacterRelationshipID; + + SELECT re.RelationshipEventID, re.CharacterRelationshipID, cr.ProjectID, cr.CharacterAID, + a.CharacterName AS CharacterAName, cr.CharacterBID, bch.CharacterName AS CharacterBName, + rt.TypeName AS RelationshipTypeName, rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, + rc.SortOrder AS CategorySortOrder, rt.SortOrder AS RelationshipTypeSortOrder, cr.IsReciprocal, + re.SceneID, s.SceneNumber, s.SceneTitle, c.ChapterNumber, bk.BookTitle, re.RelationshipStateID, + rs.StateName AS RelationshipStateName, re.Intensity, re.IsKnownToOtherCharacter, re.Description, + re.CreatedDate, re.UpdatedDate + FROM dbo.RelationshipEvents re + INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID + INNER JOIN dbo.Characters a ON a.CharacterID = cr.CharacterAID + INNER JOIN dbo.Characters bch ON bch.CharacterID = cr.CharacterBID + INNER JOIN dbo.RelationshipTypes rt ON rt.RelationshipTypeID = cr.RelationshipTypeID + INNER JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID + INNER JOIN dbo.RelationshipStates rs ON rs.RelationshipStateID = re.RelationshipStateID + INNER JOIN dbo.Scenes s ON s.SceneID = re.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books bk ON bk.BookID = c.BookID + WHERE cr.ProjectID = @ProjectID + ORDER BY bk.SortOrder, c.SortOrder, s.SortOrder, re.RelationshipEventID; + + SELECT l.LocationID, l.ProjectID, l.ParentLocationID, parent.LocationName AS ParentLocationName, + l.LocationName, l.LocationTypeID, lt.TypeName AS LocationTypeName, l.Description, + l.LocationName AS LocationPath, CAST(0 AS int) AS Depth, l.ShowInQuickAddBar, + l.CreatedDate, l.UpdatedDate, l.IsArchived, l.ArchivedDate, l.ArchivedReason + FROM dbo.Locations l + LEFT JOIN dbo.Locations parent ON parent.LocationID = l.ParentLocationID + LEFT JOIN dbo.LocationTypes lt ON lt.LocationTypeID = l.LocationTypeID + WHERE l.ProjectID = @ProjectID + ORDER BY l.LocationName; + + SELECT lr.LocationRelationshipID, lr.FromLocationID, fl.LocationName AS FromLocationName, + lr.ToLocationID, tl.LocationName AS ToLocationName, lr.LocationRelationshipTypeID, + lrt.TypeName AS RelationshipTypeName, lr.IsBidirectional, lr.Strength, + lr.CanHearNormalSpeech, lr.CanHearLoudNoise, lr.CanSeeMovement, lr.CanSeeClearly, + lr.CanTravelDirectly, lr.Notes, lr.CreatedDate, lr.UpdatedDate, lr.IsArchived + FROM dbo.LocationRelationships lr + INNER JOIN dbo.Locations fl ON fl.LocationID = lr.FromLocationID + INNER JOIN dbo.Locations tl ON tl.LocationID = lr.ToLocationID + INNER JOIN dbo.LocationRelationshipTypes lrt ON lrt.LocationRelationshipTypeID = lr.LocationRelationshipTypeID + WHERE fl.ProjectID = @ProjectID OR tl.ProjectID = @ProjectID + ORDER BY fl.LocationName, tl.LocationName; + + SELECT sal.SceneAssetLocationID, sal.SceneID, sal.StoryAssetID, sa.AssetName, sal.LocationID, + l.LocationName, l.LocationName AS LocationPath, sal.Description, sal.CreatedDate, sal.UpdatedDate + FROM dbo.SceneAssetLocations sal + INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID + INNER JOIN dbo.Locations l ON l.LocationID = sal.LocationID + WHERE sa.ProjectID = @ProjectID + ORDER BY sal.SceneID, sa.AssetName; + + SELECT sd.SceneDependencyID, ISNULL(sd.ProjectID, @ProjectID) AS ProjectID, sd.SourceSceneID, sd.TargetSceneID, + sd.SceneDependencyTypeID, sdt.TypeName AS SceneDependencyTypeName, sd.Description, + sd.IsHardDependency, CAST(NULL AS nvarchar(50)) AS DependencyDirection, sb.BookID AS SourceBookID, sb.BookTitle AS SourceBookTitle, + sc.ChapterID AS SourceChapterID, sc.ChapterNumber AS SourceChapterNumber, ss.SceneNumber AS SourceSceneNumber, + ss.SceneTitle AS SourceSceneTitle, tb.BookID AS TargetBookID, tb.BookTitle AS TargetBookTitle, + tc.ChapterID AS TargetChapterID, tc.ChapterNumber AS TargetChapterNumber, ts.SceneNumber AS TargetSceneNumber, + ts.SceneTitle AS TargetSceneTitle, sd.CreatedDate, sd.UpdatedDate, sd.IsArchived + FROM dbo.SceneDependencies sd + INNER JOIN dbo.SceneDependencyTypes sdt ON sdt.SceneDependencyTypeID = sd.SceneDependencyTypeID + INNER JOIN dbo.Scenes ss ON ss.SceneID = sd.SourceSceneID + INNER JOIN dbo.Chapters sc ON sc.ChapterID = ss.ChapterID + INNER JOIN dbo.Books sb ON sb.BookID = sc.BookID + INNER JOIN dbo.Scenes ts ON ts.SceneID = sd.TargetSceneID + INNER JOIN dbo.Chapters tc ON tc.ChapterID = ts.ChapterID + INNER JOIN dbo.Books tb ON tb.BookID = tc.BookID + WHERE sd.ProjectID = @ProjectID OR sb.ProjectID = @ProjectID OR tb.ProjectID = @ProjectID + ORDER BY sb.SortOrder, sc.SortOrder, ss.SortOrder, sd.SceneDependencyID; + + SELECT ValidationRunID, ProjectID, BookID, StartedDate, CompletedDate, TriggeredBy, ScopeDescription + FROM dbo.ValidationRuns + WHERE ProjectID = @ProjectID + ORDER BY StartedDate DESC; + + SELECT cw.ContinuityWarningID, cw.ValidationRunID, cw.ProjectID, cw.BookID, b.BookTitle, 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 + ORDER BY cw.LastDetectedDate DESC, cw.ContinuityWarningID; + + SELECT sn.SceneNoteID, sn.SceneID, sn.SceneNoteTypeID, snt.TypeName, sn.NoteTitle, sn.NoteText, + sn.SortOrder, sn.IsPinned, sn.IsResolved, sn.CreatedDate, sn.UpdatedDate + FROM dbo.SceneNotes sn + INNER JOIN dbo.SceneNoteTypes snt ON snt.SceneNoteTypeID = sn.SceneNoteTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = sn.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY sn.SceneID, sn.SortOrder, sn.SceneNoteID; + + SELECT sw.SceneWorkflowID, sw.SceneID, sw.DraftStatus, sw.EstimatedWordCount, sw.ActualWordCount, + sw.DraftedDate, sw.LastWorkedOn, sw.ReadyForDraft, sw.ReadyForRevision, sw.ReadyForPolish, + sw.IsBlocked, sw.BlockedReason, sw.Priority, sw.CreatedDate, sw.UpdatedDate + FROM dbo.SceneWorkflow sw + INNER JOIN dbo.Scenes s ON s.SceneID = sw.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY sw.SceneID; + + SELECT sci.SceneChecklistItemID, sci.SceneID, sci.ItemText, sci.IsCompleted, sci.SortOrder, + sci.CreatedDate, sci.UpdatedDate + FROM dbo.SceneChecklistItems sci + INNER JOIN dbo.Scenes s ON s.SceneID = sci.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY sci.SceneID, sci.SortOrder, sci.SceneChecklistItemID; + + SELECT sa.SceneAttachmentID, sa.SceneID, sa.SceneAttachmentTypeID, sat.TypeName, sa.Title, + sa.FilePath, sa.ExternalUrl, sa.Notes, sa.CreatedDate, sa.UpdatedDate + FROM dbo.SceneAttachments sa + INNER JOIN dbo.SceneAttachmentTypes sat ON sat.SceneAttachmentTypeID = sa.SceneAttachmentTypeID + INNER JOIN dbo.Scenes s ON s.SceneID = sa.SceneID + INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY sa.SceneID, sa.SceneAttachmentID; + + SELECT cg.ChapterGoalID, cg.ChapterID, cg.GoalSummary, cg.EmotionalGoal, cg.ReaderTakeaway, + cg.MustInclude, cg.RisksOrConcerns, cg.CreatedDate, cg.UpdatedDate + FROM dbo.ChapterGoals cg + INNER JOIN dbo.Chapters c ON c.ChapterID = cg.ChapterID + INNER JOIN dbo.Books b ON b.BookID = c.BookID + WHERE b.ProjectID = @ProjectID + ORDER BY b.SortOrder, c.SortOrder; + + SELECT sc.ScenarioID, sc.ProjectID, sc.BookID, sc.ScenarioName, sc.Description, sc.BaseCreatedDate, + sc.IsAppliedToMain, sc.IsArchived, sc.ArchivedDate, sc.ArchivedReason, + sc.CreatedDate, sc.UpdatedDate, p.ProjectName, b.BookTitle, + (SELECT COUNT(1) FROM dbo.ScenarioSceneOrder sso WHERE sso.ScenarioID = sc.ScenarioID) AS SceneCount, + (SELECT COUNT(1) FROM dbo.ScenarioWarnings sw WHERE sw.ScenarioID = sc.ScenarioID) AS WarningCount + FROM dbo.Scenarios sc + INNER JOIN dbo.Projects p ON p.ProjectID = sc.ProjectID + LEFT JOIN dbo.Books b ON b.BookID = sc.BookID + WHERE sc.ProjectID = @ProjectID + ORDER BY sc.IsArchived, sc.UpdatedDate DESC; + + SELECT sso.ScenarioSceneOrderID, sso.ScenarioID, sso.SceneID, sso.ProposedChapterID, sso.ProposedSortOrder, + sso.Notes, sso.CreatedDate, sso.UpdatedDate + FROM dbo.ScenarioSceneOrder sso + INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sso.ScenarioID + WHERE sc.ProjectID = @ProjectID + ORDER BY sso.ScenarioID, sso.ProposedSortOrder; + + SELECT svr.ScenarioValidationRunID, svr.ScenarioID, svr.StartedDate, svr.CompletedDate, svr.WarningCount + FROM dbo.ScenarioValidationRuns svr + INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = svr.ScenarioID + WHERE sc.ProjectID = @ProjectID + ORDER BY svr.StartedDate DESC; + + SELECT sw.ScenarioWarningID, sw.ScenarioValidationRunID, sw.ScenarioID, sw.SceneID, sw.EntityType, + sw.EntityID, sw.WarningTypeName, sw.SeverityName, sw.Message, sw.Details, sw.CreatedDate + FROM dbo.ScenarioWarnings sw + INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sw.ScenarioID + WHERE sc.ProjectID = @ProjectID + ORDER BY sw.CreatedDate DESC, sw.ScenarioWarningID; + + SELECT GenreMetricPresetID, PresetKey, PresetName, Description, SortOrder, IsActive + FROM dbo.GenreMetricPresets + ORDER BY SortOrder, PresetName; + + SELECT gpmm.GenreMetricPresetID, gmp.PresetKey, gmp.PresetName, + gpmm.MetricTypeID, mt.MetricName, gpmm.SortOrder + FROM dbo.GenreMetricPresetMetrics gpmm + INNER JOIN dbo.GenreMetricPresets gmp ON gmp.GenreMetricPresetID = gpmm.GenreMetricPresetID + INNER JOIN dbo.SceneMetricTypes mt ON mt.MetricTypeID = gpmm.MetricTypeID + WHERE EXISTS + ( + SELECT 1 + FROM dbo.SceneMetricTypes projectMetric + WHERE projectMetric.MetricTypeID = gpmm.MetricTypeID + AND + ( + projectMetric.ProjectID IS NULL + OR projectMetric.ProjectID = @ProjectID + OR EXISTS + ( + SELECT 1 + FROM dbo.ProjectTimelineMetricSettings ptms + WHERE ptms.ProjectID = @ProjectID + AND ptms.MetricTypeID = projectMetric.MetricTypeID + ) + ) + ) + ORDER BY gmp.SortOrder, gpmm.SortOrder, mt.MetricName; + + SELECT pua.ProjectUserAccessID, pua.ProjectID, pua.UserID, u.Email, u.DisplayName, + pua.AccessRole, pua.InvitedByUserID, invitedBy.Email AS InvitedByEmail, + pua.InvitedDateUTC, pua.AcceptedDateUTC, pua.IsActive + FROM dbo.ProjectUserAccess pua + INNER JOIN dbo.AppUser u ON u.UserID = pua.UserID + LEFT JOIN dbo.AppUser invitedBy ON invitedBy.UserID = pua.InvitedByUserID + WHERE pua.ProjectID = @ProjectID + AND pua.IsActive = 1 + ORDER BY CASE WHEN pua.AccessRole = N'Owner' THEN 0 ELSE 1 END, u.Email; + + SELECT UserFileID, UserID, ProjectID, EntityType, EntityID, FileName, OriginalFileName, ContentType, + FileSizeBytes, StoragePath, UploadedDateUTC, DeletedDateUTC, IsDeleted + FROM dbo.UserFile + WHERE ProjectID = @ProjectID + ORDER BY IsDeleted, UploadedDateUTC, UserFileID; +END; +GO + diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index f5ecf61..6eb537d 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -704,26 +704,30 @@ public sealed class PlotLineEditViewModel [Display(Name = "Type")] public int PlotLineTypeID { get; set; } + [Display(Name = "Importance")] + public int PlotImportanceID { get; set; } = 2; + public int? BookID { get; set; } public string? Description { get; set; } [Display(Name = "Parent plot line")] public int? ParentPlotLineID { get; set; } + [Display(Name = "Emerges from plot line")] + public int? EmergesFromPlotLineID { get; set; } + [Display(Name = "Sort order")] public int SortOrder { get; set; } [Display(Name = "Colour")] public string Colour { get; set; } = "#2f6f63"; - [Display(Name = "Main plot")] - public bool IsMainPlot { get; set; } - [Display(Name = "Visible on timeline")] public bool IsVisibleOnTimeline { get; set; } = true; public Project? Project { get; set; } public IReadOnlyList PlotLineTypes { get; set; } = []; + public IReadOnlyList PlotImportance { get; set; } = []; public IReadOnlyList BookOptions { get; set; } = []; public IReadOnlyList ParentPlotLineOptions { get; set; } = []; } diff --git a/PlotLine/Views/PlotLines/Edit.cshtml b/PlotLine/Views/PlotLines/Edit.cshtml index ca2f56f..98edcac 100644 --- a/PlotLine/Views/PlotLines/Edit.cshtml +++ b/PlotLine/Views/PlotLines/Edit.cshtml @@ -48,6 +48,10 @@ +
+ + +
+
+ + +
@@ -72,12 +82,6 @@
-
- -