216 lines
7.5 KiB
C#
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);
|
|
}
|
|
}
|