Phase 9B is implemented: database foundation only, no schedule generation or UI.

This commit is contained in:
Nick Beckley 2026-06-12 21:03:22 +01:00
parent b2cfcd08fa
commit e66b43064a
6 changed files with 734 additions and 0 deletions

View File

@ -272,6 +272,22 @@ public interface IWriterWorkspaceRepository
Task<IReadOnlyList<StoryBibleAttentionItem>> GetWriterDashboardRemindersAsync(int? projectId);
}
public interface IWritingScheduleRepository
{
Task<IReadOnlyList<WritingPlan>> GetWritingPlansByUserAsync(int userId);
Task<WritingPlan?> GetWritingPlanByIDAsync(int writingPlanId);
Task<int> CreateWritingPlanAsync(WritingPlan plan);
Task UpdateWritingPlanAsync(WritingPlan plan);
Task DeleteWritingPlanAsync(int writingPlanId);
Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive);
Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByPlanAsync(int writingPlanId);
Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate);
Task<int> CreateScheduleItemAsync(WritingScheduleItem item);
Task UpdateScheduleItemAsync(WritingScheduleItem item);
Task DeleteScheduleItemAsync(int writingScheduleItemId);
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
}
public interface IExportRepository
{
Task<ExportPacket> GetStoryBibleAsync(int projectId);
@ -842,6 +858,154 @@ public sealed class WriterWorkspaceRepository(ISqlConnectionFactory connectionFa
}
}
public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFactory) : IWritingScheduleRepository
{
public async Task<IReadOnlyList<WritingPlan>> GetWritingPlansByUserAsync(int userId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingPlan>("dbo.GetWritingPlansByUser", new { UserID = userId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<WritingPlan?> GetWritingPlanByIDAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleOrDefaultAsync<WritingPlan>("dbo.GetWritingPlanByID", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
}
public async Task<int> CreateWritingPlanAsync(WritingPlan plan)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByPlanAsync(int writingPlanId)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleItem>("dbo.GetScheduleItemsByPlan", new { WritingPlanID = writingPlanId }, commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<IReadOnlyList<WritingScheduleItem>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate)
{
using var connection = connectionFactory.CreateConnection();
var rows = await connection.QueryAsync<WritingScheduleItem>(
"dbo.GetScheduleItemsByDateRange",
new { WritingPlanID = writingPlanId, StartDate = startDate, EndDate = endDate },
commandType: CommandType.StoredProcedure);
return rows.ToList();
}
public async Task<int> CreateScheduleItemAsync(WritingScheduleItem item)
{
using var connection = connectionFactory.CreateConnection();
return await connection.QuerySingleAsync<int>(
"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<ExportPacket> GetStoryBibleAsync(int projectId)

View File

@ -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; }

View File

@ -96,6 +96,7 @@ public class Program
builder.Services.AddScoped<IAnalyticsRepository, AnalyticsRepository>();
builder.Services.AddScoped<IStoryBibleRepository, StoryBibleRepository>();
builder.Services.AddScoped<IWriterWorkspaceRepository, WriterWorkspaceRepository>();
builder.Services.AddScoped<IWritingScheduleRepository, WritingScheduleRepository>();
builder.Services.AddScoped<IExportRepository, ExportRepository>();
builder.Services.AddScoped<IProjectBackupRepository, ProjectBackupRepository>();
builder.Services.AddScoped<IProjectRestorePointRepository, ProjectRestorePointRepository>();
@ -127,6 +128,7 @@ public class Program
builder.Services.AddScoped<IStoryBibleService, StoryBibleService>();
builder.Services.AddScoped<IRelationshipMapService, RelationshipMapService>();
builder.Services.AddScoped<IWriterWorkspaceService, WriterWorkspaceService>();
builder.Services.AddScoped<IWritingScheduleService, WritingScheduleService>();
builder.Services.AddScoped<IStorageService, StorageService>();
builder.Services.AddScoped<IExportService, ExportService>();
builder.Services.AddScoped<IProjectExportService, ProjectExportService>();

View File

@ -256,6 +256,22 @@ public interface IWriterWorkspaceService
Task DeleteAttachmentAsync(int sceneAttachmentId);
}
public interface IWritingScheduleService
{
Task<IReadOnlyList<WritingPlanViewModel>> GetWritingPlansByUserAsync(int userId);
Task<WritingPlanViewModel?> GetWritingPlanByIDAsync(int writingPlanId);
Task<int> CreateWritingPlanAsync(WritingPlanViewModel model);
Task UpdateWritingPlanAsync(WritingPlanViewModel model);
Task DeleteWritingPlanAsync(int writingPlanId);
Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive);
Task<IReadOnlyList<WritingScheduleItemViewModel>> GetScheduleItemsByPlanAsync(int writingPlanId);
Task<IReadOnlyList<WritingScheduleItemViewModel>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate);
Task<int> CreateScheduleItemAsync(WritingScheduleItemViewModel model);
Task UpdateScheduleItemAsync(WritingScheduleItemViewModel model);
Task DeleteScheduleItemAsync(int writingScheduleItemId);
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
}
public interface IStorageService
{
Task<StorageUsage?> 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<IReadOnlyList<WritingPlanViewModel>> GetWritingPlansByUserAsync(int userId)
{
var plans = await writingSchedule.GetWritingPlansByUserAsync(userId);
return plans.Select(ToViewModel).ToList();
}
public async Task<WritingPlanViewModel?> GetWritingPlanByIDAsync(int writingPlanId)
{
var plan = await writingSchedule.GetWritingPlanByIDAsync(writingPlanId);
return plan is null ? null : ToViewModel(plan);
}
public Task<int> 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<IReadOnlyList<WritingScheduleItemViewModel>> GetScheduleItemsByPlanAsync(int writingPlanId)
{
var items = await writingSchedule.GetScheduleItemsByPlanAsync(writingPlanId);
return items.Select(ToViewModel).ToList();
}
public async Task<IReadOnlyList<WritingScheduleItemViewModel>> GetScheduleItemsByDateRangeAsync(int writingPlanId, DateTime startDate, DateTime endDate)
{
var items = await writingSchedule.GetScheduleItemsByDateRangeAsync(writingPlanId, startDate, endDate);
return items.Select(ToViewModel).ToList();
}
public Task<int> 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)

View File

@ -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

View File

@ -321,6 +321,41 @@ public sealed class WriterDashboardViewModel
public IReadOnlyList<StoryBibleAttentionItem> 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();