diff --git a/PlotLine/Data/ImportRepository.cs b/PlotLine/Data/ImportRepository.cs index 6432449..6da35c2 100644 --- a/PlotLine/Data/ImportRepository.cs +++ b/PlotLine/Data/ImportRepository.cs @@ -66,9 +66,11 @@ public sealed class ImportRepository(ISqlConnectionFactory connectionFactory) : LocationTypes = (await result.ReadAsync()).ToList(), LocationRelationshipTypes = (await result.ReadAsync()).ToList(), 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(), + PlotEventTypes = (await result.ReadAsync()).ToList(), AssetKinds = (await result.ReadAsync()).ToList(), AssetStates = (await result.ReadAsync()).ToList(), AssetEventTypes = (await result.ReadAsync()).ToList(), diff --git a/PlotLine/Data/ProjectBackupRepository.cs b/PlotLine/Data/ProjectBackupRepository.cs index 6acd57e..b890e0e 100644 --- a/PlotLine/Data/ProjectBackupRepository.cs +++ b/PlotLine/Data/ProjectBackupRepository.cs @@ -17,7 +17,8 @@ public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFact using var result = await connection.QueryMultipleAsync( "dbo.ProjectBackup_Export", new { ProjectID = projectId, UserID = userId }, - commandType: CommandType.StoredProcedure); + commandType: CommandType.StoredProcedure, + commandTimeout: 180); var project = await result.ReadSingleOrDefaultAsync(); var data = new ProjectBackupData @@ -50,7 +51,12 @@ public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFact RelationshipEvents = (await result.ReadAsync()).ToList(), Locations = (await result.ReadAsync()).ToList(), LocationRelationships = (await result.ReadAsync()).ToList(), + FloorPlans = (await result.ReadAsync()).ToList(), + FloorPlanFloors = (await result.ReadAsync()).ToList(), + FloorPlanBlocks = (await result.ReadAsync()).ToList(), + FloorPlanTransitions = (await result.ReadAsync()).ToList(), SceneAssetLocations = (await result.ReadAsync()).ToList(), + SceneFloorPlanOccupancy = (await result.ReadAsync()).ToList(), SceneDependencies = (await result.ReadAsync()).ToList(), ValidationRuns = (await result.ReadAsync()).ToList(), ContinuityWarnings = (await result.ReadAsync()).ToList(), @@ -139,7 +145,12 @@ public sealed class ProjectBackupRepository(ISqlConnectionFactory connectionFact nameof(data.RelationshipEvents), nameof(data.Locations), nameof(data.LocationRelationships), + nameof(data.FloorPlans), + nameof(data.FloorPlanFloors), + nameof(data.FloorPlanBlocks), + nameof(data.FloorPlanTransitions), nameof(data.SceneAssetLocations), + nameof(data.SceneFloorPlanOccupancy), nameof(data.SceneDependencies), nameof(data.ValidationRuns), nameof(data.ContinuityWarnings), diff --git a/PlotLine/Models/ImportModels.cs b/PlotLine/Models/ImportModels.cs index 9c001d2..0ad835a 100644 --- a/PlotLine/Models/ImportModels.cs +++ b/PlotLine/Models/ImportModels.cs @@ -38,9 +38,11 @@ public sealed class ProjectBackupRelationshipImportLookups public IReadOnlyList LocationTypes { get; init; } = []; public IReadOnlyList LocationRelationshipTypes { get; init; } = []; 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 IReadOnlyList AssetKinds { get; init; } = []; public IReadOnlyList AssetStates { get; init; } = []; public IReadOnlyList AssetEventTypes { get; init; } = []; diff --git a/PlotLine/Models/ProjectBackupModels.cs b/PlotLine/Models/ProjectBackupModels.cs index 2e92392..603766f 100644 --- a/PlotLine/Models/ProjectBackupModels.cs +++ b/PlotLine/Models/ProjectBackupModels.cs @@ -3,7 +3,7 @@ namespace PlotLine.Models; public sealed class ProjectBackupExport { public string Format { get; init; } = "PlotDirector.ProjectBackup"; - public string FormatVersion { get; init; } = "1.0"; + public string FormatVersion { get; init; } = "1.1"; public DateTime ExportedAtUtc { get; init; } public string SourceApplication { get; init; } = "PlotDirector"; public Project? Project { get; init; } @@ -41,7 +41,12 @@ public sealed class ProjectBackupData public IReadOnlyList RelationshipEvents { get; init; } = []; public IReadOnlyList Locations { get; init; } = []; public IReadOnlyList LocationRelationships { get; init; } = []; + public IReadOnlyList FloorPlans { get; init; } = []; + public IReadOnlyList FloorPlanFloors { get; init; } = []; + public IReadOnlyList FloorPlanBlocks { get; init; } = []; + public IReadOnlyList FloorPlanTransitions { get; init; } = []; public IReadOnlyList SceneAssetLocations { get; init; } = []; + public IReadOnlyList SceneFloorPlanOccupancy { get; init; } = []; public IReadOnlyList SceneDependencies { get; init; } = []; public IReadOnlyList ValidationRuns { get; init; } = []; public IReadOnlyList ContinuityWarnings { get; init; } = []; diff --git a/PlotLine/Services/ImportServices.cs b/PlotLine/Services/ImportServices.cs index a4fcc9d..9e06248 100644 --- a/PlotLine/Services/ImportServices.cs +++ b/PlotLine/Services/ImportServices.cs @@ -533,7 +533,7 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService validation.Errors.Add("The project backup format is not recognised."); } - if (!string.Equals(backup.FormatVersion, "1.0", StringComparison.Ordinal)) + if (!SupportedProjectBackupVersions.Contains(backup.FormatVersion, StringComparer.Ordinal)) { validation.Errors.Add($"Project backup format version \"{backup.FormatVersion}\" is not supported."); } @@ -744,12 +744,15 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService "SceneOutcomes", "PlotLines", "PlotThreads", "ThreadEvents", "AssetStates", "StoryAssets", "AssetEvents", "AssetDependencies", "AssetCustodyEvents", "AssetCustodyEventCharacters", "Characters", "SceneCharacters", "CharacterAttributeTypes", "CharacterAttributeEvents", "CharacterKnowledge", "CharacterRelationships", - "RelationshipEvents", "Locations", "LocationRelationships", "SceneAssetLocations", "SceneDependencies", - "ValidationRuns", "ContinuityWarnings", "SceneNotes", "SceneWorkflow", "SceneChecklistItems", + "RelationshipEvents", "Locations", "LocationRelationships", "FloorPlans", "FloorPlanFloors", + "FloorPlanBlocks", "FloorPlanTransitions", "SceneAssetLocations", "SceneFloorPlanOccupancy", + "SceneDependencies", "ValidationRuns", "ContinuityWarnings", "SceneNotes", "SceneWorkflow", "SceneChecklistItems", "SceneAttachments", "ChapterGoals", "Scenarios", "ScenarioSceneOrder", "ScenarioValidationRuns", "ScenarioWarnings" ]; + private static readonly string[] SupportedProjectBackupVersions = ["1.0", "1.1"]; + private static void RemapProjectBackupForImport( ProjectBackupExport backup, int projectId, @@ -792,9 +795,11 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService var locationTypeIds = LookupByName(relationshipLookups.LocationTypes); var locationRelationshipTypeIds = LookupByName(relationshipLookups.LocationRelationshipTypes); var plotLineTypeIds = LookupByName(relationshipLookups.PlotLineTypes); + var plotImportanceIds = LookupByName(relationshipLookups.PlotImportance); var threadTypeIds = LookupByName(relationshipLookups.ThreadTypes); var threadStatusIds = LookupByName(relationshipLookups.ThreadStatuses); var threadEventTypeIds = LookupByName(relationshipLookups.ThreadEventTypes); + var plotEventTypeIds = LookupByName(relationshipLookups.PlotEventTypes); var assetKindIds = LookupByName(relationshipLookups.AssetKinds); var assetStateIds = LookupByName(relationshipLookups.AssetStates); var assetEventTypeIds = LookupByName(relationshipLookups.AssetEventTypes); @@ -929,6 +934,35 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService location.LocationTypeID = ResolveNullableLookupID(locationTypeIds, location.LocationTypeID, location.LocationTypeName, $"Location \"{location.LocationName}\" type"); } + foreach (var floorPlan in backup.Data.FloorPlans) + { + floorPlan.FloorPlanID = NewId("FloorPlans", floorPlan.FloorPlanID); + floorPlan.ProjectID = projectId; + floorPlan.LocationID = ExistingOrNewNullable("Locations", floorPlan.LocationID); + } + + foreach (var floor in backup.Data.FloorPlanFloors) + { + floor.FloorPlanFloorID = NewId("FloorPlanFloors", floor.FloorPlanFloorID); + floor.FloorPlanID = ExistingOrNew("FloorPlans", floor.FloorPlanID); + floor.LocationID = ExistingOrNewNullable("Locations", floor.LocationID); + } + + foreach (var block in backup.Data.FloorPlanBlocks) + { + block.FloorPlanBlockID = NewId("FloorPlanBlocks", block.FloorPlanBlockID); + block.FloorPlanFloorID = ExistingOrNew("FloorPlanFloors", block.FloorPlanFloorID); + block.LocationID = ExistingOrNew("Locations", block.LocationID); + } + + foreach (var transition in backup.Data.FloorPlanTransitions) + { + transition.FloorPlanTransitionID = NewId("FloorPlanTransitions", transition.FloorPlanTransitionID); + transition.FloorPlanFloorID = ExistingOrNew("FloorPlanFloors", transition.FloorPlanFloorID); + transition.FromLocationID = ExistingOrNew("Locations", transition.FromLocationID); + transition.ToLocationID = ExistingOrNew("Locations", transition.ToLocationID); + } + foreach (var chapter in backup.Data.Chapters) { chapter.ChapterID = NewId("Chapters", chapter.ChapterID); @@ -943,6 +977,8 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService scene.ChapterID = ExistingOrNew("Chapters", scene.ChapterID); scene.POVCharacterID = ExistingOrNewNullable("Characters", scene.POVCharacterID); scene.PrimaryLocationID = ExistingOrNewNullable("Locations", scene.PrimaryLocationID); + scene.FloorPlanID = ExistingOrNewNullable("FloorPlans", scene.FloorPlanID); + scene.InitialFloorPlanFloorID = ExistingOrNewNullable("FloorPlanFloors", scene.InitialFloorPlanFloorID); scene.TimeModeID = ResolveLookupID(timeModeIds, scene.TimeModeID, scene.TimeModeName, $"Scene \"{scene.SceneTitle}\" time mode"); scene.DurationUnitID = ResolveNullableLookupID(durationUnitIds, scene.DurationUnitID, scene.DurationUnitName, $"Scene \"{scene.SceneTitle}\" duration unit"); scene.TimeConfidenceID = ResolveLookupID(timeConfidenceIds, scene.TimeConfidenceID, scene.TimeConfidenceName, $"Scene \"{scene.SceneTitle}\" time confidence"); @@ -996,11 +1032,13 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService plotLine.ProjectID = projectId; plotLine.BookID = ExistingOrNewNullable("Books", plotLine.BookID); plotLine.PlotLineTypeID = ResolveLookupID(plotLineTypeIds, plotLine.PlotLineTypeID, plotLine.PlotLineTypeName, $"Plot line \"{plotLine.PlotLineName}\" type"); + plotLine.PlotImportanceID = ResolveLookupID(plotImportanceIds, plotLine.PlotImportanceID, plotLine.PlotImportanceName, $"Plot line \"{plotLine.PlotLineName}\" importance"); } foreach (var plotLine in backup.Data.PlotLines) { plotLine.ParentPlotLineID = ExistingOrNewNullable("PlotLines", plotLine.ParentPlotLineID); + plotLine.EmergesFromPlotLineID = ExistingOrNewNullable("PlotLines", plotLine.EmergesFromPlotLineID); } foreach (var thread in backup.Data.PlotThreads) @@ -1022,6 +1060,8 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService threadEvent.PlotLineID = ExistingOrNew("PlotLines", threadEvent.PlotLineID); threadEvent.SceneID = ExistingOrNew("Scenes", threadEvent.SceneID); threadEvent.EventTypeID = ResolveLookupID(threadEventTypeIds, threadEvent.EventTypeID, threadEvent.EventTypeName, $"Thread event \"{threadEvent.EventTitle}\" type"); + threadEvent.PlotEventTypeID = ResolveLookupID(plotEventTypeIds, threadEvent.PlotEventTypeID, threadEvent.PlotEventTypeName, $"Thread event \"{threadEvent.EventTitle}\" plot event type"); + threadEvent.TargetPlotLineID = ExistingOrNewNullable("PlotLines", threadEvent.TargetPlotLineID); } foreach (var asset in backup.Data.StoryAssets) @@ -1149,6 +1189,17 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService sceneAssetLocation.LocationID = ExistingOrNew("Locations", sceneAssetLocation.LocationID); } + foreach (var occupancy in backup.Data.SceneFloorPlanOccupancy) + { + occupancy.SceneFloorPlanOccupancyID = NewId("SceneFloorPlanOccupancy", occupancy.SceneFloorPlanOccupancyID); + occupancy.SceneID = ExistingOrNew("Scenes", occupancy.SceneID); + occupancy.FloorPlanID = ExistingOrNew("FloorPlans", occupancy.FloorPlanID); + occupancy.FloorPlanFloorID = ExistingOrNewNullable("FloorPlanFloors", occupancy.FloorPlanFloorID); + occupancy.CharacterID = ExistingOrNewNullable("Characters", occupancy.CharacterID); + occupancy.StoryAssetID = ExistingOrNewNullable("StoryAssets", occupancy.StoryAssetID); + occupancy.LocationID = ExistingOrNew("Locations", occupancy.LocationID); + } + foreach (var dependency in backup.Data.SceneDependencies) { dependency.SceneDependencyID = NewId("SceneDependencies", dependency.SceneDependencyID); @@ -1261,6 +1312,11 @@ public sealed class ImportService(IImportRepository imports, ICurrentUserService "relationship" or "characterrelationship" or "character relationship" => "CharacterRelationships", "thread event" or "threadevent" => "ThreadEvents", "asset event" or "assetevent" => "AssetEvents", + "floorplan" or "floor plan" => "FloorPlans", + "floorplanfloor" or "floor plan floor" => "FloorPlanFloors", + "floorplanblock" or "floor plan block" => "FloorPlanBlocks", + "floorplantransition" or "floor plan transition" => "FloorPlanTransitions", + "scenefloorplanoccupancy" or "scene floor plan occupancy" => "SceneFloorPlanOccupancy", _ => null }; diff --git a/PlotLine/Sql/084_ProjectBackupFloorPlansAndVisualIdentity.sql b/PlotLine/Sql/084_ProjectBackupFloorPlansAndVisualIdentity.sql new file mode 100644 index 0000000..8328617 --- /dev/null +++ b/PlotLine/Sql/084_ProjectBackupFloorPlansAndVisualIdentity.sql @@ -0,0 +1,875 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +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.FloorPlanID, s.InitialFloorPlanFloorID, 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.ImagePath, sa.ThumbnailPath, 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, ImagePath, ThumbnailPath, 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 fp.FloorPlanID, fp.ProjectID, fp.LocationID, lp.LocationName, COALESCE(NULLIF(lp.LocationPath, N''), lp.LocationName) AS LocationPath, + fp.Name, fp.Description, fp.CreatedDate, fp.ModifiedDate, + (SELECT COUNT(1) FROM dbo.FloorPlanFloors fpf WHERE fpf.FloorPlanID = fp.FloorPlanID) AS FloorCount, + (SELECT COUNT(1) FROM dbo.FloorPlanBlocks fpb INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpb.FloorPlanFloorID WHERE fpf.FloorPlanID = fp.FloorPlanID) AS BlockCount + FROM dbo.FloorPlans fp + LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = fp.LocationID + WHERE fp.ProjectID = @ProjectID + ORDER BY fp.Name, fp.FloorPlanID; + + SELECT fpf.FloorPlanFloorID, fpf.FloorPlanID, fpf.LocationID, lp.LocationName, COALESCE(NULLIF(lp.LocationPath, N''), lp.LocationName) AS LocationPath, + fpf.Name, fpf.Description, fpf.SortOrder, fpf.GridWidth, fpf.GridHeight, + fpf.BackgroundImagePath, fpf.BackgroundImageOriginalFileName, fpf.BackgroundOpacity, fpf.BackgroundScale, + fpf.BackgroundOffsetX, fpf.BackgroundOffsetY, fpf.BackgroundLocked, fpf.CreatedDate, fpf.ModifiedDate + FROM dbo.FloorPlanFloors fpf + INNER JOIN dbo.FloorPlans fp ON fp.FloorPlanID = fpf.FloorPlanID + LEFT JOIN dbo.LocationPaths lp ON lp.LocationID = fpf.LocationID + WHERE fp.ProjectID = @ProjectID + ORDER BY fp.Name, fpf.SortOrder, fpf.Name, fpf.FloorPlanFloorID; + + SELECT fpb.FloorPlanBlockID, fpb.FloorPlanFloorID, fpb.LocationID, lp.LocationName, + COALESCE(NULLIF(lp.LocationPath, N''), lp.LocationName) AS LocationPath, + fpb.X, fpb.Y, fpb.WidthCells, fpb.HeightCells, fpb.BlockType, fpb.LabelOverride, fpb.Notes, + fpb.CreatedDate, fpb.ModifiedDate + FROM dbo.FloorPlanBlocks fpb + INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpb.FloorPlanFloorID + INNER JOIN dbo.FloorPlans fp ON fp.FloorPlanID = fpf.FloorPlanID + INNER JOIN dbo.LocationPaths lp ON lp.LocationID = fpb.LocationID + WHERE fp.ProjectID = @ProjectID + ORDER BY fp.Name, fpf.SortOrder, fpb.Y, fpb.X, lp.LocationName; + + SELECT fpt.FloorPlanTransitionID, fpt.FloorPlanFloorID, fpt.FromLocationID, + fromPath.LocationName AS FromLocationName, COALESCE(NULLIF(fromPath.LocationPath, N''), fromPath.LocationName) AS FromLocationPath, + fpt.ToLocationID, toPath.LocationName AS ToLocationName, COALESCE(NULLIF(toPath.LocationPath, N''), toPath.LocationName) AS ToLocationPath, + fpt.TransitionType, fpt.Notes, fpt.IsLocked, fpt.DisplayLabel, fpt.PositionWithinLocation, + fpt.CreatedDate, fpt.ModifiedDate + FROM dbo.FloorPlanTransitions fpt + INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpt.FloorPlanFloorID + INNER JOIN dbo.FloorPlans fp ON fp.FloorPlanID = fpf.FloorPlanID + INNER JOIN dbo.LocationPaths fromPath ON fromPath.LocationID = fpt.FromLocationID + INNER JOIN dbo.LocationPaths toPath ON toPath.LocationID = fpt.ToLocationID + WHERE fp.ProjectID = @ProjectID + ORDER BY fp.Name, fpf.SortOrder, fromPath.LocationName, toPath.LocationName, fpt.TransitionType; + + 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 o.SceneFloorPlanOccupancyID, o.SceneID, o.FloorPlanID, o.FloorPlanFloorID, fpf.Name AS FloorName, + o.CharacterID, c.CharacterName, c.ShortName AS CharacterShortName, c.ImagePath AS CharacterImagePath, c.ThumbnailPath AS CharacterThumbnailPath, + o.StoryAssetID, sa.AssetName, sa.ImagePath AS AssetImagePath, sa.ThumbnailPath AS AssetThumbnailPath, + o.LocationID, lp.LocationName, COALESCE(NULLIF(lp.LocationPath, N''), lp.LocationName) AS LocationPath, + o.CreatedDate, o.ModifiedDate + FROM dbo.SceneFloorPlanOccupancy o + INNER JOIN dbo.Scenes s ON s.SceneID = o.SceneID + INNER JOIN dbo.Chapters chp ON chp.ChapterID = s.ChapterID + INNER JOIN dbo.Books b ON b.BookID = chp.BookID + INNER JOIN dbo.LocationPaths lp ON lp.LocationID = o.LocationID + LEFT JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = o.FloorPlanFloorID + LEFT JOIN dbo.Characters c ON c.CharacterID = o.CharacterID + LEFT JOIN dbo.StoryAssets sa ON sa.StoryAssetID = o.StoryAssetID + WHERE b.ProjectID = @ProjectID + ORDER BY b.SortOrder, chp.SortOrder, s.SortOrder, COALESCE(c.CharacterName, 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 + +CREATE OR ALTER PROCEDURE dbo.ProjectRestore_ApplyFromJson + @ProjectID int, + @SourceRestorePointID int = NULL, + @SafetyRestorePointID int = NULL, + @JsonData nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + SET XACT_ABORT ON; + + IF ISJSON(@JsonData) <> 1 + THROW 51001, 'Restore JSON is invalid.', 1; + + IF JSON_VALUE(@JsonData, '$.Format') <> N'PlotDirector.ProjectBackup' + THROW 51002, 'Restore JSON format is not recognised.', 1; + + IF JSON_VALUE(@JsonData, '$.FormatVersion') NOT IN (N'1.0', N'1.1') + THROW 51003, 'Restore JSON format version is not supported.', 1; + + IF TRY_CONVERT(int, JSON_VALUE(@JsonData, '$.Project.ProjectID')) <> @ProjectID + THROW 51004, 'Restore point does not belong to this project.', 1; + + DECLARE @RestoreTables table (TableName sysname NOT NULL PRIMARY KEY); + INSERT INTO @RestoreTables (TableName) + VALUES + (N'ProjectTimelineSettings'), (N'ProjectTimelineMetricSettings'), (N'TimelineViewPresets'), + (N'SceneMetricTypes'), (N'SceneMetricValues'), (N'Books'), (N'Chapters'), (N'Scenes'), + (N'ScenePurposes'), (N'SceneOutcomes'), (N'PlotLines'), (N'PlotThreads'), (N'ThreadEvents'), + (N'AssetStates'), (N'StoryAssets'), (N'AssetEvents'), (N'AssetDependencies'), + (N'AssetCustodyEvents'), (N'AssetCustodyEventCharacters'), (N'Characters'), (N'SceneCharacters'), + (N'CharacterAttributeTypes'), (N'CharacterAttributeEvents'), (N'CharacterKnowledge'), + (N'CharacterRelationships'), (N'RelationshipEvents'), (N'Locations'), (N'LocationRelationships'), + (N'FloorPlans'), (N'FloorPlanFloors'), (N'FloorPlanBlocks'), (N'FloorPlanTransitions'), + (N'SceneAssetLocations'), (N'SceneFloorPlanOccupancy'), (N'SceneDependencies'), (N'ValidationRuns'), (N'ContinuityWarnings'), + (N'SceneNotes'), (N'SceneWorkflow'), (N'SceneChecklistItems'), (N'SceneAttachments'), + (N'ChapterGoals'), (N'Scenarios'), (N'ScenarioSceneOrder'), (N'ScenarioValidationRuns'), + (N'ScenarioWarnings'); + + BEGIN TRANSACTION; + + DECLARE @TableName sysname; + DECLARE DisableCursor CURSOR LOCAL FAST_FORWARD FOR SELECT TableName FROM @RestoreTables; + OPEN DisableCursor; + FETCH NEXT FROM DisableCursor INTO @TableName; + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @DisableSql nvarchar(max) = N'ALTER TABLE dbo.' + QUOTENAME(@TableName) + N' NOCHECK CONSTRAINT ALL;'; + EXEC (@DisableSql); + FETCH NEXT FROM DisableCursor INTO @TableName; + END + CLOSE DisableCursor; + DEALLOCATE DisableCursor; + + DELETE sw FROM dbo.ScenarioWarnings sw INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sw.ScenarioID WHERE sc.ProjectID = @ProjectID; + DELETE svr FROM dbo.ScenarioValidationRuns svr INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = svr.ScenarioID WHERE sc.ProjectID = @ProjectID; + DELETE sso FROM dbo.ScenarioSceneOrder sso INNER JOIN dbo.Scenarios sc ON sc.ScenarioID = sso.ScenarioID WHERE sc.ProjectID = @ProjectID; + DELETE FROM dbo.Scenarios WHERE ProjectID = @ProjectID; + + DELETE FROM dbo.ContinuityWarnings WHERE ProjectID = @ProjectID; + DELETE FROM dbo.ValidationRuns WHERE ProjectID = @ProjectID; + + DELETE sa FROM dbo.SceneAttachments sa 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; + DELETE sci 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; + DELETE sw 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; + DELETE sn FROM dbo.SceneNotes sn 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; + DELETE cg 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; + + DELETE sfpo FROM dbo.SceneFloorPlanOccupancy sfpo + INNER JOIN dbo.Scenes s ON s.SceneID = sfpo.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; + + DELETE sd FROM dbo.SceneDependencies sd + LEFT JOIN dbo.Scenes ss ON ss.SceneID = sd.SourceSceneID + LEFT JOIN dbo.Chapters sc ON sc.ChapterID = ss.ChapterID + LEFT JOIN dbo.Books sb ON sb.BookID = sc.BookID + LEFT JOIN dbo.Scenes ts ON ts.SceneID = sd.TargetSceneID + LEFT JOIN dbo.Chapters tc ON tc.ChapterID = ts.ChapterID + LEFT JOIN dbo.Books tb ON tb.BookID = tc.BookID + WHERE sd.ProjectID = @ProjectID OR sb.ProjectID = @ProjectID OR tb.ProjectID = @ProjectID; + + DELETE re FROM dbo.RelationshipEvents re INNER JOIN dbo.CharacterRelationships cr ON cr.CharacterRelationshipID = re.CharacterRelationshipID WHERE cr.ProjectID = @ProjectID; + DELETE FROM dbo.CharacterRelationships WHERE ProjectID = @ProjectID; + DELETE ck FROM dbo.CharacterKnowledge ck INNER JOIN dbo.Characters ch ON ch.CharacterID = ck.CharacterID WHERE ch.ProjectID = @ProjectID; + DELETE cae FROM dbo.CharacterAttributeEvents cae INNER JOIN dbo.Characters ch ON ch.CharacterID = cae.CharacterID WHERE ch.ProjectID = @ProjectID; + DELETE sc FROM dbo.SceneCharacters sc INNER JOIN dbo.Characters ch ON ch.CharacterID = sc.CharacterID WHERE ch.ProjectID = @ProjectID; + + DELETE acec FROM dbo.AssetCustodyEventCharacters acec INNER JOIN dbo.AssetCustodyEvents ace ON ace.AssetCustodyEventID = acec.AssetCustodyEventID INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE ace FROM dbo.AssetCustodyEvents ace INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ace.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE ad FROM dbo.AssetDependencies ad INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ad.SourceAssetID WHERE sa.ProjectID = @ProjectID; + DELETE ae FROM dbo.AssetEvents ae INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = ae.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE sal FROM dbo.SceneAssetLocations sal INNER JOIN dbo.StoryAssets sa ON sa.StoryAssetID = sal.StoryAssetID WHERE sa.ProjectID = @ProjectID; + DELETE FROM dbo.StoryAssets WHERE ProjectID = @ProjectID; + + DELETE fpt FROM dbo.FloorPlanTransitions fpt INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpt.FloorPlanFloorID INNER JOIN dbo.FloorPlans fp ON fp.FloorPlanID = fpf.FloorPlanID WHERE fp.ProjectID = @ProjectID; + DELETE fpb FROM dbo.FloorPlanBlocks fpb INNER JOIN dbo.FloorPlanFloors fpf ON fpf.FloorPlanFloorID = fpb.FloorPlanFloorID INNER JOIN dbo.FloorPlans fp ON fp.FloorPlanID = fpf.FloorPlanID WHERE fp.ProjectID = @ProjectID; + DELETE fpf FROM dbo.FloorPlanFloors fpf INNER JOIN dbo.FloorPlans fp ON fp.FloorPlanID = fpf.FloorPlanID WHERE fp.ProjectID = @ProjectID; + DELETE FROM dbo.FloorPlans WHERE ProjectID = @ProjectID; + + DELETE lr FROM dbo.LocationRelationships lr INNER JOIN dbo.Locations l ON l.LocationID = lr.FromLocationID WHERE l.ProjectID = @ProjectID; + DELETE FROM dbo.Locations WHERE ProjectID = @ProjectID; + + DELETE te FROM dbo.ThreadEvents te INNER JOIN dbo.PlotThreads pt ON pt.PlotThreadID = te.PlotThreadID INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pl.ProjectID = @ProjectID; + DELETE pt FROM dbo.PlotThreads pt INNER JOIN dbo.PlotLines pl ON pl.PlotLineID = pt.PlotLineID WHERE pl.ProjectID = @ProjectID; + DELETE FROM dbo.PlotLines WHERE ProjectID = @ProjectID; + + DELETE FROM dbo.TimelineViewPresets WHERE ProjectID = @ProjectID; + DELETE FROM dbo.ProjectTimelineMetricSettings WHERE ProjectID = @ProjectID; + DELETE FROM dbo.ProjectTimelineSettings WHERE ProjectID = @ProjectID; + + DELETE smv 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 b.ProjectID = @ProjectID; + DELETE FROM dbo.SceneMetricTypes WHERE ProjectID = @ProjectID; + DELETE FROM dbo.CharacterAttributeTypes WHERE ProjectID = @ProjectID; + DELETE FROM dbo.AssetStates WHERE ProjectID = @ProjectID; + + DELETE sp FROM dbo.ScenePurposes sp 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; + DELETE so 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; + DELETE s FROM dbo.Scenes s INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE c FROM dbo.Chapters c INNER JOIN dbo.Books b ON b.BookID = c.BookID WHERE b.ProjectID = @ProjectID; + DELETE FROM dbo.Books WHERE ProjectID = @ProjectID; + DELETE FROM dbo.Characters WHERE ProjectID = @ProjectID; + + UPDATE p + SET ProjectName = src.ProjectName, + Description = src.Description, + CreatedDate = src.CreatedDate, + UpdatedDate = SYSUTCDATETIME(), + IsArchived = src.IsArchived, + ArchivedDate = src.ArchivedDate, + ArchivedReason = src.ArchivedReason + FROM dbo.Projects p + CROSS APPLY OPENJSON(@JsonData, '$.Project') + WITH + ( + ProjectID int '$.ProjectID', + ProjectName nvarchar(200) '$.ProjectName', + Description nvarchar(max) '$.Description', + CreatedDate datetime2 '$.CreatedDate', + IsArchived bit '$.IsArchived', + ArchivedDate datetime2 '$.ArchivedDate', + ArchivedReason nvarchar(1000) '$.ArchivedReason' + ) src + WHERE p.ProjectID = @ProjectID AND src.ProjectID = @ProjectID; + + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneMetricTypes', N'$.Data.SceneMetricTypes', @JsonData, @ProjectID, 1; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetStates', N'$.Data.AssetStates', @JsonData, @ProjectID, 1; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterAttributeTypes', N'$.Data.CharacterAttributeTypes', @JsonData, @ProjectID, 1; + EXEC dbo.ProjectRestore_InsertJsonTable N'Books', N'$.Data.Books', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Chapters', N'$.Data.Chapters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Characters', N'$.Data.Characters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Locations', N'$.Data.Locations', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'FloorPlans', N'$.Data.FloorPlans', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'FloorPlanFloors', N'$.Data.FloorPlanFloors', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'FloorPlanBlocks', N'$.Data.FloorPlanBlocks', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'FloorPlanTransitions', N'$.Data.FloorPlanTransitions', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Scenes', N'$.Data.Scenes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenePurposes', N'$.Data.ScenePurposes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneOutcomes', N'$.Data.SceneOutcomes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneMetricValues', N'$.Data.SceneMetricValues', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ProjectTimelineSettings', N'$.Data.TimelineSettings', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ProjectTimelineMetricSettings', N'$.Data.TimelineMetricSettings', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'TimelineViewPresets', N'$.Data.TimelineViewPresets', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'PlotLines', N'$.Data.PlotLines', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'PlotThreads', N'$.Data.PlotThreads', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ThreadEvents', N'$.Data.ThreadEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'StoryAssets', N'$.Data.StoryAssets', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetEvents', N'$.Data.AssetEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetDependencies', N'$.Data.AssetDependencies', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetCustodyEvents', N'$.Data.AssetCustodyEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'AssetCustodyEventCharacters', N'$.Data.AssetCustodyCharacters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneCharacters', N'$.Data.SceneCharacters', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterAttributeEvents', N'$.Data.CharacterAttributeEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterKnowledge', N'$.Data.CharacterKnowledge', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'CharacterRelationships', N'$.Data.CharacterRelationships', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'RelationshipEvents', N'$.Data.RelationshipEvents', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'LocationRelationships', N'$.Data.LocationRelationships', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneAssetLocations', N'$.Data.SceneAssetLocations', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneFloorPlanOccupancy', N'$.Data.SceneFloorPlanOccupancy', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneDependencies', N'$.Data.SceneDependencies', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ValidationRuns', N'$.Data.ValidationRuns', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ContinuityWarnings', N'$.Data.ContinuityWarnings', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneNotes', N'$.Data.SceneNotes', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneWorkflow', N'$.Data.SceneWorkflow', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneChecklistItems', N'$.Data.SceneChecklistItems', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'SceneAttachments', N'$.Data.SceneAttachments', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ChapterGoals', N'$.Data.ChapterGoals', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'Scenarios', N'$.Data.Scenarios', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioSceneOrder', N'$.Data.ScenarioSceneOrder', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioValidationRuns', N'$.Data.ScenarioValidationRuns', @JsonData, @ProjectID, 0; + EXEC dbo.ProjectRestore_InsertJsonTable N'ScenarioWarnings', N'$.Data.ScenarioWarnings', @JsonData, @ProjectID, 0; + + DECLARE EnableCursor CURSOR LOCAL FAST_FORWARD FOR SELECT TableName FROM @RestoreTables; + OPEN EnableCursor; + FETCH NEXT FROM EnableCursor INTO @TableName; + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @EnableSql nvarchar(max) = N'ALTER TABLE dbo.' + QUOTENAME(@TableName) + N' WITH CHECK CHECK CONSTRAINT ALL;'; + EXEC (@EnableSql); + FETCH NEXT FROM EnableCursor INTO @TableName; + END + CLOSE EnableCursor; + DEALLOCATE EnableCursor; + + IF @SourceRestorePointID IS NOT NULL AND @SafetyRestorePointID IS NOT NULL + BEGIN + INSERT INTO dbo.ProjectRestoreHistory (ProjectID, SourceRestorePointID, SafetyRestorePointID, PerformedBy, Notes) + VALUES (@ProjectID, @SourceRestorePointID, @SafetyRestorePointID, SUSER_SNAME(), N'Project restored from saved restore point.'); + END; + + COMMIT TRANSACTION; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.ProjectBackup_GetRelationshipImportLookups +AS +BEGIN + SET NOCOUNT ON; + + SELECT RevisionStatusID AS LookupID, StatusName AS LookupName FROM dbo.RevisionStatuses ORDER BY SortOrder, StatusName; + SELECT TimeModeID AS LookupID, TimeModeName AS LookupName FROM dbo.TimeModes ORDER BY SortOrder, TimeModeName; + SELECT DurationUnitID AS LookupID, DurationUnitName AS LookupName FROM dbo.DurationUnits ORDER BY SortOrder, DurationUnitName; + SELECT TimeConfidenceID AS LookupID, TimeConfidenceName AS LookupName FROM dbo.TimeConfidences ORDER BY SortOrder, TimeConfidenceName; + SELECT LocationTypeID AS LookupID, TypeName AS LookupName FROM dbo.LocationTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT LocationRelationshipTypeID AS LookupID, TypeName AS LookupName FROM dbo.LocationRelationshipTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT PlotLineTypeID AS LookupID, TypeName AS LookupName FROM dbo.PlotLineTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT PlotImportanceID AS LookupID, ImportanceName AS LookupName FROM dbo.PlotImportance WHERE IsActive = 1 ORDER BY SortOrder, ImportanceName; + SELECT ThreadTypeID AS LookupID, TypeName AS LookupName FROM dbo.ThreadTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT ThreadStatusID AS LookupID, StatusName AS LookupName FROM dbo.ThreadStatuses WHERE IsActive = 1 ORDER BY SortOrder, StatusName; + SELECT ThreadEventTypeID AS LookupID, TypeName AS LookupName FROM dbo.ThreadEventTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT PlotEventTypeID AS LookupID, TypeName AS LookupName FROM dbo.PlotEventTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT AssetKindID AS LookupID, KindName AS LookupName FROM dbo.AssetKinds WHERE IsActive = 1 ORDER BY SortOrder, KindName; + SELECT AssetStateID AS LookupID, StateName AS LookupName FROM dbo.AssetStates WHERE IsActive = 1 AND ProjectID IS NULL ORDER BY SortOrder, StateName; + SELECT AssetEventTypeID AS LookupID, TypeName AS LookupName FROM dbo.AssetEventTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT AssetDependencyTypeID AS LookupID, TypeName AS LookupName FROM dbo.AssetDependencyTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT AssetCustodyEventTypeID AS LookupID, TypeName AS LookupName FROM dbo.AssetCustodyEventTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT CustodyRoleID AS LookupID, RoleName AS LookupName FROM dbo.CustodyRoles WHERE IsActive = 1 ORDER BY SortOrder, RoleName; + SELECT CharacterRoleInSceneTypeID AS LookupID, TypeName AS LookupName FROM dbo.CharacterRoleInSceneTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT PresenceTypeID AS LookupID, TypeName AS LookupName FROM dbo.PresenceTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT KnowledgeStateID AS LookupID, StateName AS LookupName FROM dbo.KnowledgeStates WHERE IsActive = 1 ORDER BY SortOrder, StateName; + SELECT SceneDependencyTypeID AS LookupID, TypeName AS LookupName FROM dbo.SceneDependencyTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT SceneNoteTypeID AS LookupID, TypeName AS LookupName FROM dbo.SceneNoteTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT SceneAttachmentTypeID AS LookupID, TypeName AS LookupName FROM dbo.SceneAttachmentTypes WHERE IsActive = 1 ORDER BY SortOrder, TypeName; + SELECT ScenePurposeTypeID AS LookupID, PurposeName AS LookupName FROM dbo.ScenePurposeTypes ORDER BY SortOrder, PurposeName; + SELECT MetricTypeID AS LookupID, MetricName AS LookupName FROM dbo.SceneMetricTypes WHERE ProjectID IS NULL ORDER BY SortOrder, MetricName; + + SELECT rt.RelationshipTypeID, rt.TypeName, rt.IsPermanentDefault, rt.SortOrder, rt.IsActive, + rt.RelationshipCategoryID, rc.CategoryName AS RelationshipCategoryName, COALESCE(rc.SortOrder, 900) AS CategorySortOrder + FROM dbo.RelationshipTypes rt + LEFT JOIN dbo.RelationshipCategories rc ON rc.RelationshipCategoryID = rt.RelationshipCategoryID + WHERE rt.IsActive = 1 + ORDER BY COALESCE(rc.SortOrder, 900), rt.SortOrder, rt.TypeName; + + SELECT RelationshipStateID, StateName, SortOrder, IsActive + FROM dbo.RelationshipStates + WHERE IsActive = 1 + ORDER BY SortOrder, StateName; +END; +GO