diff --git a/.appdata/SqlRunner/Program.cs b/.appdata/SqlRunner/Program.cs index e74eb81..a4e44ad 100644 --- a/.appdata/SqlRunner/Program.cs +++ b/.appdata/SqlRunner/Program.cs @@ -4,7 +4,15 @@ using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; var connectionString = "Server=localhost;Database=PlotLine;User Id=PlotLineApp;Password=t6eb1cvd9bNB27czde#h9r;MultipleActiveResultSets=true;Encrypt=false;"; -var scriptPath = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "PlotLine", "Sql", "026_TimelineConfigurationSettings.sql")); +var scriptPath = args.Length > 0 + ? Path.GetFullPath(args[0]) + : throw new ArgumentException("Pass the SQL script path as the first argument."); + +if (args.Length > 1 && !string.IsNullOrWhiteSpace(args[1])) +{ + connectionString = args[1]; +} + var script = await File.ReadAllTextAsync(scriptPath); var batches = Regex.Split(script, @"^\s*GO\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase); diff --git a/.appdata/SqlRunner/SqlRunner.csproj b/.appdata/SqlRunner/SqlRunner.csproj index d59d872..e74bdaa 100644 --- a/.appdata/SqlRunner/SqlRunner.csproj +++ b/.appdata/SqlRunner/SqlRunner.csproj @@ -8,7 +8,10 @@ - + + bin\Debug\net10.0\Microsoft.Data.SqlClient.dll + true + diff --git a/PlotLine/Controllers/ExportsController.cs b/PlotLine/Controllers/ExportsController.cs index 7cb4b82..f312824 100644 --- a/PlotLine/Controllers/ExportsController.cs +++ b/PlotLine/Controllers/ExportsController.cs @@ -3,7 +3,7 @@ using PlotLine.Services; namespace PlotLine.Controllers; -public sealed class ExportsController(IExportService exports) : Controller +public sealed class ExportsController(IExportService exports, IProjectExportService projectExports, ILogger logger) : Controller { public async Task StoryBible(int projectId, string format = "html") => ExportResult(await exports.ExportStoryBibleAsync(projectId, format), format); @@ -29,6 +29,27 @@ public sealed class ExportsController(IExportService exports) : Controller public async Task Search(int projectId, string q, string format = "html") => ExportResult(await exports.ExportSearchResultsAsync(projectId, q, format), format); + public async Task ProjectBackup(int projectId) + { + try + { + var export = await projectExports.ExportAsync(projectId); + if (export is null) + { + TempData["ProjectExportMessage"] = "Project backup could not be created because the project was not found."; + return RedirectToAction("Index", "Projects"); + } + + return File(System.Text.Encoding.UTF8.GetBytes(export.Json), export.ContentType, export.FileName); + } + catch (Exception ex) + { + logger.LogError(ex, "Project backup export failed for project {ProjectID}.", projectId); + TempData["ProjectExportMessage"] = "Project backup could not be created. Please try again, or check the application logs if the problem continues."; + return RedirectToAction("Details", "Projects", new { id = projectId }); + } + } + private IActionResult ExportResult((string FileName, string ContentType, string Content) export, string format) { if (string.Equals(format, "html", StringComparison.OrdinalIgnoreCase)) diff --git a/PlotLine/Data/ProjectBackupRepository.cs b/PlotLine/Data/ProjectBackupRepository.cs new file mode 100644 index 0000000..7c894cc --- /dev/null +++ b/PlotLine/Data/ProjectBackupRepository.cs @@ -0,0 +1,129 @@ +using System.Data; +using Dapper; +using PlotLine.Models; + +namespace PlotLine.Data; + +public interface IProjectBackupRepository +{ + Task GetProjectBackupAsync(int projectId); +} + +public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFactory) : IProjectBackupRepository +{ + public async Task GetProjectBackupAsync(int projectId) + { + using var connection = connectionFactory.CreateConnection(); + using var result = await connection.QueryMultipleAsync( + "dbo.ProjectBackup_Export", + new { ProjectID = projectId }, + commandType: CommandType.StoredProcedure); + + var project = await result.ReadSingleOrDefaultAsync(); + var data = new ProjectBackupData + { + Books = (await result.ReadAsync()).ToList(), + Chapters = (await result.ReadAsync()).ToList(), + Scenes = (await result.ReadAsync()).ToList(), + ScenePurposes = (await result.ReadAsync()).ToList(), + SceneOutcomes = (await result.ReadAsync()).ToList(), + SceneMetricTypes = (await result.ReadAsync()).ToList(), + SceneMetricValues = (await result.ReadAsync()).ToList(), + TimelineSettings = (await result.ReadAsync()).ToList(), + TimelineMetricSettings = (await result.ReadAsync()).ToList(), + TimelineViewPresets = (await result.ReadAsync()).ToList(), + PlotLines = (await result.ReadAsync()).ToList(), + PlotThreads = (await result.ReadAsync()).ToList(), + ThreadEvents = (await result.ReadAsync()).ToList(), + StoryAssets = (await result.ReadAsync()).ToList(), + AssetStates = (await result.ReadAsync()).ToList(), + AssetEvents = (await result.ReadAsync()).ToList(), + AssetDependencies = (await result.ReadAsync()).ToList(), + AssetCustodyEvents = (await result.ReadAsync()).ToList(), + AssetCustodyCharacters = (await result.ReadAsync()).ToList(), + Characters = (await result.ReadAsync()).ToList(), + SceneCharacters = (await result.ReadAsync()).ToList(), + CharacterAttributeTypes = (await result.ReadAsync()).ToList(), + CharacterAttributeEvents = (await result.ReadAsync()).ToList(), + CharacterKnowledge = (await result.ReadAsync()).ToList(), + CharacterRelationships = (await result.ReadAsync()).ToList(), + RelationshipEvents = (await result.ReadAsync()).ToList(), + Locations = (await result.ReadAsync()).ToList(), + LocationRelationships = (await result.ReadAsync()).ToList(), + SceneAssetLocations = (await result.ReadAsync()).ToList(), + SceneDependencies = (await result.ReadAsync()).ToList(), + ValidationRuns = (await result.ReadAsync()).ToList(), + ContinuityWarnings = (await result.ReadAsync()).ToList(), + SceneNotes = (await result.ReadAsync()).ToList(), + SceneWorkflow = (await result.ReadAsync()).ToList(), + SceneChecklistItems = (await result.ReadAsync()).ToList(), + SceneAttachments = (await result.ReadAsync()).ToList(), + ChapterGoals = (await result.ReadAsync()).ToList(), + Scenarios = (await result.ReadAsync()).ToList(), + ScenarioSceneOrder = (await result.ReadAsync()).ToList(), + ScenarioValidationRuns = (await result.ReadAsync()).ToList(), + ScenarioWarnings = (await result.ReadAsync()).ToList() + }; + + if (project is null) + { + return null; + } + + return new ProjectBackupExport + { + ExportedAtUtc = DateTime.UtcNow, + Project = project, + Data = data, + Diagnostics = new ProjectBackupDiagnostics + { + ProjectId = projectId, + IncludedEntitySets = + [ + nameof(data.Books), + nameof(data.Chapters), + nameof(data.Scenes), + nameof(data.ScenePurposes), + nameof(data.SceneOutcomes), + nameof(data.SceneMetricTypes), + nameof(data.SceneMetricValues), + nameof(data.TimelineSettings), + nameof(data.TimelineMetricSettings), + nameof(data.TimelineViewPresets), + nameof(data.PlotLines), + nameof(data.PlotThreads), + nameof(data.ThreadEvents), + nameof(data.StoryAssets), + nameof(data.AssetStates), + nameof(data.AssetEvents), + nameof(data.AssetDependencies), + nameof(data.AssetCustodyEvents), + nameof(data.AssetCustodyCharacters), + nameof(data.Characters), + nameof(data.SceneCharacters), + nameof(data.CharacterAttributeTypes), + nameof(data.CharacterAttributeEvents), + nameof(data.CharacterKnowledge), + nameof(data.CharacterRelationships), + nameof(data.RelationshipEvents), + nameof(data.Locations), + nameof(data.LocationRelationships), + nameof(data.SceneAssetLocations), + nameof(data.SceneDependencies), + nameof(data.ValidationRuns), + nameof(data.ContinuityWarnings), + nameof(data.SceneNotes), + nameof(data.SceneWorkflow), + nameof(data.SceneChecklistItems), + nameof(data.SceneAttachments), + nameof(data.ChapterGoals), + nameof(data.Scenarios), + nameof(data.ScenarioSceneOrder), + nameof(data.ScenarioValidationRuns), + nameof(data.ScenarioWarnings) + ], + Note = "Original database IDs are included for diagnostics. Future import/restore should map by logical project structure and names where practical." + } + }; + } +} diff --git a/PlotLine/Models/ProjectBackupModels.cs b/PlotLine/Models/ProjectBackupModels.cs new file mode 100644 index 0000000..86bdae8 --- /dev/null +++ b/PlotLine/Models/ProjectBackupModels.cs @@ -0,0 +1,132 @@ +namespace PlotLine.Models; + +public sealed class ProjectBackupExport +{ + public string Format { get; init; } = "PlotWeaver.ProjectBackup"; + public string FormatVersion { get; init; } = "1.0"; + public DateTime ExportedAtUtc { get; init; } + public string SourceApplication { get; init; } = "PlotWeaver Studio"; + public Project? Project { get; init; } + public ProjectBackupData Data { get; init; } = new(); + public ProjectBackupDiagnostics Diagnostics { get; init; } = new(); +} + +public sealed class ProjectBackupData +{ + public IReadOnlyList Books { get; init; } = []; + public IReadOnlyList Chapters { get; init; } = []; + public IReadOnlyList Scenes { get; init; } = []; + public IReadOnlyList ScenePurposes { get; init; } = []; + public IReadOnlyList SceneOutcomes { get; init; } = []; + public IReadOnlyList SceneMetricTypes { get; init; } = []; + public IReadOnlyList SceneMetricValues { get; init; } = []; + public IReadOnlyList TimelineSettings { get; init; } = []; + public IReadOnlyList TimelineMetricSettings { get; init; } = []; + public IReadOnlyList TimelineViewPresets { get; init; } = []; + public IReadOnlyList PlotLines { get; init; } = []; + public IReadOnlyList PlotThreads { get; init; } = []; + public IReadOnlyList ThreadEvents { get; init; } = []; + public IReadOnlyList StoryAssets { get; init; } = []; + public IReadOnlyList AssetStates { get; init; } = []; + public IReadOnlyList AssetEvents { get; init; } = []; + public IReadOnlyList AssetDependencies { get; init; } = []; + public IReadOnlyList AssetCustodyEvents { get; init; } = []; + public IReadOnlyList AssetCustodyCharacters { get; init; } = []; + public IReadOnlyList Characters { get; init; } = []; + public IReadOnlyList SceneCharacters { get; init; } = []; + public IReadOnlyList CharacterAttributeTypes { get; init; } = []; + public IReadOnlyList CharacterAttributeEvents { get; init; } = []; + public IReadOnlyList CharacterKnowledge { get; init; } = []; + public IReadOnlyList CharacterRelationships { get; init; } = []; + public IReadOnlyList RelationshipEvents { get; init; } = []; + public IReadOnlyList Locations { get; init; } = []; + public IReadOnlyList LocationRelationships { get; init; } = []; + public IReadOnlyList SceneAssetLocations { get; init; } = []; + public IReadOnlyList SceneDependencies { get; init; } = []; + public IReadOnlyList ValidationRuns { get; init; } = []; + public IReadOnlyList ContinuityWarnings { get; init; } = []; + public IReadOnlyList SceneNotes { get; init; } = []; + public IReadOnlyList SceneWorkflow { get; init; } = []; + public IReadOnlyList SceneChecklistItems { get; init; } = []; + public IReadOnlyList SceneAttachments { get; init; } = []; + public IReadOnlyList ChapterGoals { get; init; } = []; + public IReadOnlyList Scenarios { get; init; } = []; + public IReadOnlyList ScenarioSceneOrder { get; init; } = []; + public IReadOnlyList ScenarioValidationRuns { get; init; } = []; + public IReadOnlyList ScenarioWarnings { get; init; } = []; +} + +public sealed class ProjectBackupDiagnostics +{ + public int ProjectId { get; init; } + public IReadOnlyList IncludedEntitySets { get; init; } = []; + public string? Note { get; init; } +} + +public sealed class ProjectBackupResult +{ + public string FileName { get; init; } = string.Empty; + public string ContentType { get; init; } = "application/json"; + public string Json { get; init; } = string.Empty; +} + +public sealed class ProjectBackupScenePurpose +{ + public int SceneID { get; set; } + public int ScenePurposeTypeID { get; set; } + public string PurposeName { get; set; } = string.Empty; + public int SortOrder { get; set; } +} + +public sealed class ProjectBackupSceneOutcome +{ + public int SceneOutcomeID { get; set; } + public int SceneID { get; set; } + public string OutcomeDescription { get; set; } = string.Empty; + public int? OutcomeTypeID { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime UpdatedDate { get; set; } +} + +public sealed class ProjectBackupAssetCustodyCharacter +{ + public int AssetCustodyEventCharacterID { get; set; } + public int AssetCustodyEventID { get; set; } + public int? CharacterID { get; set; } + public string? CharacterName { get; set; } + public string? CharacterNameText { get; set; } + public int CustodyRoleID { get; set; } + public string CustodyRoleName { get; set; } = string.Empty; +} + +public sealed class ProjectBackupValidationRun +{ + public int ValidationRunID { get; set; } + public int ProjectID { get; set; } + public int? BookID { get; set; } + public DateTime StartedDate { get; set; } + public DateTime? CompletedDate { get; set; } + public string? TriggeredBy { get; set; } + public string? ScopeDescription { get; set; } +} + +public sealed class ProjectBackupScenarioSceneOrder +{ + public int ScenarioSceneOrderID { get; set; } + public int ScenarioID { get; set; } + public int SceneID { get; set; } + public int? ProposedChapterID { get; set; } + public int ProposedSortOrder { get; set; } + public string? Notes { get; set; } + public DateTime CreatedDate { get; set; } + public DateTime UpdatedDate { get; set; } +} + +public sealed class ProjectBackupScenarioValidationRun +{ + public int ScenarioValidationRunID { get; set; } + public int ScenarioID { get; set; } + public DateTime StartedDate { get; set; } + public DateTime? CompletedDate { get; set; } + public int WarningCount { get; set; } +} diff --git a/PlotLine/Program.cs b/PlotLine/Program.cs index 8204f58..d469edd 100644 --- a/PlotLine/Program.cs +++ b/PlotLine/Program.cs @@ -37,6 +37,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -58,6 +59,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/PlotLine/Services/ProjectExportService.cs b/PlotLine/Services/ProjectExportService.cs new file mode 100644 index 0000000..67876c2 --- /dev/null +++ b/PlotLine/Services/ProjectExportService.cs @@ -0,0 +1,58 @@ +using System.Text.Json; +using PlotLine.Data; +using PlotLine.Models; + +namespace PlotLine.Services; + +public interface IProjectExportService +{ + Task ExportAsync(int projectId); +} + +public sealed class ProjectExportService(IProjectBackupRepository backups, ILogger logger) : IProjectExportService +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + public async Task ExportAsync(int projectId) + { + var backup = await backups.GetProjectBackupAsync(projectId); + if (backup?.Project is null) + { + logger.LogWarning("Project backup export requested for missing project {ProjectID}.", projectId); + return null; + } + + var json = JsonSerializer.Serialize(backup, JsonOptions); + var fileName = BuildFileName(backup.Project.ProjectName, backup.ExportedAtUtc); + + logger.LogInformation("Project backup export generated for project {ProjectID} with {ByteCount} JSON bytes.", projectId, json.Length); + + return new ProjectBackupResult + { + FileName = fileName, + ContentType = "application/json", + Json = json + }; + } + + private static string BuildFileName(string projectName, DateTime exportedAtUtc) + { + var safeTitle = new string(projectName + .Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c) + .ToArray()) + .Trim(); + + safeTitle = string.Join("_", safeTitle.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + + if (string.IsNullOrWhiteSpace(safeTitle)) + { + safeTitle = "PlotWeaver_Project"; + } + + return $"{safeTitle}_{exportedAtUtc:yyyy-MM-dd_HHmm}.plotweaver"; + } +} diff --git a/PlotLine/Sql/034_ProjectBackupExport.sql b/PlotLine/Sql/034_ProjectBackupExport.sql new file mode 100644 index 0000000..f1d5c89 --- /dev/null +++ b/PlotLine/Sql/034_ProjectBackupExport.sql @@ -0,0 +1,458 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectBackup_Export + @ProjectID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT ProjectID, ProjectName, Description, CreatedDate, UpdatedDate, IsArchived + FROM dbo.Projects + WHERE ProjectID = @ProjectID; + + SELECT BookID, ProjectID, BookTitle, BookNumber, Description, SortOrder, CreatedDate, UpdatedDate, IsArchived + 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 + 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 + 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, mt.SortOrder, mt.IsActive, CAST(1 AS bit) AS IsEnabledForProject, + (SELECT COUNT(1) FROM dbo.SceneMetricValues smv WHERE smv.MetricTypeID = mt.MetricTypeID) AS ValueCount + FROM dbo.SceneMetricTypes mt + 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 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 + 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.Description, pl.ParentPlotLineID, parent.PlotLineName AS ParentPlotLineName, + pl.SortOrder, pl.Colour, pl.IsMainPlot, pl.IsVisibleOnTimeline, pl.CreatedDate, pl.UpdatedDate, pl.IsArchived + FROM dbo.PlotLines pl + INNER JOIN dbo.PlotLineTypes plt ON plt.PlotLineTypeID = pl.PlotLineTypeID + LEFT JOIN dbo.Books b ON b.BookID = pl.BookID + LEFT JOIN dbo.PlotLines parent ON parent.PlotLineID = pl.ParentPlotLineID + 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 + 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.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 + 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 + 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, ast.StateName, ast.SortOrder, ast.Description, + ast.IsResolvedState, ast.IsActive + FROM dbo.AssetStates ast + 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 + 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 + 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 + 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.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; +END; +GO diff --git a/PlotLine/Views/Projects/Details.cshtml b/PlotLine/Views/Projects/Details.cshtml index 374c4da..c528329 100644 --- a/PlotLine/Views/Projects/Details.cshtml +++ b/PlotLine/Views/Projects/Details.cshtml @@ -61,6 +61,7 @@ Story dynamics Phase 1 Checklist Archived Items + Download Project Backup Add book
@@ -69,6 +70,11 @@ +@if (TempData["ProjectExportMessage"] is string projectExportMessage) +{ +
@projectExportMessage
+} +

Books

@if (!Model.Books.Any())