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 _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 Items { get; } = new(); public ObservableCollection OverdueItems { get; } = new(); public ObservableCollection OpenItems { get; } = new(); public ObservableCollection 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 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 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(), }; 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); } }