using System.Data; using Dapper; using Microsoft.Data.SqlClient; using PlotLine.Models; namespace PlotLine.Data; public interface IProjectRepository { Task> ListAsync(); Task> ListForUserAsync(int userId); Task> ListActiveForUserAsync(int userId); Task> ListArchivedForUserAsync(int userId); Task> ListOwnedForAccountDeletionAsync(int userId); Task GetAsync(int projectId); Task GetForUserAsync(int projectId, int userId); Task GetArchivedForOwnerAsync(int projectId, int userId); Task GetDashboardMetricsAsync(int projectId); Task> ListDashboardAttentionThreadsAsync(int projectId, int count); Task> ListGenreMetricPresetsAsync(); Task SaveAsync(Project project); Task SaveAsync(Project project, int userId, string? genreMetricPresetKey); Task SaveAsync(Project project, string? genreMetricPresetKey); Task ArchiveAsync(int projectId); Task ArchiveForUserAsync(int projectId, int userId); Task ArchiveForOwnerAsync(int projectId, int userId); Task RestoreForOwnerAsync(int projectId, int userId); Task> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId); Task HardDeleteForOwnerAsync(int projectId, int userId); Task> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId); Task HardDeleteForAccountDeletionAsync(int projectId, int userId); } public interface IProjectCollaborationRepository { Task GetOwnerUserIdAsync(int projectId); Task> ListCollaboratorsAsync(int projectId); Task> ListPendingInvitationsAsync(int projectId); Task CollaboratorCountsForOwnerAsync(int ownerUserId, int collaboratorUserId); Task AddCollaboratorAsync(int projectId, int userId, int invitedByUserId); Task RemoveCollaboratorAsync(int projectId, int userId); Task CreatePendingInvitationAsync(int projectId, string emailAddress, int invitedByUserId); } public interface IProjectActivityRepository { Task> ListForProjectAsync(int projectId); Task> ListRecentForProjectAsync(int projectId, int count); Task RecordAsync(int projectId, int? userId, string activityType, string entityType, int? entityId, string? entityName, string? description = null); } public interface IBookRepository { Task> ListByProjectAsync(int projectId); Task> ListByProjectsAsync(IEnumerable projectIds); Task GetAsync(int bookId); Task GetCoverPathsAsync(int bookId); Task SaveAsync(Book book); Task UpdateCoverAsync(int bookId, string? thumbnailPath, string? standardPath); Task ArchiveAsync(int bookId); } public interface IManuscriptDocumentRepository { Task GetByBookAsync(int bookId, int userId); Task GetByGuidAsync(Guid documentGuid, int userId); Task SaveAsync(int bookId, Guid documentGuid, int userId); Task UnlinkBookAsync(int bookId, Guid? documentGuid, int userId); Task UpdateLastSyncAsync(int manuscriptDocumentId, int userId); Task UpdateLastOpenedAsync(int manuscriptDocumentId, int userId); } public interface IChapterRepository { Task> ListByBookAsync(int bookId); Task GetAsync(int chapterId); Task SaveAsync(Chapter chapter); Task ArchiveAsync(int chapterId); } public interface ISceneRepository { Task> ListByChapterAsync(int chapterId); Task> ListByFloorPlanAsync(int floorPlanId); Task GetAsync(int sceneId); Task SaveAsync(Scene scene); Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId); Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId); Task SavePurposesAsync(int sceneId, IEnumerable purposeIds); Task SaveMetricValuesAsync(int sceneId, IEnumerable metricValues); Task SaveMetricValueAsync(int sceneId, int metricTypeId, int value); Task ArchiveAsync(int sceneId); Task MoveAsync(int sceneId, string direction); Task> ListChapterOptionsAsync(int projectId); Task GetMovePreviewAsync(int sceneId, int targetChapterId, string position); Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber); Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position); Task RenumberChapterAsync(int chapterId); } public interface ISceneFloorPlanOccupancyRepository { Task> ListBySceneAsync(int sceneId); Task> ListByFloorPlanAsync(int floorPlanId); Task> ListParticipantsByFloorPlanAsync(int floorPlanId); Task SaveAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId, IEnumerable occupancyRows); } public interface ILookupRepository { Task GetAllAsync(); } public interface ITimelineRepository { Task GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable metricTypeIds, bool includePlotLines); Task> GetSceneOverviewsAsync(int projectId, int? bookId); } public interface ITimelinePresetRepository { Task> ListAsync(int projectId, int? bookId); Task GetAsync(int timelineViewPresetId); Task SaveAsync(TimelineViewPreset preset); Task ArchiveAsync(int timelineViewPresetId); } public interface ITimelineSettingsRepository { Task GetAsync(int projectId); Task> ListMetricTypesAsync(int projectId); Task> ListMetricSettingsAsync(int projectId); Task SaveAsync(ProjectTimelineSettings settings, IEnumerable metricTypeIds); } public interface ISceneMetricTypeRepository { Task> ListForManagementAsync(int projectId); Task GetAsync(int metricTypeId, int projectId); Task SaveAsync(SceneMetricType metric); Task SetActiveAsync(int metricTypeId, int projectId, bool isActive); Task MoveAsync(int metricTypeId, int projectId, string direction); Task AddMissingDefaultsAsync(int projectId); Task RemoveUnusedAsync(int metricTypeId, int projectId); Task HasProjectMetricsAsync(int projectId); } public interface IPlotRepository { Task GetLookupsAsync(); Task> ListPlotLinesAsync(int projectId); Task GetPlotLineAsync(int plotLineId); Task SavePlotLineAsync(PlotLineItem plotLine); Task ArchivePlotLineAsync(int plotLineId); Task> ListPlotThreadsByProjectAsync(int projectId); Task> ListPlotThreadsByPlotLineAsync(int plotLineId); Task GetPlotThreadAsync(int plotThreadId); Task SavePlotThreadAsync(PlotThread plotThread); Task ArchivePlotThreadAsync(int plotThreadId); Task> ListThreadEventsBySceneAsync(int sceneId); Task> ListThreadEventsByPlotThreadAsync(int plotThreadId); Task SaveThreadEventAsync(ThreadEvent threadEvent); Task SaveThreadEventTargetsAsync(int threadEventId, IEnumerable plotLineIds); Task DeleteThreadEventAsync(int threadEventId); Task> ListSceneOptionsAsync(int projectId); } public interface IAssetRepository { Task GetLookupsAsync(int? projectId = null); Task> ListAssetsAsync(int projectId); Task GetAssetAsync(int storyAssetId); Task SaveAssetAsync(StoryAsset asset); Task> ListAliasesAsync(int storyAssetId); Task AddAliasAsync(int storyAssetId, string alias, int? sortOrder); Task UpdateAliasAsync(int assetAliasId, string alias, int? sortOrder); Task DeleteAliasAsync(int assetAliasId); Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath); Task ArchiveAssetAsync(int storyAssetId); Task> ListEventsBySceneAsync(int sceneId); Task> ListEventsByAssetAsync(int storyAssetId); Task SaveEventAsync(AssetEvent assetEvent); Task DeleteEventAsync(int assetEventId); Task> ListSceneAssetSuggestionsAsync(int sceneId, int userId); Task AcceptSceneAssetSuggestionAsync(int suggestionId, int userId); Task RejectSceneAssetSuggestionAsync(int suggestionId, int userId); Task> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId); Task> ListDependenciesByAssetAsync(int storyAssetId); Task SaveDependencyAsync(AssetDependency dependency); Task DeleteDependencyAsync(int assetDependencyId); Task> ListCustodyBySceneAsync(int sceneId); Task> ListCustodyByAssetAsync(int storyAssetId); Task> ListCustodyCharactersByProjectAsync(int projectId); Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId); Task DeleteCustodyEventAsync(int assetCustodyEventId); Task<(IReadOnlyList Assets, IReadOnlyList Events)> GetTimelineAsync(int projectId, int? bookId); Task> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId); } public interface ICharacterRepository { Task GetLookupsAsync(int? projectId = null); Task> ListCharactersAsync(int projectId); Task GetCharacterAsync(int characterId); Task SaveCharacterAsync(Character character); Task> ListAliasesAsync(int characterId); Task AddAliasAsync(int characterId, string alias, int? sortOrder); Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder); Task DeleteAliasAsync(int characterAliasId); Task> ListSexValuesAsync(int ownerUserId); Task GetSexValueAsync(int characterSexValueId, int ownerUserId); Task GetOrCreateSexValueAsync(int ownerUserId, string sexName); Task UpdateCharacterImageAsync(int characterId, string? imagePath, string? thumbnailPath); Task UpdateCharacterAvatarAsync(int characterId, int? sourceCharacterImageId, string? avatarImagePath, string? avatarThumbnailPath); Task> ListCharacterImagesAsync(int characterId); Task GetCharacterImageAsync(int characterImageId); Task CreateCharacterImageAsync(CharacterImage image); Task DeleteCharacterImageAsync(int characterImageId); Task ArchiveCharacterAsync(int characterId); Task> ListSceneCharactersAsync(int sceneId); Task SaveSceneCharacterAsync(SceneCharacter sceneCharacter); Task DeleteSceneCharacterAsync(int sceneCharacterId); Task> ListSceneCharacterSuggestionsAsync(int sceneId, int userId); Task AcceptSceneCharacterSuggestionAsync(int suggestionId, int userId); Task RejectSceneCharacterSuggestionAsync(int suggestionId, int userId); Task> CountSceneCharacterSuggestionsByChapterAsync(int chapterId, int userId); Task> ListAttributeEventsBySceneAsync(int sceneId); Task> ListAttributeEventsByCharacterAsync(int characterId); Task SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent); Task> ListKnowledgeBySceneAsync(int sceneId); Task> ListKnowledgeByCharacterAsync(int characterId); Task SaveKnowledgeAsync(CharacterKnowledgeItem knowledge); Task DeleteKnowledgeAsync(int characterKnowledgeId); Task> ListRelationshipsByProjectAsync(int projectId); Task> ListRelationshipsByCharacterAsync(int characterId); Task> ListInitialRelationshipsByCharacterAsync(int characterId); Task GetRelationshipAsync(int characterRelationshipId); Task SaveRelationshipAsync(CharacterRelationship relationship); Task ArchiveRelationshipAsync(int characterRelationshipId); Task> ListRelationshipEventsBySceneAsync(int sceneId); Task> ListRelationshipEventsByCharacterAsync(int characterId); Task SaveRelationshipEventAsync(RelationshipEvent relationshipEvent); Task DeleteRelationshipEventAsync(int relationshipEventId); Task<(IReadOnlyList Characters, IReadOnlyList Appearances)> GetTimelineAsync(int projectId, int? bookId); Task GetRelationshipMapAsync(int projectId); } public interface ILocationRepository { Task GetLookupsAsync(); Task> ListByProjectAsync(int projectId); Task GetAsync(int locationId); Task SaveAsync(LocationItem location); Task> ListAliasesAsync(int locationId); Task AddAliasAsync(int locationId, string alias, int? sortOrder); Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder); Task DeleteAliasAsync(int locationAliasId); Task ArchiveAsync(int locationId); Task> ListRelationshipsByProjectAsync(int projectId); Task> ListRelationshipsByLocationAsync(int locationId); Task SaveRelationshipAsync(LocationRelationship relationship); Task ArchiveRelationshipAsync(int locationRelationshipId); Task> ListSceneAssetLocationsAsync(int sceneId); Task SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation); Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId); Task> ListSceneLocationSuggestionsAsync(int sceneId, int userId); Task AcceptSceneLocationSuggestionAsync(int suggestionId, int userId); Task RejectSceneLocationSuggestionAsync(int suggestionId, int userId); Task> CountSceneLocationSuggestionsByChapterAsync(int chapterId, int userId); Task> ListScenesByLocationAsync(int locationId); Task> ListCharactersByLocationAsync(int locationId); Task> ListAssetsByLocationAsync(int locationId); } public interface IFloorPlanRepository { Task> ListByProjectAsync(int projectId); Task GetAsync(int floorPlanId); Task<(FloorPlan? FloorPlan, IReadOnlyList Floors, IReadOnlyList Blocks, IReadOnlyList Transitions)> GetEditorAsync(int floorPlanId); Task GetFloorAsync(int floorPlanFloorId); Task> ListBlocksByFloorAsync(int floorPlanFloorId); Task> ListBlocksByPlanAsync(int floorPlanId); Task> ListFloorsByPlanAsync(int floorPlanId); Task GetBlockAsync(int floorPlanBlockId); Task SavePlanAsync(FloorPlan floorPlan); Task DeletePlanAsync(int floorPlanId); Task SaveFloorAsync(FloorPlanFloor floor); Task UpdateFloorBackgroundAsync(FloorPlanFloor floor); Task DeleteFloorAsync(int floorPlanFloorId); Task SaveBlockAsync(FloorPlanBlock block); Task DeleteBlockAsync(int floorPlanBlockId); Task SaveTransitionAsync(FloorPlanTransition transition); Task DeleteTransitionAsync(int floorPlanTransitionId); } public interface IWarningRepository { Task GetLookupsAsync(); Task ValidateProjectAsync(int projectId); Task ValidateBookAsync(int bookId); Task ValidateSceneAsync(int sceneId); Task> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional); Task> ListDashboardUnresolvedAsync(int projectId, int count); Task> ListBySceneAsync(int sceneId); Task> GetSceneCountsAsync(int projectId, int? bookId); Task DismissAsync(int continuityWarningId); Task MarkIntentionalAsync(int continuityWarningId); Task RestoreAsync(int continuityWarningId); Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId); } public interface IContinuityWarningAcknowledgementRepository { Task> ListByProjectAsync(int projectId); Task GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId); Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement); Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId); } public interface ISceneDependencyRepository { Task GetLookupsAsync(); Task> ListBySceneAsync(int sceneId); Task> ListByProjectAsync(int projectId, int? bookId); Task> GetCountsAsync(int projectId, int? bookId); Task SaveAsync(SceneDependency dependency); Task ArchiveAsync(int sceneDependencyId); } public interface IAnalyticsRepository { Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber); } public interface IStoryBibleRepository { Task> GetCharacterSummariesAsync(int projectId); Task> GetThreadSummariesAsync(int projectId); Task> GetAssetSummariesAsync(int projectId); Task> GetRelationshipSummariesAsync(int projectId); Task> GetLocationSummariesAsync(int projectId); Task> GetTimelineSummaryAsync(int projectId); Task> GetOpenQuestionsAsync(int projectId); Task> GetRevisionProgressAsync(int projectId); Task> SearchAsync(int projectId, string? query); } public interface IWriterWorkspaceRepository { Task> ListNoteTypesAsync(); Task> ListAttachmentTypesAsync(); Task GetWorkflowAsync(int sceneId); Task SaveWorkflowAsync(SceneWorkflow workflow); Task> ListNotesAsync(int sceneId); Task SaveNoteAsync(SceneNote note); Task DeleteNoteAsync(int sceneNoteId); Task> ListChecklistItemsAsync(int sceneId); Task SaveChecklistItemAsync(SceneChecklistItem item); Task DeleteChecklistItemAsync(int sceneChecklistItemId); Task> ListAttachmentsAsync(int sceneId); Task SaveAttachmentAsync(SceneAttachment attachment); Task DeleteAttachmentAsync(int sceneAttachmentId); Task GetChapterGoalAsync(int chapterId); Task SaveChapterGoalAsync(ChapterGoal goal); Task> GetChapterWorkflowAsync(int chapterId); Task> GetWriterDashboardScenesAsync(int? projectId, string queue); Task> GetWriterDashboardRemindersAsync(int? projectId); } public interface IWritingScheduleRepository { Task> GetWritingPlansByUserAsync(int userId); Task GetWritingPlanByIDAsync(int writingPlanId); Task CreateWritingPlanAsync(WritingPlan plan); Task UpdateWritingPlanAsync(WritingPlan plan); Task DeleteWritingPlanAsync(int writingPlanId); Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive); Task> GetScheduleItemsByPlanAsync(int writingPlanId); Task> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate); Task CreateScheduleItemAsync(WritingScheduleItem item); Task UpdateScheduleItemAsync(WritingScheduleItem item); Task DeleteScheduleItemAsync(int writingScheduleItemId); Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc); Task> GetWritingPlansForManagementAsync(int userId); Task> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes); Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList items); Task> GetWritingPlanSummariesByUserAsync(int userId, int? projectId = null); Task> GetSchedulePreviewItemsAsync(int writingPlanId); Task DeleteScheduleItemsByPlanAsync(int writingPlanId); Task> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null); Task UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc); Task> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null); Task UpdatePlannedScheduleItemDatesAsync(int writingPlanId, IReadOnlyList changes); } public interface IExportRepository { Task GetStoryBibleAsync(int projectId); Task GetNarrativeTimelineAsync(int projectId); Task GetCharacterReferenceAsync(int characterId); Task GetSceneBriefAsync(int sceneId); Task GetChapterBriefAsync(int chapterId); Task GetWriterSessionBriefAsync(int sceneId); Task GetResearchPackAsync(int projectId); Task GetSearchResultsAsync(int projectId, string query); } public interface IDynamicsRepository { Task<(Character? Character, IReadOnlyList ArcPoints, IReadOnlyList RelationshipEvents, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetCharacterArcAsync(int characterId); Task<(CharacterRelationship? Relationship, IReadOnlyList Events, IReadOnlyList Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId); Task<(Character? Character, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetKnowledgeTimelineAsync(int characterId); Task<(IReadOnlyList Pairings, IReadOnlyList Issues)> GetStoryDynamicsAsync(int projectId); Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId); } public interface IScenarioRepository { Task> ListAsync(int projectId); Task GetAsync(int scenarioId); Task CreateAsync(int projectId, int? bookId, string scenarioName, string? description); Task<(IReadOnlyList Books, IReadOnlyList Chapters, IReadOnlyList Scenes, IReadOnlyList Warnings)> GetTimelineAsync(int scenarioId); Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position); Task ValidateAsync(int scenarioId); Task<(IReadOnlyList MovedScenes, IReadOnlyList Warnings)> CompareAsync(int scenarioId); Task ApplyAsync(int scenarioId); Task ArchiveAsync(int scenarioId); } public interface IArchiveRepository { Task> ListAsync(int? projectId, string? entityType); Task> ListForUserAsync(int? projectId, string? entityType, int userId); Task> ListProjectFiltersAsync(); Task> ListProjectFiltersForUserAsync(int userId); Task ArchiveAsync(string entityType, int entityId, string? reason); Task RestoreAsync(string entityType, int entityId); Task DeleteForeverAsync(string entityType, int entityId, string confirmationText, int userId); } public interface IAcceptanceRepository { Task> GetPhase1ChecklistAsync(int? projectId); } public sealed class TimelineData { public Project? Project { get; init; } public IReadOnlyList Books { get; init; } = []; public IReadOnlyList Chapters { get; init; } = []; public IReadOnlyList Scenes { get; init; } = []; public IReadOnlyList PurposeLabels { get; init; } = []; public IReadOnlyList MetricTypes { get; init; } = []; public IReadOnlyList MetricPoints { get; init; } = []; public IReadOnlyList PlotLines { get; init; } = []; public IReadOnlyList ThreadEvents { get; init; } = []; } public sealed class TimelineSceneOverviewRow { public int SceneID { get; set; } public int? CharacterID { get; set; } public string? CharacterName { get; set; } public bool IsPov { get; set; } public int? StoryAssetID { get; set; } public string? AssetName { get; set; } public int? AssetImportance { get; set; } public string? AssetEventTypeName { get; set; } public string? LocationName { get; set; } } public sealed class LocationRepository(ISqlConnectionFactory connectionFactory) : ILocationRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.LocationLookup_All", commandType: CommandType.StoredProcedure); return new LocationLookupData { LocationTypes = (await result.ReadAsync()).ToList(), RelationshipTypes = (await result.ReadAsync()).ToList() }; } public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Location_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Location_Get", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(LocationItem location) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Location_Save", new { location.LocationID, location.ProjectID, location.ParentLocationID, location.LocationName, location.LocationTypeID, location.Description, location.ShowInQuickAddBar, location.ExcludeFromCompanionDetection, location.DetectionPriority }, commandType: CommandType.StoredProcedure); } public async Task> ListAliasesAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.LocationAlias_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task AddAliasAsync(int locationId, string alias, int? sortOrder) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.LocationAlias_Add", new { LocationID = locationId, Alias = alias, SortOrder = sortOrder }, commandType: CommandType.StoredProcedure); } public async Task UpdateAliasAsync(int locationAliasId, string alias, int? sortOrder) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.LocationAlias_Update", new { LocationAliasID = locationAliasId, Alias = alias, SortOrder = sortOrder }, commandType: CommandType.StoredProcedure); } public async Task DeleteAliasAsync(int locationAliasId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.LocationAlias_Delete", new { LocationAliasID = locationAliasId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Location_Archive", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationRelationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipsByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationRelationship_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipAsync(LocationRelationship relationship) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.LocationRelationship_Save", new { relationship.LocationRelationshipID, relationship.FromLocationID, relationship.ToLocationID, relationship.LocationRelationshipTypeID, relationship.IsBidirectional, relationship.Strength, relationship.CanHearNormalSpeech, relationship.CanHearLoudNoise, relationship.CanSeeMovement, relationship.CanSeeClearly, relationship.CanTravelDirectly, relationship.Notes }, commandType: CommandType.StoredProcedure); } public async Task ArchiveRelationshipAsync(int locationRelationshipId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.LocationRelationship_Archive", new { LocationRelationshipID = locationRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneAssetLocationsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAssetLocation_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveSceneAssetLocationAsync(SceneAssetLocation sceneAssetLocation, bool updateCurrentLocation) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneAssetLocation_Save", new { sceneAssetLocation.SceneAssetLocationID, sceneAssetLocation.SceneID, sceneAssetLocation.StoryAssetID, sceneAssetLocation.LocationID, sceneAssetLocation.Description, UpdateCurrentLocation = updateCurrentLocation }, commandType: CommandType.StoredProcedure); } public async Task DeleteSceneAssetLocationAsync(int sceneAssetLocationId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneAssetLocation_Delete", new { SceneAssetLocationID = sceneAssetLocationId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneLocationSuggestionsAsync(int sceneId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneLocationSuggestions_ListByScene", new { SceneID = sceneId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task AcceptSceneLocationSuggestionAsync(int suggestionId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneLocationSuggestion_Accept", new { SceneLocationSuggestionID = suggestionId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task RejectSceneLocationSuggestionAsync(int suggestionId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneLocationSuggestion_Reject", new { SceneLocationSuggestionID = suggestionId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task> CountSceneLocationSuggestionsByChapterAsync(int chapterId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneLocationSuggestions_CountByChapter", new { ChapterID = chapterId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListScenesByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationScene_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListCharactersByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.LocationCharacter_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAssetsByLocationAsync(int locationId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAssetLocation_ListByLocation", new { LocationID = locationId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class FloorPlanRepository(ISqlConnectionFactory connectionFactory) : IFloorPlanRepository { public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.FloorPlan_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.FloorPlan_Get", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); } public async Task<(FloorPlan? FloorPlan, IReadOnlyList Floors, IReadOnlyList Blocks, IReadOnlyList Transitions)> GetEditorAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.FloorPlan_GetEditor", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); var floorPlan = await result.ReadSingleOrDefaultAsync(); var floors = (await result.ReadAsync()).ToList(); var blocks = (await result.ReadAsync()).ToList(); var transitions = (await result.ReadAsync()).ToList(); return (floorPlan, floors, blocks, transitions); } public async Task GetFloorAsync(int floorPlanFloorId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.FloorPlanFloor_Get", new { FloorPlanFloorID = floorPlanFloorId }, commandType: CommandType.StoredProcedure); } public async Task> ListBlocksByFloorAsync(int floorPlanFloorId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.FloorPlanBlock_ListByFloor", new { FloorPlanFloorID = floorPlanFloorId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListBlocksByPlanAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.FloorPlanBlock_ListByPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListFloorsByPlanAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.FloorPlanFloor_ListByPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetBlockAsync(int floorPlanBlockId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.FloorPlanBlock_Get", new { FloorPlanBlockID = floorPlanBlockId }, commandType: CommandType.StoredProcedure); } public async Task SavePlanAsync(FloorPlan floorPlan) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.FloorPlan_Save", new { floorPlan.FloorPlanID, floorPlan.ProjectID, floorPlan.LocationID, floorPlan.Name, floorPlan.Description }, commandType: CommandType.StoredProcedure); } public async Task DeletePlanAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.FloorPlan_Delete", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); } public async Task SaveFloorAsync(FloorPlanFloor floor) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.FloorPlanFloor_Save", new { floor.FloorPlanFloorID, floor.FloorPlanID, floor.LocationID, floor.Name, floor.Description, floor.SortOrder, floor.GridWidth, floor.GridHeight }, commandType: CommandType.StoredProcedure); } public async Task UpdateFloorBackgroundAsync(FloorPlanFloor floor) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QuerySingleAsync( "dbo.FloorPlanFloor_UpdateBackground", new { floor.FloorPlanFloorID, floor.FloorPlanID, floor.BackgroundImagePath, floor.BackgroundImageOriginalFileName, floor.BackgroundOpacity, floor.BackgroundScale, floor.BackgroundOffsetX, floor.BackgroundOffsetY, floor.BackgroundLocked }, commandType: CommandType.StoredProcedure); return rows > 0; } public async Task DeleteFloorAsync(int floorPlanFloorId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.FloorPlanFloor_Delete", new { FloorPlanFloorID = floorPlanFloorId }, commandType: CommandType.StoredProcedure); } public async Task SaveBlockAsync(FloorPlanBlock block) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.FloorPlanBlock_Save", new { block.FloorPlanBlockID, block.FloorPlanFloorID, block.LocationID, block.X, block.Y, block.WidthCells, block.HeightCells, block.BlockType, block.LabelOverride, block.Notes }, commandType: CommandType.StoredProcedure); } public async Task DeleteBlockAsync(int floorPlanBlockId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.FloorPlanBlock_Delete", new { FloorPlanBlockID = floorPlanBlockId }, commandType: CommandType.StoredProcedure); } public async Task SaveTransitionAsync(FloorPlanTransition transition) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.FloorPlanTransition_Save", new { transition.FloorPlanTransitionID, transition.FloorPlanFloorID, transition.FromLocationID, transition.ToLocationID, transition.TransitionType, transition.Notes, transition.IsLocked, transition.DisplayLabel, transition.PositionWithinLocation }, commandType: CommandType.StoredProcedure); } public async Task DeleteTransitionAsync(int floorPlanTransitionId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.FloorPlanTransition_Delete", new { FloorPlanTransitionID = floorPlanTransitionId }, commandType: CommandType.StoredProcedure); } } public sealed class WarningRepository(ISqlConnectionFactory connectionFactory) : IWarningRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.WarningLookup_All", commandType: CommandType.StoredProcedure); return new WarningLookupData { WarningTypes = (await result.ReadAsync()).ToList(), Severities = (await result.ReadAsync()).ToList() }; } public async Task ValidateProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateProject", new { ProjectID = projectId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task ValidateBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateBook", new { BookID = bookId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task ValidateSceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ContinuityValidation_ValidateScene", new { SceneID = sceneId, TriggeredBy = "User" }, commandType: CommandType.StoredProcedure); } public async Task> ListAsync(int projectId, int? bookId, int? sceneId, int? warningTypeId, int? warningSeverityId, string? entityType, bool includeDismissed, bool includeIntentional) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ContinuityWarning_List", new { ProjectID = projectId, BookID = bookId, SceneID = sceneId, WarningTypeID = warningTypeId, WarningSeverityID = warningSeverityId, EntityType = string.IsNullOrWhiteSpace(entityType) ? null : entityType, IncludeDismissed = includeDismissed, IncludeIntentional = includeIntentional }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListDashboardUnresolvedAsync(int projectId, int count) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ContinuityWarning_ListDashboardUnresolved", new { ProjectID = projectId, Count = count }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ContinuityWarning_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetSceneCountsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ContinuityWarning_SceneCounts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task DismissAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_Dismiss", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task MarkIntentionalAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_MarkIntentional", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task RestoreAsync(int continuityWarningId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ContinuityWarning_Restore", new { ContinuityWarningID = continuityWarningId }, commandType: CommandType.StoredProcedure); } public async Task FinaliseSceneDependencyWarningsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneDependency_FinaliseWarnings", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); } } public sealed class ContinuityWarningAcknowledgementRepository(ISqlConnectionFactory connectionFactory) : IContinuityWarningAcknowledgementRepository { public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ContinuityWarningAcknowledgement_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.ContinuityWarningAcknowledgement_Get", new { ProjectID = projectId, WarningType = warningType, SceneID = sceneId, CharacterID = characterId, AssetID = assetId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(ContinuityWarningAcknowledgement acknowledgement) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ContinuityWarningAcknowledgement_Save", new { acknowledgement.ProjectID, acknowledgement.WarningType, acknowledgement.SceneID, acknowledgement.CharacterID, acknowledgement.AssetID, acknowledgement.AcknowledgedByUserID, acknowledgement.Notes }, commandType: CommandType.StoredProcedure); } public async Task DeleteAsync(int projectId, string warningType, int? sceneId, int? characterId, int? assetId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ContinuityWarningAcknowledgement_Delete", new { ProjectID = projectId, WarningType = warningType, SceneID = sceneId, CharacterID = characterId, AssetID = assetId }, commandType: CommandType.StoredProcedure); } } public sealed class AnalyticsData { public IReadOnlyList MetricPoints { get; init; } = []; public IReadOnlyList RevisionSummary { get; init; } = []; public IReadOnlyList RevisionAttentionScenes { get; init; } = []; public IReadOnlyList PurposeSummary { get; init; } = []; public IReadOnlyList SceneGaps { get; init; } = []; public IReadOnlyList ThreadHealth { get; init; } = []; public IReadOnlyList AssetHealth { get; init; } = []; public IReadOnlyList WarningSeverityCounts { get; init; } = []; public IReadOnlyList WarningTypeCounts { get; init; } = []; public IReadOnlyList WarningHotspots { get; init; } = []; public IReadOnlyList PovSummary { get; init; } = []; public IReadOnlyList MultiPovChapters { get; init; } = []; public IReadOnlyList CharacterAppearances { get; init; } = []; public IReadOnlyList BusyScenes { get; init; } = []; public IReadOnlyList LocationUsage { get; init; } = []; } public sealed class AnalyticsRepository(ISqlConnectionFactory connectionFactory) : IAnalyticsRepository { public async Task GetStoryHealthAsync(int projectId, int? bookId, decimal? startChapterNumber, decimal? endChapterNumber, decimal? startSceneNumber, decimal? endSceneNumber) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync( "dbo.Analytics_StoryHealth", new { ProjectID = projectId, BookID = bookId, StartChapterNumber = startChapterNumber, EndChapterNumber = endChapterNumber, StartSceneNumber = startSceneNumber, EndSceneNumber = endSceneNumber }, commandType: CommandType.StoredProcedure); return new AnalyticsData { MetricPoints = (await result.ReadAsync()).ToList(), RevisionSummary = (await result.ReadAsync()).ToList(), RevisionAttentionScenes = (await result.ReadAsync()).ToList(), PurposeSummary = (await result.ReadAsync()).ToList(), SceneGaps = (await result.ReadAsync()).ToList(), ThreadHealth = (await result.ReadAsync()).ToList(), AssetHealth = (await result.ReadAsync()).ToList(), WarningSeverityCounts = (await result.ReadAsync()).ToList(), WarningTypeCounts = (await result.ReadAsync()).ToList(), WarningHotspots = (await result.ReadAsync()).ToList(), PovSummary = (await result.ReadAsync()).ToList(), MultiPovChapters = (await result.ReadAsync()).ToList(), CharacterAppearances = (await result.ReadAsync()).ToList(), BusyScenes = (await result.ReadAsync()).ToList(), LocationUsage = (await result.ReadAsync()).ToList() }; } } public sealed class StoryBibleRepository(ISqlConnectionFactory connectionFactory) : IStoryBibleRepository { public async Task> GetCharacterSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_CharacterSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetThreadSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_ThreadSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetAssetSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_AssetSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetRelationshipSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_RelationshipSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetLocationSummariesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_LocationSummaries", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetTimelineSummaryAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_TimelineSummary", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetOpenQuestionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_OpenQuestions", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> GetRevisionProgressAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_RevisionProgress", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure)).ToList(); } public async Task> SearchAsync(int projectId, string? query) { using var connection = connectionFactory.CreateConnection(); return (await connection.QueryAsync("dbo.StoryBible_Search", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure)).ToList(); } } public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFactory) : IWriterWorkspaceRepository { public async Task> ListNoteTypesAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneNoteType_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAttachmentTypesAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAttachmentType_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetWorkflowAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.SceneWorkflow_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); } public async Task SaveWorkflowAsync(SceneWorkflow workflow) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.SceneWorkflow_Save", new { workflow.SceneID, workflow.DraftStatus, workflow.EstimatedWordCount, workflow.ActualWordCount, workflow.DraftedDate, workflow.LastWorkedOn, workflow.ReadyForDraft, workflow.ReadyForRevision, workflow.ReadyForPolish, workflow.IsBlocked, workflow.BlockedReason, workflow.Priority }, commandType: CommandType.StoredProcedure); } public async Task> ListNotesAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneNote_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveNoteAsync(SceneNote note) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneNote_Save", new { note.SceneNoteID, note.SceneID, note.SceneNoteTypeID, note.NoteTitle, note.NoteText, note.SortOrder, note.IsPinned, note.IsResolved }, commandType: CommandType.StoredProcedure); } public async Task DeleteNoteAsync(int sceneNoteId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneNote_Delete", new { SceneNoteID = sceneNoteId }, commandType: CommandType.StoredProcedure); } public async Task> ListChecklistItemsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneChecklist_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveChecklistItemAsync(SceneChecklistItem item) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneChecklist_Save", new { item.SceneChecklistItemID, item.SceneID, item.ItemText, item.IsCompleted, item.SortOrder }, commandType: CommandType.StoredProcedure); } public async Task DeleteChecklistItemAsync(int sceneChecklistItemId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneChecklist_Delete", new { SceneChecklistItemID = sceneChecklistItemId }, commandType: CommandType.StoredProcedure); } public async Task> ListAttachmentsAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneAttachment_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAttachmentAsync(SceneAttachment attachment) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneAttachment_Save", new { attachment.SceneAttachmentID, attachment.SceneID, attachment.SceneAttachmentTypeID, attachment.Title, attachment.FilePath, attachment.ExternalUrl, attachment.Notes }, commandType: CommandType.StoredProcedure); } public async Task DeleteAttachmentAsync(int sceneAttachmentId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneAttachment_Delete", new { SceneAttachmentID = sceneAttachmentId }, commandType: CommandType.StoredProcedure); } public async Task GetChapterGoalAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.ChapterGoal_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } public async Task SaveChapterGoalAsync(ChapterGoal goal) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ChapterGoal_Save", new { goal.ChapterID, goal.GoalSummary, goal.EmotionalGoal, goal.ReaderTakeaway, goal.MustInclude, goal.RisksOrConcerns }, commandType: CommandType.StoredProcedure); } public async Task> GetChapterWorkflowAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ChapterWorkflow_Summary", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetWriterDashboardScenesAsync(int? projectId, string queue) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.WriterDashboard_Scenes", new { ProjectID = projectId, Queue = queue }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetWriterDashboardRemindersAsync(int? projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.WriterDashboard_Reminders", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFactory) : IWritingScheduleRepository { public async Task> GetWritingPlansByUserAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.GetWritingPlansByUser", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetWritingPlanByIDAsync(int writingPlanId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.GetWritingPlanByID", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure); } public async Task CreateWritingPlanAsync(WritingPlan plan) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CreateWritingPlan", new { plan.UserID, plan.ProjectID, plan.BookID, plan.PlanName, plan.GoalType, plan.StartDate, plan.DeadlineDate, plan.AvailableDaysJson, plan.SessionLengthMinutes, plan.IncludeBlockedScenes, plan.RebalanceMode, plan.IsActive }, commandType: CommandType.StoredProcedure); } public async Task UpdateWritingPlanAsync(WritingPlan plan) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.UpdateWritingPlan", new { plan.WritingPlanID, plan.UserID, plan.ProjectID, plan.BookID, plan.PlanName, plan.GoalType, plan.StartDate, plan.DeadlineDate, plan.AvailableDaysJson, plan.SessionLengthMinutes, plan.IncludeBlockedScenes, plan.RebalanceMode, plan.IsActive }, commandType: CommandType.StoredProcedure); } public async Task DeleteWritingPlanAsync(int writingPlanId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.DeleteWritingPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure); } public async Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SetWritingPlanActive", new { WritingPlanID = writingPlanId, IsActive = isActive }, commandType: CommandType.StoredProcedure); } public async Task> GetScheduleItemsByPlanAsync(int writingPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.GetScheduleItemsByPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.GetScheduleItemsByDateRange", new { WritingPlanID = writingPlanId, StartDate = startDate, EndDate = endDate }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task CreateScheduleItemAsync(WritingScheduleItem item) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CreateScheduleItem", new { item.WritingPlanID, item.SceneID, item.ScheduledDate, item.TaskType, item.EstimatedMinutes, item.TargetWords, item.ScheduleStatus, item.CompletedDateUtc, item.Notes }, commandType: CommandType.StoredProcedure); } public async Task UpdateScheduleItemAsync(WritingScheduleItem item) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.UpdateScheduleItem", new { item.WritingScheduleItemID, item.WritingPlanID, item.SceneID, item.ScheduledDate, item.TaskType, item.EstimatedMinutes, item.TargetWords, item.ScheduleStatus, item.CompletedDateUtc, item.Notes }, commandType: CommandType.StoredProcedure); } public async Task DeleteScheduleItemAsync(int writingScheduleItemId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.DeleteScheduleItem", new { WritingScheduleItemID = writingScheduleItemId }, commandType: CommandType.StoredProcedure); } public async Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.UpdateScheduleItemStatus", new { WritingScheduleItemID = writingScheduleItemId, ScheduleStatus = scheduleStatus, CompletedDateUtc = completedDateUtc }, commandType: CommandType.StoredProcedure); } public async Task> GetWritingPlansForManagementAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.WritingPlan_ListForManagement", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.WritingScheduleScene_ListForPlanning", new { ProjectID = projectId, BookID = bookId, IncludeBlockedScenes = includeBlockedScenes }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList items) { using var connection = connectionFactory.CreateConnection(); connection.Open(); using var transaction = connection.BeginTransaction(); try { await connection.ExecuteAsync( "dbo.WritingScheduleItem_DeleteByPlan", new { WritingPlanID = writingPlanId }, transaction, commandType: CommandType.StoredProcedure); foreach (var item in items) { await connection.ExecuteAsync( "dbo.CreateScheduleItem", new { item.WritingPlanID, item.SceneID, item.ScheduledDate, item.TaskType, item.EstimatedMinutes, item.TargetWords, item.ScheduleStatus, item.CompletedDateUtc, item.Notes }, transaction, commandType: CommandType.StoredProcedure); } transaction.Commit(); } catch { transaction.Rollback(); throw; } } public async Task> GetWritingPlanSummariesByUserAsync(int userId, int? projectId = null) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.WritingPlanSummary_ListByUser", new { UserID = userId, ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetSchedulePreviewItemsAsync(int writingPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.WritingSchedulePreview_ListByPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task DeleteScheduleItemsByPlanAsync(int writingPlanId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.WritingScheduleItem_DeleteByPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure); } public async Task> GetActiveScheduleDashboardItemsAsync(int userId, int? projectId = null) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.WritingScheduleDashboard_ListActive", new { UserID = userId, ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc) { using var connection = connectionFactory.CreateConnection(); return await connection.ExecuteAsync( "dbo.WritingScheduleItem_UpdatePlannedStatusForUser", new { WritingScheduleItemID = writingScheduleItemId, UserID = userId, ScheduleStatus = scheduleStatus, CompletedDateUtc = completedDateUtc }, commandType: CommandType.StoredProcedure); } public async Task> GetPlansRequiringRebalanceAsync(int userId, DateTime today, int? projectId = null) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.WritingPlanSummary_ListRequiringRebalance", new { UserID = userId, Today = today.Date, ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task UpdatePlannedScheduleItemDatesAsync(int writingPlanId, IReadOnlyList changes) { if (changes.Count == 0) { return; } using var connection = connectionFactory.CreateConnection(); connection.Open(); using var transaction = connection.BeginTransaction(); try { foreach (var change in changes) { await connection.ExecuteAsync( "dbo.WritingScheduleItem_UpdatePlannedDate", new { WritingPlanID = writingPlanId, change.WritingScheduleItemID, ScheduledDate = change.ScheduledDate.Date }, transaction, commandType: CommandType.StoredProcedure); } transaction.Commit(); } catch { transaction.Rollback(); throw; } } } public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository { public async Task GetStoryBibleAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_StoryBible", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Story Bible", Characters = (await result.ReadAsync()).ToList(), PlotThreads = (await result.ReadAsync()).ToList(), Assets = (await result.ReadAsync()).ToList(), Relationships = (await result.ReadAsync()).ToList(), Locations = (await result.ReadAsync()).ToList(), Timeline = (await result.ReadAsync()).ToList(), OpenQuestions = (await result.ReadAsync()).ToList(), RevisionItems = (await result.ReadAsync()).ToList() }; } public async Task GetNarrativeTimelineAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Export_NarrativeTimeline", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Narrative Timeline Summary", Timeline = rows.ToList() }; } public async Task GetCharacterReferenceAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_CharacterReference", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Character Reference Pack", Character = await result.ReadSingleOrDefaultAsync(), CharacterRelationships = (await result.ReadAsync()).ToList(), CharacterAppearances = (await result.ReadAsync()).ToList(), CharacterKnowledge = (await result.ReadAsync()).ToList(), CharacterCustody = (await result.ReadAsync()).ToList(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetSceneBriefAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_SceneBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Scene Brief", SceneHeader = await result.ReadSingleOrDefaultAsync(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetChapterBriefAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_ChapterBrief", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Chapter Brief", ChapterHeader = await result.ReadSingleOrDefaultAsync(), Timeline = (await result.ReadAsync()).ToList(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetWriterSessionBriefAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Export_WriterSessionBrief", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Writing Session Brief", SceneHeader = await result.ReadSingleOrDefaultAsync(), Items = (await result.ReadAsync()).ToList() }; } public async Task GetResearchPackAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Export_ResearchPack", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = "Research Pack", ResearchItems = rows.ToList() }; } public async Task GetSearchResultsAsync(int projectId, string query) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Export_SearchResults", new { ProjectID = projectId, Query = query }, commandType: CommandType.StoredProcedure); return new ExportPacket { Title = $"Search Results: {query}", SearchResults = rows.ToList() }; } } public sealed class DynamicsRepository(ISqlConnectionFactory connectionFactory) : IDynamicsRepository { public async Task<(Character? Character, IReadOnlyList ArcPoints, IReadOnlyList RelationshipEvents, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetCharacterArcAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_CharacterArc", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return ( await result.ReadSingleOrDefaultAsync(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task<(CharacterRelationship? Relationship, IReadOnlyList Events, IReadOnlyList Issues)> GetRelationshipEvolutionAsync(int characterRelationshipId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_RelationshipEvolution", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure); return ( await result.ReadSingleOrDefaultAsync(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task<(Character? Character, IReadOnlyList KnowledgeEvents, IReadOnlyList Issues)> GetKnowledgeTimelineAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_KnowledgeTimeline", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return ( await result.ReadSingleOrDefaultAsync(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task<(IReadOnlyList Pairings, IReadOnlyList Issues)> GetStoryDynamicsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Dynamics_StoryDashboard", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return ((await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task RunContinuityChecksAsync(int projectId, int? bookId, int? sceneId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Dynamics_RunContinuityChecks", new { ProjectID = projectId, BookID = bookId, SceneID = sceneId }, commandType: CommandType.StoredProcedure); } } public sealed class ScenarioRepository(ISqlConnectionFactory connectionFactory) : IScenarioRepository { public async Task> ListAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Scenario_List", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Scenario_Get", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } public async Task CreateAsync(int projectId, int? bookId, string scenarioName, string? description) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Scenario_Create", new { ProjectID = projectId, BookID = bookId, ScenarioName = scenarioName, Description = description }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Books, IReadOnlyList Chapters, IReadOnlyList Scenes, IReadOnlyList Warnings)> GetTimelineAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scenario_Timeline", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); return ( (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task MoveSceneAsync(int scenarioId, int sceneId, int? anchorSceneId, int? targetChapterId, string position) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scenario_MoveScene", new { ScenarioID = scenarioId, SceneID = sceneId, AnchorSceneID = anchorSceneId, TargetChapterID = targetChapterId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task ValidateAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.Scenario_Validate", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList MovedScenes, IReadOnlyList Warnings)> CompareAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scenario_Compare", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); return ((await result.ReadAsync()).ToList(), (await result.ReadAsync()).ToList()); } public async Task ApplyAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scenario_ApplyToMain", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int scenarioId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scenario_Archive", new { ScenarioID = scenarioId }, commandType: CommandType.StoredProcedure); } } public sealed class ArchiveRepository(ISqlConnectionFactory connectionFactory) : IArchiveRepository { public async Task> ListAsync(int? projectId, string? entityType) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Archive_List", new { ProjectID = projectId, EntityType = entityType }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListForUserAsync(int? projectId, string? entityType, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Archive_ListForUser", new { ProjectID = projectId, EntityType = entityType, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListProjectFiltersAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Archive_ProjectFilterList", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListProjectFiltersForUserAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Archive_ProjectFilterListForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task ArchiveAsync(string entityType, int entityId, string? reason) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Archive_Entity", new { EntityType = entityType, EntityID = entityId, ArchivedReason = reason }, commandType: CommandType.StoredProcedure); } public async Task RestoreAsync(string entityType, int entityId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Archive_Restore", new { EntityType = entityType, EntityID = entityId }, commandType: CommandType.StoredProcedure); } public async Task DeleteForeverAsync(string entityType, int entityId, string confirmationText, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Archive_DeleteForever", new { EntityType = entityType, EntityID = entityId, ConfirmationText = confirmationText, UserID = userId }, commandType: CommandType.StoredProcedure); } } public sealed class AcceptanceRepository(ISqlConnectionFactory connectionFactory) : IAcceptanceRepository { public async Task> GetPhase1ChecklistAsync(int? projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Acceptance_Phase1Checklist", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class SceneDependencyRepository(ISqlConnectionFactory connectionFactory) : ISceneDependencyRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependencyLookup_All", commandType: CommandType.StoredProcedure); return new SceneDependencyLookupData { DependencyTypes = rows.ToList() }; } public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListByProjectAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_ListByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> GetCountsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneDependency_Counts", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAsync(SceneDependency dependency) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneDependency_Save", new { dependency.SceneDependencyID, dependency.ProjectID, dependency.SourceSceneID, dependency.TargetSceneID, dependency.SceneDependencyTypeID, dependency.Description, dependency.IsHardDependency }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int sceneDependencyId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneDependency_Archive", new { SceneDependencyID = sceneDependencyId }, commandType: CommandType.StoredProcedure); } } public sealed class ProjectRepository(ISqlConnectionFactory connectionFactory) : IProjectRepository { public async Task> ListAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Project_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListForUserAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Project_ListForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListActiveForUserAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Project_ListActiveForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListArchivedForUserAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Project_ListArchivedForUser", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListOwnedForAccountDeletionAsync(int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Project_ListOwnedForAccountDeletion", new { UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Project_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task GetForUserAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Project_GetForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task GetArchivedForOwnerAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Project_GetArchivedForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task GetDashboardMetricsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ProjectDashboard_Metrics", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task> ListDashboardAttentionThreadsAsync(int projectId, int count) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectDashboard_AttentionThreads", new { ProjectID = projectId, Count = count }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListGenreMetricPresetsAsync() { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.GenreMetricPreset_List", commandType: CommandType.StoredProcedure); return rows.ToList(); } public Task SaveAsync(Project project) => SaveAsync(project, null); public Task SaveAsync(Project project, int userId, string? genreMetricPresetKey) => SaveInternalAsync(project, userId, genreMetricPresetKey); public async Task SaveAsync(Project project, string? genreMetricPresetKey) => await SaveInternalAsync(project, null, genreMetricPresetKey); private async Task SaveInternalAsync(Project project, int? userId, string? genreMetricPresetKey) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Project_Save", new { project.ProjectID, project.ProjectName, project.Description, GenreMetricPresetKey = genreMetricPresetKey, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Project_Archive", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveForUserAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Project_ArchiveForUser", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveForOwnerAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); var rowsAffected = await connection.QuerySingleAsync( "dbo.Project_ArchiveForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); return rowsAffected > 0; } public async Task RestoreForOwnerAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); var rowsAffected = await connection.QuerySingleAsync( "dbo.Project_RestoreForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); return rowsAffected > 0; } public async Task> ListHardDeleteFilePathsForOwnerAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectHardDelete_ListFilePathsForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.Where(path => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } public async Task HardDeleteForOwnerAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ProjectHardDelete_ForOwner", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task> ListHardDeleteFilePathsForAccountDeletionAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectHardDelete_ListFilePathsForAccountDeletion", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task HardDeleteForAccountDeletionAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ProjectHardDelete_ForAccountDeletion", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); } } public sealed class ProjectCollaborationRepository(ISqlConnectionFactory connectionFactory) : IProjectCollaborationRepository { public async Task GetOwnerUserIdAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.ProjectCollaboration_GetOwner", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task> ListCollaboratorsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectCollaboration_ListCollaborators", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListPendingInvitationsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectCollaboration_ListPendingInvitations", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task CollaboratorCountsForOwnerAsync(int ownerUserId, int collaboratorUserId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ProjectCollaboration_CollaboratorCountsForOwner", new { OwnerUserID = ownerUserId, CollaboratorUserID = collaboratorUserId }, commandType: CommandType.StoredProcedure); } public async Task AddCollaboratorAsync(int projectId, int userId, int invitedByUserId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ProjectCollaboration_AddCollaborator", new { ProjectID = projectId, UserID = userId, InvitedByUserID = invitedByUserId }, commandType: CommandType.StoredProcedure); } public async Task RemoveCollaboratorAsync(int projectId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ProjectCollaboration_RemoveCollaborator", new { ProjectID = projectId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task CreatePendingInvitationAsync(int projectId, string emailAddress, int invitedByUserId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ProjectCollaboration_CreatePendingInvitation", new { ProjectID = projectId, EmailAddress = emailAddress, InvitedByUserID = invitedByUserId }, commandType: CommandType.StoredProcedure); } } public sealed class ProjectActivityRepository(ISqlConnectionFactory connectionFactory) : IProjectActivityRepository { public async Task> ListForProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectActivity_ListForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRecentForProjectAsync(int projectId, int count) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectActivity_ListRecent", new { ProjectID = projectId, Count = count }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task RecordAsync(int projectId, int? userId, string activityType, string entityType, int? entityId, string? entityName, string? description = null) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ProjectAudit_Record", new { ProjectID = projectId, UserID = userId, ActivityType = activityType, EntityType = entityType, EntityID = entityId, EntityName = entityName, Description = description }, commandType: CommandType.StoredProcedure); } } public sealed class BookRepository(ISqlConnectionFactory connectionFactory) : IBookRepository { public async Task> ListByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Book_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListByProjectsAsync(IEnumerable projectIds) { var ids = projectIds.Distinct().ToArray(); if (ids.Length == 0) { return []; } using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Book_ListByProjects", new { ProjectIDs = string.Join(',', ids) }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Book_Get", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } public async Task GetCoverPathsAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.Book_GetCoverPaths", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Book book) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Book_Save", new { book.BookID, book.ProjectID, book.BookTitle, book.Subtitle, book.Tagline, book.ShortDescription, book.Status, book.BookNumber, book.Description, book.TargetWordCount, book.WritingStartDate, book.TargetCompletionDate, book.PublicationDate, book.UsesExplicitScenes }, commandType: CommandType.StoredProcedure); } public async Task UpdateCoverAsync(int bookId, string? thumbnailPath, string? standardPath) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Book_UpdateCover", new { BookID = bookId, CoverThumbnailPath = thumbnailPath, CoverStandardPath = standardPath }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Book_Archive", new { BookID = bookId }, commandType: CommandType.StoredProcedure); } } public sealed class ManuscriptDocumentRepository(ISqlConnectionFactory connectionFactory) : IManuscriptDocumentRepository { public async Task GetByBookAsync(int bookId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.ManuscriptDocument_GetByBook", new { BookID = bookId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task GetByGuidAsync(Guid documentGuid, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.ManuscriptDocument_GetByGuid", new { DocumentGuid = documentGuid, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(int bookId, Guid documentGuid, int userId) { using var connection = connectionFactory.CreateConnection(); try { return await connection.QuerySingleOrDefaultAsync( "dbo.ManuscriptDocument_Save", new { BookID = bookId, DocumentGuid = documentGuid, UserID = userId }, commandType: CommandType.StoredProcedure); } catch (SqlException ex) when (ex.Number == 51000) { throw new InvalidOperationException(ex.Message, ex); } } public async Task UnlinkBookAsync(int bookId, Guid? documentGuid, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.ManuscriptDocument_UnlinkBook", new { BookID = bookId, DocumentGuid = documentGuid, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task UpdateLastSyncAsync(int manuscriptDocumentId, int userId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ManuscriptDocument_UpdateLastSync", new { ManuscriptDocumentID = manuscriptDocumentId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task UpdateLastOpenedAsync(int manuscriptDocumentId, int userId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ManuscriptDocument_UpdateLastOpened", new { ManuscriptDocumentID = manuscriptDocumentId, UserID = userId }, commandType: CommandType.StoredProcedure); } } public sealed class ChapterRepository(ISqlConnectionFactory connectionFactory) : IChapterRepository { public async Task> ListByBookAsync(int bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Chapter_ListByBook", new { BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Chapter_Get", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(Chapter chapter) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Chapter_Save", new { chapter.ChapterID, chapter.BookID, chapter.ChapterNumber, chapter.ChapterTitle, chapter.Summary, chapter.ChapterPurposeID, chapter.RevisionStatusID }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); try { await connection.ExecuteAsync("dbo.Chapter_Archive", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } catch (SqlException ex) when (ex.Number == 51000) { throw new InvalidOperationException(ex.Message, ex); } } } public sealed class SceneRepository(ISqlConnectionFactory connectionFactory) : ISceneRepository { public async Task> ListByChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Scene_ListByChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListByFloorPlanAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Scene_ListByFloorPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Scene_Get", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); var scene = await result.ReadSingleOrDefaultAsync(); if (scene is null) { return null; } scene.SelectedPurposeTypeIds = (await result.ReadAsync()).ToList(); scene.Metrics = (await result.ReadAsync()).ToList(); return scene; } public async Task SaveAsync(Scene scene) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Scene_Save", new { scene.SceneID, scene.ChapterID, scene.SceneNumber, scene.SceneTitle, scene.Summary, scene.TimeModeID, scene.StartDateTime, scene.EndDateTime, scene.DurationAmount, scene.DurationUnitID, scene.RelativeTimeText, scene.TimeConfidenceID, scene.ScenePurposeNotes, scene.SceneOutcomeNotes, scene.RevisionStatusID, scene.PrimaryLocationID, scene.FloorPlanID, scene.InitialFloorPlanFloorID }, commandType: CommandType.StoredProcedure); } public async Task UpdatePovCharacterAsync(int sceneId, int? povCharacterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_UpdatePovCharacter", new { SceneID = sceneId, POVCharacterID = povCharacterId }, commandType: CommandType.StoredProcedure); } public async Task SaveFloorPlanLinkAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_FloorPlanLink_Save", new { SceneID = sceneId, FloorPlanID = floorPlanId, InitialFloorPlanFloorID = initialFloorPlanFloorId }, commandType: CommandType.StoredProcedure); } public async Task SavePurposesAsync(int sceneId, IEnumerable purposeIds) { using var connection = connectionFactory.CreateConnection(); var cleanPurposeIds = purposeIds .Where(id => id > 0) .Distinct() .ToList(); await connection.ExecuteAsync( "dbo.Scene_SavePurposes", new { SceneID = sceneId, PurposeIds = string.Join(',', cleanPurposeIds) }, commandType: CommandType.StoredProcedure); } public async Task SaveMetricValuesAsync(int sceneId, IEnumerable metricValues) { using var connection = connectionFactory.CreateConnection(); foreach (var metric in metricValues) { await connection.ExecuteAsync( "dbo.SceneMetric_SaveValue", new { SceneID = sceneId, metric.MetricTypeID, metric.Value, metric.Notes }, commandType: CommandType.StoredProcedure); } } public async Task SaveMetricValueAsync(int sceneId, int metricTypeId, int value) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.SceneMetric_SaveValue", new { SceneID = sceneId, MetricTypeID = metricTypeId, Value = value, Notes = (string?)null }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); try { await connection.ExecuteAsync("dbo.Scene_Archive", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); } catch (SqlException ex) when (ex.Number == 51000) { throw new InvalidOperationException(ex.Message, ex); } } public async Task MoveAsync(int sceneId, string direction) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_Move", new { SceneID = sceneId, Direction = direction }, commandType: CommandType.StoredProcedure); } public async Task> ListChapterOptionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.ChapterOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetMovePreviewAsync(int sceneId, int targetChapterId, string position) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneMovePreview_Get", new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task MoveToChapterAsync(int sceneId, int targetChapterId, string position, bool renumber) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_MoveToChapter", new { SceneID = sceneId, TargetChapterID = targetChapterId, Position = position, Renumber = renumber }, commandType: CommandType.StoredProcedure); } public async Task MoveRelativeAsync(int sceneId, int anchorSceneId, string position) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_MoveRelative", new { SceneID = sceneId, AnchorSceneID = anchorSceneId, Position = position }, commandType: CommandType.StoredProcedure); } public async Task RenumberChapterAsync(int chapterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Scene_RenumberChapter", new { ChapterID = chapterId }, commandType: CommandType.StoredProcedure); } } public sealed class SceneFloorPlanOccupancyRepository(ISqlConnectionFactory connectionFactory) : ISceneFloorPlanOccupancyRepository { public async Task> ListBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneFloorPlanOccupancy_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListByFloorPlanAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneFloorPlanOccupancy_ListByFloorPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListParticipantsByFloorPlanAsync(int floorPlanId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneFloorPlanParticipant_ListByFloorPlan", new { FloorPlanID = floorPlanId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAsync(int sceneId, int? floorPlanId, int? initialFloorPlanFloorId, IEnumerable occupancyRows) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Scene_FloorPlanLink_Save", new { SceneID = sceneId, FloorPlanID = floorPlanId, InitialFloorPlanFloorID = initialFloorPlanFloorId }, commandType: CommandType.StoredProcedure); foreach (var row in occupancyRows) { await connection.ExecuteAsync( "dbo.SceneFloorPlanOccupancy_Save", new { SceneID = sceneId, FloorPlanID = floorPlanId, row.CharacterID, row.StoryAssetID, LocationID = row.LocationID == 0 ? (int?)null : row.LocationID }, commandType: CommandType.StoredProcedure); } } } public sealed class LookupRepository(ISqlConnectionFactory connectionFactory) : ILookupRepository { public async Task GetAllAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.Lookup_All", commandType: CommandType.StoredProcedure); return new LookupData { RevisionStatuses = (await result.ReadAsync()).ToList(), TimeModes = (await result.ReadAsync()).ToList(), DurationUnits = (await result.ReadAsync()).ToList(), TimeConfidences = (await result.ReadAsync()).ToList(), ScenePurposeTypes = (await result.ReadAsync()).ToList(), ChapterPurposeTypes = (await result.ReadAsync()).ToList() }; } } public sealed class TimelineRepository(ISqlConnectionFactory connectionFactory) : ITimelineRepository { public async Task GetByProjectAsync(int projectId, int? bookId, bool includeMetrics, IEnumerable metricTypeIds, bool includePlotLines) { using var connection = connectionFactory.CreateConnection(); var selectedMetricIds = string.Join(',', metricTypeIds.Where(id => id > 0).Distinct()); using var result = await connection.QueryMultipleAsync( "dbo.Timeline_GetByProject", new { ProjectID = projectId, BookID = bookId, IncludeMetrics = includeMetrics, SelectedMetricTypeIDs = selectedMetricIds, IncludePlotLines = includePlotLines }, commandType: CommandType.StoredProcedure); var project = await result.ReadSingleOrDefaultAsync(); var books = (await result.ReadAsync()).ToList(); var chapters = (await result.ReadAsync()).ToList(); var scenes = (await result.ReadAsync()).ToList(); var purposeLabels = (await result.ReadAsync()).ToList(); var metricTypes = (await result.ReadAsync()).ToList(); var metricPoints = (await result.ReadAsync()).ToList(); var plotLines = (await result.ReadAsync()).ToList(); var threadEvents = (await result.ReadAsync()).ToList(); await PopulateThreadEventTargetsAsync(connection, threadEvents); return new TimelineData { Project = project, Books = books, Chapters = chapters, Scenes = scenes, PurposeLabels = purposeLabels, MetricTypes = metricTypes, MetricPoints = metricPoints, PlotLines = plotLines, ThreadEvents = threadEvents }; } private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList threadEvents) { var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList(); if (threadEventIds.Count == 0) { return; } var targetRows = await connection.QueryAsync( "dbo.ThreadEventTarget_ListByEvents", new { ThreadEventIDs = string.Join(',', threadEventIds) }, commandType: CommandType.StoredProcedure); var targetsByEvent = targetRows .GroupBy(x => x.PlotEventID) .ToDictionary(x => x.Key, x => (IReadOnlyList)x.Select(row => row.PlotLineID).ToList()); foreach (var threadEvent in threadEvents) { if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets)) { threadEvent.TargetPlotLineIDs = targets; } } } private sealed class ThreadEventTargetRow { public int PlotEventID { get; set; } public int PlotLineID { get; set; } } public async Task> GetSceneOverviewsAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.Timeline_SceneOverviews", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class TimelineSettingsRepository(ISqlConnectionFactory connectionFactory) : ITimelineSettingsRepository { public async Task GetAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.ProjectTimelineSettings_Get", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task> ListMetricTypesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectTimelineSettings_MetricTypes", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListMetricSettingsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.ProjectTimelineMetricSettings_List", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAsync(ProjectTimelineSettings settings, IEnumerable metricTypeIds) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.ProjectTimelineSettings_Save", new { settings.ProjectID, settings.ShowSceneCards, settings.ShowMetricShape, settings.ShowPlotLines, settings.ShowStoryAssets, settings.ShowCharacterAppearances, settings.ShowWarnings, MetricTypeIDs = string.Join(',', metricTypeIds.Where(id => id > 0).Distinct()) }, commandType: CommandType.StoredProcedure); } } public sealed class SceneMetricTypeRepository(ISqlConnectionFactory connectionFactory) : ISceneMetricTypeRepository { public async Task> ListForManagementAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneMetricType_ListForProjectManagement", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int metricTypeId, int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneMetricType_GetForProject", new { MetricTypeID = metricTypeId, ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(SceneMetricType metric) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneMetricType_SaveForProject", new { metric.MetricTypeID, metric.ProjectID, metric.MetricName, metric.Description, metric.MinValue, metric.MaxValue, metric.DefaultValue, metric.SortOrder, metric.IsActive }, commandType: CommandType.StoredProcedure); } public async Task SetActiveAsync(int metricTypeId, int projectId, bool isActive) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneMetricType_SetActiveForProject", new { MetricTypeID = metricTypeId, ProjectID = projectId, IsActive = isActive }, commandType: CommandType.StoredProcedure); } public async Task MoveAsync(int metricTypeId, int projectId, string direction) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneMetricType_MoveForProject", new { MetricTypeID = metricTypeId, ProjectID = projectId, Direction = direction }, commandType: CommandType.StoredProcedure); } public async Task AddMissingDefaultsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneMetricType_AddMissingDefaultsForProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task RemoveUnusedAsync(int metricTypeId, int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneMetricType_RemoveUnusedForProject", new { MetricTypeID = metricTypeId, ProjectID = projectId }, commandType: CommandType.StoredProcedure); } public async Task HasProjectMetricsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync("dbo.SceneMetricType_ProjectHasMetrics", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); } } public sealed class TimelinePresetRepository(ISqlConnectionFactory connectionFactory) : ITimelinePresetRepository { public async Task> ListAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.TimelineViewPreset_List", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAsync(int timelineViewPresetId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.TimelineViewPreset_Get", new { TimelineViewPresetID = timelineViewPresetId }, commandType: CommandType.StoredProcedure); } public async Task SaveAsync(TimelineViewPreset preset) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.TimelineViewPreset_Save", new { preset.TimelineViewPresetID, preset.ProjectID, preset.BookID, preset.PresetName, preset.FilterJson, preset.VisibilityJson, preset.FocusType, preset.FocusID }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAsync(int timelineViewPresetId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.TimelineViewPreset_Archive", new { TimelineViewPresetID = timelineViewPresetId }, commandType: CommandType.StoredProcedure); } } public sealed class PlotRepository(ISqlConnectionFactory connectionFactory) : IPlotRepository { public async Task GetLookupsAsync() { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.PlotLookup_All", commandType: CommandType.StoredProcedure); return new PlotLookupData { PlotLineTypes = (await result.ReadAsync()).ToList(), PlotImportance = (await result.ReadAsync()).ToList(), ThreadTypes = (await result.ReadAsync()).ToList(), ThreadStatuses = (await result.ReadAsync()).ToList(), ThreadEventTypes = (await result.ReadAsync()).ToList(), PlotEventTypes = (await result.ReadAsync()).ToList() }; } public async Task> ListPlotLinesAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotLine_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetPlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.PlotLine_Get", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); } public async Task SavePlotLineAsync(PlotLineItem plotLine) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.PlotLine_Save", new { plotLine.PlotLineID, plotLine.ProjectID, plotLine.BookID, plotLine.PlotLineName, plotLine.PlotLineTypeID, plotLine.PlotImportanceID, plotLine.Description, plotLine.ParentPlotLineID, plotLine.EmergesFromPlotLineID, plotLine.SortOrder, plotLine.Colour, plotLine.IsVisibleOnTimeline }, commandType: CommandType.StoredProcedure); } public async Task ArchivePlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.PlotLine_Archive", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); } public async Task> ListPlotThreadsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotThread_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListPlotThreadsByPlotLineAsync(int plotLineId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.PlotThread_ListByPlotLine", new { PlotLineID = plotLineId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetPlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.PlotThread_Get", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); } public async Task SavePlotThreadAsync(PlotThread plotThread) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.PlotThread_Save", new { plotThread.PlotThreadID, plotThread.PlotLineID, plotThread.ThreadTitle, plotThread.ThreadTypeID, plotThread.ThreadStatusID, plotThread.Importance, plotThread.Summary, plotThread.IntroducedSceneID, plotThread.PlannedResolutionSceneID, plotThread.ActualResolutionSceneID }, commandType: CommandType.StoredProcedure); } public async Task ArchivePlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.PlotThread_Archive", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure); } public async Task> ListThreadEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = (await connection.QueryAsync("dbo.ThreadEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure)).ToList(); await PopulateThreadEventTargetsAsync(connection, rows); return rows; } public async Task> ListThreadEventsByPlotThreadAsync(int plotThreadId) { using var connection = connectionFactory.CreateConnection(); var rows = (await connection.QueryAsync("dbo.ThreadEvent_ListByPlotThread", new { PlotThreadID = plotThreadId }, commandType: CommandType.StoredProcedure)).ToList(); await PopulateThreadEventTargetsAsync(connection, rows); return rows; } private static async Task PopulateThreadEventTargetsAsync(IDbConnection connection, IReadOnlyList threadEvents) { var threadEventIds = threadEvents.Select(x => x.ThreadEventID).ToList(); if (threadEventIds.Count == 0) { return; } var targetRows = await connection.QueryAsync( "dbo.ThreadEventTarget_ListByEvents", new { ThreadEventIDs = string.Join(',', threadEventIds) }, commandType: CommandType.StoredProcedure); var targetsByEvent = targetRows .GroupBy(x => x.PlotEventID) .ToDictionary(x => x.Key, x => (IReadOnlyList)x.Select(row => row.PlotLineID).ToList()); foreach (var threadEvent in threadEvents) { if (targetsByEvent.TryGetValue(threadEvent.ThreadEventID, out var targets)) { threadEvent.TargetPlotLineIDs = targets; } } } private sealed class ThreadEventTargetRow { public int PlotEventID { get; set; } public int PlotLineID { get; set; } } public async Task SaveThreadEventAsync(ThreadEvent threadEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.ThreadEvent_Save", new { threadEvent.ThreadEventID, threadEvent.PlotThreadID, threadEvent.SceneID, threadEvent.EventTypeID, threadEvent.PlotEventTypeID, threadEvent.TargetPlotLineID, threadEvent.EventTitle, threadEvent.EventDescription }, commandType: CommandType.StoredProcedure); } public async Task SaveThreadEventTargetsAsync(int threadEventId, IEnumerable plotLineIds) { using var connection = connectionFactory.CreateConnection(); var targetIds = plotLineIds.Where(id => id > 0).Distinct().ToList(); await connection.ExecuteAsync( "dbo.ThreadEventTarget_SaveForEvent", new { ThreadEventID = threadEventId, PlotLineIDs = string.Join(',', targetIds) }, commandType: CommandType.StoredProcedure); } public async Task DeleteThreadEventAsync(int threadEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.ThreadEvent_Delete", new { ThreadEventID = threadEventId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneOptionsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneOption_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class AssetRepository(ISqlConnectionFactory connectionFactory) : IAssetRepository { public async Task GetLookupsAsync(int? projectId = null) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.AssetLookup_All", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new AssetLookupData { AssetKinds = (await result.ReadAsync()).ToList(), AssetStates = (await result.ReadAsync()).ToList(), AssetEventTypes = (await result.ReadAsync()).ToList(), AssetDependencyTypes = (await result.ReadAsync()).ToList(), AssetCustodyEventTypes = (await result.ReadAsync()).ToList(), CustodyRoles = (await result.ReadAsync()).ToList() }; } public async Task> ListAssetsAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.StoryAsset_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.StoryAsset_Get", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); } public async Task SaveAssetAsync(StoryAsset asset) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.StoryAsset_Save", new { asset.StoryAssetID, asset.ProjectID, asset.AssetName, asset.AssetKindID, asset.Description, asset.Importance, asset.CurrentStateID, asset.CurrentLocationID, asset.IsResolved, asset.ShowInQuickAddBar, asset.ImagePath, asset.ThumbnailPath }, commandType: CommandType.StoredProcedure); } public async Task> ListAliasesAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.AssetAlias_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task AddAliasAsync(int storyAssetId, string alias, int? sortOrder) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetAlias_Add", new { StoryAssetID = storyAssetId, Alias = alias, SortOrder = sortOrder }, commandType: CommandType.StoredProcedure); } public async Task UpdateAliasAsync(int assetAliasId, string alias, int? sortOrder) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.AssetAlias_Update", new { AssetAliasID = assetAliasId, Alias = alias, SortOrder = sortOrder }, commandType: CommandType.StoredProcedure); } public async Task DeleteAliasAsync(int assetAliasId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.AssetAlias_Delete", new { AssetAliasID = assetAliasId }, commandType: CommandType.StoredProcedure); } public async Task UpdateAssetImageAsync(int storyAssetId, string? imagePath, string? thumbnailPath) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.StoryAsset_UpdateImage", new { StoryAssetID = storyAssetId, ImagePath = imagePath, ThumbnailPath = thumbnailPath }, commandType: CommandType.StoredProcedure); } public async Task ArchiveAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.StoryAsset_Archive", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); } public async Task> ListEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListEventsByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveEventAsync(AssetEvent assetEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetEvent_Save", new { assetEvent.AssetEventID, assetEvent.StoryAssetID, assetEvent.SceneID, assetEvent.AssetEventTypeID, assetEvent.FromStateID, assetEvent.ToStateID, assetEvent.EventTitle, assetEvent.EventDescription }, commandType: CommandType.StoredProcedure); } public async Task DeleteEventAsync(int assetEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetEvent_Delete", new { AssetEventID = assetEventId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneAssetSuggestionsAsync(int sceneId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneAssetSuggestions_ListByScene", new { SceneID = sceneId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task AcceptSceneAssetSuggestionAsync(int suggestionId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneAssetSuggestion_Accept", new { SceneAssetSuggestionID = suggestionId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task RejectSceneAssetSuggestionAsync(int suggestionId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneAssetSuggestion_Reject", new { SceneAssetSuggestionID = suggestionId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task> CountSceneAssetSuggestionsByChapterAsync(int chapterId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneAssetSuggestions_CountByChapter", new { ChapterID = chapterId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListDependenciesByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetDependency_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveDependencyAsync(AssetDependency dependency) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetDependency_Save", new { dependency.AssetDependencyID, dependency.SourceAssetID, dependency.TargetAssetID, dependency.AssetDependencyTypeID, dependency.SourceRequiredStateID, dependency.TargetBlockedStateID, dependency.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteDependencyAsync(int assetDependencyId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetDependency_Delete", new { AssetDependencyID = assetDependencyId }, commandType: CommandType.StoredProcedure); } public async Task> ListCustodyBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return await WithValidCustodianSummariesAsync(connection, rows.ToList()); } public async Task> ListCustodyByAssetAsync(int storyAssetId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.AssetCustodyEvent_ListByAsset", new { StoryAssetID = storyAssetId }, commandType: CommandType.StoredProcedure); return await WithValidCustodianSummariesAsync(connection, rows.ToList()); } public async Task> ListCustodyCharactersByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.AssetCustodyCharacter_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } private static async Task> WithValidCustodianSummariesAsync(IDbConnection connection, List rows) { if (rows.Count == 0) { return rows; } var eventIds = rows.Select(x => x.AssetCustodyEventID).Distinct().ToList(); var validCustodians = await connection.QueryAsync( "dbo.AssetCustodySummary_ListByEvents", new { AssetCustodyEventIDs = string.Join(',', eventIds) }, commandType: CommandType.StoredProcedure); var summaries = validCustodians .GroupBy(x => x.AssetCustodyEventID) .ToDictionary( x => x.Key, x => string.Join(", ", x .Select(row => string.IsNullOrWhiteSpace(row.RoleName) ? row.CharacterName : $"{row.CharacterName} ({row.RoleName})") .Where(label => !string.IsNullOrWhiteSpace(label)) .Distinct(StringComparer.OrdinalIgnoreCase))); foreach (var row in rows) { row.CustodianSummary = summaries.GetValueOrDefault(row.AssetCustodyEventID); } return rows.Where(x => !string.IsNullOrWhiteSpace(x.CustodianSummary)).ToList(); } private sealed class AssetCustodySummaryRow { public int AssetCustodyEventID { get; set; } public string CharacterName { get; set; } = string.Empty; public string? RoleName { get; set; } } public async Task SaveCustodyEventAsync(AssetCustodyEvent custodyEvent, string custodianNames, IEnumerable custodianCharacterIds, int custodyRoleId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.AssetCustodyEvent_Save", new { custodyEvent.AssetCustodyEventID, custodyEvent.StoryAssetID, custodyEvent.SceneID, custodyEvent.AssetCustodyEventTypeID, custodyEvent.Description, CustodianNames = custodianNames, CustodianCharacterIds = string.Join(',', custodianCharacterIds.Where(id => id > 0).Distinct()), CustodyRoleID = custodyRoleId }, commandType: CommandType.StoredProcedure); } public async Task DeleteCustodyEventAsync(int assetCustodyEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.AssetCustodyEvent_Delete", new { AssetCustodyEventID = assetCustodyEventId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Assets, IReadOnlyList Events)> GetTimelineAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.AssetTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var assets = (await result.ReadAsync()).ToList(); var events = (await result.ReadAsync()).ToList(); return (assets, events); } public async Task> CheckDependencyIssuesAsync(int storyAssetId, int sceneId, int? toStateId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.AssetDependency_CheckForEvent", new { StoryAssetID = storyAssetId, SceneID = sceneId, ToStateID = toStateId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } } public sealed class CharacterRepository(ISqlConnectionFactory connectionFactory) : ICharacterRepository { public async Task GetLookupsAsync(int? projectId = null) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.CharacterLookup_All", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new CharacterLookupData { RoleTypes = (await result.ReadAsync()).ToList(), PresenceTypes = (await result.ReadAsync()).ToList(), AttributeTypes = (await result.ReadAsync()).ToList(), KnowledgeStates = (await result.ReadAsync()).ToList(), RelationshipTypes = (await result.ReadAsync()).ToList(), RelationshipStates = (await result.ReadAsync()).ToList(), RelationshipCategories = (await result.ReadAsync()).ToList() }; } public async Task> ListCharactersAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Character_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync("dbo.Character_Get", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); } public async Task SaveCharacterAsync(Character character) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Character_Save", new { character.CharacterID, character.ProjectID, character.CharacterName, character.ShortName, character.SexValueID, character.Sex, character.BirthDate, character.AgeAtSeriesStart, character.AgeReferenceSceneID, character.Height, character.EyeColour, character.CharacterImportance, character.ShowInQuickAddBar, character.DefaultDescription, character.ImagePath, character.ThumbnailPath }, commandType: CommandType.StoredProcedure); } public async Task> ListAliasesAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.CharacterAlias_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task AddAliasAsync(int characterId, string alias, int? sortOrder) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterAlias_Add", new { CharacterID = characterId, Alias = alias, SortOrder = sortOrder }, commandType: CommandType.StoredProcedure); } public async Task UpdateAliasAsync(int characterAliasId, string alias, int? sortOrder) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.CharacterAlias_Update", new { CharacterAliasID = characterAliasId, Alias = alias, SortOrder = sortOrder }, commandType: CommandType.StoredProcedure); } public async Task DeleteAliasAsync(int characterAliasId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.CharacterAlias_Delete", new { CharacterAliasID = characterAliasId }, commandType: CommandType.StoredProcedure); } public async Task> ListSexValuesAsync(int ownerUserId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.CharacterSexValue_ListForOwner", new { OwnerUserID = ownerUserId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetSexValueAsync(int characterSexValueId, int ownerUserId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.CharacterSexValue_Get", new { CharacterSexValueID = characterSexValueId, OwnerUserID = ownerUserId }, commandType: CommandType.StoredProcedure); } public async Task GetOrCreateSexValueAsync(int ownerUserId, string sexName) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterSexValue_GetOrCreate", new { OwnerUserID = ownerUserId, SexName = sexName }, commandType: CommandType.StoredProcedure); } public async Task UpdateCharacterImageAsync(int characterId, string? imagePath, string? thumbnailPath) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Character_UpdateImage", new { CharacterID = characterId, ImagePath = imagePath, ThumbnailPath = thumbnailPath }, commandType: CommandType.StoredProcedure); } public async Task UpdateCharacterAvatarAsync(int characterId, int? sourceCharacterImageId, string? avatarImagePath, string? avatarThumbnailPath) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.Character_UpdateAvatar", new { CharacterID = characterId, AvatarSourceCharacterImageID = sourceCharacterImageId, AvatarImagePath = avatarImagePath, AvatarThumbnailPath = avatarThumbnailPath }, commandType: CommandType.StoredProcedure); } public async Task> ListCharacterImagesAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.CharacterImage_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetCharacterImageAsync(int characterImageId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.CharacterImage_Get", new { CharacterImageID = characterImageId }, commandType: CommandType.StoredProcedure); } public async Task CreateCharacterImageAsync(CharacterImage image) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterImage_Create", new { image.CharacterID, image.ProjectID, image.ImagePath, image.ThumbnailPath, image.Caption, InputSortOrder = image.SortOrder == 0 ? (int?)null : image.SortOrder }, commandType: CommandType.StoredProcedure); } public async Task DeleteCharacterImageAsync(int characterImageId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync( "dbo.CharacterImage_Delete", new { CharacterImageID = characterImageId }, commandType: CommandType.StoredProcedure); } public async Task ArchiveCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Character_Archive", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneCharactersAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.SceneCharacter_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveSceneCharacterAsync(SceneCharacter sceneCharacter) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.SceneCharacter_Save", new { sceneCharacter.SceneCharacterID, sceneCharacter.SceneID, sceneCharacter.CharacterID, sceneCharacter.RoleInSceneTypeID, sceneCharacter.PresenceTypeID, sceneCharacter.LocationID, sceneCharacter.EntryLocationID, sceneCharacter.ExitLocationID, sceneCharacter.AppearanceNotes, sceneCharacter.OutfitDescription, sceneCharacter.PhysicalCondition, sceneCharacter.EmotionalState, sceneCharacter.KnowledgeNotes }, commandType: CommandType.StoredProcedure); } public async Task DeleteSceneCharacterAsync(int sceneCharacterId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.SceneCharacter_Delete", new { SceneCharacterID = sceneCharacterId }, commandType: CommandType.StoredProcedure); } public async Task> ListSceneCharacterSuggestionsAsync(int sceneId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneCharacterSuggestions_ListByScene", new { SceneID = sceneId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task AcceptSceneCharacterSuggestionAsync(int suggestionId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneCharacterSuggestion_Accept", new { SceneCharacterSuggestionID = suggestionId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task RejectSceneCharacterSuggestionAsync(int suggestionId, int userId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.SceneCharacterSuggestion_Reject", new { SceneCharacterSuggestionID = suggestionId, UserID = userId }, commandType: CommandType.StoredProcedure); } public async Task> CountSceneCharacterSuggestionsByChapterAsync(int chapterId, int userId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync( "dbo.SceneCharacterSuggestions_CountByChapter", new { ChapterID = chapterId, UserID = userId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAttributeEventsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterAttributeEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListAttributeEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterAttributeEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveAttributeEventAsync(CharacterAttributeEvent attributeEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterAttributeEvent_Save", new { attributeEvent.CharacterAttributeEventID, attributeEvent.CharacterID, attributeEvent.CharacterAttributeTypeID, attributeEvent.SceneID, attributeEvent.AttributeValue, attributeEvent.Description }, commandType: CommandType.StoredProcedure); } public async Task> ListKnowledgeBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterKnowledge_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListKnowledgeByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.CharacterKnowledge_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveKnowledgeAsync(CharacterKnowledgeItem knowledge) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.CharacterKnowledge_Save", new { knowledge.CharacterKnowledgeID, knowledge.CharacterID, knowledge.StoryAssetID, knowledge.PlotThreadID, knowledge.SceneID, knowledge.KnowledgeStateID, knowledge.SourceEventID, knowledge.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteKnowledgeAsync(int characterKnowledgeId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.CharacterKnowledge_Delete", new { CharacterKnowledgeID = characterKnowledgeId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipsByProjectAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task GetRelationshipAsync(int characterRelationshipId) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleOrDefaultAsync( "dbo.Relationship_Get", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task SaveRelationshipAsync(CharacterRelationship relationship) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.Relationship_Save", new { relationship.CharacterRelationshipID, relationship.ProjectID, relationship.CharacterAID, relationship.CharacterBID, relationship.RelationshipTypeID, relationship.IsPermanent, relationship.StartSceneID, relationship.EndSceneID, relationship.IsKnownToReader, relationship.Notes, relationship.IsInitialRelationship, relationship.InitialBookID, relationship.ReaderInitiallyKnows, relationship.InitialRelationshipStateID, relationship.InitialIntensity, relationship.IsReciprocal }, commandType: CommandType.StoredProcedure); } public async Task> ListInitialRelationshipsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.Relationship_ListInitialByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task ArchiveRelationshipAsync(int characterRelationshipId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.Relationship_Archive", new { CharacterRelationshipID = characterRelationshipId }, commandType: CommandType.StoredProcedure); } public async Task> ListRelationshipEventsBySceneAsync(int sceneId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.RelationshipEvent_ListByScene", new { SceneID = sceneId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task> ListRelationshipEventsByCharacterAsync(int characterId) { using var connection = connectionFactory.CreateConnection(); var rows = await connection.QueryAsync("dbo.RelationshipEvent_ListByCharacter", new { CharacterID = characterId }, commandType: CommandType.StoredProcedure); return rows.ToList(); } public async Task SaveRelationshipEventAsync(RelationshipEvent relationshipEvent) { using var connection = connectionFactory.CreateConnection(); return await connection.QuerySingleAsync( "dbo.RelationshipEvent_Save", new { relationshipEvent.RelationshipEventID, relationshipEvent.CharacterRelationshipID, relationshipEvent.SceneID, relationshipEvent.RelationshipStateID, relationshipEvent.Intensity, relationshipEvent.IsKnownToOtherCharacter, relationshipEvent.Description }, commandType: CommandType.StoredProcedure); } public async Task DeleteRelationshipEventAsync(int relationshipEventId) { using var connection = connectionFactory.CreateConnection(); await connection.ExecuteAsync("dbo.RelationshipEvent_Delete", new { RelationshipEventID = relationshipEventId }, commandType: CommandType.StoredProcedure); } public async Task<(IReadOnlyList Characters, IReadOnlyList Appearances)> GetTimelineAsync(int projectId, int? bookId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.CharacterTimeline_GetByProject", new { ProjectID = projectId, BookID = bookId }, commandType: CommandType.StoredProcedure); var characters = (await result.ReadAsync()).ToList(); var appearances = (await result.ReadAsync()).ToList(); return (characters, appearances); } public async Task GetRelationshipMapAsync(int projectId) { using var connection = connectionFactory.CreateConnection(); using var result = await connection.QueryMultipleAsync("dbo.RelationshipMap_GetByProject", new { ProjectID = projectId }, commandType: CommandType.StoredProcedure); return new RelationshipMapData { Characters = (await result.ReadAsync()).ToList(), Books = (await result.ReadAsync()).ToList(), Chapters = (await result.ReadAsync()).ToList(), Scenes = (await result.ReadAsync()).ToList(), Relationships = (await result.ReadAsync()).ToList(), RelationshipEvents = (await result.ReadAsync()).ToList(), RelationshipCategories = (await result.ReadAsync()).ToList() }; } }