Mitarbeit bewerten begonnen, Schülerdaten, Gruppen

This commit is contained in:
2026-06-23 10:12:52 +02:00
parent fdbaa9ef4d
commit b2211270b4
32 changed files with 1968 additions and 49 deletions
@@ -0,0 +1,33 @@
<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.AddSessionDialog"
x:DataType="vm:AddSessionDialogViewModel"
Title="Bewertungszeitpunkt anlegen"
Width="380" SizeToContent="Height"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="14">
<TextBlock Text="Neuer Bewertungszeitpunkt" FontSize="18" FontWeight="SemiBold"/>
<StackPanel Spacing="4">
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ" x:Name="DateBox"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Kommentar (optional)" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Comment}" PlaceholderText="z. B. Stunde 12 Säure-Base-Reaktion"/>
</StackPanel>
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
IsVisible="{Binding ValidationMessage, 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="Anlegen" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,27 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class AddSessionDialog : Window
{
public AddSessionDialog() => InitializeComponent();
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
this.FindControl<TextBox>("DateBox")?.Focus();
}
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is AddSessionDialogViewModel 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);
}
@@ -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.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.AddStudentToGroupDialog"
x:DataType="vm:AddStudentToGroupDialogViewModel"
Title="Schüler hinzufügen"
Width="420" Height="500"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
<!-- Überschrift + Suche -->
<StackPanel Grid.Row="0" Spacing="12" Margin="0,0,0,12">
<TextBlock Text="Schüler zur Gruppe hinzufügen" FontSize="18" FontWeight="SemiBold"/>
<TextBox Text="{Binding SearchText}"
PlaceholderText="Schüler suchen …"
x:Name="SearchBox"/>
</StackPanel>
<!-- Schülerliste -->
<ListBox Grid.Row="1"
ItemsSource="{Binding AvailableStudents}"
SelectedItem="{Binding SelectedStudent}"
BorderThickness="1">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:StudentPickerItem">
<TextBlock Text="{Binding FullName}" Padding="4,2"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Validation + Buttons -->
<StackPanel Grid.Row="2" Spacing="12" Margin="0,16,0,0">
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Grid ColumnDefinitions="*,8,*">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Hinzufügen" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,27 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class AddStudentToGroupDialog : Window
{
public AddStudentToGroupDialog() => InitializeComponent();
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
this.FindControl<TextBox>("SearchBox")?.Focus();
}
private void OnSave(object? s, RoutedEventArgs e)
{
if (DataContext is AddStudentToGroupDialogViewModel 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);
}
@@ -1,6 +1,7 @@
<UserControl 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.GroupDetailView"
x:DataType="vm:GroupDetailViewModel">
@@ -20,17 +21,14 @@
<Run Text="{Binding StudentCount}"/>
<Run Text=" Schüler"/>
</TextBlock>
<Button Content=" Schüler" Command="{Binding AddStudentCommand}"/>
<Button Content=" Klausur" Command="{Binding AddExamCommand}"/>
<Button Content=" Schüler" Command="{Binding AddStudentCommand}"/>
<Button Content=" Austragen" Command="{Binding RemoveStudentCommand}"
IsVisible="{Binding SelectedStudent, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Button Content=" Klausur" Command="{Binding AddExamCommand}"/>
</StackPanel>
</Grid>
</Border>
<!--
Avalonia 12: TabbedPage ersetzt manuelles Tab-System.
Kein ActiveTab-Property, kein IsVisible-Binding, keine eigenen Tab-Buttons.
TabPlacement="Top" → Tabs oben (wie Browser-Tabs).
-->
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
<!-- Tab: Übersicht -->
@@ -47,6 +45,7 @@
<!-- Tab: Schüler -->
<ContentPage Header="Schüler">
<DataGrid ItemsSource="{Binding Students}"
SelectedItem="{Binding SelectedStudent}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="Horizontal"
@@ -61,6 +60,11 @@
</DataGrid>
</ContentPage>
<!-- Tab: Mitarbeit -->
<ContentPage Header="Mitarbeit">
<views:ParticipationTabView DataContext="{Binding ParticipationTab}"/>
</ContentPage>
<!-- Tab: Klausuren -->
<ContentPage Header="Klausuren">
<DataGrid ItemsSource="{Binding Exams}"
@@ -1,3 +1,35 @@
using Avalonia.Controls;
using LehrerApp.Core.Interfaces;
using LehrerApp.Desktop.ViewModels.Groups;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
public partial class GroupDetailView : UserControl { public GroupDetailView() => InitializeComponent(); }
public partial class GroupDetailView : UserControl
{
public GroupDetailView() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is GroupDetailViewModel vm)
vm.OnAddStudent = ShowAddStudentDialog;
}
private async Task<bool> ShowAddStudentDialog()
{
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false;
var dialogVm = new AddStudentToGroupDialogViewModel(
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IEnrollmentRepository>(),
vm.Group.Id,
vm.Group.SchoolYear);
var dialog = new AddStudentToGroupDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return false;
return await dialog.ShowDialog<bool>(owner);
}
}
@@ -66,7 +66,7 @@
<!-- Rechte Spalte: Platzhalter wenn keine Auswahl -->
<StackPanel Grid.Column="1" HorizontalAlignment="Center"
VerticalAlignment="Center" Spacing="8"
IsVisible="{Binding !SelectedGroup}">
IsVisible="{Binding SelectedGroup, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Text="Gruppe auswählen" FontSize="16" Opacity="0.4"
HorizontalAlignment="Center"/>
<TextBlock Text="oder Neue Gruppe anlegen" FontSize="12" Opacity="0.3"
@@ -74,14 +74,15 @@
</StackPanel>
<!-- Rechte Spalte: Gruppen-Übersicht wenn ausgewählt -->
<ScrollViewer Grid.Column="1" IsVisible="{Binding SelectedGroup}">
<ScrollViewer Grid.Column="1"
IsVisible="{Binding SelectedGroup, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Margin="28,24" Spacing="20">
<!-- Gruppenname und Kurzinfos -->
<StackPanel Spacing="4">
<TextBlock Text="{Binding SelectedGroup.DisplayName}"
<TextBlock Text="{Binding SelectedGroupDisplayName}"
FontSize="24" FontWeight="SemiBold" TextWrapping="Wrap"/>
<TextBlock Text="{Binding SelectedGroup.Subtitle}"
<TextBlock Text="{Binding SelectedGroupSubtitle}"
FontSize="12" Opacity="0.55"/>
</StackPanel>
@@ -0,0 +1,57 @@
<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.ParticipationQuickInputDialog"
x:DataType="vm:QuickInputViewModel"
Title="Schnelleingabe Mitarbeit"
Width="420" SizeToContent="Height"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
<!-- Schülername + Fortschritt -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"/>
<TextBlock Text="{Binding CurrentAspectLabel}" FontSize="13" Opacity="0.5"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding ProgressText}"
VerticalAlignment="Top" FontSize="13" Opacity="0.4"/>
</Grid>
<!-- Aspektliste -->
<ItemsControl Grid.Row="1" ItemsSource="{Binding AspectRows}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:QuickAspectRow">
<Grid ColumnDefinitions="4,8,*,44" Margin="0,3">
<!-- Aktiv-Balken links -->
<Border Grid.Column="0" CornerRadius="2"
Background="{DynamicResource SystemAccentColor}"
IsVisible="{Binding IsActive}"/>
<!-- Index-Badge -->
<TextBlock Grid.Column="1" Text="{Binding Index, StringFormat='[{0}]'}"
FontFamily="Monospace" FontSize="11" Opacity="0.4"
VerticalAlignment="Center" Margin="0,0,6,0"/>
<!-- Aspektname: bold wenn aktiv, gedimmt wenn nicht -->
<TextBlock Grid.Column="2" Text="{Binding Label}" FontWeight="SemiBold"
IsVisible="{Binding IsActive}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="{Binding Label}" Opacity="0.55"
IsVisible="{Binding IsActive, Converter={x:Static BoolConverters.Not}}"
VerticalAlignment="Center"/>
<!-- Aktueller Wert -->
<TextBlock Grid.Column="3" Text="{Binding DisplayLabel}"
FontFamily="Monospace" FontSize="16" FontWeight="Bold"
TextAlignment="Right" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Tastenkürzel-Legende + Schließen-Button -->
<StackPanel Grid.Row="2" Margin="0,16,0,0" Spacing="6">
<TextBlock Opacity="0.35" FontSize="11" TextWrapping="Wrap"
Text="15 bewerten · Q/W/E/R/T Aspekt wählen · Leertaste nächster Aspekt · Enter nächster Schüler · Backspace vorheriger · +/ anpassen · Esc schließen"/>
<Button Content="Schließen" HorizontalAlignment="Stretch" Click="OnClose"/>
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,54 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class ParticipationQuickInputDialog : Window
{
public ParticipationQuickInputDialog() => InitializeComponent();
protected override void OnOpened(EventArgs e)
{
base.OnOpened(e);
Focus();
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (DataContext is not QuickInputViewModel vm) { base.OnKeyDown(e); return; }
switch (e.Key)
{
case Key.D1 or Key.NumPad1: vm.SetRatingByNumber(1); e.Handled = true; break;
case Key.D2 or Key.NumPad2: vm.SetRatingByNumber(2); e.Handled = true; break;
case Key.D3 or Key.NumPad3: vm.SetRatingByNumber(3); e.Handled = true; break;
case Key.D4 or Key.NumPad4: vm.SetRatingByNumber(4); e.Handled = true; break;
case Key.D5 or Key.NumPad5: vm.SetRatingByNumber(5); e.Handled = true; break;
case Key.Q: vm.SelectAspect(0); e.Handled = true; break;
case Key.W: vm.SelectAspect(1); e.Handled = true; break;
case Key.E: vm.SelectAspect(2); e.Handled = true; break;
case Key.R: vm.SelectAspect(3); e.Handled = true; break;
case Key.T: vm.SelectAspect(4); e.Handled = true; break;
case Key.Space: vm.NextAspect(); e.Handled = true; break;
case Key.Enter:
if (vm.IsLastStudent) Close();
else vm.NextStudent();
e.Handled = true;
break;
case Key.Back: vm.PreviousStudent(); e.Handled = true; break;
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;
case Key.Escape: Close(); e.Handled = true; break;
default: base.OnKeyDown(e); break;
}
}
private void OnClose(object? s, RoutedEventArgs e) => Close();
}
@@ -0,0 +1,58 @@
<UserControl 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.ParticipationTabView"
x:DataType="vm:ParticipationTabViewModel">
<Grid ColumnDefinitions="220,*">
<!-- Linke Seite: Sitzungsliste -->
<Border Grid.Column="0"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,1,0">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="6" Margin="10,10,10,6">
<Button Content=" Sitzung" Command="{Binding AddSessionCommand}" HorizontalAlignment="Stretch"/>
<Button Content="Schnell" Command="{Binding QuickInputCommand}"/>
</StackPanel>
<ListBox Grid.Row="1"
ItemsSource="{Binding Sessions}"
SelectedItem="{Binding SelectedSession}"
BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ParticipationSessionItem">
<TextBlock Text="{Binding Display}" TextWrapping="Wrap" Padding="4,3" FontSize="12"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<!-- Rechte Seite: Bewertungsraster (wird per Code-Behind aufgebaut) -->
<Grid Grid.Column="1" RowDefinitions="Auto,*">
<TextBlock Grid.Row="0"
Text="{Binding SelectedSessionDisplay}"
FontSize="13" FontWeight="SemiBold" Margin="12,10,12,4"
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<!-- DataGrid: Spalten werden in ParticipationTabView.axaml.cs dynamisch erzeugt -->
<DataGrid Grid.Row="1"
x:Name="RatingGrid"
ItemsSource="{Binding StudentRows}"
AutoGenerateColumns="False"
IsReadOnly="True"
GridLinesVisibility="All"
CanUserReorderColumns="False"
CanUserResizeColumns="True"
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock Grid.Row="1"
Text="{Binding NoDataText}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Opacity="0.35" FontSize="14"
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNull}}"/>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,121 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class ParticipationTabView : UserControl
{
private ParticipationTabViewModel? _vm;
public ParticipationTabView() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is ParticipationTabViewModel vm)
{
_vm = vm;
vm.OnAddSession = ShowAddSessionDialog;
vm.OnQuickInput = ShowQuickInputDialog;
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
BuildColumns();
}
}
private void BuildColumns()
{
var grid = this.FindControl<DataGrid>("RatingGrid");
if (grid is null || _vm is null) return;
grid.Columns.Clear();
grid.Columns.Add(new DataGridTextColumn
{
Header = "Schüler",
Binding = new Binding("Name"),
Width = new DataGridLength(160, DataGridLengthUnitType.Pixel),
});
foreach (var (aspect, i) in _vm.Aspects.Select((a, i) => (a, i)))
{
var capturedIndex = i;
grid.Columns.Add(new DataGridTemplateColumn
{
Header = $"{aspect.Label} [{AspectShortcut(i)}]",
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
CellTemplate = BuildCellTemplate(capturedIndex),
});
}
}
private static IDataTemplate BuildCellTemplate(int aspectIndex)
{
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
{
if (row is null) return new TextBlock();
var cell = row.Cells.ElementAtOrDefault(aspectIndex);
if (cell is null) return new TextBlock();
var panel = new StackPanel
{
Orientation = Avalonia.Layout.Orientation.Horizontal,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Spacing = 2,
Margin = new Avalonia.Thickness(0, 2),
};
foreach (var (label, val) in new[] { ("−−", -2), ("", -1), ("", 0), ("+", 1), ("++", 2) })
{
var btn = new Button
{
Content = label,
Padding = new Avalonia.Thickness(5, 1),
FontSize = 11,
Opacity = cell.Value == val ? 1.0 : 0.3,
};
var capturedVal = val;
btn.Click += (_, _) => cell.SetValue(capturedVal);
cell.PropertyChanged += (_, pe) =>
{
if (pe.PropertyName == nameof(RatingCell.Value))
btn.Opacity = cell.Value == capturedVal ? 1.0 : 0.3;
};
panel.Children.Add(btn);
}
return panel;
});
}
private static string AspectShortcut(int i) => i switch
{
0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => ""
};
private async Task<LehrerApp.Core.Models.ParticipationSession?> ShowAddSessionDialog()
{
var vm = new AddSessionDialogViewModel();
var dialog = new AddSessionDialog { DataContext = vm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
var ok = await dialog.ShowDialog<bool>(owner);
return ok ? vm.Result : null;
}
private async Task ShowQuickInputDialog(ParticipationTabViewModel tabVm)
{
if (tabVm.StudentRows.Count == 0) return;
var quickVm = new QuickInputViewModel(
tabVm.StudentRows.ToList(),
tabVm.Aspects.ToList());
var dialog = new ParticipationQuickInputDialog { DataContext = quickVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null)
await dialog.ShowDialog(owner);
}
}