feat(ui): WeeklyReportModalViewModel with default-range logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default);
|
||||||
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
|
Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end);
|
||||||
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
|
Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end);
|
||||||
|
Task<AppSettingsDto?> GetAppSettingsAsync();
|
||||||
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
|
Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day);
|
||||||
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
|
Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text);
|
||||||
Task UpdateDailyNoteAsync(string id, string text);
|
Task UpdateDailyNoteAsync(string id, string text);
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,13 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
public virtual Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
||||||
public virtual Task AbortPlanningMergeAsync(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 QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
public virtual Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult<string?>(null);
|
||||||
|
public virtual Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult("");
|
||||||
|
public virtual Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult<AppSettingsDto?>(null);
|
||||||
|
public virtual Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
|
||||||
|
public virtual Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text) => Task.FromResult<DailyNoteDto?>(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));
|
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
}
|
}
|
||||||
|
|||||||
24
tests/ClaudeDo.Ui.Tests/ViewModels/WeeklyReportRangeTests.cs
Normal file
24
tests/ClaudeDo.Ui.Tests/ViewModels/WeeklyReportRangeTests.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user