feat(ui): tickets + plan day button, fallback list and prime action

This commit is contained in:
mika kuns
2026-10-01 08:44:21 +02:00
parent 9fc2e26edb
commit e35b054a2e
16 changed files with 242 additions and 12 deletions
@@ -581,6 +581,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
Bind(null, "prep");
IsPrepMode = true;
_ = Prep.LoadLastPrepLogIfEmptyAsync();
_ = Prep.RefreshTicketsConfiguredAsync();
}
/// <param name="source">What triggered the selection (<see cref="TasksIslandViewModel.SelectionSource"/>);
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals.Settings;
namespace ClaudeDo.Ui.ViewModels.Islands;
@@ -20,6 +21,9 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
[ObservableProperty] private bool _isPrepRunning;
// The button only makes sense with a configured ticket system — otherwise it would just plan the day.
[ObservableProperty] private bool _ticketsConfigured;
// Covers the gap between the click and the worker's PrepStartedEvent broadcast — IsPrepRunning
// stays false until that event arrives, so without this the button click looks like a no-op.
public OperationStatus PrepOperation { get; } = new();
@@ -57,6 +61,7 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
{
if (e.PropertyName != nameof(OperationStatus.IsRunning)) return;
PlanDayCommand.NotifyCanExecuteChanged();
ImportTicketsAndPlanDayCommand.NotifyCanExecuteChanged();
OnPropertyChanged(nameof(IsPlanDayEnabled));
};
}
@@ -78,6 +83,25 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
catch { }
}
[RelayCommand(CanExecute = nameof(CanPlanDay))]
private async System.Threading.Tasks.Task ImportTicketsAndPlanDayAsync()
{
using var op = PrepOperation.Begin(Loc.T("ops.reports.runningTicketImport"));
try { await _worker.RunTicketImportNowAsync(); }
catch { }
}
/// <summary>Reads whether the ticket system has a base URL and a token; keeps the last value when the worker is unreachable.</summary>
public async System.Threading.Tasks.Task RefreshTicketsConfiguredAsync()
{
if (!TicketSettingsTabViewModel.FeatureEnabled) { TicketsConfigured = false; return; }
TicketSettingsDto? dto;
try { dto = await _worker.GetTicketSettingsAsync(); }
catch { return; }
if (dto is null) return;
TicketsConfigured = !string.IsNullOrWhiteSpace(dto.ApiBaseUrl) && dto.TokenSet;
}
public async System.Threading.Tasks.Task LoadLastPrepLogIfEmptyAsync()
{
if (IsPrepRunning || PrepLog.Count > 0) return;
@@ -39,7 +39,7 @@ public sealed partial class PrimeClaudeTabViewModel : ViewModelBase
}
private void UpdateAnyFillMyDay() =>
AnyFillMyDay = Rows.Any(r => r.Kind == PrimeActionKind.FillMyDay);
AnyFillMyDay = Rows.Any(r => r.Kind is PrimeActionKind.FillMyDay or PrimeActionKind.ImportTickets);
public async Task LoadAsync()
{
@@ -48,7 +48,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
/// ItemsControl share one selection.</summary>
public string RadioGroup => $"prime-kind-{Id}";
// Three booleans rather than an enum-to-bool converter: RadioButton.IsChecked binds
// One boolean per kind rather than an enum-to-bool converter: RadioButton.IsChecked binds
// TwoWay to a bool, and compiled bindings stay simple.
public bool IsPing
{
@@ -68,6 +68,12 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
set { if (value) Kind = PrimeActionKind.Custom; }
}
public bool IsImportTickets
{
get => Kind == PrimeActionKind.ImportTickets;
set { if (value) Kind = PrimeActionKind.ImportTickets; }
}
public bool ShowPrompt => Kind != PrimeActionKind.Ping;
public string PromptLabel => Kind == PrimeActionKind.Custom
@@ -83,6 +89,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
OnPropertyChanged(nameof(IsPing));
OnPropertyChanged(nameof(IsFillMyDay));
OnPropertyChanged(nameof(IsCustom));
OnPropertyChanged(nameof(IsImportTickets));
OnPropertyChanged(nameof(ShowPrompt));
OnPropertyChanged(nameof(PromptLabel));
OnPropertyChanged(nameof(PromptPlaceholder));
@@ -1,10 +1,16 @@
using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
/// <summary>One fallback-list choice; Id null = no fallback.</summary>
public sealed record FallbackListOption(string? Id, string Name);
public sealed partial class TicketSettingsTabViewModel : ViewModelBase
{
/// <summary>
@@ -21,6 +27,7 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
public const bool FeatureEnabled = true;
private readonly IWorkerClient _worker;
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
[ObservableProperty] private string _apiBaseUrl = "";
[ObservableProperty] private string _token = "";
@@ -33,7 +40,17 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
// Never shows the stored token — TicketSettingsDto only ever carries the bool.
public string TokenPlaceholder => TokenSet ? Loc.T("settings.tickets.tokenSet") : "";
public TicketSettingsTabViewModel(IWorkerClient worker) => _worker = worker;
public ObservableCollection<FallbackListOption> FallbackLists { get; } = new();
[ObservableProperty] private FallbackListOption? _selectedFallbackList;
// Only a successful load may write the fallback back — otherwise an offline load would clear it.
private bool _fallbackLoaded;
public TicketSettingsTabViewModel(IWorkerClient worker, IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
_worker = worker;
_dbFactory = dbFactory;
}
public async Task LoadAsync()
{
@@ -45,6 +62,17 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
}
ApiBaseUrl = dto.ApiBaseUrl ?? "";
TokenSet = dto.TokenSet;
await using var ctx = await _dbFactory.CreateDbContextAsync();
var lists = await ctx.Lists.AsNoTracking()
.OrderBy(l => l.SortOrder).ThenBy(l => l.Name)
.Select(l => new FallbackListOption(l.Id, l.Name))
.ToListAsync();
FallbackLists.Clear();
FallbackLists.Add(new FallbackListOption(null, Loc.T("settings.tickets.fallbackNone")));
foreach (var option in lists) FallbackLists.Add(option);
SelectedFallbackList = FallbackLists.FirstOrDefault(o => o.Id == dto.FallbackListId) ?? FallbackLists[0];
_fallbackLoaded = true;
}
public async Task SaveAsync()
@@ -56,6 +84,8 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase
Token = "";
TokenSet = true;
}
if (_fallbackLoaded)
await _worker.SetTicketFallbackListAsync(SelectedFallbackList?.Id);
}
[RelayCommand]
@@ -108,7 +108,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
Prime = prime;
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
Tickets = new TicketSettingsTabViewModel(worker);
Tickets = new TicketSettingsTabViewModel(worker, dbFactory);
_categories = new List<SettingsCategory>
{