Phase 9C.5 Schedule Preview.
This commit is contained in:
parent
82eb3c85c0
commit
a56b900863
@ -6,7 +6,9 @@ using PlotLine.ViewModels;
|
|||||||
namespace PlotLine.Controllers;
|
namespace PlotLine.Controllers;
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public sealed class WriterController(IWriterWorkspaceService writerWorkspace) : Controller
|
public sealed class WriterController(
|
||||||
|
IWriterWorkspaceService writerWorkspace,
|
||||||
|
IWritingScheduleService writingSchedule) : Controller
|
||||||
{
|
{
|
||||||
public async Task<IActionResult> Index(int? projectId)
|
public async Task<IActionResult> Index(int? projectId)
|
||||||
{
|
{
|
||||||
@ -20,6 +22,38 @@ public sealed class WriterController(IWriterWorkspaceService writerWorkspace) :
|
|||||||
return model is null ? NotFound() : View(model);
|
return model is null ? NotFound() : View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> SchedulePreview(int? writingPlanId)
|
||||||
|
{
|
||||||
|
var model = await writingSchedule.GetSchedulePreviewAsync(writingPlanId);
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> DeleteGeneratedSchedule(int writingPlanId)
|
||||||
|
{
|
||||||
|
await writingSchedule.DeleteScheduleAsync(writingPlanId);
|
||||||
|
TempData["SchedulePreviewMessage"] = "Generated schedule deleted.";
|
||||||
|
return RedirectToAction(nameof(SchedulePreview), new { writingPlanId });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> SaveChapterGoal(ChapterGoalEditViewModel model)
|
public async Task<IActionResult> SaveChapterGoal(ChapterGoalEditViewModel model)
|
||||||
|
|||||||
@ -288,6 +288,9 @@ public interface IWritingScheduleRepository
|
|||||||
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
|
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
|
||||||
Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes);
|
Task<IReadOnlyList<WritingScheduleScene>> GetSchedulingScenesAsync(int projectId, int? bookId, bool includeBlockedScenes);
|
||||||
Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items);
|
Task ReplaceScheduleItemsAsync(int writingPlanId, IReadOnlyList<WritingScheduleItem> items);
|
||||||
|
Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId);
|
||||||
|
Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId);
|
||||||
|
Task DeleteScheduleItemsByPlanAsync(int writingPlanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IExportRepository
|
public interface IExportRepository
|
||||||
@ -1088,6 +1091,70 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<WritingPlanSummary>(
|
||||||
|
"""
|
||||||
|
SELECT wp.WritingPlanID,
|
||||||
|
wp.UserID,
|
||||||
|
wp.ProjectID,
|
||||||
|
p.ProjectName,
|
||||||
|
wp.BookID,
|
||||||
|
b.BookTitle,
|
||||||
|
wp.PlanName,
|
||||||
|
wp.DeadlineDate,
|
||||||
|
wp.IsActive
|
||||||
|
FROM dbo.WritingPlans wp
|
||||||
|
INNER JOIN dbo.Projects p ON p.ProjectID = wp.ProjectID
|
||||||
|
LEFT JOIN dbo.Books b ON b.BookID = wp.BookID
|
||||||
|
WHERE wp.UserID = @UserID
|
||||||
|
ORDER BY wp.IsActive DESC, wp.DeadlineDate, wp.PlanName, wp.WritingPlanID;
|
||||||
|
""",
|
||||||
|
new { UserID = userId });
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<WritingSchedulePreviewItem>(
|
||||||
|
"""
|
||||||
|
SELECT wsi.WritingScheduleItemID,
|
||||||
|
wsi.WritingPlanID,
|
||||||
|
wsi.SceneID,
|
||||||
|
wsi.ScheduledDate,
|
||||||
|
wsi.TaskType,
|
||||||
|
b.BookTitle,
|
||||||
|
c.ChapterNumber,
|
||||||
|
c.ChapterTitle,
|
||||||
|
s.SceneNumber,
|
||||||
|
s.SceneTitle,
|
||||||
|
sw.Priority,
|
||||||
|
wsi.EstimatedMinutes,
|
||||||
|
wsi.TargetWords,
|
||||||
|
wsi.ScheduleStatus,
|
||||||
|
COALESCE(sw.IsBlocked, 0) AS IsBlocked
|
||||||
|
FROM dbo.WritingScheduleItems wsi
|
||||||
|
INNER JOIN dbo.Scenes s ON s.SceneID = wsi.SceneID
|
||||||
|
INNER JOIN dbo.Chapters c ON c.ChapterID = s.ChapterID
|
||||||
|
INNER JOIN dbo.Books b ON b.BookID = c.BookID
|
||||||
|
LEFT JOIN dbo.SceneWorkflow sw ON sw.SceneID = s.SceneID
|
||||||
|
WHERE wsi.WritingPlanID = @WritingPlanID
|
||||||
|
ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID;
|
||||||
|
""",
|
||||||
|
new { WritingPlanID = writingPlanId });
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteScheduleItemsByPlanAsync(int writingPlanId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
await connection.ExecuteAsync(
|
||||||
|
"DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;",
|
||||||
|
new { WritingPlanID = writingPlanId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
|
public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
|
||||||
|
|||||||
@ -1619,6 +1619,38 @@ public sealed class WritingScheduleScene
|
|||||||
public int SceneSortOrder { get; set; }
|
public int SceneSortOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WritingPlanSummary
|
||||||
|
{
|
||||||
|
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 DateTime DeadlineDate { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class WritingSchedulePreviewItem
|
||||||
|
{
|
||||||
|
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 string BookTitle { get; set; } = string.Empty;
|
||||||
|
public decimal ChapterNumber { get; set; }
|
||||||
|
public string ChapterTitle { get; set; } = string.Empty;
|
||||||
|
public decimal SceneNumber { get; set; }
|
||||||
|
public string SceneTitle { get; set; } = string.Empty;
|
||||||
|
public int? Priority { get; set; }
|
||||||
|
public int? EstimatedMinutes { get; set; }
|
||||||
|
public int? TargetWords { get; set; }
|
||||||
|
public string ScheduleStatus { get; set; } = string.Empty;
|
||||||
|
public bool IsBlocked { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class ChapterWorkflowScene
|
public sealed class ChapterWorkflowScene
|
||||||
{
|
{
|
||||||
public int SceneID { get; set; }
|
public int SceneID { get; set; }
|
||||||
|
|||||||
@ -272,6 +272,8 @@ public interface IWritingScheduleService
|
|||||||
Task DeleteScheduleItemAsync(int writingScheduleItemId);
|
Task DeleteScheduleItemAsync(int writingScheduleItemId);
|
||||||
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
|
Task UpdateScheduleItemStatusAsync(int writingScheduleItemId, string scheduleStatus, DateTime? completedDateUtc);
|
||||||
Task<IReadOnlyList<WritingScheduleItemViewModel>> GenerateSchedule(int writingPlanId);
|
Task<IReadOnlyList<WritingScheduleItemViewModel>> GenerateSchedule(int writingPlanId);
|
||||||
|
Task<WritingSchedulePreviewViewModel> GetSchedulePreviewAsync(int? writingPlanId);
|
||||||
|
Task DeleteScheduleAsync(int writingPlanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IStorageService
|
public interface IStorageService
|
||||||
@ -3277,7 +3279,9 @@ public sealed class WriterWorkspaceService(
|
|||||||
public Task DeleteAttachmentAsync(int sceneAttachmentId) => writerWorkspace.DeleteAttachmentAsync(sceneAttachmentId);
|
public Task DeleteAttachmentAsync(int sceneAttachmentId) => writerWorkspace.DeleteAttachmentAsync(sceneAttachmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class WritingScheduleService(IWritingScheduleRepository writingSchedule) : IWritingScheduleService
|
public sealed class WritingScheduleService(
|
||||||
|
IWritingScheduleRepository writingSchedule,
|
||||||
|
ICurrentUserService currentUser) : IWritingScheduleService
|
||||||
{
|
{
|
||||||
public async Task<IReadOnlyList<WritingPlanViewModel>> GetWritingPlansByUserAsync(int userId)
|
public async Task<IReadOnlyList<WritingPlanViewModel>> GetWritingPlansByUserAsync(int userId)
|
||||||
{
|
{
|
||||||
@ -3375,6 +3379,44 @@ public sealed class WritingScheduleService(IWritingScheduleRepository writingSch
|
|||||||
return savedItems.Select(ToViewModel).ToList();
|
return savedItems.Select(ToViewModel).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WritingSchedulePreviewViewModel> GetSchedulePreviewAsync(int? writingPlanId)
|
||||||
|
{
|
||||||
|
if (!currentUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return new WritingSchedulePreviewViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
var plans = await writingSchedule.GetWritingPlanSummariesByUserAsync(currentUser.UserId.Value);
|
||||||
|
var selectedPlan = writingPlanId.HasValue
|
||||||
|
? plans.FirstOrDefault(x => x.WritingPlanID == writingPlanId.Value)
|
||||||
|
: plans.FirstOrDefault();
|
||||||
|
var items = selectedPlan is null
|
||||||
|
? []
|
||||||
|
: await writingSchedule.GetSchedulePreviewItemsAsync(selectedPlan.WritingPlanID);
|
||||||
|
|
||||||
|
return new WritingSchedulePreviewViewModel
|
||||||
|
{
|
||||||
|
SelectedWritingPlanID = selectedPlan?.WritingPlanID,
|
||||||
|
Plans = plans,
|
||||||
|
SelectedPlan = selectedPlan,
|
||||||
|
Items = items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteScheduleAsync(int writingPlanId)
|
||||||
|
{
|
||||||
|
if (!currentUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plans = await writingSchedule.GetWritingPlanSummariesByUserAsync(currentUser.UserId.Value);
|
||||||
|
if (plans.Any(x => x.WritingPlanID == writingPlanId))
|
||||||
|
{
|
||||||
|
await writingSchedule.DeleteScheduleItemsByPlanAsync(writingPlanId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidatePlan(WritingPlanViewModel model)
|
private static void ValidatePlan(WritingPlanViewModel model)
|
||||||
{
|
{
|
||||||
if (model.DeadlineDate.Date < model.StartDate.Date)
|
if (model.DeadlineDate.Date < model.StartDate.Date)
|
||||||
|
|||||||
@ -356,6 +356,20 @@ public sealed class WritingScheduleItemViewModel
|
|||||||
public DateTime ModifiedDateUtc { get; set; }
|
public DateTime ModifiedDateUtc { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WritingSchedulePreviewViewModel
|
||||||
|
{
|
||||||
|
public int? SelectedWritingPlanID { get; set; }
|
||||||
|
public IReadOnlyList<WritingPlanSummary> Plans { get; set; } = [];
|
||||||
|
public WritingPlanSummary? SelectedPlan { get; set; }
|
||||||
|
public IReadOnlyList<WritingSchedulePreviewItem> Items { get; set; } = [];
|
||||||
|
public int TotalTasks => Items.Count;
|
||||||
|
public int DraftTasks => Items.Count(x => x.TaskType == "Draft");
|
||||||
|
public int RevisionTasks => Items.Count(x => x.TaskType == "Revise");
|
||||||
|
public int PolishTasks => Items.Count(x => x.TaskType == "Polish");
|
||||||
|
public int TotalEstimatedMinutes => Items.Sum(x => x.EstimatedMinutes ?? 0);
|
||||||
|
public DateTime? ProjectedFinishDate => Items.Count == 0 ? null : Items.Max(x => x.ScheduledDate);
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class CharacterArcViewModel
|
public sealed class CharacterArcViewModel
|
||||||
{
|
{
|
||||||
public Project Project { get; set; } = new();
|
public Project Project { get; set; } = new();
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
<h1>Writer Workspace <help-icon key="workspace.overview" /></h1>
|
<h1>Writer Workspace <help-icon key="workspace.overview" /></h1>
|
||||||
<p class="lead-text">A practical control room for drafting, revision, polish, and story readiness.</p>
|
<p class="lead-text">A practical control room for drafting, revision, polish, and story readiness.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<a class="btn btn-outline-secondary" asp-action="SchedulePreview">Schedule Preview</a>
|
||||||
<form asp-action="Index" method="get" class="story-bible-search">
|
<form asp-action="Index" method="get" class="story-bible-search">
|
||||||
<select class="form-select" name="projectId" asp-items="Model.ProjectOptions">
|
<select class="form-select" name="projectId" asp-items="Model.ProjectOptions">
|
||||||
<option value="">All projects</option>
|
<option value="">All projects</option>
|
||||||
@ -16,6 +18,7 @@
|
|||||||
<button class="btn btn-primary" type="submit">Focus</button>
|
<button class="btn btn-primary" type="submit">Focus</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="writer-dashboard-grid">
|
<div class="writer-dashboard-grid">
|
||||||
<section class="writer-dashboard-panel">
|
<section class="writer-dashboard-panel">
|
||||||
|
|||||||
182
PlotLine/Views/Writer/SchedulePreview.cshtml
Normal file
182
PlotLine/Views/Writer/SchedulePreview.cshtml
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
@model WritingSchedulePreviewViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Schedule Preview";
|
||||||
|
var message = TempData["SchedulePreviewMessage"] as string;
|
||||||
|
var error = TempData["SchedulePreviewError"] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||||
|
<a asp-controller="Writer" asp-action="Index">Writer Workspace</a>
|
||||||
|
<span>Schedule Preview</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="page-heading compact">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Validation</p>
|
||||||
|
<h1>Schedule Preview</h1>
|
||||||
|
<p class="lead-text">Generated writing schedule items.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">@message</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(error))
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">@error</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!Model.Plans.Any())
|
||||||
|
{
|
||||||
|
<section class="story-bible-section">
|
||||||
|
<p class="muted">No writing plans found.</p>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<section class="story-bible-section">
|
||||||
|
<div class="story-bible-section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Plan</p>
|
||||||
|
<h2>Writing Plan Selection</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form asp-action="SchedulePreview" method="get" class="story-bible-search mb-3">
|
||||||
|
<select class="form-select" name="writingPlanId">
|
||||||
|
@foreach (var plan in Model.Plans)
|
||||||
|
{
|
||||||
|
<option value="@plan.WritingPlanID" selected="@(plan.WritingPlanID == Model.SelectedWritingPlanID)">
|
||||||
|
@plan.PlanName - @plan.ProjectName / @(plan.BookTitle ?? "All Books") - @plan.DeadlineDate.ToString("dd MMM yyyy") - @(plan.IsActive ? "Active" : "Inactive")
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary" type="submit">Select</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Plan Name</th>
|
||||||
|
<th>Project</th>
|
||||||
|
<th>Book</th>
|
||||||
|
<th>Deadline Date</th>
|
||||||
|
<th>Active</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.DeadlineDate.ToString("dd MMM yyyy")</td>
|
||||||
|
<td>@(plan.IsActive ? "Yes" : "No")</td>
|
||||||
|
<td>
|
||||||
|
@if (plan.WritingPlanID == Model.SelectedWritingPlanID)
|
||||||
|
{
|
||||||
|
<span class="status-pill">Selected</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<a asp-action="SchedulePreview" asp-route-writingPlanId="@plan.WritingPlanID">Select</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (Model.SelectedPlan is not null)
|
||||||
|
{
|
||||||
|
<section class="story-bible-section">
|
||||||
|
<div class="button-row">
|
||||||
|
<form asp-action="GenerateSchedule" method="post">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="story-bible-section">
|
||||||
|
<div class="story-bible-section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Summary</p>
|
||||||
|
<h2>Schedule Summary</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="story-bible-grid story-bible-grid-compact">
|
||||||
|
<article class="story-bible-card"><h3>Total Tasks</h3><strong class="writer-kpi">@Model.TotalTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Draft Tasks</h3><strong class="writer-kpi">@Model.DraftTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Revision Tasks</h3><strong class="writer-kpi">@Model.RevisionTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Polish Tasks</h3><strong class="writer-kpi">@Model.PolishTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Total Estimated Minutes</h3><strong class="writer-kpi">@Model.TotalEstimatedMinutes</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Projected Finish Date</h3><strong class="writer-kpi">@(Model.ProjectedFinishDate?.ToString("dd MMM yyyy") ?? "-")</strong></article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="story-bible-section">
|
||||||
|
<div class="story-bible-section-heading">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Items</p>
|
||||||
|
<h2>Schedule Preview Table</h2>
|
||||||
|
</div>
|
||||||
|
<span class="soft-count">@Model.Items.Count items</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!Model.Items.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No schedule generated.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Scheduled Date</th>
|
||||||
|
<th>Task Type</th>
|
||||||
|
<th>Book</th>
|
||||||
|
<th>Chapter</th>
|
||||||
|
<th>Scene</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Estimated Minutes</th>
|
||||||
|
<th>Target Words</th>
|
||||||
|
<th>Schedule Status</th>
|
||||||
|
<th>Blocked</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in Model.Items)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@item.ScheduledDate.ToString("dd MMM yyyy")</td>
|
||||||
|
<td>@item.TaskType</td>
|
||||||
|
<td>@item.BookTitle</td>
|
||||||
|
<td>Chapter @item.ChapterNumber: @item.ChapterTitle</td>
|
||||||
|
<td>Scene @item.SceneNumber: @item.SceneTitle</td>
|
||||||
|
<td>@(item.Priority?.ToString() ?? "-")</td>
|
||||||
|
<td>@(item.EstimatedMinutes?.ToString() ?? "-")</td>
|
||||||
|
<td>@(item.TargetWords?.ToString() ?? "-")</td>
|
||||||
|
<td>@item.ScheduleStatus</td>
|
||||||
|
<td>@(item.IsBlocked ? "Yes" : "No")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user