Stundenplan: verwaiste Stunden per "FixIt"-Button verknüpfen
CI / build-and-test (push) Canceled after 0s

Der Stundenplan erkennt eine Stunde nur über Datum+Stundennummer
(kein gespeichertes Verknüpfungsfeld). Per JSON-Import oder KI ohne
Stundenplan-Bezug angelegte Stunden ohne passende Stundennummer
tauchten dort nie auf - ein Klick auf den Termin bot nur "neu anlegen"
an und hätte eine Dublette erzeugt. Neuer Button im Anlegen-Dialog
sucht stattdessen nach einer passenden vorhandenen Stunde und hängt
sie auf den geklickten Termin um.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-02 21:27:06 +02:00
co-authored by Claude Sonnet 5
parent 3cccb3226c
commit 94946ffefb
6 changed files with 265 additions and 0 deletions
@@ -185,6 +185,9 @@
<Button Grid.Column="2" Content="🤖 KI-Unterstützung für diese Stunde" HorizontalAlignment="Stretch"
Click="OnAiAssist" IsVisible="{Binding CanAiAssist}"
ToolTip.Tip="Fragt die KI gezielt zu genau dieser Stunde — Speicherstand geht dabei direkt in die Datenbank, nicht über die Felder hier."/>
<Button Grid.Column="2" Content="🔧 Vorhandene Stunde verknüpfen" HorizontalAlignment="Stretch"
Click="OnFixIt" IsVisible="{Binding IsNewLesson}"
ToolTip.Tip="Sucht nach einer bereits vorhandenen Stunde (z.B. durch Import oder KI ohne Stundenplan-Bezug angelegt) und verknüpft sie mit diesem Termin, statt eine neue Stunde anzulegen."/>
<Button Grid.Column="4" Content="{Binding SaveButtonText}"
HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
@@ -6,7 +6,9 @@ using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.Views.Shared;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;
namespace LehrerApp.Desktop.Views.Groups;
@@ -200,4 +202,59 @@ public partial class LessonDialog : Window
if (updated is not null) vm.MarkAppliedExternally(updated);
Close(true);
}
/// Deckt die Lücke ab, dass der Stundenplan eine Stunde nur über Datum+Stundennummer findet
/// (kein gespeichertes Verknüpfungsfeld) — eine per JSON-Import oder KI ohne Stundennummer
/// angelegte Stunde taucht dort nie auf und würde sonst beim Klick auf den Termin dupliziert.
/// Statt neu anzulegen, wird die gefundene vorhandene Stunde auf diesen Termin umgehängt.
private async void OnFixIt(object? s, RoutedEventArgs e)
{
if (DataContext is not LessonDialogViewModel vm) return;
if (!DateOnly.TryParseExact(vm.DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
{
App.Services.GetRequiredService<NotificationService>()
.ShowError("Bitte zuerst ein gültiges Datum eintragen.");
return;
}
var lessonRepo = App.Services.GetRequiredService<ILessonRepository>();
var candidates = LessonFixItSearch.FindCandidates(lessonRepo, vm.GroupId, date);
if (candidates.Count == 0)
{
App.Services.GetRequiredService<NotificationService>()
.ShowError("Keine passende bestehende Stunde gefunden.");
return;
}
Lesson chosen;
if (candidates.Count == 1)
{
var info = new ConfirmDialogInfo
{
Title = "Vorhandene Stunde verknüpfen?",
Message = $"Vorhandene Stunde „{candidates[0].Topic}“ vom {candidates[0].Date:dd.MM.yyyy} gefunden. " +
"Mit diesem Termin verknüpfen, statt eine neue Stunde anzulegen?",
ConfirmText = "Verknüpfen",
};
var confirm = new ConfirmDialog { DataContext = info };
if (!await confirm.ShowDialog<bool>(this)) return;
chosen = candidates[0];
}
else
{
var pickerVm = new LinkExistingLessonDialogViewModel(candidates,
App.Services.GetRequiredService<IUnitRepository>());
var picker = new LinkExistingLessonDialog { DataContext = pickerVm };
if (!await picker.ShowDialog<bool>(this) || pickerVm.Result is not { } picked) return;
chosen = picked;
}
chosen.UnitId = vm.UnitId;
chosen.Date = date;
chosen.LessonNumber = vm.LessonNumber;
lessonRepo.Save(chosen);
vm.MarkAppliedExternally(chosen);
Close(true);
}
}
@@ -0,0 +1,38 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.LinkExistingLessonDialog"
x:DataType="vm:LinkExistingLessonDialogViewModel"
Title="Vorhandene Stunde verknüpfen"
Width="470" Height="360" MinWidth="420" MinHeight="320"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="16">
<TextBlock Text="Vorhandene Stunde verknüpfen" Classes="dialogtitle"/>
<TextBlock Text="Mehrere Stunden kommen für diesen Termin infrage — bitte die passende auswählen."
FontSize="12" TextWrapping="Wrap" Opacity="0.75"/>
<StackPanel Spacing="5">
<TextBlock Text="Stunde" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:LessonLinkOption">
<StackPanel>
<TextBlock Text="{Binding Label}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Detail}" FontSize="11" Opacity="0.6"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<TextBlock Text="{Binding Error}" Foreground="Red" FontSize="11"
IsVisible="{Binding Error, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Verknüpfen" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,21 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class LinkExistingLessonDialog : Window
{
public LinkExistingLessonDialog() => InitializeComponent();
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is LinkExistingLessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
{
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close(true);
}
}
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
}