Phase 9G: Writing Plan Management
This commit is contained in:
parent
04dc07e5f5
commit
8f006874ac
@ -29,6 +29,65 @@ public sealed class WriterController(
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> WritingPlans()
|
||||
{
|
||||
var model = await writingSchedule.GetWritingPlansForManagementAsync();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> EditWritingPlan(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var model = await writingSchedule.GetWritingPlanSettingsAsync(id);
|
||||
return View(model);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EditWritingPlan(WritingPlanSettingsEditViewModel model)
|
||||
{
|
||||
if (!Request.Form.ContainsKey(nameof(model.SelectedWritingDays)))
|
||||
{
|
||||
model.SelectedWritingDays = [];
|
||||
}
|
||||
|
||||
if (!model.SelectedWritingDays.Any())
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Select at least one writing day.");
|
||||
ModelState.AddModelError(nameof(model.SelectedWritingDays), "Select at least one writing day.");
|
||||
}
|
||||
|
||||
if (model.DeadlineDate.Date < model.StartDate.Date)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.DeadlineDate), "Deadline date must be on or after the start date.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await PopulatePlanSettingsDisplayAsync(model);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await writingSchedule.UpdateWritingPlanSettingsAsync(model);
|
||||
TempData["WritingPlansMessage"] = "Writing plan updated.";
|
||||
return RedirectToAction(nameof(WritingPlans));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ex.Message);
|
||||
await PopulatePlanSettingsDisplayAsync(model);
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IActionResult> PreviewRebalance(int writingPlanId)
|
||||
{
|
||||
var model = await writingSchedule.GetSchedulePreviewAsync(writingPlanId);
|
||||
@ -120,8 +179,20 @@ public sealed class WriterController(
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteGeneratedSchedule(int writingPlanId)
|
||||
{
|
||||
await writingSchedule.DeleteScheduleAsync(writingPlanId);
|
||||
TempData["SchedulePreviewMessage"] = "Generated schedule deleted.";
|
||||
try
|
||||
{
|
||||
await writingSchedule.DeleteScheduleAsync(writingPlanId);
|
||||
TempData["SchedulePreviewMessage"] = "Generated schedule deleted.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["SchedulePreviewError"] = ex.Message;
|
||||
}
|
||||
catch
|
||||
{
|
||||
TempData["SchedulePreviewError"] = "Generated schedule could not be deleted.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(SchedulePreview), new { writingPlanId });
|
||||
}
|
||||
|
||||
@ -142,6 +213,74 @@ public sealed class WriterController(
|
||||
return RedirectToAction(nameof(SchedulePreview), new { writingPlanId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ActivateWritingPlan(int writingPlanId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await writingSchedule.ActivateWritingPlanAsync(writingPlanId);
|
||||
TempData["WritingPlansMessage"] = "Writing plan activated.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["WritingPlansError"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WritingPlans));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeactivateWritingPlan(int writingPlanId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await writingSchedule.DeactivateWritingPlanAsync(writingPlanId);
|
||||
TempData["WritingPlansMessage"] = "Writing plan deactivated.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["WritingPlansError"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WritingPlans));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteWritingPlan(int writingPlanId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await writingSchedule.DeleteWritingPlanAsync(writingPlanId);
|
||||
TempData["WritingPlansMessage"] = "Writing plan deleted.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["WritingPlansError"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WritingPlans));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> RegenerateWritingPlanSchedule(int writingPlanId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = await writingSchedule.RegenerateScheduleAsync(writingPlanId);
|
||||
TempData["SchedulePreviewMessage"] = $"{items.Count} item{(items.Count == 1 ? "" : "s")} generated.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["SchedulePreviewError"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(SchedulePreview), new { writingPlanId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> MarkScheduleItemComplete(int writingScheduleItemId, int? projectId)
|
||||
@ -171,4 +310,21 @@ public sealed class WriterController(
|
||||
await writerWorkspace.SaveChapterGoalAsync(model);
|
||||
return RedirectToAction(nameof(Chapter), new { id = model.ChapterID });
|
||||
}
|
||||
|
||||
private async Task PopulatePlanSettingsDisplayAsync(WritingPlanSettingsEditViewModel model)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existing = await writingSchedule.GetWritingPlanSettingsAsync(model.WritingPlanID);
|
||||
model.ProjectName = existing.ProjectName;
|
||||
model.BookName = existing.BookName;
|
||||
model.GoalType = existing.GoalType;
|
||||
model.StartDate = existing.StartDate;
|
||||
model.RebalanceModeOptions = existing.RebalanceModeOptions;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// The service will surface the ownership/not-found validation message on submit.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,6 +286,7 @@ public interface IWritingScheduleRepository
|
||||
Task UpdateScheduleItemAsync(WritingScheduleItem item);
|
||||
Task DeleteScheduleItemAsync(int writingScheduleItemId);
|
||||
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
|
||||
Task<IReadOnlyList<WritingPlanManagementItem>> GetWritingPlansForManagementAsync(int userId);
|
||||
Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes);
|
||||
Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items);
|
||||
Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId);
|
||||
@ -1014,6 +1015,48 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WritingPlanManagementItem>> GetWritingPlansForManagementAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<WritingPlanManagementItem>(
|
||||
"""
|
||||
SELECT wp.WritingPlanID,
|
||||
wp.UserID,
|
||||
wp.ProjectID,
|
||||
p.ProjectName,
|
||||
wp.BookID,
|
||||
b.BookTitle,
|
||||
wp.PlanName,
|
||||
wp.GoalType,
|
||||
wp.StartDate,
|
||||
wp.DeadlineDate,
|
||||
wp.SessionLengthMinutes,
|
||||
wp.IsActive,
|
||||
SUM(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN 1 ELSE 0 END) AS PlannedTaskCount,
|
||||
MAX(CASE WHEN wsi.ScheduleStatus = N'Planned' THEN wsi.ScheduledDate ELSE NULL END) AS ProjectedFinishDate
|
||||
FROM dbo.WritingPlans wp
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID
|
||||
LEFT JOIN dbo.Books b ON b.BookID = wp.BookID
|
||||
LEFT JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID
|
||||
WHERE wp.UserID = @UserID
|
||||
GROUP BY wp.WritingPlanID,
|
||||
wp.UserID,
|
||||
wp.ProjectID,
|
||||
p.ProjectName,
|
||||
wp.BookID,
|
||||
b.BookTitle,
|
||||
wp.PlanName,
|
||||
wp.GoalType,
|
||||
wp.StartDate,
|
||||
wp.DeadlineDate,
|
||||
wp.SessionLengthMinutes,
|
||||
wp.IsActive
|
||||
ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID;
|
||||
""",
|
||||
new { UserID = userId });
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
|
||||
@ -1632,6 +1632,24 @@ public sealed class WritingPlanSummary
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WritingPlanManagementItem
|
||||
{
|
||||
public int WritingPlanID { get; set; }
|
||||
public int UserID { get; set; }
|
||||
public int ProjectID { get; set; }
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
public int? BookID { get; set; }
|
||||
public string? BookTitle { 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 int SessionLengthMinutes { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public int PlannedTaskCount { get; set; }
|
||||
public DateTime? ProjectedFinishDate { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WritingSchedulePreviewItem
|
||||
{
|
||||
public int WritingScheduleItemID { get; set; }
|
||||
|
||||
@ -274,6 +274,12 @@ public interface IWritingScheduleService
|
||||
Task<IReadOnlyList<WritingScheduleItemViewModel>> GenerateSchedule(int writingPlanId);
|
||||
Task<WritingSchedulePreviewViewModel> GetSchedulePreviewAsync(int? writingPlanId);
|
||||
Task DeleteScheduleAsync(int writingPlanId);
|
||||
Task<WritingPlansManagementViewModel> GetWritingPlansForManagementAsync();
|
||||
Task<WritingPlanSettingsEditViewModel> GetWritingPlanSettingsAsync(int writingPlanId);
|
||||
Task UpdateWritingPlanSettingsAsync(WritingPlanSettingsEditViewModel model);
|
||||
Task ActivateWritingPlanAsync(int writingPlanId);
|
||||
Task DeactivateWritingPlanAsync(int writingPlanId);
|
||||
Task<IReadOnlyList<WritingScheduleItemViewModel>> RegenerateScheduleAsync(int writingPlanId);
|
||||
Task<WritingScheduleSetupViewModel> GetScheduleSetupAsync(int? projectId);
|
||||
Task PopulateScheduleSetupOptionsAsync(WritingScheduleSetupViewModel model);
|
||||
Task<WritingScheduleSetupResult> CreateScheduleFromSetupAsync(WritingScheduleSetupViewModel model);
|
||||
@ -3318,11 +3324,18 @@ public sealed class WritingScheduleService(
|
||||
return writingSchedule.UpdateWritingPlanAsync(ToModel(model));
|
||||
}
|
||||
|
||||
public Task DeleteWritingPlanAsync(int writingPlanId) =>
|
||||
writingSchedule.DeleteWritingPlanAsync(writingPlanId);
|
||||
public async Task DeleteWritingPlanAsync(int writingPlanId)
|
||||
{
|
||||
await GetPlanForCurrentUserAsync(writingPlanId);
|
||||
await writingSchedule.DeleteScheduleItemsByPlanAsync(writingPlanId);
|
||||
await writingSchedule.DeleteWritingPlanAsync(writingPlanId);
|
||||
}
|
||||
|
||||
public Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive) =>
|
||||
writingSchedule.SetWritingPlanActiveAsync(writingPlanId, isActive);
|
||||
public async Task SetWritingPlanActiveAsync(int writingPlanId, bool isActive)
|
||||
{
|
||||
await GetPlanForCurrentUserAsync(writingPlanId);
|
||||
await writingSchedule.SetWritingPlanActiveAsync(writingPlanId, isActive);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WritingScheduleItemViewModel>> GetScheduleItemsByPlanAsync(int writingPlanId)
|
||||
{
|
||||
@ -3358,6 +3371,11 @@ public sealed class WritingScheduleService(
|
||||
{
|
||||
var plan = await writingSchedule.GetWritingPlanByIDAsync(writingPlanId)
|
||||
?? throw new InvalidOperationException("Writing plan not found.");
|
||||
if (currentUser.UserId.HasValue && plan.UserID != currentUser.UserId.Value)
|
||||
{
|
||||
throw new InvalidOperationException("Writing plan not found.");
|
||||
}
|
||||
|
||||
var planViewModel = ToViewModel(plan);
|
||||
ValidatePlan(planViewModel);
|
||||
|
||||
@ -3430,6 +3448,90 @@ public sealed class WritingScheduleService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<WritingPlansManagementViewModel> GetWritingPlansForManagementAsync()
|
||||
{
|
||||
if (!currentUser.UserId.HasValue)
|
||||
{
|
||||
return new WritingPlansManagementViewModel();
|
||||
}
|
||||
|
||||
var plans = await writingSchedule.GetWritingPlansForManagementAsync(currentUser.UserId.Value);
|
||||
return new WritingPlansManagementViewModel { Plans = plans };
|
||||
}
|
||||
|
||||
public async Task<WritingPlanSettingsEditViewModel> GetWritingPlanSettingsAsync(int writingPlanId)
|
||||
{
|
||||
var plan = await GetPlanForCurrentUserAsync(writingPlanId);
|
||||
var project = await projects.GetAsync(plan.ProjectID);
|
||||
var book = plan.BookID.HasValue ? await books.GetAsync(plan.BookID.Value) : null;
|
||||
var selectedDays = ParseAvailableDays(plan.AvailableDaysJson)
|
||||
.Select(x => x.ToString())
|
||||
.ToList();
|
||||
|
||||
var model = new WritingPlanSettingsEditViewModel
|
||||
{
|
||||
WritingPlanID = plan.WritingPlanID,
|
||||
PlanName = plan.PlanName,
|
||||
ProjectName = project?.ProjectName ?? string.Empty,
|
||||
BookName = book?.BookTitle ?? "All Books",
|
||||
GoalType = plan.GoalType,
|
||||
StartDate = plan.StartDate.Date,
|
||||
DeadlineDate = plan.DeadlineDate.Date,
|
||||
SessionLengthMinutes = plan.SessionLengthMinutes,
|
||||
SelectedWritingDays = selectedDays,
|
||||
IncludeBlockedScenes = plan.IncludeBlockedScenes,
|
||||
RebalanceMode = plan.RebalanceMode,
|
||||
IsActive = plan.IsActive
|
||||
};
|
||||
PopulatePlanSettingsOptions(model);
|
||||
return model;
|
||||
}
|
||||
|
||||
public async Task UpdateWritingPlanSettingsAsync(WritingPlanSettingsEditViewModel model)
|
||||
{
|
||||
var plan = await GetPlanForCurrentUserAsync(model.WritingPlanID);
|
||||
ValidatePlanSettings(model);
|
||||
var writingDays = NormaliseWritingDays(model.SelectedWritingDays);
|
||||
if (writingDays.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Select at least one writing day.");
|
||||
}
|
||||
|
||||
plan.PlanName = model.PlanName.Trim();
|
||||
plan.DeadlineDate = model.DeadlineDate.Date;
|
||||
plan.AvailableDaysJson = JsonSerializer.Serialize(writingDays);
|
||||
plan.SessionLengthMinutes = model.SessionLengthMinutes;
|
||||
plan.IncludeBlockedScenes = model.IncludeBlockedScenes;
|
||||
plan.RebalanceMode = model.RebalanceMode;
|
||||
plan.IsActive = model.IsActive;
|
||||
|
||||
await writingSchedule.UpdateWritingPlanAsync(plan);
|
||||
}
|
||||
|
||||
public async Task ActivateWritingPlanAsync(int writingPlanId)
|
||||
{
|
||||
await GetPlanForCurrentUserAsync(writingPlanId);
|
||||
await writingSchedule.SetWritingPlanActiveAsync(writingPlanId, true);
|
||||
}
|
||||
|
||||
public async Task DeactivateWritingPlanAsync(int writingPlanId)
|
||||
{
|
||||
await GetPlanForCurrentUserAsync(writingPlanId);
|
||||
await writingSchedule.SetWritingPlanActiveAsync(writingPlanId, false);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<WritingScheduleItemViewModel>> RegenerateScheduleAsync(int writingPlanId)
|
||||
{
|
||||
await GetPlanForCurrentUserAsync(writingPlanId);
|
||||
await writingSchedule.DeleteScheduleItemsByPlanAsync(writingPlanId);
|
||||
return await GenerateSchedule(writingPlanId);
|
||||
}
|
||||
|
||||
private static void PopulatePlanSettingsOptions(WritingPlanSettingsEditViewModel model)
|
||||
{
|
||||
model.RebalanceModeOptions = ScheduleOptionList(["Ask", "Automatic", "Never"], model.RebalanceMode);
|
||||
}
|
||||
|
||||
public async Task<WritingScheduleSetupViewModel> GetScheduleSetupAsync(int? projectId)
|
||||
{
|
||||
var projectRows = currentUser.UserId.HasValue
|
||||
@ -3690,6 +3792,22 @@ public sealed class WritingScheduleService(
|
||||
return plan;
|
||||
}
|
||||
|
||||
private async Task<WritingPlan> GetPlanForCurrentUserAsync(int writingPlanId)
|
||||
{
|
||||
if (!currentUser.UserId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException("Sign in before managing a writing schedule.");
|
||||
}
|
||||
|
||||
var plan = await writingSchedule.GetWritingPlanByIDAsync(writingPlanId);
|
||||
if (plan is null || plan.UserID != currentUser.UserId.Value)
|
||||
{
|
||||
throw new InvalidOperationException("Writing plan not found.");
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
private static void ValidateSetup(WritingScheduleSetupViewModel model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.PlanName))
|
||||
@ -3713,6 +3831,24 @@ public sealed class WritingScheduleService(
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePlanSettings(WritingPlanSettingsEditViewModel model)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.PlanName))
|
||||
{
|
||||
throw new InvalidOperationException("Plan name is required.");
|
||||
}
|
||||
|
||||
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 minutes must be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> NormaliseWritingDays(IEnumerable<string>? selectedDays)
|
||||
{
|
||||
var selected = (selectedDays ?? [])
|
||||
|
||||
@ -356,6 +356,48 @@ public sealed class WritingPlanViewModel
|
||||
public DateTime ModifiedDateUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WritingPlansManagementViewModel
|
||||
{
|
||||
public IReadOnlyList<WritingPlanManagementItem> Plans { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class WritingPlanSettingsEditViewModel
|
||||
{
|
||||
public int WritingPlanID { get; set; }
|
||||
|
||||
[Required, StringLength(200)]
|
||||
public string PlanName { get; set; } = string.Empty;
|
||||
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
public string BookName { get; set; } = "All Books";
|
||||
public string GoalType { get; set; } = string.Empty;
|
||||
|
||||
[DataType(DataType.Date)]
|
||||
public DateTime StartDate { get; set; }
|
||||
|
||||
[DataType(DataType.Date)]
|
||||
public DateTime DeadlineDate { get; set; }
|
||||
|
||||
[Range(1, int.MaxValue, ErrorMessage = "Session length minutes must be greater than zero.")]
|
||||
public int SessionLengthMinutes { get; set; }
|
||||
|
||||
public List<string> SelectedWritingDays { get; set; } = [];
|
||||
public bool IncludeBlockedScenes { get; set; }
|
||||
public string RebalanceMode { get; set; } = "Ask";
|
||||
public bool IsActive { get; set; }
|
||||
public IReadOnlyList<SelectListItem> RebalanceModeOptions { get; set; } = [];
|
||||
public IReadOnlyList<string> WritingDayOptions { get; set; } =
|
||||
[
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday"
|
||||
];
|
||||
}
|
||||
|
||||
public sealed class WritingScheduleItemViewModel
|
||||
{
|
||||
public int WritingScheduleItemID { get; set; }
|
||||
|
||||
105
PlotLine/Views/Writer/EditWritingPlan.cshtml
Normal file
105
PlotLine/Views/Writer/EditWritingPlan.cshtml
Normal file
@ -0,0 +1,105 @@
|
||||
@model WritingPlanSettingsEditViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Edit Writing Plan";
|
||||
var selectedDays = Model.SelectedWritingDays.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||
<a asp-controller="Writer" asp-action="Index">Writer Workspace</a>
|
||||
<a asp-controller="Writer" asp-action="WritingPlans">Writing Plans</a>
|
||||
<span>Edit Plan</span>
|
||||
</nav>
|
||||
|
||||
<div class="page-heading compact">
|
||||
<div>
|
||||
<p class="eyebrow">Schedule admin</p>
|
||||
<h1>Edit Writing Plan</h1>
|
||||
<p class="lead-text">Update plan settings without changing the project, book, or goal type.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="story-bible-section">
|
||||
<form asp-action="EditWritingPlan" method="post" class="story-bible-card chapter-goal-form">
|
||||
<div asp-validation-summary="ModelOnly" class="alert alert-warning"></div>
|
||||
<input type="hidden" asp-for="WritingPlanID" />
|
||||
<input type="hidden" asp-for="StartDate" />
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label asp-for="PlanName" class="form-label">Plan Name</label>
|
||||
<input asp-for="PlanName" class="form-control" />
|
||||
<span asp-validation-for="PlanName" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Project</label>
|
||||
<input class="form-control" value="@Model.ProjectName" readonly />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Book</label>
|
||||
<input class="form-control" value="@Model.BookName" readonly />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Goal Type</label>
|
||||
<input class="form-control" value="@Model.GoalType" readonly />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Start Date</label>
|
||||
<input class="form-control" value='@Model.StartDate.ToString("yyyy-MM-dd")' readonly />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label asp-for="DeadlineDate" class="form-label">Deadline Date</label>
|
||||
<input asp-for="DeadlineDate" class="form-control" type="date" />
|
||||
<span asp-validation-for="DeadlineDate" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label asp-for="SessionLengthMinutes" class="form-label">Session Length Minutes</label>
|
||||
<input asp-for="SessionLengthMinutes" class="form-control" type="number" min="1" />
|
||||
<span asp-validation-for="SessionLengthMinutes" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label asp-for="RebalanceMode" class="form-label">Rebalance Mode</label>
|
||||
<select asp-for="RebalanceMode" asp-items="Model.RebalanceModeOptions" class="form-select"></select>
|
||||
<span asp-validation-for="RebalanceMode" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label">Available Writing Days</label>
|
||||
<div class="writer-checkbox-grid">
|
||||
@foreach (var day in Model.WritingDayOptions)
|
||||
{
|
||||
<label>
|
||||
<input type="checkbox" name="SelectedWritingDays" value="@day" checked="@selectedDays.Contains(day)" />
|
||||
@day
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
<span asp-validation-for="SelectedWritingDays" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="col-12 writer-checkbox-grid">
|
||||
<label>
|
||||
<input asp-for="IncludeBlockedScenes" />
|
||||
Include Blocked Scenes
|
||||
</label>
|
||||
<label>
|
||||
<input asp-for="IsActive" />
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-row mt-3">
|
||||
<button class="btn btn-primary" type="submit">Save Plan</button>
|
||||
<a class="btn btn-outline-secondary" asp-action="WritingPlans">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
@ -12,6 +12,7 @@
|
||||
<p class="lead-text">A practical control room for drafting, revision, polish, and story readiness.</p>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-primary" asp-action="WritingPlans">Writing Plans</a>
|
||||
<a class="btn btn-outline-primary" asp-action="ScheduleSetup" asp-route-projectId="@Model.ProjectID">Schedule Setup</a>
|
||||
<a class="btn btn-outline-secondary" asp-action="SchedulePreview">Schedule Preview</a>
|
||||
<form asp-action="Index" method="get" class="story-bible-search">
|
||||
|
||||
@ -107,10 +107,7 @@ else
|
||||
<input type="hidden" name="writingPlanId" value="@Model.SelectedPlan.WritingPlanID" />
|
||||
<button class="btn btn-primary" type="submit">Generate Schedule</button>
|
||||
</form>
|
||||
<form asp-action="DeleteGeneratedSchedule" method="post" onsubmit="return confirm('Delete generated schedule items for this writing plan?');">
|
||||
<input type="hidden" name="writingPlanId" value="@Model.SelectedPlan.WritingPlanID" />
|
||||
<button class="btn btn-outline-danger" type="submit">Delete Generated Schedule</button>
|
||||
</form>
|
||||
<button class="btn btn-outline-danger" type="button" data-bs-toggle="modal" data-bs-target="#deleteGeneratedScheduleModal">Delete Generated Schedule</button>
|
||||
@if (Model.SelectedPlanRequiresRebalance)
|
||||
{
|
||||
<a class="btn btn-outline-primary" asp-action="PreviewRebalance" asp-route-writingPlanId="@Model.SelectedPlan.WritingPlanID">Preview Rebalance</a>
|
||||
@ -118,6 +115,28 @@ else
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="modal fade" id="deleteGeneratedScheduleModal" tabindex="-1" aria-labelledby="deleteGeneratedScheduleModalTitle" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="deleteGeneratedScheduleModalTitle">Delete generated schedule?</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Delete generated schedule items for <strong>@Model.SelectedPlan.PlanName</strong>?</p>
|
||||
<p class="muted mb-0">This removes the generated schedule items for this plan. The writing plan itself will remain.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form asp-action="DeleteGeneratedSchedule" method="post" class="m-0">
|
||||
<input type="hidden" name="writingPlanId" value="@Model.SelectedPlan.WritingPlanID" />
|
||||
<button class="btn btn-danger" type="submit">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.RebalancePreview is not null)
|
||||
{
|
||||
<section class="story-bible-section">
|
||||
|
||||
115
PlotLine/Views/Writer/WritingPlans.cshtml
Normal file
115
PlotLine/Views/Writer/WritingPlans.cshtml
Normal file
@ -0,0 +1,115 @@
|
||||
@model WritingPlansManagementViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Writing Plans";
|
||||
var message = TempData["WritingPlansMessage"] as string;
|
||||
var error = TempData["WritingPlansError"] as string;
|
||||
}
|
||||
|
||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||
<a asp-controller="Writer" asp-action="Index">Writer Workspace</a>
|
||||
<span>Writing Plans</span>
|
||||
</nav>
|
||||
|
||||
<div class="page-heading compact">
|
||||
<div>
|
||||
<p class="eyebrow">Schedule admin</p>
|
||||
<h1>Writing Plans</h1>
|
||||
<p class="lead-text">Manage writing plans and their generated schedules.</p>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-primary" asp-action="ScheduleSetup">Create Writing Schedule</a>
|
||||
<a class="btn btn-outline-secondary" asp-action="SchedulePreview">Schedule Preview</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
<div class="alert alert-info">@message</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(error))
|
||||
{
|
||||
<div class="alert alert-warning">@error</div>
|
||||
}
|
||||
|
||||
<section class="story-bible-section">
|
||||
<div class="story-bible-section-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Plans</p>
|
||||
<h2>Writing Plans</h2>
|
||||
</div>
|
||||
<span class="soft-count">@Model.Plans.Count plans</span>
|
||||
</div>
|
||||
|
||||
@if (!Model.Plans.Any())
|
||||
{
|
||||
<p class="muted">No writing plans found.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plan Name</th>
|
||||
<th>Project</th>
|
||||
<th>Book</th>
|
||||
<th>Goal Type</th>
|
||||
<th>Start Date</th>
|
||||
<th>Deadline Date</th>
|
||||
<th>Session Length</th>
|
||||
<th>Active</th>
|
||||
<th>Planned Tasks</th>
|
||||
<th>Projected Finish Date</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var plan in Model.Plans)
|
||||
{
|
||||
<tr>
|
||||
<td>@plan.PlanName</td>
|
||||
<td>@plan.ProjectName</td>
|
||||
<td>@(plan.BookTitle ?? "All Books")</td>
|
||||
<td>@plan.GoalType</td>
|
||||
<td>@plan.StartDate.ToString("dd MMM yyyy")</td>
|
||||
<td>@plan.DeadlineDate.ToString("dd MMM yyyy")</td>
|
||||
<td>@plan.SessionLengthMinutes minutes</td>
|
||||
<td>@(plan.IsActive ? "Yes" : "No")</td>
|
||||
<td>@plan.PlannedTaskCount</td>
|
||||
<td>@(plan.ProjectedFinishDate?.ToString("dd MMM yyyy") ?? "-")</td>
|
||||
<td>
|
||||
<div class="button-row">
|
||||
<a class="btn btn-sm btn-outline-secondary" asp-action="SchedulePreview" asp-route-writingPlanId="@plan.WritingPlanID">View Schedule</a>
|
||||
<a class="btn btn-sm btn-outline-primary" asp-action="EditWritingPlan" asp-route-id="@plan.WritingPlanID">Edit Plan</a>
|
||||
@if (plan.IsActive)
|
||||
{
|
||||
<form asp-action="DeactivateWritingPlan" method="post">
|
||||
<input type="hidden" name="writingPlanId" value="@plan.WritingPlanID" />
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">Deactivate Plan</button>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form asp-action="ActivateWritingPlan" method="post">
|
||||
<input type="hidden" name="writingPlanId" value="@plan.WritingPlanID" />
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit">Activate Plan</button>
|
||||
</form>
|
||||
}
|
||||
<form asp-action="RegenerateWritingPlanSchedule" method="post">
|
||||
<input type="hidden" name="writingPlanId" value="@plan.WritingPlanID" />
|
||||
<button class="btn btn-sm btn-outline-primary" type="submit">Regenerate Schedule</button>
|
||||
</form>
|
||||
<form asp-action="DeleteWritingPlan" method="post" onsubmit="return confirm('Delete this writing plan and its generated schedule items?');">
|
||||
<input type="hidden" name="writingPlanId" value="@plan.WritingPlanID" />
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">Delete Plan</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
Loading…
x
Reference in New Issue
Block a user