From e66b43064aecdd9a53f93e4fad2bc5c10eda6550 Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Fri, 12 Jun 2026 21:03:22 +0100 Subject: [PATCH] Phase 9B is implemented: database foundation only, no schedule generation or UI. --- PlotLine/Data/Repositories.cs | 164 +++++++++ PlotLine/Models/CoreModels.cs | 35 ++ PlotLine/Program.cs | 2 + PlotLine/Services/CoreServices.cs | 175 ++++++++++ .../056_Phase9B_WritingScheduleFoundation.sql | 323 ++++++++++++++++++ PlotLine/ViewModels/CoreViewModels.cs | 35 ++ 6 files changed, 734 insertions(+) create mode 100644 PlotLine/Sql/056_Phase9B_WritingScheduleFoundation.sql diff --git a/PlotLine/Data/Repositories.cs b/PlotLine/Data/Repositories.cs index a54cd16..ef7c6f0 100644 --- a/PlotLine/Data/Repositories.cs +++ b/PlotLine/Data/Repositories.cs @@ -272,6 +272,22 @@ public interface IWriterWorkspaceRepository 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); +} + public interface IExportRepository { Task GetStoryBibleAsync(int projectId); @@ -842,6 +858,154 @@ public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFa } } +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 sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository { public async Task GetStoryBibleAsync(int projectId) diff --git a/PlotLine/Models/CoreModels.cs b/PlotLine/Models/CoreModels.cs index 2903e84..1488c1c 100644 --- a/PlotLine/Models/CoreModels.cs +++ b/PlotLine/Models/CoreModels.cs @@ -1572,6 +1572,41 @@ public sealed class WriterDashboardScene public string? PinnedNotes { get; set; } } +public sealed class WritingPlan +{ + public int WritingPlanID { get; set; } + public int UserID { get; set; } + public int ProjectID { get; set; } + public int? BookID { get; set; } + public string PlanName { get; set; } = string.Empty; + public string GoalType { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public DateTime DeadlineDate { get; set; } + public string AvailableDaysJson { get; set; } = string.Empty; + public int SessionLengthMinutes { get; set; } + public bool IncludeBlockedScenes { get; set; } + public string RebalanceMode { get; set; } = "Ask"; + public bool IsActive { get; set; } = true; + public DateTime CreatedDateUtc { get; set; } + public DateTime ModifiedDateUtc { get; set; } +} + +public sealed class WritingScheduleItem +{ + public int WritingScheduleItemID { get; set; } + public int WritingPlanID { get; set; } + public int SceneID { get; set; } + public DateTime ScheduledDate { get; set; } + public string TaskType { get; set; } = string.Empty; + public int? EstimatedMinutes { get; set; } + public int? TargetWords { get; set; } + public string ScheduleStatus { get; set; } = string.Empty; + public DateTime? CompletedDateUtc { get; set; } + public string? Notes { get; set; } + public DateTime CreatedDateUtc { get; set; } + public DateTime ModifiedDateUtc { get; set; } +} + public sealed class ChapterWorkflowScene { public int SceneID { get; set; } diff --git a/PlotLine/Program.cs b/PlotLine/Program.cs index 20bbd46..6a106f5 100644 --- a/PlotLine/Program.cs +++ b/PlotLine/Program.cs @@ -96,6 +96,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -127,6 +128,7 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/PlotLine/Services/CoreServices.cs b/PlotLine/Services/CoreServices.cs index c657f9c..e021b40 100644 --- a/PlotLine/Services/CoreServices.cs +++ b/PlotLine/Services/CoreServices.cs @@ -256,6 +256,22 @@ public interface IWriterWorkspaceService Task DeleteAttachmentAsync(int sceneAttachmentId); } +public interface IWritingScheduleService +{ + Task> GetWritingPlansByUserAsync(int userId); + Task GetWritingPlanByIDAsync(int writingPlanId); + Task CreateWritingPlanAsync(WritingPlanViewModel model); + Task UpdateWritingPlanAsync(WritingPlanViewModel model); + Task DeleteWritingPlanAsync(int writingPlanId); + Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive); + Task> GetScheduleItemsByPlanAsync(int writingPlanId); + Task> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate); + Task CreateScheduleItemAsync(WritingScheduleItemViewModel model); + Task UpdateScheduleItemAsync(WritingScheduleItemViewModel model); + Task DeleteScheduleItemAsync(int writingScheduleItemId); + Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc); +} + public interface IStorageService { Task GetSceneStorageUsageAsync(int sceneId); @@ -3259,6 +3275,165 @@ public sealed class WriterWorkspaceService( public Task DeleteAttachmentAsync(int sceneAttachmentId) => writerWorkspace.DeleteAttachmentAsync(sceneAttachmentId); } +public sealed class WritingScheduleService(IWritingScheduleRepository writingSchedule) : IWritingScheduleService +{ + public async Task> GetWritingPlansByUserAsync(int userId) + { + var plans = await writingSchedule.GetWritingPlansByUserAsync(userId); + return plans.Select(ToViewModel).ToList(); + } + + public async Task GetWritingPlanByIDAsync(int writingPlanId) + { + var plan = await writingSchedule.GetWritingPlanByIDAsync(writingPlanId); + return plan is null ? null : ToViewModel(plan); + } + + public Task CreateWritingPlanAsync(WritingPlanViewModel model) + { + ValidatePlan(model); + return writingSchedule.CreateWritingPlanAsync(ToModel(model)); + } + + public Task UpdateWritingPlanAsync(WritingPlanViewModel model) + { + ValidatePlan(model); + return writingSchedule.UpdateWritingPlanAsync(ToModel(model)); + } + + public Task DeleteWritingPlanAsync(int writingPlanId) => + writingSchedule.DeleteWritingPlanAsync(writingPlanId); + + public Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive) => + writingSchedule.SetWritingPlanActiveAsync(writingPlanId, isActive); + + public async Task> GetScheduleItemsByPlanAsync(int writingPlanId) + { + var items = await writingSchedule.GetScheduleItemsByPlanAsync(writingPlanId); + return items.Select(ToViewModel).ToList(); + } + + public async Task> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate) + { + var items = await writingSchedule.GetScheduleItemsByDateRangeAsync(writingPlanId, startDate, endDate); + return items.Select(ToViewModel).ToList(); + } + + public Task CreateScheduleItemAsync(WritingScheduleItemViewModel model) + { + ValidateScheduleItem(model); + return writingSchedule.CreateScheduleItemAsync(ToModel(model)); + } + + public Task UpdateScheduleItemAsync(WritingScheduleItemViewModel model) + { + ValidateScheduleItem(model); + return writingSchedule.UpdateScheduleItemAsync(ToModel(model)); + } + + public Task DeleteScheduleItemAsync(int writingScheduleItemId) => + writingSchedule.DeleteScheduleItemAsync(writingScheduleItemId); + + public Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc) => + writingSchedule.UpdateScheduleItemStatusAsync(writingScheduleItemId, scheduleStatus, completedDateUtc); + + private static void ValidatePlan(WritingPlanViewModel model) + { + if (model.DeadlineDate.Date < model.StartDate.Date) + { + throw new InvalidOperationException("Deadline date must be on or after the start date."); + } + + if (model.SessionLengthMinutes <= 0) + { + throw new InvalidOperationException("Session length must be greater than zero."); + } + } + + private static void ValidateScheduleItem(WritingScheduleItemViewModel model) + { + if (model.TargetWords < 0) + { + throw new InvalidOperationException("Target words cannot be negative."); + } + + if (model.EstimatedMinutes < 0) + { + throw new InvalidOperationException("Estimated minutes cannot be negative."); + } + } + + private static WritingPlanViewModel ToViewModel(WritingPlan plan) => new() + { + WritingPlanID = plan.WritingPlanID, + UserID = plan.UserID, + ProjectID = plan.ProjectID, + BookID = plan.BookID, + PlanName = plan.PlanName, + GoalType = plan.GoalType, + StartDate = plan.StartDate, + DeadlineDate = plan.DeadlineDate, + AvailableDaysJson = plan.AvailableDaysJson, + SessionLengthMinutes = plan.SessionLengthMinutes, + IncludeBlockedScenes = plan.IncludeBlockedScenes, + RebalanceMode = plan.RebalanceMode, + IsActive = plan.IsActive, + CreatedDateUtc = plan.CreatedDateUtc, + ModifiedDateUtc = plan.ModifiedDateUtc + }; + + private static WritingPlan ToModel(WritingPlanViewModel model) => new() + { + WritingPlanID = model.WritingPlanID, + UserID = model.UserID, + ProjectID = model.ProjectID, + BookID = model.BookID, + PlanName = model.PlanName, + GoalType = model.GoalType, + StartDate = model.StartDate, + DeadlineDate = model.DeadlineDate, + AvailableDaysJson = model.AvailableDaysJson, + SessionLengthMinutes = model.SessionLengthMinutes, + IncludeBlockedScenes = model.IncludeBlockedScenes, + RebalanceMode = model.RebalanceMode, + IsActive = model.IsActive, + CreatedDateUtc = model.CreatedDateUtc, + ModifiedDateUtc = model.ModifiedDateUtc + }; + + private static WritingScheduleItemViewModel ToViewModel(WritingScheduleItem item) => new() + { + WritingScheduleItemID = item.WritingScheduleItemID, + WritingPlanID = item.WritingPlanID, + SceneID = item.SceneID, + ScheduledDate = item.ScheduledDate, + TaskType = item.TaskType, + EstimatedMinutes = item.EstimatedMinutes, + TargetWords = item.TargetWords, + ScheduleStatus = item.ScheduleStatus, + CompletedDateUtc = item.CompletedDateUtc, + Notes = item.Notes, + CreatedDateUtc = item.CreatedDateUtc, + ModifiedDateUtc = item.ModifiedDateUtc + }; + + private static WritingScheduleItem ToModel(WritingScheduleItemViewModel model) => new() + { + WritingScheduleItemID = model.WritingScheduleItemID, + WritingPlanID = model.WritingPlanID, + SceneID = model.SceneID, + ScheduledDate = model.ScheduledDate, + TaskType = model.TaskType, + EstimatedMinutes = model.EstimatedMinutes, + TargetWords = model.TargetWords, + ScheduleStatus = model.ScheduleStatus, + CompletedDateUtc = model.CompletedDateUtc, + Notes = model.Notes, + CreatedDateUtc = model.CreatedDateUtc, + ModifiedDateUtc = model.ModifiedDateUtc + }; +} + public sealed class ExportService(IExportRepository exports) : IExportService { public async Task<(string FileName, string ContentType, string Content)> ExportStoryBibleAsync(int projectId, string format) diff --git a/PlotLine/Sql/056_Phase9B_WritingScheduleFoundation.sql b/PlotLine/Sql/056_Phase9B_WritingScheduleFoundation.sql new file mode 100644 index 0000000..9e08d10 --- /dev/null +++ b/PlotLine/Sql/056_Phase9B_WritingScheduleFoundation.sql @@ -0,0 +1,323 @@ +SET ANSI_NULLS ON; +GO +SET QUOTED_IDENTIFIER ON; +GO + +IF OBJECT_ID(N'dbo.WritingPlans', N'U') IS NULL +BEGIN + CREATE TABLE dbo.WritingPlans + ( + WritingPlanID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_WritingPlans PRIMARY KEY, + UserID int NOT NULL, + ProjectID int NOT NULL, + BookID int NULL, + PlanName nvarchar(200) NOT NULL, + GoalType nvarchar(50) NOT NULL, + StartDate date NOT NULL, + DeadlineDate date NOT NULL, + AvailableDaysJson nvarchar(max) NOT NULL, + SessionLengthMinutes int NOT NULL, + IncludeBlockedScenes bit NOT NULL CONSTRAINT DF_WritingPlans_IncludeBlockedScenes DEFAULT 0, + RebalanceMode nvarchar(50) NOT NULL CONSTRAINT DF_WritingPlans_RebalanceMode DEFAULT N'Ask', + IsActive bit NOT NULL CONSTRAINT DF_WritingPlans_IsActive DEFAULT 1, + CreatedDateUtc datetime2 NOT NULL CONSTRAINT DF_WritingPlans_CreatedDateUtc DEFAULT GETUTCDATE(), + ModifiedDateUtc datetime2 NOT NULL CONSTRAINT DF_WritingPlans_ModifiedDateUtc DEFAULT GETUTCDATE(), + CONSTRAINT FK_WritingPlans_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID), + CONSTRAINT FK_WritingPlans_Projects FOREIGN KEY (ProjectID) REFERENCES dbo.Projects(ProjectID), + CONSTRAINT FK_WritingPlans_Books FOREIGN KEY (BookID) REFERENCES dbo.Books(BookID) + ); +END; +GO + +IF OBJECT_ID(N'dbo.WritingScheduleItems', N'U') IS NULL +BEGIN + CREATE TABLE dbo.WritingScheduleItems + ( + WritingScheduleItemID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_WritingScheduleItems PRIMARY KEY, + WritingPlanID int NOT NULL, + SceneID int NOT NULL, + ScheduledDate date NOT NULL, + TaskType nvarchar(50) NOT NULL, + EstimatedMinutes int NULL, + TargetWords int NULL, + ScheduleStatus nvarchar(50) NOT NULL, + CompletedDateUtc datetime2 NULL, + Notes nvarchar(1000) NULL, + CreatedDateUtc datetime2 NOT NULL CONSTRAINT DF_WritingScheduleItems_CreatedDateUtc DEFAULT GETUTCDATE(), + ModifiedDateUtc datetime2 NOT NULL CONSTRAINT DF_WritingScheduleItems_ModifiedDateUtc DEFAULT GETUTCDATE(), + CONSTRAINT FK_WritingScheduleItems_WritingPlans FOREIGN KEY (WritingPlanID) REFERENCES dbo.WritingPlans(WritingPlanID), + CONSTRAINT FK_WritingScheduleItems_Scenes FOREIGN KEY (SceneID) REFERENCES dbo.Scenes(SceneID) + ); +END; +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingPlans_UserID' AND object_id = OBJECT_ID(N'dbo.WritingPlans')) + CREATE INDEX IX_WritingPlans_UserID ON dbo.WritingPlans(UserID); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingPlans_ProjectID' AND object_id = OBJECT_ID(N'dbo.WritingPlans')) + CREATE INDEX IX_WritingPlans_ProjectID ON dbo.WritingPlans(ProjectID); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingPlans_IsActive' AND object_id = OBJECT_ID(N'dbo.WritingPlans')) + CREATE INDEX IX_WritingPlans_IsActive ON dbo.WritingPlans(IsActive); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingScheduleItems_WritingPlanID' AND object_id = OBJECT_ID(N'dbo.WritingScheduleItems')) + CREATE INDEX IX_WritingScheduleItems_WritingPlanID ON dbo.WritingScheduleItems(WritingPlanID); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingScheduleItems_SceneID' AND object_id = OBJECT_ID(N'dbo.WritingScheduleItems')) + CREATE INDEX IX_WritingScheduleItems_SceneID ON dbo.WritingScheduleItems(SceneID); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingScheduleItems_ScheduledDate' AND object_id = OBJECT_ID(N'dbo.WritingScheduleItems')) + CREATE INDEX IX_WritingScheduleItems_ScheduledDate ON dbo.WritingScheduleItems(ScheduledDate); +GO + +IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_WritingScheduleItems_ScheduleStatus' AND object_id = OBJECT_ID(N'dbo.WritingScheduleItems')) + CREATE INDEX IX_WritingScheduleItems_ScheduleStatus ON dbo.WritingScheduleItems(ScheduleStatus); +GO + +CREATE OR ALTER PROCEDURE dbo.GetWritingPlansByUser + @UserID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT WritingPlanID, UserID, ProjectID, BookID, PlanName, GoalType, StartDate, DeadlineDate, + AvailableDaysJson, SessionLengthMinutes, IncludeBlockedScenes, RebalanceMode, IsActive, + CreatedDateUtc, ModifiedDateUtc + FROM dbo.WritingPlans + WHERE UserID = @UserID + ORDER BY IsActive DESC, DeadlineDate, StartDate, WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.GetWritingPlanByID + @WritingPlanID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT WritingPlanID, UserID, ProjectID, BookID, PlanName, GoalType, StartDate, DeadlineDate, + AvailableDaysJson, SessionLengthMinutes, IncludeBlockedScenes, RebalanceMode, IsActive, + CreatedDateUtc, ModifiedDateUtc + FROM dbo.WritingPlans + WHERE WritingPlanID = @WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CreateWritingPlan + @UserID int, + @ProjectID int, + @BookID int = NULL, + @PlanName nvarchar(200), + @GoalType nvarchar(50), + @StartDate date, + @DeadlineDate date, + @AvailableDaysJson nvarchar(max), + @SessionLengthMinutes int, + @IncludeBlockedScenes bit = 0, + @RebalanceMode nvarchar(50) = N'Ask', + @IsActive bit = 1 +AS +BEGIN + SET NOCOUNT ON; + + INSERT dbo.WritingPlans + ( + UserID, ProjectID, BookID, PlanName, GoalType, StartDate, DeadlineDate, AvailableDaysJson, + SessionLengthMinutes, IncludeBlockedScenes, RebalanceMode, IsActive + ) + VALUES + ( + @UserID, @ProjectID, @BookID, @PlanName, @GoalType, @StartDate, @DeadlineDate, @AvailableDaysJson, + @SessionLengthMinutes, @IncludeBlockedScenes, @RebalanceMode, @IsActive + ); + + SELECT CONVERT(int, SCOPE_IDENTITY()); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.UpdateWritingPlan + @WritingPlanID int, + @UserID int, + @ProjectID int, + @BookID int = NULL, + @PlanName nvarchar(200), + @GoalType nvarchar(50), + @StartDate date, + @DeadlineDate date, + @AvailableDaysJson nvarchar(max), + @SessionLengthMinutes int, + @IncludeBlockedScenes bit = 0, + @RebalanceMode nvarchar(50) = N'Ask', + @IsActive bit = 1 +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.WritingPlans + SET UserID = @UserID, + ProjectID = @ProjectID, + BookID = @BookID, + PlanName = @PlanName, + GoalType = @GoalType, + StartDate = @StartDate, + DeadlineDate = @DeadlineDate, + AvailableDaysJson = @AvailableDaysJson, + SessionLengthMinutes = @SessionLengthMinutes, + IncludeBlockedScenes = @IncludeBlockedScenes, + RebalanceMode = @RebalanceMode, + IsActive = @IsActive, + ModifiedDateUtc = GETUTCDATE() + WHERE WritingPlanID = @WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.DeleteWritingPlan + @WritingPlanID int +AS +BEGIN + SET NOCOUNT ON; + + DELETE FROM dbo.WritingScheduleItems + WHERE WritingPlanID = @WritingPlanID; + + DELETE FROM dbo.WritingPlans + WHERE WritingPlanID = @WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.SetWritingPlanActive + @WritingPlanID int, + @IsActive bit +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.WritingPlans + SET IsActive = @IsActive, + ModifiedDateUtc = GETUTCDATE() + WHERE WritingPlanID = @WritingPlanID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.GetScheduleItemsByPlan + @WritingPlanID int +AS +BEGIN + SET NOCOUNT ON; + + SELECT WritingScheduleItemID, WritingPlanID, SceneID, ScheduledDate, TaskType, EstimatedMinutes, + TargetWords, ScheduleStatus, CompletedDateUtc, Notes, CreatedDateUtc, ModifiedDateUtc + FROM dbo.WritingScheduleItems + WHERE WritingPlanID = @WritingPlanID + ORDER BY ScheduledDate, WritingScheduleItemID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.GetScheduleItemsByDateRange + @WritingPlanID int, + @StartDate date, + @EndDate date +AS +BEGIN + SET NOCOUNT ON; + + SELECT WritingScheduleItemID, WritingPlanID, SceneID, ScheduledDate, TaskType, EstimatedMinutes, + TargetWords, ScheduleStatus, CompletedDateUtc, Notes, CreatedDateUtc, ModifiedDateUtc + FROM dbo.WritingScheduleItems + WHERE WritingPlanID = @WritingPlanID + AND ScheduledDate >= @StartDate + AND ScheduledDate <= @EndDate + ORDER BY ScheduledDate, WritingScheduleItemID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.CreateScheduleItem + @WritingPlanID int, + @SceneID int, + @ScheduledDate date, + @TaskType nvarchar(50), + @EstimatedMinutes int = NULL, + @TargetWords int = NULL, + @ScheduleStatus nvarchar(50), + @CompletedDateUtc datetime2 = NULL, + @Notes nvarchar(1000) = NULL +AS +BEGIN + SET NOCOUNT ON; + + INSERT dbo.WritingScheduleItems + ( + WritingPlanID, SceneID, ScheduledDate, TaskType, EstimatedMinutes, TargetWords, + ScheduleStatus, CompletedDateUtc, Notes + ) + VALUES + ( + @WritingPlanID, @SceneID, @ScheduledDate, @TaskType, @EstimatedMinutes, @TargetWords, + @ScheduleStatus, @CompletedDateUtc, @Notes + ); + + SELECT CONVERT(int, SCOPE_IDENTITY()); +END; +GO + +CREATE OR ALTER PROCEDURE dbo.UpdateScheduleItem + @WritingScheduleItemID int, + @WritingPlanID int, + @SceneID int, + @ScheduledDate date, + @TaskType nvarchar(50), + @EstimatedMinutes int = NULL, + @TargetWords int = NULL, + @ScheduleStatus nvarchar(50), + @CompletedDateUtc datetime2 = NULL, + @Notes nvarchar(1000) = NULL +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.WritingScheduleItems + SET WritingPlanID = @WritingPlanID, + SceneID = @SceneID, + ScheduledDate = @ScheduledDate, + TaskType = @TaskType, + EstimatedMinutes = @EstimatedMinutes, + TargetWords = @TargetWords, + ScheduleStatus = @ScheduleStatus, + CompletedDateUtc = @CompletedDateUtc, + Notes = @Notes, + ModifiedDateUtc = GETUTCDATE() + WHERE WritingScheduleItemID = @WritingScheduleItemID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.DeleteScheduleItem + @WritingScheduleItemID int +AS +BEGIN + SET NOCOUNT ON; + + DELETE FROM dbo.WritingScheduleItems + WHERE WritingScheduleItemID = @WritingScheduleItemID; +END; +GO + +CREATE OR ALTER PROCEDURE dbo.UpdateScheduleItemStatus + @WritingScheduleItemID int, + @ScheduleStatus nvarchar(50), + @CompletedDateUtc datetime2 = NULL +AS +BEGIN + SET NOCOUNT ON; + + UPDATE dbo.WritingScheduleItems + SET ScheduleStatus = @ScheduleStatus, + CompletedDateUtc = @CompletedDateUtc, + ModifiedDateUtc = GETUTCDATE() + WHERE WritingScheduleItemID = @WritingScheduleItemID; +END; +GO diff --git a/PlotLine/ViewModels/CoreViewModels.cs b/PlotLine/ViewModels/CoreViewModels.cs index 64d92f0..a6aa689 100644 --- a/PlotLine/ViewModels/CoreViewModels.cs +++ b/PlotLine/ViewModels/CoreViewModels.cs @@ -321,6 +321,41 @@ public sealed class WriterDashboardViewModel public IReadOnlyList StoryHealthReminders { get; set; } = []; } +public sealed class WritingPlanViewModel +{ + public int WritingPlanID { get; set; } + public int UserID { get; set; } + public int ProjectID { get; set; } + public int? BookID { get; set; } + public string PlanName { get; set; } = string.Empty; + public string GoalType { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public DateTime DeadlineDate { get; set; } + public string AvailableDaysJson { get; set; } = string.Empty; + public int SessionLengthMinutes { get; set; } + public bool IncludeBlockedScenes { get; set; } + public string RebalanceMode { get; set; } = "Ask"; + public bool IsActive { get; set; } = true; + public DateTime CreatedDateUtc { get; set; } + public DateTime ModifiedDateUtc { get; set; } +} + +public sealed class WritingScheduleItemViewModel +{ + public int WritingScheduleItemID { get; set; } + public int WritingPlanID { get; set; } + public int SceneID { get; set; } + public DateTime ScheduledDate { get; set; } + public string TaskType { get; set; } = string.Empty; + public int? EstimatedMinutes { get; set; } + public int? TargetWords { get; set; } + public string ScheduleStatus { get; set; } = string.Empty; + public DateTime? CompletedDateUtc { get; set; } + public string? Notes { get; set; } + public DateTime CreatedDateUtc { get; set; } + public DateTime ModifiedDateUtc { get; set; } +} + public sealed class CharacterArcViewModel { public Project Project { get; set; } = new();