Phase 9E: Daily Writing Dashboard.
This commit is contained in:
parent
218cc87327
commit
e83425eaac
@ -13,6 +13,7 @@ public sealed class WriterController(
|
|||||||
public async Task<IActionResult> Index(int? projectId)
|
public async Task<IActionResult> Index(int? projectId)
|
||||||
{
|
{
|
||||||
var model = await writerWorkspace.GetDashboardAsync(projectId);
|
var model = await writerWorkspace.GetDashboardAsync(projectId);
|
||||||
|
model.WritingSchedule = await writingSchedule.GetTodayDashboardAsync();
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +109,28 @@ public sealed class WriterController(
|
|||||||
return RedirectToAction(nameof(SchedulePreview), new { writingPlanId });
|
return RedirectToAction(nameof(SchedulePreview), new { writingPlanId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> 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<IActionResult> 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]
|
[HttpPost]
|
||||||
[ValidateAntiForgeryToken]
|
[ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> SaveChapterGoal(ChapterGoalEditViewModel model)
|
public async Task<IActionResult> SaveChapterGoal(ChapterGoalEditViewModel model)
|
||||||
|
|||||||
@ -291,6 +291,8 @@ public interface IWritingScheduleRepository
|
|||||||
Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId);
|
Task<IReadOnlyList<WritingPlanSummary>> GetWritingPlanSummariesByUserAsync(int userId);
|
||||||
Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId);
|
Task<IReadOnlyList<WritingSchedulePreviewItem>> GetSchedulePreviewItemsAsync(int writingPlanId);
|
||||||
Task DeleteScheduleItemsByPlanAsync(int writingPlanId);
|
Task DeleteScheduleItemsByPlanAsync(int writingPlanId);
|
||||||
|
Task<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId);
|
||||||
|
Task<int> UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IExportRepository
|
public interface IExportRepository
|
||||||
@ -1155,6 +1157,68 @@ public sealed class WritingScheduleRepository(ISqlConnectionFactory connectionFa
|
|||||||
"DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;",
|
"DELETE FROM dbo.WritingScheduleItems WHERE WritingPlanID = @WritingPlanID;",
|
||||||
new { WritingPlanID = writingPlanId });
|
new { WritingPlanID = writingPlanId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<WritingScheduleDashboardItem>> GetActiveScheduleDashboardItemsAsync(int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<WritingScheduleDashboardItem>(
|
||||||
|
"""
|
||||||
|
SELECT wsi.WritingScheduleItemID,
|
||||||
|
wsi.WritingPlanID,
|
||||||
|
wp.PlanName,
|
||||||
|
wsi.SceneID,
|
||||||
|
wsi.ScheduledDate,
|
||||||
|
wsi.TaskType,
|
||||||
|
b.BookTitle,
|
||||||
|
c.ChapterNumber,
|
||||||
|
c.ChapterTitle,
|
||||||
|
c.SortOrder AS ChapterSortOrder,
|
||||||
|
s.SceneNumber,
|
||||||
|
s.SceneTitle,
|
||||||
|
s.SortOrder AS SceneSortOrder,
|
||||||
|
sw.Priority,
|
||||||
|
wsi.EstimatedMinutes,
|
||||||
|
wsi.TargetWords,
|
||||||
|
wsi.ScheduleStatus,
|
||||||
|
COALESCE(sw.IsBlocked, 0) AS IsBlocked
|
||||||
|
FROM dbo.WritingPlans wp
|
||||||
|
INNER JOIN dbo.WritingScheduleItems wsi ON wsi.WritingPlanID = wp.WritingPlanID
|
||||||
|
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 wp.UserID = @UserID
|
||||||
|
AND wp.IsActive = 1
|
||||||
|
ORDER BY wsi.ScheduledDate, wsi.WritingScheduleItemID;
|
||||||
|
""",
|
||||||
|
new { UserID = userId });
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> UpdatePlannedScheduleItemStatusForUserAsync(int writingScheduleItemId, int userId, string scheduleStatus, DateTime? completedDateUtc)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.ExecuteAsync(
|
||||||
|
"""
|
||||||
|
UPDATE wsi
|
||||||
|
SET ScheduleStatus = @ScheduleStatus,
|
||||||
|
CompletedDateUtc = @CompletedDateUtc,
|
||||||
|
ModifiedDateUtc = SYSUTCDATETIME()
|
||||||
|
FROM dbo.WritingScheduleItems wsi
|
||||||
|
INNER JOIN dbo.WritingPlans wp ON wp.WritingPlanID = wsi.WritingPlanID
|
||||||
|
WHERE wsi.WritingScheduleItemID = @WritingScheduleItemID
|
||||||
|
AND wp.UserID = @UserID
|
||||||
|
AND wp.IsActive = 1
|
||||||
|
AND wsi.ScheduleStatus = N'Planned';
|
||||||
|
""",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
WritingScheduleItemID = writingScheduleItemId,
|
||||||
|
UserID = userId,
|
||||||
|
ScheduleStatus = scheduleStatus,
|
||||||
|
CompletedDateUtc = completedDateUtc
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
|
public sealed class ExportRepository(ISqlConnectionFactory connectionFactory) : IExportRepository
|
||||||
|
|||||||
@ -1651,6 +1651,28 @@ public sealed class WritingSchedulePreviewItem
|
|||||||
public bool IsBlocked { get; set; }
|
public bool IsBlocked { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WritingScheduleDashboardItem
|
||||||
|
{
|
||||||
|
public int WritingScheduleItemID { get; set; }
|
||||||
|
public int WritingPlanID { get; set; }
|
||||||
|
public string PlanName { get; set; } = string.Empty;
|
||||||
|
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 int ChapterSortOrder { get; set; }
|
||||||
|
public decimal SceneNumber { get; set; }
|
||||||
|
public string SceneTitle { get; set; } = string.Empty;
|
||||||
|
public int SceneSortOrder { get; set; }
|
||||||
|
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; }
|
||||||
|
|||||||
@ -277,6 +277,9 @@ public interface IWritingScheduleService
|
|||||||
Task<WritingScheduleSetupViewModel> GetScheduleSetupAsync(int? projectId);
|
Task<WritingScheduleSetupViewModel> GetScheduleSetupAsync(int? projectId);
|
||||||
Task PopulateScheduleSetupOptionsAsync(WritingScheduleSetupViewModel model);
|
Task PopulateScheduleSetupOptionsAsync(WritingScheduleSetupViewModel model);
|
||||||
Task<WritingScheduleSetupResult> CreateScheduleFromSetupAsync(WritingScheduleSetupViewModel model);
|
Task<WritingScheduleSetupResult> CreateScheduleFromSetupAsync(WritingScheduleSetupViewModel model);
|
||||||
|
Task<WritingScheduleDashboardViewModel> GetTodayDashboardAsync();
|
||||||
|
Task<bool> MarkScheduleItemCompleteAsync(int writingScheduleItemId);
|
||||||
|
Task<bool> SkipScheduleItemAsync(int writingScheduleItemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IStorageService
|
public interface IStorageService
|
||||||
@ -3528,6 +3531,76 @@ public sealed class WritingScheduleService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WritingScheduleDashboardViewModel> GetTodayDashboardAsync()
|
||||||
|
{
|
||||||
|
if (!currentUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return new WritingScheduleDashboardViewModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
var plans = await writingSchedule.GetWritingPlanSummariesByUserAsync(currentUser.UserId.Value);
|
||||||
|
var activePlanCount = plans.Count(x => x.IsActive);
|
||||||
|
var items = await writingSchedule.GetActiveScheduleDashboardItemsAsync(currentUser.UserId.Value);
|
||||||
|
var today = DateTime.Today;
|
||||||
|
var plannedItems = items
|
||||||
|
.Where(x => string.Equals(x.ScheduleStatus, "Planned", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new WritingScheduleDashboardViewModel
|
||||||
|
{
|
||||||
|
ActivePlanCount = activePlanCount,
|
||||||
|
TodayTasks = plannedItems
|
||||||
|
.Where(x => x.ScheduledDate.Date == today)
|
||||||
|
.OrderByDescending(x => x.Priority ?? 0)
|
||||||
|
.ThenBy(x => x.ScheduledDate)
|
||||||
|
.ThenBy(x => x.ChapterSortOrder)
|
||||||
|
.ThenBy(x => x.SceneSortOrder)
|
||||||
|
.ThenBy(x => x.WritingScheduleItemID)
|
||||||
|
.ToList(),
|
||||||
|
UpcomingTasks = plannedItems
|
||||||
|
.Where(x => x.ScheduledDate.Date > today)
|
||||||
|
.OrderBy(x => x.ScheduledDate)
|
||||||
|
.ThenBy(x => x.WritingScheduleItemID)
|
||||||
|
.Take(5)
|
||||||
|
.ToList(),
|
||||||
|
TotalPlannedTasks = items.Count,
|
||||||
|
CompletedTasks = items.Count(x => string.Equals(x.ScheduleStatus, "Done", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
SkippedTasks = items.Count(x => string.Equals(x.ScheduleStatus, "Skipped", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
RemainingTasks = plannedItems.Count,
|
||||||
|
ProjectedFinishDate = plannedItems.Count == 0 ? null : plannedItems.Max(x => x.ScheduledDate)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> MarkScheduleItemCompleteAsync(int writingScheduleItemId)
|
||||||
|
{
|
||||||
|
if (!currentUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var affectedRows = await writingSchedule.UpdatePlannedScheduleItemStatusForUserAsync(
|
||||||
|
writingScheduleItemId,
|
||||||
|
currentUser.UserId.Value,
|
||||||
|
"Done",
|
||||||
|
DateTime.UtcNow);
|
||||||
|
return affectedRows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SkipScheduleItemAsync(int writingScheduleItemId)
|
||||||
|
{
|
||||||
|
if (!currentUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var affectedRows = await writingSchedule.UpdatePlannedScheduleItemStatusForUserAsync(
|
||||||
|
writingScheduleItemId,
|
||||||
|
currentUser.UserId.Value,
|
||||||
|
"Skipped",
|
||||||
|
null);
|
||||||
|
return affectedRows > 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static void ValidateSetup(WritingScheduleSetupViewModel model)
|
private static void ValidateSetup(WritingScheduleSetupViewModel model)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(model.PlanName))
|
if (string.IsNullOrWhiteSpace(model.PlanName))
|
||||||
|
|||||||
@ -314,6 +314,7 @@ public sealed class WriterDashboardViewModel
|
|||||||
{
|
{
|
||||||
public int? ProjectID { get; set; }
|
public int? ProjectID { get; set; }
|
||||||
public IReadOnlyList<SelectListItem> ProjectOptions { get; set; } = [];
|
public IReadOnlyList<SelectListItem> ProjectOptions { get; set; } = [];
|
||||||
|
public WritingScheduleDashboardViewModel WritingSchedule { get; set; } = new();
|
||||||
public IReadOnlyList<WriterDashboardScene> WriteNext { get; set; } = [];
|
public IReadOnlyList<WriterDashboardScene> WriteNext { get; set; } = [];
|
||||||
public IReadOnlyList<WriterDashboardScene> ContinueWriting { get; set; } = [];
|
public IReadOnlyList<WriterDashboardScene> ContinueWriting { get; set; } = [];
|
||||||
public IReadOnlyList<WriterDashboardScene> RevisionQueue { get; set; } = [];
|
public IReadOnlyList<WriterDashboardScene> RevisionQueue { get; set; } = [];
|
||||||
@ -321,6 +322,20 @@ public sealed class WriterDashboardViewModel
|
|||||||
public IReadOnlyList<StoryBibleAttentionItem> StoryHealthReminders { get; set; } = [];
|
public IReadOnlyList<StoryBibleAttentionItem> StoryHealthReminders { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class WritingScheduleDashboardViewModel
|
||||||
|
{
|
||||||
|
public int ActivePlanCount { get; set; }
|
||||||
|
public IReadOnlyList<WritingScheduleDashboardItem> TodayTasks { get; set; } = [];
|
||||||
|
public IReadOnlyList<WritingScheduleDashboardItem> UpcomingTasks { get; set; } = [];
|
||||||
|
public int TotalPlannedTasks { get; set; }
|
||||||
|
public int CompletedTasks { get; set; }
|
||||||
|
public int SkippedTasks { get; set; }
|
||||||
|
public int RemainingTasks { get; set; }
|
||||||
|
public DateTime? ProjectedFinishDate { get; set; }
|
||||||
|
public bool HasActivePlans => ActivePlanCount > 0;
|
||||||
|
public bool HasActiveSchedule => TotalPlannedTasks > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class WritingPlanViewModel
|
public sealed class WritingPlanViewModel
|
||||||
{
|
{
|
||||||
public int WritingPlanID { get; set; }
|
public int WritingPlanID { get; set; }
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
@model WriterDashboardViewModel
|
@model WriterDashboardViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Writer Workspace";
|
ViewData["Title"] = "Writer Workspace";
|
||||||
|
var message = TempData["WriterWorkspaceMessage"] as string;
|
||||||
|
var error = TempData["WriterWorkspaceError"] as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="page-heading writer-dashboard-heading">
|
<div class="page-heading writer-dashboard-heading">
|
||||||
@ -21,6 +23,156 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">Daily plan</p>
|
||||||
|
<h2>Today's Writing</h2>
|
||||||
|
</div>
|
||||||
|
<span class="soft-count">@Model.WritingSchedule.TodayTasks.Count tasks</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!Model.WritingSchedule.HasActivePlans)
|
||||||
|
{
|
||||||
|
<p class="muted">No writing schedule configured.</p>
|
||||||
|
<a class="btn btn-primary" asp-action="ScheduleSetup" asp-route-projectId="@Model.ProjectID">Create Writing Schedule</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@if (!Model.WritingSchedule.TodayTasks.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No writing tasks scheduled for today.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<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>Blocked</th>
|
||||||
|
<th>Writing Plan</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in Model.WritingSchedule.TodayTasks)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<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.IsBlocked ? "Yes" : "No")</td>
|
||||||
|
<td>@item.PlanName</td>
|
||||||
|
<td>
|
||||||
|
<div class="button-row">
|
||||||
|
<form asp-action="MarkScheduleItemComplete" method="post">
|
||||||
|
<input type="hidden" name="writingScheduleItemId" value="@item.WritingScheduleItemID" />
|
||||||
|
<input type="hidden" name="projectId" value="@Model.ProjectID" />
|
||||||
|
<button class="btn btn-sm btn-primary" type="submit">Mark Complete</button>
|
||||||
|
</form>
|
||||||
|
<form asp-action="SkipScheduleItem" method="post">
|
||||||
|
<input type="hidden" name="writingScheduleItemId" value="@item.WritingScheduleItemID" />
|
||||||
|
<input type="hidden" name="projectId" value="@Model.ProjectID" />
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="submit">Skip</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="story-bible-section-heading mt-4">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Upcoming work</p>
|
||||||
|
<h2>Coming Up</h2>
|
||||||
|
</div>
|
||||||
|
<span class="soft-count">@Model.WritingSchedule.UpcomingTasks.Count tasks</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!Model.WritingSchedule.UpcomingTasks.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">No upcoming writing tasks.</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>Estimated Minutes</th>
|
||||||
|
<th>Target Words</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in Model.WritingSchedule.UpcomingTasks)
|
||||||
|
{
|
||||||
|
<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.EstimatedMinutes?.ToString() ?? "-")</td>
|
||||||
|
<td>@(item.TargetWords?.ToString() ?? "-")</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="story-bible-section-heading mt-4">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Schedule</p>
|
||||||
|
<h2>Progress Summary</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!Model.WritingSchedule.HasActiveSchedule)
|
||||||
|
{
|
||||||
|
<p class="muted">No active schedule</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="story-bible-grid story-bible-grid-compact">
|
||||||
|
<article class="story-bible-card"><h3>Active Plans</h3><strong class="writer-kpi">@Model.WritingSchedule.ActivePlanCount</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Total Planned Tasks</h3><strong class="writer-kpi">@Model.WritingSchedule.TotalPlannedTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Completed Tasks</h3><strong class="writer-kpi">@Model.WritingSchedule.CompletedTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Skipped Tasks</h3><strong class="writer-kpi">@Model.WritingSchedule.SkippedTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Remaining Tasks</h3><strong class="writer-kpi">@Model.WritingSchedule.RemainingTasks</strong></article>
|
||||||
|
<article class="story-bible-card"><h3>Projected Finish Date</h3><strong class="writer-kpi">@(Model.WritingSchedule.ProjectedFinishDate?.ToString("dd MMM yyyy") ?? "-")</strong></article>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="writer-dashboard-grid">
|
<div class="writer-dashboard-grid">
|
||||||
<section class="writer-dashboard-panel">
|
<section class="writer-dashboard-panel">
|
||||||
<div class="story-bible-section-heading">
|
<div class="story-bible-section-heading">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user