2 Commits
Author SHA1 Message Date
admin 3cccb3226c Merge remote-tracking branch 'origin/main'
CI / build-and-test (push) Canceled after 0s
2026-09-02 20:09:24 +02:00
adminandClaude Sonnet 5 988a293c42 Planung: Stunde einer anderen Einheit zuweisen ("Einheit wechseln")
Bisher war die Einheit einer Stunde nach dem Anlegen fix - bei falscher
Auswahl im Erstellen-Dialog blieb nur Löschen+Neuanlegen. Neuer Button
öffnet einen Dialog zur Auswahl der Ziel-Einheit; die Ansicht springt
danach automatisch dorthin mit der Stunde vorausgewählt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 20:07:53 +02:00
6 changed files with 161 additions and 0 deletions
@@ -93,6 +93,30 @@ public class PlanningTabViewModelTests
Assert.Equal(new DateOnly(2025, 9, 8), byUnit[l2.Id].Date); // unverändert Assert.Equal(new DateOnly(2025, 9, 8), byUnit[l2.Id].Date); // unverändert
} }
[Fact]
public async Task ChangeLessonUnit_OrdnetStundeDerZieleinheitZuUndWaehltSieAus()
{
var (vm, units, lessons, groupId) = BuildScenario();
var unitA = new Unit { GroupId = groupId, Title = "Mechanik" };
var unitB = new Unit { GroupId = groupId, Title = "Optik" };
units.Add(unitA); units.Add(unitB);
var l1 = new Lesson { UnitId = unitA.Id, GroupId = groupId, Date = new DateOnly(2025, 9, 1), Status = LessonStatus.Planned, Topic = "1" };
lessons.Add(l1);
vm.Initialize(groupId);
vm.SelectedUnit = vm.Units.Single(u => u.Id == unitA.Id);
vm.SelectedLesson = vm.Lessons.Single(l => l.Id == l1.Id);
vm.OnPickUnitChange = _ => Task.FromResult<Unit?>(unitB);
await vm.ChangeLessonUnitCommand.ExecuteAsync(null);
Assert.Empty(lessons.GetByUnit(unitA.Id));
var moved = Assert.Single(lessons.GetByUnit(unitB.Id));
Assert.Equal(l1.Id, moved.Id);
Assert.Equal(unitB.Id, vm.SelectedUnit?.Id);
Assert.Equal(l1.Id, vm.SelectedLesson?.Id);
}
[Fact] [Fact]
public async Task CopyUnit_ErhaeltAbstaendeUndSetztGroupIdAufZielgruppe() public async Task CopyUnit_ErhaeltAbstaendeUndSetztGroupIdAufZielgruppe()
{ {
@@ -5,6 +5,7 @@ using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models; using LehrerApp.Core.Models;
using LehrerApp.Core.Services; using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services; using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.ViewModels.Students;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
@@ -75,6 +76,7 @@ public partial class PlanningTabViewModel : ObservableObject
public Func<Lesson, List<string>, List<string>, Task<bool>>? OnEditLesson { get; set; } public Func<Lesson, List<string>, List<string>, Task<bool>>? OnEditLesson { get; set; }
public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; } public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; }
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; } public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
public Func<Lesson, Task<Unit?>>? OnPickUnitChange { get; set; }
public Func<Lesson, Task>? OnShowLesson { get; set; } public Func<Lesson, Task>? OnShowLesson { get; set; }
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; } public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
public Func<Unit, Task<bool>>? OnAiAssist { get; set; } public Func<Unit, Task<bool>>? OnAiAssist { get; set; }
@@ -160,6 +162,7 @@ public partial class PlanningTabViewModel : ObservableObject
EditLessonCommand.NotifyCanExecuteChanged(); EditLessonCommand.NotifyCanExecuteChanged();
DeleteLessonCommand.NotifyCanExecuteChanged(); DeleteLessonCommand.NotifyCanExecuteChanged();
MoveLessonCommand.NotifyCanExecuteChanged(); MoveLessonCommand.NotifyCanExecuteChanged();
ChangeLessonUnitCommand.NotifyCanExecuteChanged();
AdvanceLessonStatusCommand.NotifyCanExecuteChanged(); AdvanceLessonStatusCommand.NotifyCanExecuteChanged();
} }
@@ -349,6 +352,23 @@ public partial class PlanningTabViewModel : ObservableObject
new LessonSchedulingService(_lessons).Move(moved, newDate, newPeriod, shiftFollowing); new LessonSchedulingService(_lessons).Move(moved, newDate, newPeriod, shiftFollowing);
} }
/// Ordnet eine bereits angelegte Stunde einer anderen Einheit derselben Gruppe zu (Korrektur
/// einer im Erstellen-Dialog falsch gewählten Einheit, Nutzer-Feedback) — bisher war das nur
/// über Löschen + Neuanlegen möglich. Datum/Stundennummer bleiben unverändert, nur UnitId ändert
/// sich. Nach dem Speichern springt die Ansicht direkt zur Ziel-Einheit mit der Stunde
/// vorausgewählt, damit sichtbar bleibt, wohin sie verschoben wurde.
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
private async Task ChangeLessonUnit()
{
if (OnPickUnitChange is null || SelectedLesson is null) return;
var lesson = SelectedLesson.Model;
var targetUnit = await OnPickUnitChange(lesson);
if (targetUnit is null) return;
lesson.UnitId = targetUnit.Id;
_lessons.Save(lesson);
RefreshPlanning(selectUnitId: targetUnit.Id, selectLessonId: lesson.Id);
}
[RelayCommand(CanExecute = nameof(HasSelectedLesson))] [RelayCommand(CanExecute = nameof(HasSelectedLesson))]
private void AdvanceLessonStatus() private void AdvanceLessonStatus()
{ {
@@ -1206,6 +1226,38 @@ public partial class MoveLessonDialogViewModel : ObservableObject
} }
} }
// ── Dialog: Stunde einer anderen Einheit zuweisen (Korrektur der Einheit) ────
public partial class ChangeLessonUnitDialogViewModel : ObservableObject
{
[ObservableProperty] private TimetableUnitOption? _selectedUnit;
[ObservableProperty] private string _error = "";
public string CurrentUnitLabel { get; }
public ObservableCollection<TimetableUnitOption> Units { get; } = [];
public bool HasUnits => Units.Count > 0;
public Unit? Result { get; private set; }
public ChangeLessonUnitDialogViewModel(IUnitRepository units, Guid groupId, Guid currentUnitId,
string currentUnitTitle)
{
CurrentUnitLabel = currentUnitTitle;
foreach (var unit in units.GetByGroup(groupId).Where(u => u.Id != currentUnitId)
.OrderBy(u => u.Status == UnitStatus.Active ? 0 : u.Status == UnitStatus.Planned ? 1 : 2)
.ThenByDescending(u => u.StartDate)
.ThenBy(u => u.Title, StringComparer.CurrentCultureIgnoreCase))
Units.Add(new TimetableUnitOption(unit));
}
[RelayCommand]
private void Save()
{
Error = "";
if (SelectedUnit is null) { Error = "Bitte eine Ziel-Einheit auswählen."; return; }
Result = SelectedUnit.Model;
}
}
// ── Dialog: Stunden serienweise aus dem Stundenplan erzeugen (4.2.5) ──────── // ── Dialog: Stunden serienweise aus dem Stundenplan erzeugen (4.2.5) ────────
public partial class GenerateLessonSeriesDialogViewModel : ObservableObject public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
@@ -0,0 +1,47 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
xmlns:vmp="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
x:Class="LehrerApp.Desktop.Views.Groups.ChangeLessonUnitDialog"
x:DataType="vm:ChangeLessonUnitDialogViewModel"
Title="Einheit wechseln"
Width="440" Height="360" MinWidth="400" MinHeight="320"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="14">
<TextBlock Text="Einheit wechseln" Classes="dialogtitle"/>
<TextBlock FontSize="12" Opacity="0.6">
<Run Text="Bisherige Einheit: "/>
<Run Text="{Binding CurrentUnitLabel}"/>
</TextBlock>
<StackPanel Spacing="5" IsVisible="{Binding HasUnits}">
<TextBlock Text="Neue Einheit" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Units}" SelectedItem="{Binding SelectedUnit}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vmp:TimetableUnitOption">
<StackPanel>
<TextBlock Text="{Binding Label}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Detail}" FontSize="11" Opacity="0.6"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<TextBlock Text="Keine andere Einheit in dieser Gruppe vorhanden."
FontSize="12" Opacity="0.7" TextWrapping="Wrap"
IsVisible="{Binding !HasUnits}"/>
<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="Verschieben" HorizontalAlignment="Stretch" Click="OnSave"
IsEnabled="{Binding HasUnits}"/>
</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 ChangeLessonUnitDialog : Window
{
public ChangeLessonUnitDialog() => InitializeComponent();
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is ChangeLessonUnitDialogViewModel 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);
}
@@ -91,6 +91,8 @@
ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/> ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/>
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/> <Button Content="Bearbeiten" Command="{Binding EditLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/> <Button Content="Verschieben" Command="{Binding MoveLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Einheit wechseln" Command="{Binding ChangeLessonUnitCommand}" IsEnabled="{Binding !IsReadOnly}"
ToolTip.Tip="Ordnet die ausgewählte Stunde einer anderen Einheit derselben Gruppe zu."/>
<Button Content="Status weiter" Command="{Binding AdvanceLessonStatusCommand}" IsEnabled="{Binding !IsReadOnly}" <Button Content="Status weiter" Command="{Binding AdvanceLessonStatusCommand}" IsEnabled="{Binding !IsReadOnly}"
ToolTip.Tip="Entwurf/Geplant → Bereit → Durchgeführt"/> ToolTip.Tip="Entwurf/Geplant → Bereit → Durchgeführt"/>
<Button Content="Sitzung erzeugen" Command="{Binding CreateParticipationSessionCommand}" <Button Content="Sitzung erzeugen" Command="{Binding CreateParticipationSessionCommand}"
@@ -38,6 +38,7 @@ public partial class PlanningTabView : UserControl
ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson); ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson);
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog; vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
vm.OnPickMoveTarget = ShowMoveLessonDialog; vm.OnPickMoveTarget = ShowMoveLessonDialog;
vm.OnPickUnitChange = ShowChangeLessonUnitDialog;
vm.OnShowLesson = ShowLessonViewerDialog; vm.OnShowLesson = ShowLessonViewerDialog;
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog; vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
vm.OnAiAssist = ShowAiAssistDialog; vm.OnAiAssist = ShowAiAssistDialog;
@@ -136,6 +137,20 @@ public partial class PlanningTabView : UserControl
return ok ? dialogVm.Result : null; return ok ? dialogVm.Result : null;
} }
private async Task<Unit?> ShowChangeLessonUnitDialog(Lesson lesson)
{
var dialogVm = new ChangeLessonUnitDialogViewModel(
App.Services.GetRequiredService<IUnitRepository>(),
lesson.GroupId, lesson.UnitId,
(DataContext as PlanningTabViewModel)?.SelectedUnit?.Title ?? "");
var dialog = new ChangeLessonUnitDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
var ok = await dialog.ShowDialog<bool>(owner);
return ok ? dialogVm.Result : null;
}
private async Task ShowLessonViewerDialog(Lesson lesson) private async Task ShowLessonViewerDialog(Lesson lesson)
{ {
var viewerVm = new LessonViewerViewModel(lesson, var viewerVm = new LessonViewerViewModel(lesson,