From c8b5ed3912d0ffad944d46750fbcc5d5d3c5df9c Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 3 Jun 2026 10:01:17 +0200 Subject: [PATCH] feat(ui): NotesEditorViewModel with day navigation and bullet CRUD --- .../Islands/NotesEditorViewModel.cs | 83 +++++++++++++++++++ .../ViewModels/NotesEditorViewModelTests.cs | 78 +++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 src/ClaudeDo.Ui/ViewModels/Islands/NotesEditorViewModel.cs create mode 100644 tests/ClaudeDo.Ui.Tests/ViewModels/NotesEditorViewModelTests.cs diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/NotesEditorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/NotesEditorViewModel.cs new file mode 100644 index 0000000..68da613 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Islands/NotesEditorViewModel.cs @@ -0,0 +1,83 @@ +using System.Collections.ObjectModel; +using ClaudeDo.Ui.Services.Interfaces; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Islands; + +public sealed partial class NoteBulletViewModel : ViewModelBase +{ + private readonly Func _save; + private readonly Func _delete; + + public string Id { get; } + + [ObservableProperty] private string _text; + + public NoteBulletViewModel(string id, string text, + Func save, Func delete) + { + Id = id; + _text = text; + _save = save; + _delete = delete; + } + + [RelayCommand] private Task Save() => _save(this); + [RelayCommand] private Task Delete() => _delete(this); +} + +public sealed partial class NotesEditorViewModel : ViewModelBase +{ + private readonly INotesApi _api; + + public NotesEditorViewModel(INotesApi api) => _api = api; + + public ObservableCollection Bullets { get; } = new(); + + [ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today); + [ObservableProperty] private string _newBulletText = ""; + + public DateTime CurrentDate + { + get => CurrentDay.ToDateTime(TimeOnly.MinValue); + set { var d = DateOnly.FromDateTime(value); if (d != CurrentDay) _ = LoadDayAsync(d); } + } + + public string CurrentDayLabel => CurrentDay.ToString("dddd, dd.MM.yyyy"); + + public async Task LoadDayAsync(DateOnly day) + { + CurrentDay = day; + OnPropertyChanged(nameof(CurrentDate)); + OnPropertyChanged(nameof(CurrentDayLabel)); + Bullets.Clear(); + foreach (var dto in await _api.ListAsync(day)) + Bullets.Add(MakeBullet(dto.Id, dto.Text)); + } + + private NoteBulletViewModel MakeBullet(string id, string text) => + new(id, text, SaveBulletAsync, DeleteBulletAsync); + + [RelayCommand] + private async Task AddBullet() + { + var text = NewBulletText.Trim(); + if (text.Length == 0) return; + var dto = await _api.AddAsync(CurrentDay, text); + if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text)); + NewBulletText = ""; + } + + [RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1)); + [RelayCommand] private Task NextDay() => LoadDayAsync(CurrentDay.AddDays(1)); + [RelayCommand] private Task Today() => LoadDayAsync(DateOnly.FromDateTime(DateTime.Today)); + + private Task SaveBulletAsync(NoteBulletViewModel b) => _api.UpdateAsync(b.Id, b.Text); + + private async Task DeleteBulletAsync(NoteBulletViewModel b) + { + await _api.DeleteAsync(b.Id); + Bullets.Remove(b); + } +} diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/NotesEditorViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/NotesEditorViewModelTests.cs new file mode 100644 index 0000000..34f9e91 --- /dev/null +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/NotesEditorViewModelTests.cs @@ -0,0 +1,78 @@ +using ClaudeDo.Ui.Services; +using ClaudeDo.Ui.Services.Interfaces; +using ClaudeDo.Ui.ViewModels.Islands; + +namespace ClaudeDo.Ui.Tests.ViewModels; + +public class NotesEditorViewModelTests +{ + private sealed class FakeNotes : INotesApi + { + public readonly List Store = new(); + private int _seq; + public Task> ListAsync(DateOnly day) => + Task.FromResult(Store.Where(n => n.Date == day.ToString("yyyy-MM-dd")).ToList()); + public Task AddAsync(DateOnly day, string text) + { + var dto = new DailyNoteDto($"id{_seq++}", day.ToString("yyyy-MM-dd"), text, Store.Count); + Store.Add(dto); + return Task.FromResult(dto); + } + public Task UpdateAsync(string id, string text) + { + var i = Store.FindIndex(n => n.Id == id); + if (i >= 0) Store[i] = Store[i] with { Text = text }; + return Task.CompletedTask; + } + public Task DeleteAsync(string id) { Store.RemoveAll(n => n.Id == id); return Task.CompletedTask; } + } + + [Fact] + public async Task AddBullet_PersistsAndAppears_ForCurrentDay() + { + var api = new FakeNotes(); + var vm = new NotesEditorViewModel(api); + await vm.LoadDayAsync(new DateOnly(2026, 6, 1)); + + vm.NewBulletText = "Standup vorbereitet"; + await vm.AddBulletCommand.ExecuteAsync(null); + + Assert.Single(vm.Bullets); + Assert.Equal("Standup vorbereitet", vm.Bullets[0].Text); + Assert.Equal("", vm.NewBulletText); + Assert.Single(api.Store); + } + + [Fact] + public async Task PrevAndNextDay_NavigateAndReload() + { + var api = new FakeNotes(); + await api.AddAsync(new DateOnly(2026, 5, 31), "gestern"); + var vm = new NotesEditorViewModel(api); + await vm.LoadDayAsync(new DateOnly(2026, 6, 1)); + Assert.Empty(vm.Bullets); + + await vm.PrevDayCommand.ExecuteAsync(null); + Assert.Equal(new DateOnly(2026, 5, 31), vm.CurrentDay); + Assert.Single(vm.Bullets); + + await vm.NextDayCommand.ExecuteAsync(null); + Assert.Equal(new DateOnly(2026, 6, 1), vm.CurrentDay); + Assert.Empty(vm.Bullets); + } + + [Fact] + public async Task DeleteBullet_RemovesFromStoreAndList() + { + var api = new FakeNotes(); + var vm = new NotesEditorViewModel(api); + await vm.LoadDayAsync(new DateOnly(2026, 6, 1)); + vm.NewBulletText = "weg damit"; + await vm.AddBulletCommand.ExecuteAsync(null); + + await vm.Bullets[0].DeleteCommand.ExecuteAsync(null); + + Assert.Empty(vm.Bullets); + Assert.Empty(api.Store); + } +}