@@ -4,7 +4,7 @@
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.MoveLessonDialog"
|
||||
x:DataType="vm:MoveLessonDialogViewModel"
|
||||
Title="Stunde verschieben"
|
||||
Width="400" Height="260" MinWidth="360" MinHeight="240"
|
||||
Width="440" Height="430" MinWidth="400" MinHeight="400"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
@@ -13,6 +13,8 @@
|
||||
<TextBlock FontSize="12" Opacity="0.6">
|
||||
<Run Text="Bisheriges Datum: "/>
|
||||
<Run Text="{Binding CurrentDateDisplay}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding CurrentPeriodDisplay}"/>
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
@@ -22,6 +24,24 @@
|
||||
IsVisible="{Binding NewDateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Neue Stunde" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding NewPeriod}" Minimum="1" Maximum="20"
|
||||
FormatString="0" PlaceholderText="Stundennummer"/>
|
||||
<TextBlock Text="{Binding NewPeriodError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding NewPeriodError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="6"
|
||||
Padding="10" IsVisible="{Binding CanSplitDoubleLesson}">
|
||||
<StackPanel Spacing="5">
|
||||
<CheckBox Content="Nur den zweiten Teil der Doppelstunde verschieben"
|
||||
IsChecked="{Binding SplitDoubleLesson}"/>
|
||||
<TextBlock Text="Der Verlauf wird an der Dauer der ersten Stunde geteilt. Der zweite Teil wird als eigene Fortsetzungsstunde angelegt."
|
||||
FontSize="11" Opacity="0.65" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<CheckBox Content="Folgestunden automatisch verschieben"
|
||||
IsChecked="{Binding ShiftFollowingPlanned}"
|
||||
ToolTip.Tip="Verschiebt alle noch geplanten Stunden derselben Einheit, die nach dieser Stunde liegen, um denselben Zeitraum. Bereits durchgeführte Stunden bleiben unverändert."/>
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
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="Verschieben" Command="{Binding MoveLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}" IsEnabled="{Binding !IsReadOnly}"/>
|
||||
<Button Content="Status weiter" Command="{Binding AdvanceLessonStatusCommand}" IsEnabled="{Binding !IsReadOnly}"
|
||||
ToolTip.Tip="Entwurf/Geplant → Bereit → Durchgeführt"/>
|
||||
<Button Content="Sitzung erzeugen" Command="{Binding CreateParticipationSessionCommand}"
|
||||
IsEnabled="{Binding !IsReadOnly}"
|
||||
ToolTip.Tip="Legt eine Mitarbeitssitzung mit Datum und Thema dieser Stunde an."/>
|
||||
|
||||
@@ -42,6 +42,7 @@ public partial class PlanningTabView : UserControl
|
||||
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
|
||||
vm.OnAiAssist = ShowAiAssistDialog;
|
||||
vm.OnNotify = Notifications.ShowSuccess;
|
||||
vm.OnError = Notifications.ShowError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +127,7 @@ public partial class PlanningTabView : UserControl
|
||||
|
||||
private async Task<MoveLessonTarget?> ShowMoveLessonDialog(Lesson lesson)
|
||||
{
|
||||
var dialogVm = new MoveLessonDialogViewModel(lesson.Date);
|
||||
var dialogVm = new MoveLessonDialogViewModel(lesson.Date, lesson.LessonNumber);
|
||||
var dialog = new MoveLessonDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return null;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Planning"
|
||||
x:Class="LehrerApp.Desktop.Views.Planning.TimetableUnitPickerDialog"
|
||||
x:DataType="vm:TimetableUnitPickerViewModel"
|
||||
Title="Einheit für die Stunde wählen"
|
||||
Width="470" Height="390" MinWidth="420" MinHeight="360"
|
||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<StackPanel Grid.Row="0" Spacing="16">
|
||||
<TextBlock Text="Stunde anlegen" Classes="dialogtitle"/>
|
||||
<TextBlock Text="{Binding ContextLabel}" FontSize="12" Opacity="0.65"/>
|
||||
<TextBlock Text="Die Stunde braucht eine Unterrichtseinheit. Laufende Einheiten werden zuerst vorgeschlagen."
|
||||
FontSize="12" TextWrapping="Wrap" Opacity="0.75"/>
|
||||
|
||||
<StackPanel Spacing="5" IsVisible="{Binding HasUnits}">
|
||||
<TextBlock Text="Vorhandene Einheit" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding Units}" SelectedItem="{Binding SelectedUnit}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TimetableUnitOption">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Label}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="5">
|
||||
<TextBlock Text="Oder neue Einheit anlegen" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding NewUnitTitle}" PlaceholderText="Titel der neuen Einheit"/>
|
||||
</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="Weiter zur Planung" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,19 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Planning;
|
||||
|
||||
public partial class TimetableUnitPickerDialog : Window
|
||||
{
|
||||
public TimetableUnitPickerDialog() => InitializeComponent();
|
||||
|
||||
private void OnSave(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not TimetableUnitPickerViewModel vm) return;
|
||||
vm.SaveCommand.Execute(null);
|
||||
if (vm.Result is not null) Close(true);
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -120,6 +120,7 @@
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding LessonTopic}" FontSize="11" Opacity="0.75"
|
||||
TextWrapping="Wrap" IsVisible="{Binding HasLessonTopic}"/>
|
||||
<TextBlock Text="{Binding PlanningStatusLabel}" FontSize="10" Opacity="0.62"/>
|
||||
<TextBlock Text="{Binding ExamTitle}" FontSize="11" Foreground="#D85A30" FontWeight="SemiBold"
|
||||
IsVisible="{Binding HasExam}"/>
|
||||
<TextBlock Text="📓 Hausaufgabe aus letzter Stunde noch nicht kontrolliert" FontSize="11"
|
||||
@@ -258,6 +259,8 @@
|
||||
IsVisible="{Binding HasRoom}"/>
|
||||
<TextBlock Text="{Binding Topic}" FontSize="10" Foreground="White" Opacity="0.8"
|
||||
TextWrapping="Wrap" IsVisible="{Binding HasTopic}"/>
|
||||
<TextBlock Text="{Binding PlanningStatusLabel}" FontSize="9" Foreground="White"
|
||||
Opacity="0.78" Margin="0,1,0,0"/>
|
||||
<TextBlock Text="Ferien" FontSize="10" Foreground="White" FontWeight="SemiBold"
|
||||
Opacity="0.9" IsVisible="{Binding IsHoliday}" Margin="0,2,0,0"/>
|
||||
<TextBlock Text="Ausfall" FontSize="10" Foreground="White" FontWeight="SemiBold"
|
||||
@@ -293,6 +296,12 @@
|
||||
<MenuItem Header="📋 Planungsviewer" IsVisible="{Binding HasLesson}"
|
||||
Tag="{x:Static vm:TimetableLessonDestination.Viewer}"
|
||||
Click="OnWeekCellMenuItemClick"/>
|
||||
<MenuItem Header="+ Stunde anlegen" IsVisible="{Binding HasNoLesson}"
|
||||
Tag="{x:Static vm:TimetableLessonDestination.Create}"
|
||||
Click="OnWeekCellMenuItemClick"/>
|
||||
<MenuItem Header="↪ Stunde verschieben" IsVisible="{Binding HasLesson}"
|
||||
Tag="{x:Static vm:TimetableLessonDestination.Move}"
|
||||
Click="OnWeekCellMenuItemClick"/>
|
||||
<MenuItem Header="🪑 Sitzplan"
|
||||
Tag="{x:Static vm:TimetableLessonDestination.SeatingPlan}"
|
||||
Click="OnWeekCellMenuItemClick"/>
|
||||
|
||||
@@ -26,6 +26,8 @@ public partial class TimetableView : UserControl
|
||||
vm.OnImportWebUntisTimetable = ShowWebUntisTimetableDialog;
|
||||
vm.OnOpenLessonViewer = ShowLessonViewerDialog;
|
||||
vm.OnOpenTeachingMode = ShowTeachingMode;
|
||||
vm.OnCreateLesson = ShowCreateLessonDialog;
|
||||
vm.OnMoveLesson = ShowMoveLessonDialog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +35,11 @@ public partial class TimetableView : UserControl
|
||||
/// Stelle gelöst, in der "Heute"-Tagesliste statt im Wochenraster): im Wochenraster oben
|
||||
/// führt ein Klick auf eine Stunden-Kachel bislang entweder in den Planungsviewer oder zur
|
||||
/// Einheitenplanung — je nachdem, ob schon eine Lesson existiert, ohne dass das von außen
|
||||
/// erkennbar wäre. Popup-Menü (MenuFlyout, kein ComboBox mehr) mit allen vier Zielen als
|
||||
/// erkennbar wäre. Popup-Menü (MenuFlyout, kein ComboBox mehr) mit allen Zielen einschließlich
|
||||
/// Direktanlage und Verschieben als
|
||||
/// Alternative zum Direktklick, siehe <see cref="TimetableViewModel.OpenWeekCellCommand"/>
|
||||
/// für den "einheitlicheren" Standard-Klick (Unterrichtsansicht bei laufender Stunde, sonst
|
||||
/// Planungsviewer, sonst Einheitenplanung). MenuItem.Click statt Command-Binding: ein
|
||||
/// Planungsviewer, sonst Direktanlage). MenuItem.Click statt Command-Binding: ein
|
||||
/// $parent[ItemsControl]-Vorfahrenpfad (wie beim Zeilen-Button) funktioniert innerhalb eines
|
||||
/// Flyouts nicht zuverlässig, weil dessen Popup nicht im normalen visuellen Baum hängt (siehe
|
||||
/// TODO.md-Nachtrag zum Klassenlehrer-Bereich) — DataContext-Vererbung (kein Pfad-Suchen,
|
||||
@@ -58,6 +61,14 @@ public partial class TimetableView : UserControl
|
||||
if (cell.Lesson is { } viewLesson && vm.OnOpenLessonViewer is not null)
|
||||
await vm.OnOpenLessonViewer(viewLesson);
|
||||
break;
|
||||
case TimetableLessonDestination.Create:
|
||||
if (cell.Date is { } createDate && vm.OnCreateLesson is not null)
|
||||
await vm.OnCreateLesson(new TimetableLessonRequest(cell.GroupId, createDate, cell.PeriodNumber));
|
||||
break;
|
||||
case TimetableLessonDestination.Move:
|
||||
if (cell.Lesson is { } moveLesson && vm.OnMoveLesson is not null)
|
||||
await vm.OnMoveLesson(new TimetableLessonMoveRequest(moveLesson, cell.PeriodNumber));
|
||||
break;
|
||||
case TimetableLessonDestination.Planning:
|
||||
if (cell.GroupId != Guid.Empty)
|
||||
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToGroupDetail(cell.GroupId, 6);
|
||||
@@ -69,6 +80,96 @@ public partial class TimetableView : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowCreateLessonDialog(TimetableLessonRequest request)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
var groups = App.Services.GetRequiredService<IGroupRepository>();
|
||||
var group = groups.GetById(request.GroupId);
|
||||
if (owner is null || group is null || !group.IsActive) return;
|
||||
|
||||
var lessons = App.Services.GetRequiredService<ILessonRepository>();
|
||||
if (lessons.GetByGroupAndDate(request.GroupId, request.Date)
|
||||
.Any(l => l.LessonNumber == request.PeriodNumber))
|
||||
{
|
||||
App.Services.GetRequiredService<NotificationService>()
|
||||
.ShowError("Für diesen Termin existiert bereits eine Stundenplanung.");
|
||||
return;
|
||||
}
|
||||
|
||||
var units = App.Services.GetRequiredService<IUnitRepository>();
|
||||
var pickerVm = new TimetableUnitPickerViewModel(units, group.Id, group.Name,
|
||||
request.Date, request.PeriodNumber);
|
||||
var picker = new TimetableUnitPickerDialog { DataContext = pickerVm };
|
||||
if (!await picker.ShowDialog<bool>(owner) || pickerVm.Result is not { } unit) return;
|
||||
|
||||
var groupLessons = units.GetByGroup(group.Id).SelectMany(u => lessons.GetByUnit(u.Id)).ToList();
|
||||
var materials = groupLessons.SelectMany(l => l.Phases).Select(p => p.Material)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
var shorthands = groupLessons.SelectMany(l => l.Phases).Select(p => p.Shorthand)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(x => x, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
var subject = group.SubjectId is { } subjectId
|
||||
? App.Services.GetRequiredService<ISubjectRepository>().GetById(subjectId) : null;
|
||||
|
||||
var lessonVm = new LessonDialogViewModel(
|
||||
lessons,
|
||||
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||
App.Services.GetRequiredService<ITimetableSlotRepository>(),
|
||||
App.Services.GetRequiredService<PeriodScheduleService>(),
|
||||
App.Services.GetRequiredService<IAttachmentStorage>(),
|
||||
unit.Id, group.Id, group.Name, subject?.Name ?? "", materials, shorthands,
|
||||
editingLesson: null, suggestedDate: request.Date, suggestedPeriod: request.PeriodNumber);
|
||||
var lessonDialog = new LessonDialog { DataContext = lessonVm };
|
||||
if (await lessonDialog.ShowDialog<bool>(owner) && lessonVm.Result is not null)
|
||||
{
|
||||
if (pickerVm.ResultIsNew) units.Save(unit);
|
||||
(DataContext as TimetableViewModel)?.Load();
|
||||
App.Services.GetRequiredService<NotificationService>().ShowSuccess("Stunde angelegt.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowMoveLessonDialog(TimetableLessonMoveRequest request)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return;
|
||||
var lesson = request.Lesson;
|
||||
var slots = App.Services.GetRequiredService<ITimetableSlotRepository>();
|
||||
var schedule = App.Services.GetRequiredService<PeriodScheduleService>();
|
||||
var anchor = lesson.LessonNumber;
|
||||
var firstPartMinutes = anchor is int p ? schedule.GetDurationMinutes(p) : 0;
|
||||
var isDouble = anchor is int start && firstPartMinutes > 0 &&
|
||||
slots.GetByGroup(lesson.GroupId).Any(s => s.Weekday == lesson.Date.DayOfWeek &&
|
||||
s.PeriodNumber == start + 1) && lesson.Phases.Sum(x => x.DurationMinutes) > firstPartMinutes;
|
||||
|
||||
var dialogVm = new MoveLessonDialogViewModel(lesson.Date, lesson.LessonNumber, isDouble,
|
||||
isDouble && anchor is int a && request.SelectedPeriod > a);
|
||||
var dialog = new MoveLessonDialog { DataContext = dialogVm };
|
||||
if (!await dialog.ShowDialog<bool>(owner) || dialogVm.Result is not { } target) return;
|
||||
|
||||
try
|
||||
{
|
||||
var service = new LessonSchedulingService(App.Services.GetRequiredService<ILessonRepository>());
|
||||
if (target.SplitDoubleLesson)
|
||||
{
|
||||
if (target.NewPeriod is not int newPeriod)
|
||||
throw new InvalidOperationException("Für den zweiten Teil ist eine Zielstunde erforderlich.");
|
||||
service.SplitAndMoveSecondPart(lesson, firstPartMinutes, target.NewDate, newPeriod,
|
||||
schedule.GetTimes(newPeriod)?.Start);
|
||||
}
|
||||
else service.Move(lesson, target.NewDate, target.NewPeriod, target.ShiftFollowing);
|
||||
|
||||
(DataContext as TimetableViewModel)?.Load();
|
||||
App.Services.GetRequiredService<NotificationService>().ShowSuccess(
|
||||
target.SplitDoubleLesson ? "Doppelstunde getrennt und Fortsetzung verschoben." : "Stunde verschoben.");
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
App.Services.GetRequiredService<NotificationService>().ShowError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowWebUntisTimetableDialog()
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
|
||||
Reference in New Issue
Block a user