feat(board): show upcoming deadline tasks

This commit is contained in:
aj
2026-08-13 16:54:13 -04:00
parent 48a749fde3
commit 94f85731ef
6 changed files with 256 additions and 14 deletions
+1
View File
@@ -3,6 +3,7 @@
<div id="board-content" class="board-page">
<partial name="Partials/_FilterBar" model="Model" />
<partial name="Partials/_UpcomingTasks" model="Model" />
<div id="kanban-board" class="kanban-grid">
@foreach (var col in Model.Columns)
{
+42 -14
View File
@@ -19,6 +19,7 @@ public class BoardModel : PageModel
}
public List<ColumnViewModel> Columns { get; set; } = new();
public List<WorkTask> UpcomingTasks { get; private set; } = new();
public string? ActiveCategory { get; set; }
public bool HasSubtasksFilter { get; set; }
public List<string> AllCategories { get; set; } = new();
@@ -277,6 +278,43 @@ public class BoardModel : PageModel
return Partial("Partials/_SearchResults", results);
}
public static List<WorkTask> SelectUpcomingTasks(
IEnumerable<WorkTask> allTasks,
string? category,
bool hasSubtasks) =>
SelectUpcomingTasks(FilterVisibleTasks(allTasks, category, hasSubtasks));
private static IEnumerable<WorkTask> FilterVisibleTasks(
IEnumerable<WorkTask> allTasks,
string? category,
bool hasSubtasks)
{
var visibleTasks = allTasks;
if (!string.IsNullOrEmpty(category))
{
visibleTasks = visibleTasks.Where(task =>
string.Equals(task.Category, category, StringComparison.OrdinalIgnoreCase));
}
if (hasSubtasks)
{
visibleTasks = visibleTasks.Where(task => task.SubTasks.Count > 0);
}
return visibleTasks;
}
private static List<WorkTask> SelectUpcomingTasks(IEnumerable<WorkTask> visibleTasks)
{
return visibleTasks
.Where(task => task.DueAt.HasValue &&
task.Status is not WorkTaskStatus.Completed and not WorkTaskStatus.Abandoned)
.OrderBy(task => task.DueAt)
.Take(8)
.ToList();
}
private async Task<IActionResult> ReturnBoardContentAsync()
{
await LoadBoardDataAsync();
@@ -296,21 +334,11 @@ public class BoardModel : PageModel
.OrderBy(c => c)
.ToList();
// Filter to top-level only
var tasks = allTasks.Where(t => t.ParentTaskId == null).ToList();
var visibleTasks = FilterVisibleTasks(allTasks, ActiveCategory, HasSubtasksFilter).ToList();
UpcomingTasks = SelectUpcomingTasks(visibleTasks);
// Apply category filter
if (!string.IsNullOrEmpty(ActiveCategory))
{
tasks = tasks.Where(t =>
string.Equals(t.Category, ActiveCategory, StringComparison.OrdinalIgnoreCase)).ToList();
}
// Apply hasSubtasks filter
if (HasSubtasksFilter)
{
tasks = tasks.Where(t => t.SubTasks.Count > 0).ToList();
}
// Keep the Kanban collection top-level only after filters apply across all tasks.
var tasks = visibleTasks.Where(task => task.ParentTaskId == null).ToList();
// Group by status into columns
var tasksByStatus = tasks.ToLookup(t => t.Status);
@@ -3,6 +3,7 @@
<div id="board-content" class="board-page">
<partial name="Partials/_FilterBar" model="Model" />
<partial name="Partials/_UpcomingTasks" model="Model" />
<div id="kanban-board" class="kanban-grid">
@foreach (var col in Model.Columns)
{
@@ -0,0 +1,37 @@
@using TaskTracker.Api.Pages
@using TaskTracker.Core.Enums
@model BoardModel
@if (Model.UpcomingTasks.Count > 0)
{
<section class="upcoming-tasks" aria-labelledby="upcoming-tasks-heading">
<div class="upcoming-tasks-header">
<h2 id="upcoming-tasks-heading">Upcoming tasks</h2>
<span>@Model.UpcomingTasks.Count due soon</span>
</div>
<div class="upcoming-task-list">
@foreach (var task in Model.UpcomingTasks)
{
var categoryColor = BoardModel.GetCategoryColor(task.Category);
var dueAt = task.DueAt!.Value;
var dueAtUtc = dueAt.ToUniversalTime().ToString("O");
var localDueAt = dueAt.ToLocalTime().ToString("ddd, MMM d, h:mm tt");
<article class="upcoming-task"
hx-get="/board?handler=TaskDetail&id=@task.Id"
hx-target="#detail-panel"
hx-swap="innerHTML">
<div class="upcoming-task-title">@task.Title</div>
<div class="upcoming-task-meta">
<span class="upcoming-task-status upcoming-task-status--@task.Status.ToString().ToLowerInvariant()">@task.Status</span>
@if (!string.IsNullOrEmpty(task.Category))
{
<span class="task-card-category-dot" style="background: @categoryColor"></span>
<span>@task.Category</span>
}
</div>
<time class="task-countdown" aria-live="off" data-due-at="@dueAtUtc" datetime="@dueAtUtc" title="@localDueAt">@localDueAt</time>
</article>
}
</div>
</section>
}
+93
View File
@@ -474,6 +474,99 @@ body::before {
}
/* ============================================================
UPCOMING TASKS
============================================================ */
.upcoming-tasks {
margin-bottom: 20px;
padding: 14px;
border: 1px solid var(--color-border);
border-radius: 12px;
background: var(--color-surface);
}
.upcoming-tasks-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.upcoming-tasks-header h2 {
margin: 0;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.upcoming-tasks-header span {
color: var(--color-text-secondary);
font-size: 11px;
}
.upcoming-task-list {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 2px;
}
.upcoming-task {
display: grid;
flex: 0 0 210px;
gap: 6px;
padding: 10px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-elevated);
cursor: pointer;
transition: border-color 0.15s, transform 0.15s;
}
.upcoming-task:hover {
border-color: var(--color-border-hover);
transform: translateY(-1px);
}
.upcoming-task-title {
overflow: hidden;
color: var(--color-text-primary);
font-size: 13px;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.upcoming-task-meta {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
color: var(--color-text-secondary);
font-size: 11px;
}
.upcoming-task-status {
color: var(--color-status-pending);
}
.upcoming-task-status--active { color: var(--color-status-active); }
.upcoming-task-status--paused { color: var(--color-status-paused); }
.upcoming-task-status--completed { color: var(--color-status-completed); }
.upcoming-task-status--abandoned { color: var(--color-status-abandoned); }
.task-countdown {
color: var(--color-text-secondary);
font-size: 11px;
}
.task-countdown--overdue {
color: var(--color-status-abandoned);
}
/* ============================================================
KANBAN LAYOUT
============================================================ */