diff --git a/LehrerApp.Desktop.Tests/LessonFixItSearchTests.cs b/LehrerApp.Desktop.Tests/LessonFixItSearchTests.cs
new file mode 100644
index 0000000..e8182a8
--- /dev/null
+++ b/LehrerApp.Desktop.Tests/LessonFixItSearchTests.cs
@@ -0,0 +1,94 @@
+using LehrerApp.Core.Models;
+using LehrerApp.Desktop.ViewModels.Groups;
+using Xunit;
+
+namespace LehrerApp.Desktop.Tests;
+
+/// Deckt die Kandidatensuche für den "Vorhandene Stunde verknüpfen"-Button im Stunden-anlegen-
+/// Dialog ab (Nutzer-Feedback: per JSON-Import/KI ohne Stundennummer angelegte Stunden tauchen im
+/// Stundenplan nicht auf, da der dort verwendete FindLessonForSlot per Datum+Stundennummer sucht).
+public class LessonFixItSearchTests
+{
+ [Fact]
+ public void FindCandidates_FindetVerwaisteStundeOhneStundennummerAmSelbenTag()
+ {
+ var lessons = new FakeLessons();
+ var groupId = Guid.NewGuid();
+ var unitId = Guid.NewGuid();
+ var date = new DateOnly(2026, 9, 8);
+ var orphan = new Lesson { UnitId = unitId, GroupId = groupId, Date = date, LessonNumber = null, Topic = "Reflexionsgesetz" };
+ lessons.Add(orphan);
+
+ var candidates = LessonFixItSearch.FindCandidates(lessons, groupId, date);
+
+ var found = Assert.Single(candidates);
+ Assert.Equal(orphan.Id, found.Id);
+ }
+
+ [Fact]
+ public void FindCandidates_FindetStundeAmSelbenTagMitAbweichenderStundennummer()
+ {
+ var lessons = new FakeLessons();
+ var groupId = Guid.NewGuid();
+ var unitId = Guid.NewGuid();
+ var date = new DateOnly(2026, 9, 8);
+ var wrongPeriod = new Lesson { UnitId = unitId, GroupId = groupId, Date = date, LessonNumber = 7, Topic = "Reflexionsgesetz" };
+ lessons.Add(wrongPeriod);
+
+ var candidates = LessonFixItSearch.FindCandidates(lessons, groupId, date);
+
+ var found = Assert.Single(candidates);
+ Assert.Equal(wrongPeriod.Id, found.Id);
+ }
+
+ [Fact]
+ public void FindCandidates_SortiertExaktesDatumVorNahenTerminenOhneStundennummer()
+ {
+ var lessons = new FakeLessons();
+ var groupId = Guid.NewGuid();
+ var unitId = Guid.NewGuid();
+ var date = new DateOnly(2026, 9, 8);
+ var near = new Lesson { UnitId = unitId, GroupId = groupId, Date = date.AddDays(-2), LessonNumber = null, Topic = "Nah" };
+ var exact = new Lesson { UnitId = unitId, GroupId = groupId, Date = date, LessonNumber = null, Topic = "Exakt" };
+ lessons.Add(near); lessons.Add(exact);
+
+ var candidates = LessonFixItSearch.FindCandidates(lessons, groupId, date);
+
+ Assert.Equal(2, candidates.Count);
+ Assert.Equal(exact.Id, candidates[0].Id);
+ Assert.Equal(near.Id, candidates[1].Id);
+ }
+
+ [Fact]
+ public void FindCandidates_IgnoriertStundenAusserhalbDesZeitfenstersUndAnderenGruppen()
+ {
+ var lessons = new FakeLessons();
+ var groupId = Guid.NewGuid();
+ var otherGroupId = Guid.NewGuid();
+ var unitId = Guid.NewGuid();
+ var date = new DateOnly(2026, 9, 8);
+ lessons.Add(new Lesson { UnitId = unitId, GroupId = groupId, Date = date.AddDays(-30), LessonNumber = null, Topic = "Zu weit weg" });
+ lessons.Add(new Lesson { UnitId = unitId, GroupId = otherGroupId, Date = date, LessonNumber = null, Topic = "Andere Gruppe" });
+ // Gesetzte Stundennummer, aber exakt am gesuchten Datum — bewusst als Kandidat enthalten
+ // (z.B. falsch nummerierter Import); der Nutzer entscheidet im Bestätigungs-/
+ // Auswahldialog, ob sie passt.
+ lessons.Add(new Lesson { UnitId = unitId, GroupId = groupId, Date = date, LessonNumber = 3, Topic = "Falsch nummeriert, aber am gesuchten Tag" });
+
+ var candidates = LessonFixItSearch.FindCandidates(lessons, groupId, date);
+
+ var found = Assert.Single(candidates);
+ Assert.Equal("Falsch nummeriert, aber am gesuchten Tag", found.Topic);
+ }
+
+ [Fact]
+ public void FindCandidates_LeerOhnePassendeStunde()
+ {
+ var lessons = new FakeLessons();
+ var groupId = Guid.NewGuid();
+ var date = new DateOnly(2026, 9, 8);
+
+ var candidates = LessonFixItSearch.FindCandidates(lessons, groupId, date);
+
+ Assert.Empty(candidates);
+ }
+}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
index f90c505..1c1fdad 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
@@ -745,8 +745,10 @@ public partial class LessonDialogViewModel : ObservableObject
// gespeicherte Stunden sinnvoll, eine gerade erst angelegte, noch ungespeicherte Stunde hat
// keine echte Id, auf die sich die KI beziehen könnte.
public Guid UnitId => _unitId;
+ public Guid GroupId => _groupId;
public Lesson? EditingLesson => _editingLesson;
public bool CanAiAssist => _editingLesson is not null;
+ public bool IsNewLesson => _editingLesson is null;
/// Vom Code-Behind nach einer über die KI angewendeten Änderung aufgerufen: der Dialog schließt
/// sich danach mit Result != null, damit die aufrufende Liste neu lädt — die eigenen, jetzt
@@ -1258,6 +1260,56 @@ public partial class ChangeLessonUnitDialogViewModel : ObservableObject
}
}
+// ── Dialog: Vorhandene Stunde zum Verknüpfen auswählen (Stundenplan-FixIt) ────
+
+///
+/// Sucht Stunden, die zu einem Stundenplan-Termin gehören könnten, aber (z.B. durch JSON-Import
+/// oder KI-Übernahme ohne Stundenplan-Bezug) keine passende
+/// haben und deshalb von TimetableViewModel.FindLessonForSlot nicht gefunden werden.
+/// Als eigene, von Avalonia unabhängige Methode extrahiert, damit die Zuordnungslogik ohne Fenster
+/// testbar ist — Aufrufer ist LessonDialog.axaml.cs (Button "Vorhandene Stunde verknüpfen").
+///
+public static class LessonFixItSearch
+{
+ public static List FindCandidates(ILessonRepository lessons, Guid groupId, DateOnly date) =>
+ [.. lessons.GetByGroupAndRange(groupId, date.AddDays(-14), date.AddDays(14))
+ .Where(l => l.LessonNumber is null || l.Date == date)
+ .OrderBy(l => l.Date == date ? 0 : 1)
+ .ThenBy(l => Math.Abs(l.Date.DayNumber - date.DayNumber))];
+}
+
+public sealed class LessonLinkOption(Lesson lesson, string unitTitle)
+{
+ public Lesson Model { get; } = lesson;
+ public string Label { get; } = string.IsNullOrWhiteSpace(lesson.Topic) ? "(ohne Thema)" : lesson.Topic;
+ public string Detail { get; } =
+ $"{lesson.Date:dd.MM.yyyy} · {(lesson.LessonNumber is int n ? $"{n}. Stunde" : "keine Stundennummer")} · Einheit „{unitTitle}“";
+}
+
+public partial class LinkExistingLessonDialogViewModel : ObservableObject
+{
+ [ObservableProperty] private LessonLinkOption? _selectedOption;
+ [ObservableProperty] private string _error = "";
+
+ public ObservableCollection Options { get; } = [];
+ public Lesson? Result { get; private set; }
+
+ public LinkExistingLessonDialogViewModel(List candidates, IUnitRepository units)
+ {
+ foreach (var lesson in candidates)
+ Options.Add(new LessonLinkOption(lesson, units.GetById(lesson.UnitId)?.Title ?? "?"));
+ SelectedOption = Options.FirstOrDefault();
+ }
+
+ [RelayCommand]
+ private void Save()
+ {
+ Error = "";
+ if (SelectedOption is null) { Error = "Bitte eine Stunde auswählen."; return; }
+ Result = SelectedOption.Model;
+ }
+}
+
// ── Dialog: Stunden serienweise aus dem Stundenplan erzeugen (4.2.5) ────────
public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
diff --git a/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml b/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml
index cebf0b7..78388c7 100644
--- a/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml
@@ -185,6 +185,9 @@
+
diff --git a/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml.cs
index ddc9829..8f8f719 100644
--- a/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml.cs
@@ -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()
+ .ShowError("Bitte zuerst ein gültiges Datum eintragen.");
+ return;
+ }
+
+ var lessonRepo = App.Services.GetRequiredService();
+ var candidates = LessonFixItSearch.FindCandidates(lessonRepo, vm.GroupId, date);
+
+ if (candidates.Count == 0)
+ {
+ App.Services.GetRequiredService()
+ .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(this)) return;
+ chosen = candidates[0];
+ }
+ else
+ {
+ var pickerVm = new LinkExistingLessonDialogViewModel(candidates,
+ App.Services.GetRequiredService());
+ var picker = new LinkExistingLessonDialog { DataContext = pickerVm };
+ if (!await picker.ShowDialog(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);
+ }
}
diff --git a/LehrerApp.Desktop/Views/Groups/LinkExistingLessonDialog.axaml b/LehrerApp.Desktop/Views/Groups/LinkExistingLessonDialog.axaml
new file mode 100644
index 0000000..b2e0075
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Groups/LinkExistingLessonDialog.axaml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/LinkExistingLessonDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/LinkExistingLessonDialog.axaml.cs
new file mode 100644
index 0000000..277fc28
--- /dev/null
+++ b/LehrerApp.Desktop/Views/Groups/LinkExistingLessonDialog.axaml.cs
@@ -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);
+}