feat: Unterrichtsmodus - konsolidierte Ansicht für die laufende Stunde
Verlaufsplan und Sitzplan bisher über mehrere Tabs verteilt, obwohl im Unterricht selbst beides gleichzeitig gebraucht wird. Neuer Button "▶ Unterricht" in der "Heute"-Tagesliste des Stundenplans (nur bei vorhandener Lesson) öffnet ein maximiertes Fenster: links der schreibgeschützte Verlaufsplan als kompakte Kartenliste, rechts der volle, unverändert wiederverwendete SeatingPlanTabView (Drag&Drop, Schnellbewertung, Situations-Tags, PDF-Export). TeachingModeViewModel baut auf dem bestehenden LessonViewerViewModel auf, dadurch kommen "Zur Mitarbeit"/"Zu den Noten" ohne Duplizierung mit. Neue SeatingPlanTabViewModel.SelectOrCreateSessionForLesson verknüpft die Mitarbeitssitzung automatisch mit der konkreten Stunde - anders als die bestehende EnsureTodaySession darf das hier sofort beim Start passieren, da durch den expliziten Klick für genau diese Stunde eindeutig feststeht, worum es geht (keine Geistersitzungs-Gefahr wie beim bloßen Tab-Öffnen). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -235,6 +235,65 @@ public sealed class SeatingPlanViewModelTests
|
||||
Assert.False(vm.Seats[1].IsOccupied);
|
||||
}
|
||||
|
||||
// ── Unterrichtsmodus (14.x): Sitzung mit einer konkreten Stunde verknüpfen ────────────────
|
||||
|
||||
[Fact]
|
||||
public void SelectOrCreateSessionForLesson_BereitsVerknuepfteSitzungVorhanden_WaehltSieAus()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var lesson = new Lesson { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var existingSession = new ParticipationSession
|
||||
{ GroupId = groupId, Date = lesson.Date, LessonId = lesson.Id, Comment = "Redox" };
|
||||
var sessions = new FakeSessions([existingSession]);
|
||||
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
|
||||
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
|
||||
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
|
||||
vm.Initialize(groupId, isReadOnly: false);
|
||||
|
||||
vm.SelectOrCreateSessionForLesson(lesson);
|
||||
|
||||
Assert.Equal(existingSession.Id, vm.SelectedSession?.Id);
|
||||
Assert.Single(sessions.GetByGroup(groupId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectOrCreateSessionForLesson_KeineSitzungVorhanden_LegtVerknuepfteAnUndWaehltSieAus()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var lesson = new Lesson
|
||||
{ GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var sessions = new FakeSessions([]);
|
||||
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
|
||||
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
|
||||
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
|
||||
vm.Initialize(groupId, isReadOnly: false);
|
||||
|
||||
vm.SelectOrCreateSessionForLesson(lesson);
|
||||
|
||||
var created = Assert.Single(sessions.GetByGroup(groupId));
|
||||
Assert.Equal(lesson.Id, created.LessonId);
|
||||
Assert.Equal(lesson.Topic, created.Comment);
|
||||
Assert.Equal(vm.SelectedSession?.Id, created.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectOrCreateSessionForLesson_ArchivierteGruppe_LegtKeineSitzungAn()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var lesson = new Lesson
|
||||
{ GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var sessions = new FakeSessions([]);
|
||||
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
|
||||
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([]),
|
||||
new FakeMemberships([]), sessions, new FakeEntries(), new FakeAspects());
|
||||
vm.Initialize(groupId, isReadOnly: true);
|
||||
|
||||
vm.SelectOrCreateSessionForLesson(lesson);
|
||||
|
||||
Assert.Empty(sessions.GetByGroup(groupId));
|
||||
Assert.Null(vm.SelectedSession);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowSeatUndCanToggleHidden_HaengenVonBearbeitungsmodusUndBelegungAb()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class TeachingModeViewModelTests
|
||||
{
|
||||
private static SeatingPlanTabViewModel BuildSeatingPlan(Guid groupId, FakeSessions? sessions = null,
|
||||
List<Student>? students = null, List<GroupMembership>? memberships = null,
|
||||
List<SeatingPlan>? plans = null) =>
|
||||
new(new FakeSeatingPlans(plans ?? [new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 }]),
|
||||
new FakeStudents(students ?? []), new FakeMemberships(memberships ?? []),
|
||||
sessions ?? new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||
|
||||
[Fact]
|
||||
public void Konstruktor_LaedtVerlaufsplanUndInitialisiertSitzplanFuerDieGruppe()
|
||||
{
|
||||
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||
var lesson = new Lesson
|
||||
{
|
||||
GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox",
|
||||
Phases = [new LessonPhaseStep { Name = "Einstieg", DurationMinutes = 10, Activity = "Impuls" }],
|
||||
};
|
||||
var seatingPlan = BuildSeatingPlan(group.Id);
|
||||
|
||||
var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan);
|
||||
|
||||
Assert.Equal("Q1 Chemie", vm.GroupName);
|
||||
Assert.Equal("Redox", vm.LessonInfo.Topic);
|
||||
Assert.Single(vm.LessonInfo.PhaseGroups.Single().Phases);
|
||||
Assert.True(vm.SeatingPlan.HasSelectedPlan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Konstruktor_VerknuepftDieSitzungMitDieserStunde()
|
||||
{
|
||||
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||
var lesson = new Lesson
|
||||
{ GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var sessions = new FakeSessions([]);
|
||||
var seatingPlan = BuildSeatingPlan(group.Id, sessions);
|
||||
|
||||
var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan);
|
||||
|
||||
var created = Assert.Single(sessions.GetByGroup(group.Id));
|
||||
Assert.Equal(lesson.Id, created.LessonId);
|
||||
Assert.Equal(vm.SeatingPlan.SelectedSession?.Id, created.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Konstruktor_BereitsExistierendeVerknuepfteSitzung_WirdWiederverwendet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||
var lesson = new Lesson
|
||||
{ GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var existing = new ParticipationSession
|
||||
{ GroupId = group.Id, Date = lesson.Date, LessonId = lesson.Id, Comment = "Redox" };
|
||||
var sessions = new FakeSessions([existing]);
|
||||
var seatingPlan = BuildSeatingPlan(group.Id, sessions);
|
||||
|
||||
var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan);
|
||||
|
||||
Assert.Single(sessions.GetByGroup(group.Id));
|
||||
Assert.Equal(existing.Id, vm.SeatingPlan.SelectedSession?.Id);
|
||||
}
|
||||
|
||||
/// Eine archivierte Gruppe darf im Unterrichtsmodus nicht bearbeitbar sein - genau wie in der
|
||||
/// normalen Gruppenansicht (GroupDetailViewModel.IsReadOnly).
|
||||
[Fact]
|
||||
public void Konstruktor_ArchivierteGruppe_InitialisiertSitzplanSchreibgeschuetzt()
|
||||
{
|
||||
var group = new LearningGroup { Name = "Q1 Chemie", IsActive = false };
|
||||
var lesson = new Lesson
|
||||
{ GroupId = group.Id, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var seatingPlan = BuildSeatingPlan(group.Id);
|
||||
|
||||
var vm = new TeachingModeViewModel(lesson, group, new FakeAlternativeLessonPaths([]), seatingPlan);
|
||||
|
||||
Assert.False(vm.SeatingPlan.IsEditable);
|
||||
}
|
||||
}
|
||||
@@ -275,6 +275,44 @@ public sealed class TimetableViewModelTests
|
||||
Assert.Equal("Zur Lerngruppe", vm.TodayItems[0].OpenButtonLabel);
|
||||
}
|
||||
|
||||
// ── Unterrichtsmodus (14.x) ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public async Task StartTeachingMode_MitVorhandenerLesson_OeffnetDenUnterrichtsmodus()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||
var lesson = new Lesson { GroupId = group.Id, Date = today, LessonNumber = 1, Topic = "Redox" };
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]), lessons: lessons);
|
||||
Lesson? openedLesson = null;
|
||||
vm.OnOpenTeachingMode = l => { openedLesson = l; return Task.CompletedTask; };
|
||||
|
||||
await vm.StartTeachingModeCommand.ExecuteAsync(vm.TodayItems[0]);
|
||||
|
||||
Assert.Equal(lesson.Id, openedLesson?.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartTeachingMode_OhneVorhandeneLesson_TutNichts()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "Q1 Chemie" };
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||
var vm = BuildViewModel(slots, new FakeGroups([group]));
|
||||
var opened = false;
|
||||
vm.OnOpenTeachingMode = _ => { opened = true; return Task.CompletedTask; };
|
||||
|
||||
await vm.StartTeachingModeCommand.ExecuteAsync(vm.TodayItems[0]);
|
||||
|
||||
Assert.False(opened);
|
||||
Assert.False(vm.TodayItems[0].HasLesson);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenWeekCell_MitVorhandenerLesson_OeffnetDenViewer()
|
||||
{
|
||||
|
||||
@@ -109,6 +109,32 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
||||
return option;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Für den Unterrichtsmodus (14.x): wählt eine bereits mit dieser Stunde verknüpfte Sitzung
|
||||
/// aus (angelegt z.B. über PlanningTabViewModel.CreateParticipationSession, 3.3.1) oder legt
|
||||
/// bei Bedarf eine neue, EXPLIZIT verknüpfte an (<see cref="ParticipationSession.LessonId"/>).
|
||||
/// Anders als <see cref="EnsureTodaySession"/> (anonyme "Sitzplan"-Sitzung, erst bei der
|
||||
/// ersten tatsächlichen Aktion) ist das hier bewusst sofort beim Start des Unterrichtsmodus
|
||||
/// erlaubt: welche Stunde gemeint ist, steht durch die explizite Auswahl der Lehrkraft
|
||||
/// (Klick auf "Unterrichtsmodus starten" für genau diese Stunde) bereits unzweideutig fest -
|
||||
/// keine Geistersitzungs-Gefahr wie beim bloßen Öffnen eines Tabs.
|
||||
/// </summary>
|
||||
public void SelectOrCreateSessionForLesson(Lesson lesson)
|
||||
{
|
||||
var existing = TodaySessions.FirstOrDefault(s => s.LessonId == lesson.Id);
|
||||
if (existing is not null) { SelectedSession = existing; return; }
|
||||
if (!IsEditable) return;
|
||||
|
||||
var created = new ParticipationSession
|
||||
{
|
||||
GroupId = _groupId, Date = lesson.Date, LessonId = lesson.Id, Comment = lesson.Topic,
|
||||
};
|
||||
_sessions.Save(created);
|
||||
var option = new ParticipationSessionOption(created);
|
||||
TodaySessions.Add(option);
|
||||
SelectedSession = option;
|
||||
}
|
||||
|
||||
partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData();
|
||||
|
||||
private void LoadStudentOptions()
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
/// <summary>
|
||||
/// Unterrichtsmodus (14.x): konsolidierte Ansicht für eine konkrete, heute stattfindende Stunde -
|
||||
/// Verlaufsplan und Sitzplan mit Schnellbewertung auf einem Bildschirm, statt zwischen den Tabs
|
||||
/// der Gruppe hin- und herzuspringen. Wird über TimetableViewModel.StartTeachingModeCommand aus
|
||||
/// der "Heute"-Tagesliste heraus gestartet.
|
||||
///
|
||||
/// Bewusst keine eigene DI-Registrierung als Singleton/Transient: wie LessonViewerViewModel wird
|
||||
/// diese ViewModel direkt vom Code-Behind konstruiert, das dafür nötige SeatingPlanTabViewModel
|
||||
/// aber weiterhin über DI aufgelöst (transient, siehe AppBootstrapper) und hier per Konstruktor
|
||||
/// entgegengenommen statt selbst aus App.Services zu ziehen - ViewModels greifen in diesem Code
|
||||
/// nicht selbst auf den Service-Container zu, das bleibt Aufgabe des Code-Behind.
|
||||
/// </summary>
|
||||
public class TeachingModeViewModel
|
||||
{
|
||||
public string GroupName { get; }
|
||||
public LessonViewerViewModel LessonInfo { get; }
|
||||
public SeatingPlanTabViewModel SeatingPlan { get; }
|
||||
|
||||
public TeachingModeViewModel(Lesson lesson, LearningGroup group,
|
||||
IAlternativeLessonPathRepository alternativePaths, SeatingPlanTabViewModel seatingPlan)
|
||||
{
|
||||
GroupName = group.Name;
|
||||
LessonInfo = new LessonViewerViewModel(lesson, alternativePaths);
|
||||
SeatingPlan = seatingPlan;
|
||||
SeatingPlan.Initialize(group.Id, !group.IsActive);
|
||||
SeatingPlan.SelectOrCreateSessionForLesson(lesson);
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ public partial class TimetableViewModel : ObservableObject
|
||||
public Func<Task>? OnAddSubstitution { get; set; }
|
||||
public Action<SettingsTab>? OnNavigateToSettings { get; set; }
|
||||
public Func<Lesson, Task>? OnOpenLessonViewer { get; set; }
|
||||
public Func<Lesson, Task>? OnOpenTeachingMode { get; set; }
|
||||
|
||||
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
|
||||
@@ -259,6 +260,16 @@ public partial class TimetableViewModel : ObservableObject
|
||||
else OnNavigateToGroup?.Invoke(item.GroupId);
|
||||
}
|
||||
|
||||
/// Startet den Unterrichtsmodus (14.x) für eine konkrete, heute stattfindende Stunde —
|
||||
/// bewusst nur aus der "Heute"-Tagesliste heraus erreichbar (nicht dem Wochenraster), da der
|
||||
/// Modus für das aktive Unterrichten HEUTE gedacht ist, nicht zum Durchblättern anderer Tage.
|
||||
[RelayCommand]
|
||||
private async Task StartTeachingMode(TodayLessonItem? item)
|
||||
{
|
||||
if (item?.Lesson is not { } lesson || OnOpenTeachingMode is null) return;
|
||||
await OnOpenTeachingMode(lesson);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenWeekCell(WeekCellItem? item)
|
||||
{
|
||||
@@ -784,8 +795,9 @@ public class TodayLessonItem
|
||||
public bool HasGroupId => GroupId != Guid.Empty;
|
||||
public bool HasUnhandledHomework { get; private init; }
|
||||
/// Nur gesetzt, wenn für diesen Slot/Tag bereits eine Lesson existiert — Grundlage für den
|
||||
/// Direktsprung in den Verlaufsplan-Viewer (4.5.2).
|
||||
/// Direktsprung in den Verlaufsplan-Viewer (4.5.2) und den Unterrichtsmodus (14.x).
|
||||
public Lesson? Lesson { get; private init; }
|
||||
public bool HasLesson => Lesson is not null;
|
||||
public string OpenButtonLabel => Lesson is not null ? "Verlaufsplan ansehen" : "Zur Lerngruppe";
|
||||
|
||||
public TodayLessonItem(Guid groupId, int periodNumber, string groupName, string room,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.TeachingModeWindow"
|
||||
x:DataType="vm:TeachingModeViewModel"
|
||||
Title="{Binding LessonInfo.Topic, StringFormat='Unterrichtsmodus · {0}'}"
|
||||
Width="1400" Height="850" MinWidth="1000" MinHeight="600"
|
||||
WindowState="Maximized" CanResize="True" WindowStartupLocation="CenterScreen">
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Margin="20">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,14">
|
||||
<StackPanel Grid.Column="0" Spacing="3">
|
||||
<TextBlock FontSize="20" FontWeight="SemiBold">
|
||||
<Run Text="{Binding GroupName}"/><Run Text=" · "/><Run Text="{Binding LessonInfo.Topic}"/>
|
||||
</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<TextBlock FontSize="12" Opacity="0.65">
|
||||
<Run Text="Datum: "/><Run Text="{Binding LessonInfo.DateDisplay}"/>
|
||||
</TextBlock>
|
||||
<TextBlock FontSize="12" Opacity="0.65">
|
||||
<Run Text="Beginn: "/><Run Text="{Binding LessonInfo.StartTimeDisplay}"/>
|
||||
</TextBlock>
|
||||
<TextBlock FontSize="12" Opacity="0.65">
|
||||
<Run Text="Status: "/><Run Text="{Binding LessonInfo.StatusLabel}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Content="Zur Mitarbeit" Command="{Binding LessonInfo.NavigateToParticipationCommand}"
|
||||
ToolTip.Tip="Schließt den Unterrichtsmodus und springt zum Tab 'Mitarbeit' der Lerngruppe."/>
|
||||
<Button Content="Zu den Noten" Command="{Binding LessonInfo.NavigateToGradesCommand}"/>
|
||||
<Button Content="Unterrichtsmodus beenden" Click="OnClose" Margin="16,0,0,0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="360,16,*">
|
||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,1,0" Padding="0,0,16,0">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Text="Verlaufsplan" FontSize="14" FontWeight="SemiBold"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding LessonInfo.PhaseGroups}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PhaseGroupViewItem">
|
||||
<StackPanel Margin="0,0,0,10">
|
||||
<StackPanel IsVisible="{Binding $parent[ItemsControl].((vm:LessonViewerViewModel)DataContext).HasAlternatives}">
|
||||
<TextBlock Text="{Binding Label}" FontSize="12" FontWeight="SemiBold" Opacity="0.75" Margin="0,6,0,2"/>
|
||||
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.55" TextWrapping="Wrap" Margin="0,0,0,6"
|
||||
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding Phases}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PhaseViewItem">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,6">
|
||||
<StackPanel Spacing="2">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"/>
|
||||
<TextBlock Grid.Column="1" FontSize="11" Opacity="0.6">
|
||||
<Run Text="{Binding TimeDisplay}"/><Run Text=" · "/>
|
||||
<Run Text="{Binding DurationMinutes, StringFormat='{}{0} Min.'}"/>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Activity}" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Activity, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock FontSize="11" Opacity="0.55" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Material, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<Run Text="Material: "/><Run Text="{Binding Material}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding LessonInfo.Homework, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="Hausaufgabe" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding LessonInfo.Homework}" FontSize="13" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding LessonInfo.Reflection, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="Reflexion" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding LessonInfo.Reflection}" FontSize="13" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<views:SeatingPlanTabView Grid.Column="2" DataContext="{Binding SeatingPlan}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,28 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class TeachingModeWindow : Window
|
||||
{
|
||||
public TeachingModeWindow() => InitializeComponent();
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
// Gleiches Muster wie LessonViewerDialog (4.5.3): Fenster schließt zuerst, dann Sprung
|
||||
// über die MainWindowViewModel-Singleton-Navigation in den Ziel-Tab der Gruppe.
|
||||
if (DataContext is TeachingModeViewModel vm)
|
||||
vm.LessonInfo.OnNavigateToTab = tabIndex =>
|
||||
{
|
||||
Close();
|
||||
App.Services.GetRequiredService<MainWindowViewModel>()
|
||||
.NavigateToGroupDetail(vm.LessonInfo.GroupId, tabIndex);
|
||||
};
|
||||
}
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -100,10 +100,17 @@
|
||||
Foreground="#8E6C00" FontWeight="SemiBold"
|
||||
IsVisible="{Binding HasUnhandledHomework}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="3" Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4"
|
||||
VerticalAlignment="Center" IsVisible="{Binding HasGroupId}"
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<Button Content="▶ Unterricht" FontSize="11" Padding="9,4"
|
||||
IsVisible="{Binding HasLesson}"
|
||||
ToolTip.Tip="Unterrichtsmodus: Verlaufsplan und Sitzplan mit Schnellbewertung auf einem Bildschirm."
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).StartTeachingModeCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Content="{Binding OpenButtonLabel}" FontSize="11" Padding="9,4"
|
||||
IsVisible="{Binding HasGroupId}"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenTodayLessonCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -21,9 +21,23 @@ public partial class TimetableView : UserControl
|
||||
vm.OnEditSlot = ShowSlotDialog;
|
||||
vm.OnAddSubstitution = ShowSubstitutionDialog;
|
||||
vm.OnOpenLessonViewer = ShowLessonViewerDialog;
|
||||
vm.OnOpenTeachingMode = ShowTeachingMode;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowTeachingMode(Lesson lesson)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
var group = App.Services.GetRequiredService<IGroupRepository>().GetById(lesson.GroupId);
|
||||
if (owner is null || group is null) return;
|
||||
|
||||
var teachingModeVm = new TeachingModeViewModel(lesson, group,
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||
App.Services.GetRequiredService<SeatingPlanTabViewModel>());
|
||||
var window = new TeachingModeWindow { DataContext = teachingModeVm };
|
||||
await window.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task ShowLessonViewerDialog(Lesson lesson)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
|
||||
@@ -1041,10 +1041,42 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
||||
angewendeten KI-Änderung automatisch (`LessonDialogViewModel.MarkAppliedExternally`, lädt den
|
||||
frischen Stand statt der eigenen Felder), die aufrufende Liste aktualisiert sich wie nach
|
||||
einem normalen Speichern.
|
||||
- [x] **4.5.23** Unterrichtsmodus: eine konsolidierte Ansicht für die gerade laufende Stunde
|
||||
(Nutzer-Idee — im Unterricht selbst bisher zu viele Klicks zwischen Verlaufsplan-Viewer,
|
||||
Sitzplan-Tab und Mitarbeit-Tab einer Gruppe verteilt).
|
||||
|
||||
---
|
||||
**Umsetzung:** Neuer Button "▶ Unterricht" in der "Heute"-Tagesliste des Stundenplans
|
||||
(`TimetableView.axaml`, nur sichtbar wenn für den Slot bereits eine `Lesson` existiert —
|
||||
bewusst NICHT im Wochenraster, der Modus ist für das aktive Unterrichten HEUTE gedacht,
|
||||
nicht zum Durchblättern anderer Tage) öffnet ein neues, maximiertes Fenster
|
||||
`TeachingModeWindow`: links der schreibgeschützte Verlaufsplan (Phasen, Hausaufgabe,
|
||||
Reflexion — kompakte Kartenliste statt der breiten Tabellenansicht des bestehenden
|
||||
`LessonViewerDialog`, da der Platz hier schmaler ist), rechts der volle, unverändert
|
||||
wiederverwendete `SeatingPlanTabView` (Drag&Drop, Schnellbewertung per Sitzplatz-Klick,
|
||||
Situations-Tags, Sitzplan-PDF-Export — alles bereits vorhanden, keine Duplizierung). Die
|
||||
beiden bereits bestehenden Sprungziele aus dem Verlaufsplan-Viewer ("Zur Mitarbeit"/
|
||||
"Zu den Noten", 4.5.3) sind identisch mitgekommen, da `TeachingModeViewModel` das
|
||||
bestehende `LessonViewerViewModel` unverändert für die linke Spalte wiederverwendet statt
|
||||
die Phasenanzeige zu duplizieren.
|
||||
|
||||
## 5. Schülerdokumentation
|
||||
**Sitzung automatisch mit der Stunde verknüpft:** Neue Methode
|
||||
`SeatingPlanTabViewModel.SelectOrCreateSessionForLesson(Lesson)` — anders als die
|
||||
bestehende `EnsureTodaySession()` (anonyme "Sitzplan"-Sitzung, erst bei der ersten
|
||||
tatsächlichen Aktion, um das Geistersitzungs-Problem aus 14.12/7.1.5-Nachtrag zu vermeiden)
|
||||
darf hier sofort beim Start des Unterrichtsmodus eine mit `Lesson.Id` verknüpfte Sitzung
|
||||
angelegt werden: welche Stunde gemeint ist, steht durch den expliziten Klick auf
|
||||
"▶ Unterricht" für genau diese Stunde bereits unzweideutig fest, keine passive
|
||||
Tab-Öffnung wie beim ursprünglichen Bug. Existiert bereits eine verknüpfte Sitzung (z.B.
|
||||
über "Sitzung aus der Stunde erstellen", 3.3.1, aus der Planung heraus angelegt), wird
|
||||
diese wiederverwendet statt einer zweiten.
|
||||
|
||||
**Bewusst zurückgestellt:** kein Entry-Point aus dem Dashboard (dessen "Heute"-Karte
|
||||
navigiert bisher direkt zum Mitarbeit-Tab, ein anderer Anzeigetyp als
|
||||
`TodayLessonItem`) — der Stundenplan-Tab ist der naheliegendere, bereits etablierte Ort für
|
||||
"heutige Stunden mit Lesson-Bezug" (4.5.2 nutzt denselben Ort). Kein eigenes
|
||||
Vollbild-/Kiosk-Fenster (kein `WindowState.FullScreen`, kein Precedent dafür im Code) —
|
||||
stattdessen ein maximiertes, normal schließbares Fenster, um kein neues, ungetestetes
|
||||
UI-Verhalten einzuführen.
|
||||
|
||||
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
|
||||
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
|
||||
|
||||
Reference in New Issue
Block a user