From 37738e3c8f655c14c2f7b7ae9d3b61942706e9be Mon Sep 17 00:00:00 2001 From: mika kuns Date: Tue, 2 Jun 2026 16:40:41 +0200 Subject: [PATCH] feat(ui): drive prime schedule rows from weekday toggles Co-Authored-By: Claude Sonnet 4.6 --- src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs | 4 +- .../Settings/PrimeClaudeTabViewModel.cs | 9 ++-- .../Settings/PrimeScheduleRowViewModel.cs | 37 ++++++++++++---- .../PrimeClaudeTabViewModelTests.cs | 42 ++++++++++++++----- 4 files changed, 66 insertions(+), 26 deletions(-) diff --git a/src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs b/src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs index bfb1120..1ae68b8 100644 --- a/src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs +++ b/src/ClaudeDo.Ui/Services/PrimeScheduleDto.cs @@ -2,10 +2,8 @@ namespace ClaudeDo.Ui.Services; public sealed record PrimeScheduleDto( Guid Id, - DateOnly StartDate, - DateOnly EndDate, + int Days, TimeSpan TimeOfDay, - bool WorkdaysOnly, bool Enabled, DateTimeOffset? LastRunAt, string? PromptOverride); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs index 767e4fb..1dbf5f2 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs @@ -30,8 +30,8 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase { foreach (var r in Rows) { - if (r.StartDate > r.EndDate) - return $"Schedule {r.TimeOfDay:hh\\:mm}: start date is after end date."; + if (r.DaysMask() == 0) + return $"Schedule {r.TimeOfDay:hh\\:mm}: select at least one day."; if (r.TimeOfDay < TimeSpan.Zero || r.TimeOfDay >= TimeSpan.FromDays(1)) return "Time must be between 00:00 and 23:59."; } @@ -52,13 +52,10 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase [RelayCommand] private void AddSchedule() { - var today = DateOnly.FromDateTime(DateTime.Today); var dto = new PrimeScheduleDto( Id: Guid.NewGuid(), - StartDate: today, - EndDate: today.AddDays(30), + Days: 31, // Mon–Fri TimeOfDay: new TimeSpan(7, 0, 0), - WorkdaysOnly: true, Enabled: true, LastRunAt: null, PromptOverride: null); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs index efb3ded..7830488 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs @@ -5,14 +5,20 @@ namespace ClaudeDo.Ui.ViewModels.Modals.Settings; public sealed partial class PrimeScheduleRowViewModel : ViewModelBase { + private const int Mon = 1, Tue = 2, Wed = 4, Thu = 8, Fri = 16, Sat = 32, Sun = 64; + public Guid Id { get; } public bool IsExisting { get; } [ObservableProperty] private bool _enabled; - [ObservableProperty] private DateOnly _startDate; - [ObservableProperty] private DateOnly _endDate; + [ObservableProperty] private bool _monday; + [ObservableProperty] private bool _tuesday; + [ObservableProperty] private bool _wednesday; + [ObservableProperty] private bool _thursday; + [ObservableProperty] private bool _friday; + [ObservableProperty] private bool _saturday; + [ObservableProperty] private bool _sunday; [ObservableProperty] private TimeSpan _timeOfDay; - [ObservableProperty] private bool _workdaysOnly; [ObservableProperty] private DateTimeOffset? _lastRunAt; public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—"; @@ -24,13 +30,30 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase Id = dto.Id; IsExisting = isExisting; Enabled = dto.Enabled; - StartDate = dto.StartDate; - EndDate = dto.EndDate; + Monday = (dto.Days & Mon) != 0; + Tuesday = (dto.Days & Tue) != 0; + Wednesday = (dto.Days & Wed) != 0; + Thursday = (dto.Days & Thu) != 0; + Friday = (dto.Days & Fri) != 0; + Saturday = (dto.Days & Sat) != 0; + Sunday = (dto.Days & Sun) != 0; TimeOfDay = dto.TimeOfDay; - WorkdaysOnly = dto.WorkdaysOnly; LastRunAt = dto.LastRunAt; } + public int DaysMask() + { + int m = 0; + if (Monday) m |= Mon; + if (Tuesday) m |= Tue; + if (Wednesday) m |= Wed; + if (Thursday) m |= Thu; + if (Friday) m |= Fri; + if (Saturday) m |= Sat; + if (Sunday) m |= Sun; + return m; + } + public PrimeScheduleDto ToDto() => - new(Id, StartDate, EndDate, TimeOfDay, WorkdaysOnly, Enabled, LastRunAt, null); + new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, null); } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs index f2c6032..b8c47f9 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/PrimeClaudeTabViewModelTests.cs @@ -19,13 +19,14 @@ public class PrimeClaudeTabViewModelTests public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; } } + private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time) => + new(id, days, time, true, null, null); + [Fact] public async Task Load_Populates_Rows() { var api = new FakeApi(); - api.Stored.Add(new PrimeScheduleDto( - Guid.NewGuid(), new DateOnly(2026,5,1), new DateOnly(2026,5,31), - new TimeSpan(7,0,0), true, true, null, null)); + api.Stored.Add(Dto(Guid.NewGuid(), 31, new TimeSpan(7, 0, 0))); var vm = new PrimeClaudeTabViewModel(api); await vm.LoadAsync(); Assert.Single(vm.Rows); @@ -38,8 +39,21 @@ public class PrimeClaudeTabViewModelTests vm.AddScheduleCommand.Execute(null); Assert.Single(vm.Rows); Assert.True(vm.Rows[0].Enabled); - Assert.True(vm.Rows[0].WorkdaysOnly); - Assert.Equal(new TimeSpan(7,0,0), vm.Rows[0].TimeOfDay); + Assert.True(vm.Rows[0].Monday); + Assert.True(vm.Rows[0].Friday); + Assert.False(vm.Rows[0].Saturday); + Assert.Equal(new TimeSpan(7, 0, 0), vm.Rows[0].TimeOfDay); + } + + [Fact] + public void Row_Decomposes_And_Recomposes_Days() + { + var vm = new PrimeClaudeTabViewModel(new FakeApi()); + vm.AddScheduleCommand.Execute(null); + var row = vm.Rows[0]; + Assert.Equal(31, row.DaysMask()); + row.Saturday = true; + Assert.Equal(63, row.DaysMask()); } [Fact] @@ -48,8 +62,8 @@ public class PrimeClaudeTabViewModelTests var api = new FakeApi(); var keptId = Guid.NewGuid(); var deletedId = Guid.NewGuid(); - api.Stored.Add(new PrimeScheduleDto(keptId, new(2026,5,1), new(2026,5,31), new(7,0,0), true, true, null, null)); - api.Stored.Add(new PrimeScheduleDto(deletedId, new(2026,5,1), new(2026,5,31), new(8,0,0), true, true, null, null)); + api.Stored.Add(Dto(keptId, 31, new TimeSpan(7, 0, 0))); + api.Stored.Add(Dto(deletedId, 31, new TimeSpan(8, 0, 0))); var vm = new PrimeClaudeTabViewModel(api); await vm.LoadAsync(); @@ -63,12 +77,20 @@ public class PrimeClaudeTabViewModelTests } [Fact] - public void Validate_Reports_StartAfterEnd() + public void Validate_Reports_No_Days_Selected() { var vm = new PrimeClaudeTabViewModel(new FakeApi()); vm.AddScheduleCommand.Execute(null); - vm.Rows[0].StartDate = new DateOnly(2026, 6, 1); - vm.Rows[0].EndDate = new DateOnly(2026, 5, 1); + var row = vm.Rows[0]; + row.Monday = row.Tuesday = row.Wednesday = row.Thursday = row.Friday = false; Assert.NotNull(vm.Validate()); } + + [Fact] + public void Validate_Passes_With_One_Day() + { + var vm = new PrimeClaudeTabViewModel(new FakeApi()); + vm.AddScheduleCommand.Execute(null); + Assert.Null(vm.Validate()); + } }