diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 37b0bba4..bd683e49 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -112,10 +112,11 @@ "actionPing": "Ping", "actionFillMyDay": "Mein Tag füllen", "actionCustom": "Eigener Prompt", - "actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.", + "actionImportTickets": "Tickets + Tag planen", + "actionHint": "Ping öffnet nur das Nutzungsfenster. „Mein Tag füllen“ startet die Tagesplanung. „Tickets + Tag planen“ importiert erst deine zugewiesenen Tickets in die passenden Listen und startet dann die Tagesplanung. „Eigener Prompt“ führt deinen eigenen Prompt mit den ClaudeDo-Tools aus.", "customPromptLabel": "Prompt", "customPromptPlaceholder": "Der vollständige Prompt für diesen Zeitplan (Pflichtfeld)", - "maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“.", + "maxTasksDisabledHint": "Gilt nur für Zeitpläne mit „Mein Tag füllen“ oder „Tickets + Tag planen“.", "promptOverrideLabel": "Prompt-Zusatz", "promptOverridePlaceholder": "Zusätzliche Anweisungen, die für diesen Zeitplan an den Prime-Prompt angehängt werden (optional)", "dayMo": "Mo", @@ -152,7 +153,10 @@ "token": "Personal Access Token", "tokenSet": "gespeichert", "testConnection": "Verbindung testen", - "clearToken": "Token entfernen" + "clearToken": "Token entfernen", + "fallbackList": "Fallback-Liste", + "fallbackListHint": "Ticket-Items, die Claude keiner Liste zuordnen kann, landen hier. Keine = sie werden übersprungen.", + "fallbackNone": "(keine)" }, "inherit": { "inheritedFromList": "geerbt · Liste", @@ -285,6 +289,7 @@ "copyFormattedTip": "Titel, Beschreibung und offene Schritte kopieren", "toggleEditPreviewTip": "Bearbeiten/Vorschau umschalten", "planDay": "Tag planen", + "importTicketsAndPlanDay": "Tickets + Tag planen", "prepEmpty": "Heute noch keine Vorbereitung — klick Tag planen", "roadblockReply": { "placeholder": "Auf den Roadblock antworten…", @@ -937,7 +942,8 @@ }, "reports": { "generatingWeekly": "Bericht wird erstellt…", - "runningDailyPrep": "Tagesvorbereitung läuft…" + "runningDailyPrep": "Tagesvorbereitung läuft…", + "runningTicketImport": "Tickets werden importiert, danach Tagesplanung…" }, "planning": { "finalizing": "Plan wird finalisiert…", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index e45e9de5..db1f11e4 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -112,10 +112,11 @@ "actionPing": "Ping", "actionFillMyDay": "Fill My Day", "actionCustom": "Custom prompt", - "actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.", + "actionImportTickets": "Tickets + plan day", + "actionHint": "Ping only opens the usage window. Fill My Day runs the daily prep. Tickets + plan day first imports your assigned tickets into the matching lists, then runs the daily prep. Custom runs your own prompt with the ClaudeDo tools.", "customPromptLabel": "Prompt", "customPromptPlaceholder": "The full prompt to run for this schedule (required)", - "maxTasksDisabledHint": "Only applies to Fill My Day schedules.", + "maxTasksDisabledHint": "Only applies to Fill My Day and Tickets + plan day schedules.", "promptOverrideLabel": "Prompt addition", "promptOverridePlaceholder": "Extra instructions appended to the daily prep prompt for this schedule (optional)", "dayMo": "Mo", @@ -152,7 +153,10 @@ "token": "Personal access token", "tokenSet": "stored", "testConnection": "Test connection", - "clearToken": "Remove token" + "clearToken": "Remove token", + "fallbackList": "Fallback list", + "fallbackListHint": "Imported ticket items Claude can't match to a list land here. None = they are skipped.", + "fallbackNone": "(none)" }, "inherit": { "inheritedFromList": "inherited · List", @@ -285,6 +289,7 @@ "copyFormattedTip": "Copy title, description and open steps", "toggleEditPreviewTip": "Toggle edit/preview", "planDay": "Plan day", + "importTicketsAndPlanDay": "Tickets + plan day", "prepEmpty": "No prep run today yet — click Plan day", "roadblockReply": { "placeholder": "Reply to the roadblock…", @@ -937,7 +942,8 @@ }, "reports": { "generatingWeekly": "Generating report…", - "runningDailyPrep": "Running daily prep…" + "runningDailyPrep": "Running daily prep…", + "runningTicketImport": "Importing tickets, then planning the day…" }, "planning": { "finalizing": "Finalizing plan…", diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index fb95bfa3..a7f046ff 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -190,6 +190,8 @@ public interface IWorkerClient : INotifyPropertyChanged Task GetWeekReportAsync(DateOnly start, DateOnly end); Task GenerateWeekReportAsync(DateOnly start, DateOnly end); Task RunDailyPrepNowAsync(); + /// Ticket import, then the daily prep, in one Prime run. + Task RunTicketImportNowAsync(); Task RefineTaskAsync(string taskId); event Action? RefineStartedEvent; @@ -244,6 +246,8 @@ public interface IWorkerClient : INotifyPropertyChanged Task OpenTokenTrackerDashboardAsync(); Task GetTicketSettingsAsync(); + /// Sets the fallback list for imported ticket items; null clears it. + Task SetTicketFallbackListAsync(string? listId); Task SetTicketApiBaseUrlAsync(string? baseUrl); Task SetTicketTokenAsync(string token); Task ClearTicketTokenAsync(); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 3b499133..819f6b85 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -504,6 +504,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public Task RunDailyPrepNowAsync() => InvokeTimedAsync("RunDailyPrepNow", () => _hub.InvokeAsync("RunDailyPrepNow")); + public Task RunTicketImportNowAsync() + => InvokeTimedAsync("RunTicketImportNow", () => _hub.InvokeAsync("RunTicketImportNow")); + public Task RefineTaskAsync(string taskId) => InvokeTimedAsync("RefineTask", () => _hub.InvokeAsync("RefineTask", taskId)); public Task ClearMyDayAsync() @@ -758,6 +761,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public Task GetTicketSettingsAsync() => TryInvokeAsync("GetTicketSettings"); + public Task SetTicketFallbackListAsync(string? listId) + => InvokeTimedAsync("SetTicketFallbackList", () => _hub.InvokeAsync("SetTicketFallbackList", listId)); + public Task SetTicketApiBaseUrlAsync(string? baseUrl) => InvokeTimedAsync("SetTicketApiBaseUrl", () => _hub.InvokeAsync("SetTicketApiBaseUrl", baseUrl)); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 56d2aca8..4e363abc 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -581,6 +581,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable Bind(null, "prep"); IsPrepMode = true; _ = Prep.LoadLastPrepLogIfEmptyAsync(); + _ = Prep.RefreshTicketsConfiguredAsync(); } /// What triggered the selection (); diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs index 2283a5de..3a55ed45 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/PrepPanelViewModel.cs @@ -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 { } + } + + /// Reads whether the ticket system has a base URL and a token; keeps the last value when the worker is unreachable. + 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; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs index 1f08d20e..3fc9bc8a 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeClaudeTabViewModel.cs @@ -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() { diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs index b3187e0b..07dbdaa2 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/PrimeScheduleRowViewModel.cs @@ -48,7 +48,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase /// ItemsControl share one selection. 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)); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs index bc428091..785a5c1e 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/TicketSettingsTabViewModel.cs @@ -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; +/// One fallback-list choice; Id null = no fallback. +public sealed record FallbackListOption(string? Id, string Name); + public sealed partial class TicketSettingsTabViewModel : ViewModelBase { /// @@ -21,6 +27,7 @@ public sealed partial class TicketSettingsTabViewModel : ViewModelBase public const bool FeatureEnabled = true; private readonly IWorkerClient _worker; + private readonly IDbContextFactory _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 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 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] diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index 65f1f4b5..add10d66 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -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 { diff --git a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml index 04c6623c..31607f25 100644 --- a/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml +++ b/src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml @@ -140,6 +140,11 @@ Command="{Binding Prep.PlanDayCommand}" IsEnabled="{Binding Prep.IsPlanDayEnabled}" Content="{loc:Tr details.planDay}"/> +