From ccd2ee2cc7cf96dccc150f01508343c39e60d78a Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 3 Jun 2026 09:54:10 +0200 Subject: [PATCH] feat(ui): WeeklyReportModalViewModel with default-range logic Co-Authored-By: Claude Sonnet 4.6 --- .../Services/Interfaces/IWorkerClient.cs | 1 + .../Modals/WeeklyReportModalViewModel.cs | 87 +++++++++++++++++++ tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs | 7 ++ .../ViewModels/WeeklyReportRangeTests.cs | 24 +++++ 4 files changed, 119 insertions(+) create mode 100644 src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs create mode 100644 tests/ClaudeDo.Ui.Tests/ViewModels/WeeklyReportRangeTests.cs diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index dde7431..0d6baf0 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -52,6 +52,7 @@ public interface IWorkerClient : INotifyPropertyChanged Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default); Task GetWeekReportAsync(DateOnly start, DateOnly end); Task GenerateWeekReportAsync(DateOnly start, DateOnly end); + Task GetAppSettingsAsync(); Task> GetDailyNotesAsync(DateOnly day); Task AddDailyNoteAsync(DateOnly day, string text); Task UpdateDailyNoteAsync(string id, string text); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs new file mode 100644 index 0000000..1143c34 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Modals/WeeklyReportModalViewModel.cs @@ -0,0 +1,87 @@ +using ClaudeDo.Ui.Services; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace ClaudeDo.Ui.ViewModels.Modals; + +public sealed partial class WeeklyReportModalViewModel : ViewModelBase +{ + private readonly IWorkerClient _worker; + + public WeeklyReportModalViewModel(IWorkerClient worker) => _worker = worker; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasReport))] + [NotifyPropertyChangedFor(nameof(EmptyStateVisible))] + private string? _reportMarkdown; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(EmptyStateVisible))] + [NotifyCanExecuteChangedFor(nameof(GenerateCommand))] + private bool _isBusy; + + [ObservableProperty] private DateTime? _startDate; + [ObservableProperty] private DateTime? _endDate; + [ObservableProperty] private string _statusMessage = ""; + + public bool HasReport => !string.IsNullOrWhiteSpace(ReportMarkdown); + public bool EmptyStateVisible => !HasReport && !IsBusy; + + public Action? CloseAction { get; set; } + [RelayCommand] private void Close() => CloseAction?.Invoke(); + + public static (DateOnly Start, DateOnly End) DefaultRange(DayOfWeek standup, DateOnly today) + { + int diff = ((int)today.DayOfWeek - (int)standup + 7) % 7; + if (diff == 0) diff = 7; + return (today.AddDays(-diff), today); + } + + public async Task InitializeAsync() + { + var standup = DayOfWeek.Wednesday; + var settings = await _worker.GetAppSettingsAsync(); + if (settings is not null && settings.StandupWeekday is >= 0 and <= 6) + standup = (DayOfWeek)settings.StandupWeekday; + + var (start, end) = DefaultRange(standup, DateOnly.FromDateTime(DateTime.Today)); + StartDate = start.ToDateTime(TimeOnly.MinValue); + EndDate = end.ToDateTime(TimeOnly.MinValue); + await LoadStoredAsync(); + } + + partial void OnStartDateChanged(DateTime? value) => _ = LoadStoredAsync(); + partial void OnEndDateChanged(DateTime? value) => _ = LoadStoredAsync(); + + private bool RangeValid => StartDate is not null && EndDate is not null && StartDate <= EndDate; + + private async Task LoadStoredAsync() + { + if (!RangeValid) return; + StatusMessage = ""; + try + { + ReportMarkdown = await _worker.GetWeekReportAsync( + DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value)); + } + catch (Exception ex) { StatusMessage = ex.Message; } + } + + private bool CanGenerate() => !IsBusy; + + [RelayCommand(CanExecute = nameof(CanGenerate))] + private async Task Generate() + { + if (!RangeValid) { StatusMessage = "Ungültiger Zeitraum."; return; } + IsBusy = true; + StatusMessage = "Bericht wird erstellt…"; + try + { + ReportMarkdown = await _worker.GenerateWeekReportAsync( + DateOnly.FromDateTime(StartDate!.Value), DateOnly.FromDateTime(EndDate!.Value)); + StatusMessage = ""; + } + catch (Exception ex) { StatusMessage = $"Fehler: {ex.Message}"; } + finally { IsBusy = false; } + } +} diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs index 2c2c914..9abae72 100644 --- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs +++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs @@ -60,6 +60,13 @@ public abstract class StubWorkerClient : IWorkerClient public virtual Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public virtual Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public virtual Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; + public virtual Task GetWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult(null); + public virtual Task GenerateWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult(""); + public virtual Task GetAppSettingsAsync() => Task.FromResult(null); + public virtual Task> GetDailyNotesAsync(DateOnly day) => Task.FromResult(new List()); + public virtual Task AddDailyNoteAsync(DateOnly day, string text) => Task.FromResult(null); + public virtual Task UpdateDailyNoteAsync(string id, string text) => Task.CompletedTask; + public virtual Task DeleteDailyNoteAsync(string id) => Task.CompletedTask; protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/WeeklyReportRangeTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/WeeklyReportRangeTests.cs new file mode 100644 index 0000000..a1bc00a --- /dev/null +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/WeeklyReportRangeTests.cs @@ -0,0 +1,24 @@ +using ClaudeDo.Ui.ViewModels.Modals; + +namespace ClaudeDo.Ui.Tests.ViewModels; + +public class WeeklyReportRangeTests +{ + [Fact] + public void DefaultRange_TodayIsStandupDay_GoesBackToPreviousStandup() + { + var (start, end) = WeeklyReportModalViewModel.DefaultRange( + DayOfWeek.Wednesday, new DateOnly(2026, 6, 3)); + Assert.Equal(new DateOnly(2026, 5, 27), start); + Assert.Equal(new DateOnly(2026, 6, 3), end); + } + + [Fact] + public void DefaultRange_MidWeek_StartsAtMostRecentStandup() + { + var (start, end) = WeeklyReportModalViewModel.DefaultRange( + DayOfWeek.Wednesday, new DateOnly(2026, 6, 5)); + Assert.Equal(new DateOnly(2026, 6, 3), start); + Assert.Equal(new DateOnly(2026, 6, 5), end); + } +}