feat(ui): Settings-Tab fuer die Ticketsystem-Anbindung

This commit is contained in:
mika kuns
2026-08-27 13:29:49 +02:00
parent f0a3a186dd
commit 020f93d55e
5 changed files with 158 additions and 2 deletions
@@ -0,0 +1,92 @@
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
public sealed partial class TicketSettingsTabViewModel : ViewModelBase
{
private readonly IWorkerClient _worker;
[ObservableProperty] private string _apiBaseUrl = "";
[ObservableProperty] private string _token = "";
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(TokenPlaceholder))]
private bool _tokenSet;
[ObservableProperty] private bool _isBusy;
[ObservableProperty] private string _statusMessage = "";
// 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 async Task LoadAsync()
{
var dto = await _worker.GetTicketSettingsAsync();
if (dto is null)
{
StatusMessage = Loc.T("vm.tickets.workerOffline");
return;
}
ApiBaseUrl = dto.ApiBaseUrl ?? "";
TokenSet = dto.TokenSet;
}
public async Task SaveAsync()
{
await _worker.SetTicketApiBaseUrlAsync(ApiBaseUrl);
if (!string.IsNullOrWhiteSpace(Token))
{
await _worker.SetTicketTokenAsync(Token);
Token = "";
TokenSet = true;
}
}
[RelayCommand]
private async Task TestConnection()
{
IsBusy = true;
StatusMessage = "";
try
{
await SaveAsync();
var result = await _worker.TestTicketConnectionAsync();
if (result is null || !result.Ok)
{
StatusMessage = result?.Error ?? Loc.T("vm.tickets.workerOffline");
return;
}
var missing = RequiredScopes.Where(s => !result.Scopes.Contains(s)).ToList();
StatusMessage = missing.Count == 0
? Loc.T("vm.tickets.connectionOk", result.UserName!)
: Loc.T("vm.tickets.connectionMissingScopes", result.UserName!, string.Join(", ", missing));
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private async Task ClearToken()
{
await _worker.ClearTicketTokenAsync();
Token = "";
TokenSet = false;
StatusMessage = Loc.T("vm.tickets.tokenCleared");
}
// Ohne diese drei Scopes scheitert entweder der Import oder die Statusrueckmeldung —
// der mit Abstand haeufigste Konfigurationsfehler, deshalb wird er beim Testen benannt
// statt nur geloggt.
private static readonly string[] RequiredScopes =
[
"pat:projects:read",
"pat:board:read",
"pat:tickets:write",
];
}
@@ -26,6 +26,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
public PrimeClaudeTabViewModel Prime { get; }
public OnlineInboxSettingsViewModel OnlineInbox { get; }
public SessionSkillsSettingsTabViewModel SessionSkills { get; }
public TicketSettingsTabViewModel Tickets { get; }
// Online Inbox ist unfertig und vor Usern verborgen — hier auf true für Reaktivierung.
public bool ShowOnlineInbox => false;
@@ -106,6 +107,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
Prime = prime;
OnlineInbox = new OnlineInboxSettingsViewModel(worker, onlineLoginService);
SessionSkills = new SessionSkillsSettingsTabViewModel(worker);
Tickets = new TicketSettingsTabViewModel(worker);
_categories = new List<SettingsCategory>
{
@@ -117,6 +119,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
new("SessionSkills", new LocalizedString(localizer, "settings.sidebar.categorySkills"), "Erweitert", true, true),
new("Reports", new LocalizedString(localizer, "settings.sidebar.categoryReports"), "Erweitert", true, true),
new("OnlineInbox", new LocalizedString(localizer, "settings.onlineInbox.tabHeader"), "Erweitert", ShowOnlineInbox, false),
new("Tickets", new LocalizedString(localizer, "settings.sidebar.categoryTickets"), "Erweitert", true, false),
};
BasisCategories = _categories.Where(c => c.Group == "Basis").ToList();
ErweitertCategories = _categories.Where(c => c.Group == "Erweitert").ToList();
@@ -167,6 +170,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
await Prime.LoadAsync();
await OnlineInbox.LoadAsync();
await SessionSkills.LoadAsync(dto?.SessionSkills);
await Tickets.LoadAsync();
General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling);
await using var ctx = await _dbFactory.CreateDbContextAsync();
@@ -213,6 +217,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
await _worker.UpdateAppSettingsAsync(dto);
await Prime.SaveAsync();
await OnlineInbox.SaveAsync();
await Tickets.SaveAsync();
CloseAction?.Invoke();
}
catch (Exception ex) { StatusMessage = Loc.T("vm.settingsModal.saveFailed", ex.Message); }
@@ -690,6 +690,45 @@
</ScrollViewer>
</TabItem>
<!-- Ticketsystem: Base-URL + PAT for the Bandel ticket integration. Empty base URL
disables the feature entirely, so this tab intentionally never forces a value. -->
<TabItem Header="{loc:Tr settings.sidebar.categoryTickets}">
<ScrollViewer>
<StackPanel Spacing="14" Margin="0,8,0,0">
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.baseUrl}"/>
<TextBox Text="{Binding Tickets.ApiBaseUrl, Mode=TwoWay}"/>
<TextBlock Classes="meta" Text="{loc:Tr settings.tickets.baseUrlHint}"
TextWrapping="Wrap" Opacity="0.6"/>
</StackPanel>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,2,0,0"/>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.tickets.token}"/>
<TextBox Text="{Binding Tickets.Token, Mode=TwoWay}"
PasswordChar="•"
PlaceholderText="{Binding Tickets.TokenPlaceholder}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="btn"
Content="{loc:Tr settings.tickets.testConnection}"
Command="{Binding Tickets.TestConnectionCommand}"
IsEnabled="{Binding !Tickets.IsBusy}"/>
<Button Classes="btn danger"
Content="{loc:Tr settings.tickets.clearToken}"
Command="{Binding Tickets.ClearTokenCommand}"
IsEnabled="{Binding !Tickets.IsBusy}"/>
</StackPanel>
<TextBlock Classes="meta" Text="{Binding Tickets.StatusMessage}"
IsVisible="{Binding Tickets.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</DockPanel>