Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs

216 lines
7.5 KiB
C#

using System.Collections.ObjectModel;
using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TasksIslandViewModel : ViewModelBase
{
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private ListNavItemViewModel? _currentList;
private CancellationTokenSource? _loadCts;
public event EventHandler? SelectionChanged;
public event EventHandler? FocusAddTaskRequested;
public void RequestFocusAddTask() => FocusAddTaskRequested?.Invoke(this, EventArgs.Empty);
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
public ObservableCollection<TaskRowViewModel> OverdueItems { get; } = new();
public ObservableCollection<TaskRowViewModel> OpenItems { get; } = new();
public ObservableCollection<TaskRowViewModel> CompletedItems { get; } = new();
[ObservableProperty] private string _newTaskTitle = "";
[ObservableProperty] private TaskRowViewModel? _selectedTask;
[ObservableProperty] private string _headerTitle = "";
[ObservableProperty] private string _headerEyebrow = "";
[ObservableProperty] private string _subtitle = "";
[ObservableProperty] private string _statusPill = "";
[ObservableProperty] private bool _hasStatusPill;
[ObservableProperty] private bool _isShowingCompleted = true;
[ObservableProperty] private bool _hasOverdue;
[ObservableProperty] private bool _hasOpen;
[ObservableProperty] private bool _hasCompleted;
[ObservableProperty] private bool _showOpenLabel;
[ObservableProperty] private string _completedHeader = "COMPLETED";
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
_dbFactory = dbFactory;
}
public void LoadForList(ListNavItemViewModel? list)
{
_loadCts?.Cancel();
_loadCts?.Dispose();
_loadCts = new CancellationTokenSource();
var ct = _loadCts.Token;
_currentList = list;
Items.Clear();
OverdueItems.Clear();
OpenItems.Clear();
CompletedItems.Clear();
HasOverdue = false;
HasOpen = false;
HasCompleted = false;
ShowOpenLabel = false;
if (list is null) return;
HeaderTitle = list.Name;
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd", CultureInfo.InvariantCulture).ToUpperInvariant();
_ = LoadForListAsync(list, ct);
}
private async Task LoadForListAsync(ListNavItemViewModel list, CancellationToken ct)
{
try
{
await using var db = await _dbFactory.CreateDbContextAsync(ct);
var all = await db.Tasks
.Include(t => t.List)
.Include(t => t.Worktree)
.ToListAsync(ct);
ct.ThrowIfCancellationRequested();
IEnumerable<TaskEntity> filtered = list.Kind switch
{
ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running),
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
_ => Enumerable.Empty<TaskEntity>(),
};
foreach (var t in filtered)
Items.Add(TaskRowViewModel.FromEntity(t));
Regroup();
UpdateSubtitle();
}
catch (OperationCanceledException) { }
}
private void Regroup()
{
OverdueItems.Clear();
OpenItems.Clear();
CompletedItems.Clear();
var today = DateTime.Today;
foreach (var r in Items)
{
if (r.Done)
CompletedItems.Add(r);
else if (r.ScheduledFor is { } d && d.Date < today)
OverdueItems.Add(r);
else
OpenItems.Add(r);
}
HasOverdue = OverdueItems.Count > 0;
HasOpen = OpenItems.Count > 0;
HasCompleted = CompletedItems.Count > 0;
ShowOpenLabel = HasOpen && HasOverdue;
CompletedHeader = $"COMPLETED · {CompletedItems.Count}";
}
private void UpdateSubtitle()
{
var now = DateTime.Now;
var open = Items.Count(i => !i.Done);
var running = Items.Count(i => i.Status == TaskStatus.Running);
var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null);
Subtitle = open == 1 ? "1 open task" : $"{open} open tasks";
if (running > 0 || review > 0)
{
StatusPill = $"{running} running · {review} review";
HasStatusPill = true;
}
else
{
StatusPill = "";
HasStatusPill = false;
}
}
[RelayCommand]
private async Task AddAsync()
{
if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
var listId = _currentList.Id["user:".Length..];
var entity = new TaskEntity
{
Id = Guid.NewGuid().ToString("N"),
ListId = listId,
Title = NewTaskTitle.Trim(),
CreatedAt = DateTime.UtcNow,
};
await using var db = await _dbFactory.CreateDbContextAsync();
db.Tasks.Add(entity);
await db.SaveChangesAsync();
var row = TaskRowViewModel.FromEntity(entity);
Items.Insert(0, row);
Regroup();
NewTaskTitle = "";
UpdateSubtitle();
}
[RelayCommand]
private async Task ToggleDoneAsync(TaskRowViewModel row)
{
row.Done = !row.Done;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual;
row.Status = entity.Status;
await db.SaveChangesAsync();
}
Regroup();
UpdateSubtitle();
}
[RelayCommand]
private async Task ToggleStarAsync(TaskRowViewModel row)
{
row.IsStarred = !row.IsStarred;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.IsStarred = row.IsStarred;
await db.SaveChangesAsync();
}
}
[RelayCommand]
private void Select(TaskRowViewModel row) => SelectedTask = row;
[RelayCommand]
private void ToggleShowCompleted() => IsShowingCompleted = !IsShowingCompleted;
[RelayCommand]
private void Sort() { /* placeholder — UI-only */ }
[RelayCommand]
private void More() { /* placeholder — UI-only */ }
partial void OnSelectedTaskChanged(TaskRowViewModel? value)
{
foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value);
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}