using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using PlotLine.Services; using PlotLine.ViewModels; namespace PlotLine.Controllers; [Authorize] public sealed class WriterController( IWriterWorkspaceService writerWorkspace, IWritingScheduleService writingSchedule) : Controller { public async Task Index(int? projectId) { var model = await writerWorkspace.GetDashboardAsync(projectId); model.WritingSchedule = await writingSchedule.GetTodayDashboardAsync(projectId); return View(model); } public async Task Chapter(int id) { var model = await writerWorkspace.GetChapterWorkflowAsync(id); return model is null ? NotFound() : View(model); } public async Task SchedulePreview(int? writingPlanId) { var model = await writingSchedule.GetSchedulePreviewAsync(writingPlanId); return View(model); } public async Task WritingPlans() { var model = await writingSchedule.GetWritingPlansForManagementAsync(); return View(model); } public async Task EditWritingPlan(int writingPlanId) { try { var model = await writingSchedule.GetWritingPlanSettingsAsync(writingPlanId); return View(model); } catch (InvalidOperationException) { return NotFound(); } } [HttpPost] [ValidateAntiForgeryToken] public async Task EditWritingPlan(WritingPlanSettingsEditViewModel model) { if (!model.WritingDayAvailability.Any(x => x.IsAvailable)) { ModelState.AddModelError(string.Empty, "Select at least one writing day."); ModelState.AddModelError(nameof(model.WritingDayAvailability), "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 PreviewRebalance(int writingPlanId) { var model = await writingSchedule.GetSchedulePreviewAsync(writingPlanId); try { model.RebalancePreview = await writingSchedule.PreviewRebalanceAsync(writingPlanId); } catch (InvalidOperationException ex) { TempData["SchedulePreviewError"] = ex.Message; return RedirectToAction(nameof(SchedulePreview), new { writingPlanId }); } return View(nameof(SchedulePreview), model); } public async Task ScheduleSetup(int? projectId) { var model = await writingSchedule.GetScheduleSetupAsync(projectId); return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task ScheduleSetup(WritingScheduleSetupViewModel model) { if (!model.WritingDayAvailability.Any(x => x.IsAvailable)) { ModelState.AddModelError(string.Empty, "Select at least one writing day."); ModelState.AddModelError(nameof(model.WritingDayAvailability), "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 writingSchedule.PopulateScheduleSetupOptionsAsync(model); return View(model); } try { var result = await writingSchedule.CreateScheduleFromSetupAsync(model); if (string.IsNullOrWhiteSpace(result.ValidationMessage)) { TempData["SchedulePreviewMessage"] = $"Writing plan created. {result.GeneratedItemCount} item{(result.GeneratedItemCount == 1 ? "" : "s")} generated."; } else { TempData["SchedulePreviewError"] = result.ValidationMessage; } return RedirectToAction(nameof(SchedulePreview), new { writingPlanId = result.WritingPlanID }); } catch (InvalidOperationException ex) { ModelState.AddModelError(string.Empty, ex.Message); await writingSchedule.PopulateScheduleSetupOptionsAsync(model); return View(model); } } [HttpPost] [ValidateAntiForgeryToken] public async Task GenerateSchedule(int writingPlanId) { try { var items = await writingSchedule.GenerateSchedule(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 DeleteGeneratedSchedule(int writingPlanId) { 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 }); } [HttpPost] [ValidateAntiForgeryToken] public async Task ApplyRebalance(int writingPlanId) { try { await writingSchedule.ApplyRebalanceAsync(writingPlanId); TempData["SchedulePreviewMessage"] = "Schedule rebalanced successfully."; } catch (InvalidOperationException ex) { TempData["SchedulePreviewError"] = ex.Message; } return RedirectToAction(nameof(SchedulePreview), new { writingPlanId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task 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 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 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 RegenerateWritingPlanSchedule(int writingPlanId) { try { var items = await writingSchedule.RegenerateScheduleAsync(writingPlanId); TempData["WritingPlansMessage"] = $"{items.Count} item{(items.Count == 1 ? "" : "s")} generated."; } catch (InvalidOperationException ex) { TempData["WritingPlansError"] = ex.Message; } return RedirectToAction(nameof(WritingPlans)); } [HttpPost] [ValidateAntiForgeryToken] public async Task MarkScheduleItemComplete(int writingScheduleItemId, int? projectId) { var updated = await writingSchedule.MarkScheduleItemCompleteAsync(writingScheduleItemId); TempData[updated ? "WriterWorkspaceMessage" : "WriterWorkspaceError"] = updated ? "Writing task marked complete." : "Only planned writing tasks can be marked complete."; return RedirectToAction(nameof(Index), new { projectId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task SkipScheduleItem(int writingScheduleItemId, int? projectId) { var updated = await writingSchedule.SkipScheduleItemAsync(writingScheduleItemId); TempData[updated ? "WriterWorkspaceMessage" : "WriterWorkspaceError"] = updated ? "Writing task skipped." : "Only planned writing tasks can be skipped."; return RedirectToAction(nameof(Index), new { projectId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task SaveChapterGoal(ChapterGoalEditViewModel model) { 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. } } }