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(); 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 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 (!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 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) { await writingSchedule.DeleteScheduleAsync(writingPlanId); TempData["SchedulePreviewMessage"] = "Generated schedule 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 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 }); } }