Kapitel 7 abgeschlossen.

This commit is contained in:
2026-08-16 00:29:01 +02:00
parent da8d2bb1da
commit f9e398b225
47 changed files with 2636 additions and 107 deletions
@@ -17,9 +17,10 @@
<Button Content="Sortierung: Gesamt" Command="{Binding SortByTotalCommand}"/>
<Button Content="Noten verwalten" Command="{Binding ManageStudentGradesCommand}" Margin="12,0,0,0"
IsVisible="{Binding SelectedRow, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Button Content=" Sammelnote" Command="{Binding CollectiveGradeCommand}"/>
<Button Content="Zeugnisnoten" Command="{Binding ReportGradesCommand}"/>
IsVisible="{Binding SelectedRow, Converter={x:Static ObjectConverters.IsNotNull}}"
IsEnabled="{Binding !IsReadOnly}"/>
<Button Content=" Sammelnote" Command="{Binding CollectiveGradeCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Zeugnisnoten" Command="{Binding ReportGradesCommand}" IsEnabled="{Binding !IsReadOnly}"/>
</StackPanel>
<DataGrid Grid.Row="1"
@@ -7,7 +7,7 @@
x:Class="LehrerApp.Desktop.Views.Groups.GroupDetailView"
x:DataType="vm:GroupDetailViewModel">
<Grid RowDefinitions="Auto,*">
<Grid RowDefinitions="Auto,Auto,*">
<!-- Header -->
<Border Grid.Row="0" Padding="20,16"
@@ -22,18 +22,32 @@
</TextBlock>
<CheckBox Content="Ausgetretene anzeigen" IsChecked="{Binding ShowFormerStudents}"
VerticalAlignment="Center"/>
<Button Content=" Schüler" Command="{Binding AddStudentCommand}"/>
<Button Content=" Schüler" Command="{Binding AddStudentCommand}" IsEnabled="{Binding IsEditable}"/>
<Button Content="{Binding SelectedStudent.WithdrawActionLabel}"
Command="{Binding WithdrawStudentCommand}"
IsVisible="{Binding SelectedStudent, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Button Content="Austragung zurücknehmen" Command="{Binding ReinstateStudentCommand}"
IsVisible="{Binding SelectedStudent.HasExitDate}"/>
<Button Content=" Klausur" Command="{Binding AddExamCommand}"/>
<Button Content=" Klausur" Command="{Binding AddExamCommand}" IsEnabled="{Binding IsEditable}"/>
</StackPanel>
</Grid>
</Border>
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
<Border Grid.Row="1" Background="#FFF3CD" BorderBrush="#D97706" BorderThickness="0,0,0,1"
Padding="20,10" IsVisible="{Binding IsReadOnly}">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="2">
<TextBlock Text="Archivierte Lerngruppe · schreibgeschützt" FontWeight="SemiBold"
Foreground="#92400E"/>
<TextBlock Text="Alle historischen Daten bleiben sichtbar. Für Korrekturen muss die Gruppe ausdrücklich wieder aktiviert werden."
FontSize="12" Foreground="#92400E" TextWrapping="Wrap"/>
</StackPanel>
<Button Grid.Column="1" Content="Gruppe wieder aktivieren"
Command="{Binding ReactivateGroupCommand}" Margin="16,0,0,0"/>
</Grid>
</Border>
<TabbedPage Grid.Row="2" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
<!-- Tab: Übersicht -->
<ContentPage Header="Übersicht">
@@ -57,6 +71,18 @@
CanUserResizeColumns="True"
Margin="0">
<DataGrid.Columns>
<DataGridTemplateColumn Header="" Width="48">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:StudentSummary">
<Border Width="30" Height="30" CornerRadius="15" Background="{Binding AvatarColor}"
HorizontalAlignment="Center" VerticalAlignment="Center"
ToolTip.Tip="{Binding AvatarTooltip}">
<TextBlock Text="{Binding AvatarLabel}" Foreground="White" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/>
<DataGridTextColumn Header="Zeitraum" Binding="{Binding PeriodLabel}" Width="190"/>
<DataGridTextColumn Header="Status" Binding="{Binding MembershipStatus}" Width="170"/>
@@ -66,6 +92,7 @@
<DataTemplate x:DataType="vm:StudentSummary">
<ComboBox ItemsSource="{x:Static vm:StudentSummary.NiveauOptions}"
SelectedItem="{Binding NiveauName}"
IsEnabled="{Binding CanEdit}"
HorizontalAlignment="Stretch" Margin="2"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -83,7 +110,8 @@
<ContentPage Header="Klausuren">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8"
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}">
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}"
IsEnabled="{Binding IsEditable}">
<Button Content="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
<Button Content="Auswertung" Command="{Binding EvaluateExamCommand}"/>
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
@@ -3,6 +3,7 @@ using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Shared;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
@@ -24,9 +25,25 @@ public partial class GroupDetailView : UserControl
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
vm.OnGradeExam = ShowGradeExamDialog;
vm.OnEvaluateExam = ShowEvaluateExamDialog;
vm.OnConfirmReactivate = ShowReactivateConfirmDialog;
}
}
private async Task<bool> ShowReactivateConfirmDialog()
{
var dialog = new ConfirmDialog
{
DataContext = new ConfirmDialogInfo
{
Title = "Archivierte Gruppe wieder aktivieren?",
Message = "Nach der Wiederaktivierung können historische Daten dieser Gruppe wieder verändert werden.",
ConfirmText = "Wieder aktivieren",
},
};
var owner = TopLevel.GetTopLevel(this) as Window;
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
private async Task<bool> ShowAddStudentDialog()
{
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false;
@@ -91,6 +91,8 @@
<MenuFlyout>
<MenuItem Header="Details bearbeiten"
Command="{Binding EditGroupCommand}"/>
<MenuItem Header="Ins nächste Schuljahr übernehmen …"
Command="{Binding RollOverGroupCommand}"/>
<MenuItem Header="{Binding SelectedGroup.ArchiveActionLabel}"
Command="{Binding ToggleArchiveCommand}"/>
<Separator/>
@@ -15,6 +15,7 @@ public partial class GroupListView : UserControl
{
vm.OnAddGroup = ShowAddGroupDialog;
vm.OnEditGroup = ShowEditGroupDialog;
vm.OnRollOverGroup = ShowGroupRolloverDialog;
vm.OnConfirmDelete = ShowDeleteGroupDialog;
}
}
@@ -42,6 +43,23 @@ public partial class GroupListView : UserControl
await dialog.ShowDialog<bool>(owner);
}
private async Task<Guid?> ShowGroupRolloverDialog(Guid groupId)
{
var group = App.Services.GetRequiredService<LehrerApp.Core.Interfaces.IGroupRepository>()
.GetById(groupId);
var owner = TopLevel.GetTopLevel(this) as Window;
if (group is null || owner is null) return null;
var dialogVm = new GroupRolloverDialogViewModel(group,
App.Services.GetRequiredService<LehrerApp.Core.Interfaces.IStudentRepository>(),
App.Services.GetRequiredService<LehrerApp.Core.Interfaces.IGroupMembershipRepository>(),
App.Services.GetRequiredService<LehrerApp.Core.Interfaces.IGradingSchemeRepository>(),
App.Services.GetRequiredService<LehrerApp.Core.Services.SchoolYearService>(),
App.Services.GetRequiredService<LehrerApp.Core.Services.GroupRolloverService>());
var dialog = new GroupRolloverDialog { DataContext = dialogVm };
return await dialog.ShowDialog<Guid?>(owner);
}
private async Task<bool> ShowDeleteGroupDialog(GroupListItem group)
{
var dialog = new DeleteGroupDialog { DataContext = group.DisplayName };
@@ -0,0 +1,75 @@
<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.GroupRolloverDialog"
x:DataType="vm:GroupRolloverDialogViewModel"
Title="Ins nächste Schuljahr übernehmen"
Width="650" Height="720" MinWidth="560" MinHeight="600"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
<StackPanel Grid.Row="0" Spacing="12">
<TextBlock Text="Ins nächste Schuljahr übernehmen" Classes="dialogtitle"/>
<TextBlock Text="{Binding SourceSummary}" FontSize="12" Opacity="0.65"/>
<TextBlock Text="Es werden nur Gruppeneinstellungen und ausgewählte Mitgliedschaften kopiert. Noten, Klausuren, Mitarbeit, Planung und Stundenplan bleiben im alten Schuljahr."
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
<Grid ColumnDefinitions="*,12,130,12,100">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Neuer Gruppenname *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Name}"/>
<TextBlock Text="{Binding NameError}" Foreground="Red" FontSize="11"
IsVisible="{Binding NameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Zielschuljahr *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding TargetSchoolYear}" PlaceholderText="2027/28"/>
<TextBlock Text="{Binding SchoolYearError}" Foreground="Red" FontSize="11"
IsVisible="{Binding SchoolYearError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="4" Spacing="4">
<TextBlock Text="Klassenstufe" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding GradeLevel}" Minimum="1" Maximum="13" FormatString="0"/>
</StackPanel>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Schülerauswahl" FontSize="14" FontWeight="SemiBold"/>
<TextBlock Grid.Column="1" Text="{Binding SelectedCountText}" FontSize="12" Opacity="0.6"/>
</Grid>
</StackPanel>
<Grid Grid.Row="1" RowDefinitions="*,Auto" Margin="0,10,0,0">
<ListBox Grid.Row="0" ItemsSource="{Binding Students}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:GroupRolloverStudentItem">
<Grid ColumnDefinitions="Auto,*,70" Margin="4,5">
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" IsEnabled="{Binding CanSelect}"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding FullName}" FontSize="13" FontWeight="SemiBold"/>
<TextBlock Text="{Binding MembershipStatus}" FontSize="11" Opacity="0.55"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding NiveauText}" FontSize="12" Opacity="0.65"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock Grid.Row="1" Text="{Binding SelectionError}" Foreground="Red" FontSize="11" Margin="0,5,0,0"
IsVisible="{Binding SelectionError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid>
<StackPanel Grid.Row="2" Spacing="10" Margin="0,14,0,0">
<CheckBox Content="Gruppenspezifisches Notenschema übernehmen"
IsChecked="{Binding CopyGradingScheme}" IsVisible="{Binding HasGradingScheme}"/>
<CheckBox Content="Alte Gruppe nach erfolgreicher Übernahme archivieren"
IsChecked="{Binding ArchiveSource}"/>
<TextBlock Text="{Binding GeneralError}" Foreground="Red" FontSize="12" TextWrapping="Wrap"
IsVisible="{Binding GeneralError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Grid ColumnDefinitions="*,10,*">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Gruppe übernehmen" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</StackPanel>
</Grid>
</Window>
@@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class GroupRolloverDialog : Window
{
public GroupRolloverDialog() => InitializeComponent();
private void OnSave(object? sender, RoutedEventArgs e)
{
if (DataContext is not GroupRolloverDialogViewModel vm) return;
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close(vm.Result.Id);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
}
@@ -13,19 +13,19 @@
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
<Button Grid.Row="0" Content=" Sitzung" Command="{Binding AddSessionCommand}"
HorizontalAlignment="Stretch" Margin="10,10,10,6"/>
HorizontalAlignment="Stretch" Margin="10,10,10,6" IsEnabled="{Binding !IsReadOnly}"/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="6" Margin="10,0,10,6">
<Button Content="Schnell Mitarbeit" Command="{Binding QuickInputCommand}"
HorizontalAlignment="Stretch"/>
<Button Content="Anw./HA" Command="{Binding StatusQuickInputCommand}"/>
HorizontalAlignment="Stretch" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Anw./HA" Command="{Binding StatusQuickInputCommand}" IsEnabled="{Binding !IsReadOnly}"/>
</StackPanel>
<Button Grid.Row="2" Content="Ø Mitarbeitsnote" Command="{Binding ComputeGradeCommand}"
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
HorizontalAlignment="Stretch" Margin="10,0,10,6" IsEnabled="{Binding !IsReadOnly}"/>
<Button Grid.Row="3" Content="Mitarbeits-Assistent" Command="{Binding OpenWizardCommand}"
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
HorizontalAlignment="Stretch" Margin="10,0,10,6" IsEnabled="{Binding !IsReadOnly}"/>
<ListBox Grid.Row="4"
ItemsSource="{Binding Sessions}"
@@ -68,7 +68,7 @@
BorderThickness="0,0,0,1"
Padding="12,8">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" MaxHeight="200">
<ItemsControl ItemsSource="{Binding CompetencyTagGroups}">
<ItemsControl ItemsSource="{Binding CompetencyTagGroups}" IsEnabled="{Binding !IsReadOnly}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:CompetencyTagGroup">
<StackPanel Spacing="4" Margin="0,0,0,10">
@@ -58,7 +58,7 @@ public partial class ParticipationTabView : UserControl
{
Header = $"{aspect.Label} [{AspectShortcut(i)}]",
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
CellTemplate = BuildCellTemplate(idx, forCompetency: false),
CellTemplate = BuildCellTemplate(idx, forCompetency: false, _vm.IsReadOnly),
});
}
@@ -66,13 +66,13 @@ public partial class ParticipationTabView : UserControl
{
Header = "HA",
Width = new DataGridLength(50, DataGridLengthUnitType.Pixel),
CellTemplate = BuildHomeworkCellTemplate(),
CellTemplate = BuildHomeworkCellTemplate(_vm.IsReadOnly),
});
grid.Columns.Add(new DataGridTemplateColumn
{
Header = "Anwesenheit",
Width = new DataGridLength(80, DataGridLengthUnitType.Pixel),
CellTemplate = BuildAttendanceCellTemplate(),
CellTemplate = BuildAttendanceCellTemplate(_vm.IsReadOnly),
});
// Kompetenz-Spalten (opt-in)
@@ -85,13 +85,13 @@ public partial class ParticipationTabView : UserControl
{
Header = code,
Width = new DataGridLength(80, DataGridLengthUnitType.Pixel),
CellTemplate = BuildCellTemplate(idx, forCompetency: true),
CellTemplate = BuildCellTemplate(idx, forCompetency: true, _vm.IsReadOnly),
});
}
}
}
private static IDataTemplate BuildCellTemplate(int cellIndex, bool forCompetency)
private static IDataTemplate BuildCellTemplate(int cellIndex, bool forCompetency, bool isReadOnly)
{
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
{
@@ -119,6 +119,7 @@ public partial class ParticipationTabView : UserControl
Padding = new Avalonia.Thickness(5, 1),
FontSize = 11,
Opacity = cell.Value == val ? 1.0 : 0.3,
IsEnabled = !isReadOnly,
};
var capturedVal = val;
btn.Click += (_, _) => cell.SetValue(capturedVal);
@@ -134,7 +135,7 @@ public partial class ParticipationTabView : UserControl
});
}
private static IDataTemplate BuildHomeworkCellTemplate()
private static IDataTemplate BuildHomeworkCellTemplate(bool isReadOnly)
{
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
{
@@ -147,6 +148,7 @@ public partial class ParticipationTabView : UserControl
Height = 26,
Padding = new Avalonia.Thickness(3, 1),
Command = row.ToggleHomeworkCommand,
IsEnabled = !isReadOnly,
};
void RefreshHomework()
{
@@ -180,7 +182,7 @@ public partial class ParticipationTabView : UserControl
});
}
private static IDataTemplate BuildAttendanceCellTemplate()
private static IDataTemplate BuildAttendanceCellTemplate(bool isReadOnly)
{
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
{
@@ -193,6 +195,7 @@ public partial class ParticipationTabView : UserControl
Height = 26,
Padding = new Avalonia.Thickness(3, 1),
Command = row.CycleAttendanceCommand,
IsEnabled = !isReadOnly,
};
void RefreshAttendance()
{
@@ -11,10 +11,10 @@
<TextBlock Grid.Column="0" Text="Unterrichtseinheiten" FontSize="14" FontWeight="SemiBold"
VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Button Content=" Einheit" Command="{Binding AddUnitCommand}"/>
<Button Content="Bearbeiten" Command="{Binding EditUnitCommand}"/>
<Button Content=" Einheit" Command="{Binding AddUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Bearbeiten" Command="{Binding EditUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Als Vorlage kopieren" Command="{Binding CopyUnitCommand}"/>
<Button Content="Löschen" Command="{Binding DeleteUnitCommand}"/>
<Button Content="Löschen" Command="{Binding DeleteUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
</StackPanel>
</Grid>
@@ -62,15 +62,16 @@
<TextBlock Text="{Binding SelectedUnitTitleSuffix}" FontSize="14" FontWeight="SemiBold"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
<Button Content=" Stunde" Command="{Binding AddLessonCommand}"/>
<Button Content=" Stunde" Command="{Binding AddLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Serie erzeugen" Command="{Binding GenerateLessonSeriesCommand}"
IsEnabled="{Binding !IsReadOnly}"
ToolTip.Tip="Stunden für alle Termine aus dem Stundenplan im gewählten Zeitraum anlegen."/>
<Button Content="Anzeigen" Command="{Binding ShowLessonCommand}"
ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/>
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}"/>
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}"/>
<Button Content="Löschen" Command="{Binding DeleteLessonCommand}"/>
<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="Löschen" Command="{Binding DeleteLessonCommand}" IsEnabled="{Binding !IsReadOnly}"/>
</StackPanel>
</Grid>
@@ -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.Settings"
xmlns:services="clr-namespace:LehrerApp.Core.Services;assembly=LehrerApp.Core"
xmlns:shared="clr-namespace:LehrerApp.Desktop.Views.Shared"
x:Class="LehrerApp.Desktop.Views.Settings.SettingsView"
x:DataType="vm:SettingsViewModel">
@@ -270,6 +271,86 @@
</ScrollViewer>
</ContentPage>
<!-- Tab: Word-Briefvorlagen (7.1.4 / 11.5) -->
<ContentPage Header="Briefvorlagen">
<ScrollViewer>
<StackPanel Margin="32,20,32,28" Spacing="16" MaxWidth="760">
<TextBlock Text="Word-Vorlagen bleiben vollständig in Word gestaltet. Die App befüllt Inhaltssteuerelemente anhand ihres Tags und prüft die Vorlage bereits beim Import."
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
<Button Grid.Column="0" Content=" DOCX-Vorlage importieren" Click="OnImportLetterTemplateClick"/>
<TextBlock Grid.Column="1" Text="{Binding LetterTemplateStatus}" FontSize="12"
VerticalAlignment="Center" TextWrapping="Wrap"
IsVisible="{Binding LetterTemplateStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</Grid>
<TextBlock Text="Noch keine Briefvorlage importiert." Classes="emptyhint"
IsVisible="{Binding !LetterTemplateList.Count}"/>
<ItemsControl ItemsSource="{Binding LetterTemplateList}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:LetterTemplateListItem">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="1" CornerRadius="7" Padding="14,12" Margin="0,0,0,9">
<StackPanel Spacing="8">
<Grid ColumnDefinitions="*,Auto,Auto,Auto">
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="14"/>
<TextBlock FontSize="11" Opacity="0.55">
<Run Text="{Binding OriginalFileName}"/><Run Text=" · "/>
<Run Text="{Binding ValidationSummary}"/>
</TextBlock>
</StackPanel>
<Button Grid.Column="1" Content="Öffnen" FontSize="12" Padding="10,4"
Margin="8,0,0,0" Tag="{Binding}" Click="OnOpenLetterTemplateClick"/>
<Button Grid.Column="2" Content="Neu prüfen" FontSize="12" Padding="10,4"
Margin="8,0,0,0"
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).ValidateLetterTemplateCommand}"
CommandParameter="{Binding}"/>
<Button Grid.Column="3" Content="Löschen" FontSize="12" Padding="10,4"
Margin="8,0,0,0"
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteLetterTemplateCommand}"
CommandParameter="{Binding}"/>
</Grid>
<ItemsControl ItemsSource="{Binding Issues}" IsVisible="{Binding HasIssues}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:LetterTemplateIssueItem">
<Grid ColumnDefinitions="24,*" Margin="0,2">
<TextBlock Grid.Column="0" Text="{Binding Icon}" Foreground="{Binding Color}"/>
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="{Binding Color}"
FontSize="12" TextWrapping="Wrap"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="✓ Vorlage ohne Auffälligkeiten" Foreground="SeaGreen" FontSize="12"
IsVisible="{Binding HasNoIssues}"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Separator/>
<TextBlock Text="Unterstützte Tags" FontSize="15" FontWeight="SemiBold"/>
<TextBlock Text="In Word: Entwicklertools → Rich-Text- oder Nur-Text-Inhaltssteuerelement → Eigenschaften → Tag. Der Titel ist frei wählbar; ausgewertet wird der Tag."
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
<ItemsControl ItemsSource="{Binding SupportedLetterPlaceholders}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="services:LetterPlaceholder">
<Grid ColumnDefinitions="190,*" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding Tag}" FontFamily="Monospace" FontSize="12"/>
<TextBlock Grid.Column="1" Text="{Binding Description}" FontSize="12" Opacity="0.7"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</ContentPage>
<!-- Tab: Notenschema -->
<ContentPage Header="Notenschema">
<ContentPage.Resources>
@@ -91,4 +91,27 @@ public partial class SettingsView : UserControl
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
private async void OnImportLetterTemplateClick(object? sender, RoutedEventArgs e)
{
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel is null || DataContext is not SettingsViewModel vm) return;
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Word-Briefvorlage importieren",
AllowMultiple = false,
FileTypeFilter = [new FilePickerFileType("Word-Dokumente") { Patterns = ["*.docx"] }],
});
if (files.Count > 0) vm.ImportLetterTemplate(files[0].Path.LocalPath);
}
private void OnOpenLetterTemplateClick(object? sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: LetterTemplateListItem item }) return;
var service = App.Services.GetRequiredService<LehrerApp.Core.Services.LetterTemplateService>();
var path = service.GetTemplatePath(item.Model);
if (!File.Exists(path)) return;
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
}
}
@@ -72,6 +72,9 @@
<Button Grid.Column="4" Content="×" Padding="6,2"
Command="{Binding RemoveCommand}" FontSize="14"/>
</Grid>
<TextBox Text="{Binding LetterSalutation}"
PlaceholderText="Briefanrede, z.B. Sehr geehrte Frau Mustermann,"
FontSize="12"/>
<!-- Zeile 2: Telefon + E-Mail -->
<Grid ColumnDefinitions="*,10,*">
<TextBox Grid.Column="0" Text="{Binding Phone}"
@@ -28,6 +28,14 @@
</StackPanel>
</Grid>
<StackPanel Spacing="4">
<TextBlock Text="Briefanrede" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding LetterSalutation}"
PlaceholderText="z.B. Sehr geehrte Frau Mustermann,"/>
<TextBlock Text="Wird unverändert in Word-Vorlagen für Letter.Salutation eingesetzt."
FontSize="11" Opacity="0.55" TextWrapping="Wrap"/>
</StackPanel>
<Grid ColumnDefinitions="*,10,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Telefon" FontSize="12" Opacity="0.7"/>
@@ -0,0 +1,75 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
x:Class="LehrerApp.Desktop.Views.Students.CreateLetterDialog"
x:DataType="vm:CreateLetterDialogViewModel"
Title="Word-Brief erstellen" Width="580" Height="650"
MinWidth="500" MinHeight="540" CanResize="True"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24,20">
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="15">
<TextBlock Text="Word-Brief erstellen" Classes="dialogtitle"/>
<TextBlock FontSize="12" Opacity="0.65" TextWrapping="Wrap"
Text="Die Vorlage wird vor der Erzeugung erneut geprüft. Das Original bleibt unverändert; gespeichert wird eine frei bearbeitbare DOCX-Kopie."/>
<StackPanel Spacing="4">
<TextBlock Text="Schüler" FontSize="12" Opacity="0.7"/>
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Vorlage *" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Templates}" SelectedItem="{Binding SelectedTemplate}"
DisplayMemberBinding="{Binding Name}" HorizontalAlignment="Stretch"/>
<TextBlock Text="Noch keine Vorlage. Bitte unter Einstellungen → Briefvorlagen importieren."
Classes="emptyhint" IsVisible="{Binding HasNoTemplates}"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Kontakt *" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Contacts}" SelectedItem="{Binding SelectedContact}"
DisplayMemberBinding="{Binding Display}" HorizontalAlignment="Stretch"/>
<TextBlock Text="Kein aktueller Kontakt vorhanden." Classes="emptyhint"
IsVisible="{Binding HasNoContacts}"/>
</StackPanel>
<Grid ColumnDefinitions="*,12,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Lerngruppe" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding Groups}" SelectedItem="{Binding SelectedGroup}"
DisplayMemberBinding="{Binding Display}" PlaceholderText="Optional"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Briefdatum *" FontSize="12" Opacity="0.7"/>
<CalendarDatePicker SelectedDate="{Binding LetterDate}" HorizontalAlignment="Stretch"/>
</StackPanel>
</Grid>
<Border BorderBrush="#D97706" BorderThickness="1" CornerRadius="6" Padding="10"
IsVisible="{Binding HasIssues}">
<StackPanel Spacing="5">
<TextBlock Text="Vor dem Erzeugen korrigieren" FontWeight="SemiBold" Foreground="#D97706"/>
<ItemsControl ItemsSource="{Binding Issues}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:LetterGenerationIssue">
<Grid ColumnDefinitions="24,*" Margin="0,2">
<TextBlock Grid.Column="0" Text="{Binding Icon}" Foreground="{Binding Color}"/>
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="{Binding Color}"
FontSize="12" TextWrapping="Wrap"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<TextBlock Text="{Binding GenerationError}" Foreground="Red" FontSize="12" TextWrapping="Wrap"
IsVisible="{Binding GenerationError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,10,*" Margin="0,18,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="DOCX speichern …" HorizontalAlignment="Stretch"
IsEnabled="{Binding CanGenerate}" Click="OnGenerate"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,27 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using LehrerApp.Desktop.ViewModels.Students;
namespace LehrerApp.Desktop.Views.Students;
public partial class CreateLetterDialog : Window
{
public CreateLetterDialog() => InitializeComponent();
private async void OnGenerate(object? sender, RoutedEventArgs e)
{
if (DataContext is not CreateLetterDialogViewModel vm) return;
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Word-Brief speichern",
SuggestedFileName = vm.SuggestedFileName,
DefaultExtension = "docx",
FileTypeChoices = [new FilePickerFileType("Word-Dokumente") { Patterns = ["*.docx"] }],
});
if (file is null || !vm.Generate(file.Path.LocalPath)) return;
Close(file.Path.LocalPath);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
}
@@ -0,0 +1,59 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Students"
x:Class="LehrerApp.Desktop.Views.Students.ManageStudentDialog"
x:DataType="vm:ManageStudentDialogViewModel"
Title="Schüler verwalten"
Width="500" Height="500"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Grid.Row="0" Spacing="14">
<TextBlock Text="Schüler verwalten" Classes="dialogtitle"/>
<TextBlock Text="{Binding StudentName}" FontSize="15" FontWeight="SemiBold"/>
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="6" Padding="14">
<StackPanel Spacing="8">
<TextBlock Text="Deaktivieren blendet den Schüler aus den normalen Listen aus. Alle historischen Daten bleiben erhalten."
TextWrapping="Wrap" FontSize="12"/>
<TextBlock Text="Der Schüler ist bereits deaktiviert und kann wieder aktiviert werden."
TextWrapping="Wrap" FontSize="12" IsVisible="{Binding IsInactive}"/>
</StackPanel>
</Border>
<StackPanel Spacing="8" IsVisible="{Binding HasReferences}">
<TextBlock Text="Verknüpfte Daten" FontWeight="SemiBold"/>
<ItemsControl ItemsSource="{Binding ReferenceItems}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:StudentReferenceItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,2">
<TextBlock Grid.Column="0" Text="{Binding Label}"/>
<TextBlock Grid.Column="1" Text="{Binding CountDisplay}" FontWeight="SemiBold"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Eine endgültige Löschung ist deshalb gesperrt. Verwende stattdessen Deaktivieren."
Foreground="#D97706" TextWrapping="Wrap" FontSize="12"/>
</StackPanel>
<Border IsVisible="{Binding CanDelete}" BorderBrush="#C62828" BorderThickness="1"
CornerRadius="6" Padding="12">
<TextBlock Text="Es bestehen keine Verknüpfungen. Der Schüler kann endgültig gelöscht werden. Stammdaten und Kontakte gehen dabei unwiderruflich verloren."
Foreground="#C62828" TextWrapping="Wrap" FontSize="12"/>
</Border>
<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" FontSize="12"
IsVisible="{Binding ErrorMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="8" Margin="0,18,0,0">
<Button Grid.Column="0" Content="Deaktivieren" Click="OnDeactivate" IsVisible="{Binding IsActive}"/>
<Button Grid.Column="0" Content="Wieder aktivieren" Click="OnReactivate" IsVisible="{Binding IsInactive}"/>
<Button Grid.Column="1" Content="Endgültig löschen" Click="OnDeletePermanently"
IsEnabled="{Binding CanDelete}"/>
<Button Grid.Column="3" Content="Schließen" Click="OnCancel"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Students;
namespace LehrerApp.Desktop.Views.Students;
public partial class ManageStudentDialog : Window
{
public ManageStudentDialog() => InitializeComponent();
private void OnDeactivate(object? sender, RoutedEventArgs e)
{
if (DataContext is not ManageStudentDialogViewModel vm) return;
vm.DeactivateCommand.Execute(null);
Close(vm.Result);
}
private void OnReactivate(object? sender, RoutedEventArgs e)
{
if (DataContext is not ManageStudentDialogViewModel vm) return;
vm.ReactivateCommand.Execute(null);
Close(vm.Result);
}
private void OnDeletePermanently(object? sender, RoutedEventArgs e)
{
if (DataContext is not ManageStudentDialogViewModel vm) return;
vm.DeletePermanentlyCommand.Execute(null);
if (vm.Result == StudentManagementResult.Deleted) Close(vm.Result);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(StudentManagementResult.Cancelled);
}
@@ -11,16 +11,29 @@
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,Auto">
<shared:PageHeader Grid.Column="0" IsVisible="{Binding !IsEditing}" Title="{Binding StudentTitle}"/>
<Border Grid.Column="0" Width="38" Height="38" CornerRadius="19"
Background="{Binding StudentAvatarColor}" HorizontalAlignment="Left"
ToolTip.Tip="{Binding StudentAvatarTooltip}" IsVisible="{Binding !IsEditing}">
<TextBlock Text="{Binding StudentAvatarLabel}" Foreground="White" FontWeight="Bold" FontSize="15"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<shared:PageHeader Grid.Column="0" Margin="50,0,0,0" IsVisible="{Binding !IsEditing}" Title="{Binding StudentTitle}"/>
<StackPanel Grid.Column="0" Spacing="6" IsVisible="{Binding IsEditing}">
<Grid ColumnDefinitions="*,8,*">
<Grid ColumnDefinitions="*,8,*,8,170">
<TextBox Grid.Column="0" Text="{Binding EditFirstName}" PlaceholderText="Vorname"/>
<TextBox Grid.Column="2" Text="{Binding EditLastName}" PlaceholderText="Nachname"/>
<ComboBox Grid.Column="4" ItemsSource="{Binding GenderOptions}"
SelectedItem="{Binding EditGender}" PlaceholderText="Geschlecht"/>
</Grid>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Top">
<TextBlock Text="{Binding StudentStatus}" VerticalAlignment="Center" Opacity="0.65"/>
<Button Content="Word-Brief" Command="{Binding CreateLetterCommand}"
IsVisible="{Binding !IsEditing}"/>
<Button Content="Bearbeiten" Command="{Binding StartEditCommand}"
IsVisible="{Binding !IsEditing}"/>
<Button Content="Schüler verwalten" Command="{Binding ManageStudentCommand}"
IsVisible="{Binding !IsEditing}"/>
<Button Content="Speichern" Command="{Binding SaveEditCommand}"
IsVisible="{Binding IsEditing}"/>
<Button Content="Abbrechen" Command="{Binding CancelEditCommand}"
@@ -88,6 +101,9 @@
CornerRadius="4" Padding="6,2" HorizontalAlignment="Left">
<TextBlock Text="{Binding Relation}" FontSize="11" Opacity="0.7"/>
</Border>
<TextBlock Text="{Binding LetterSalutation}" FontSize="11" Opacity="0.6"
TextWrapping="Wrap"
IsVisible="{Binding LetterSalutation, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="1" Grid.Row="0" Orientation="Horizontal" Spacing="8">
@@ -23,6 +23,8 @@ public partial class StudentDetailView : UserControl
vm.OnConfirmDeleteDocumentation = ShowDeleteDocumentationDialog;
vm.OnSaveExportFile = SaveExportFile;
vm.OnConductParentCall = ShowParentCallSessionDialog;
vm.OnManageStudent = ShowManageStudentDialog;
vm.OnCreateLetter = ShowCreateLetterDialog;
}
private async Task<Contact?> ShowContactDialog(Contact? contact)
@@ -36,6 +38,32 @@ public partial class StudentDetailView : UserControl
return saved ? vm.Result : null;
}
private async Task<StudentManagementResult> ShowManageStudentDialog(Student student)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return StudentManagementResult.Cancelled;
var vm = new ManageStudentDialogViewModel(
App.Services.GetRequiredService<IStudentRepository>(), student);
var dialog = new ManageStudentDialog { DataContext = vm };
return await dialog.ShowDialog<StudentManagementResult>(owner);
}
private async Task ShowCreateLetterDialog()
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null || DataContext is not StudentDetailViewModel { Student: { } student }) return;
var vm = new CreateLetterDialogViewModel(student,
App.Services.GetRequiredService<LehrerApp.Core.Services.LetterTemplateService>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(),
App.Services.GetRequiredService<IGroupRepository>());
var dialog = new CreateLetterDialog { DataContext = vm };
var path = await dialog.ShowDialog<string?>(owner);
if (!string.IsNullOrEmpty(path) && File.Exists(path))
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
}
private void ShowAddressViewer(ContactItem contact)
{
if (!contact.HasAddress) return;
@@ -19,13 +19,25 @@
</Border>
<DockPanel Grid.Row="1">
<TextBox DockPanel.Dock="Top" Text="{Binding SearchText}"
PlaceholderText="Name suchen…" Margin="16,10,16,4"/>
PlaceholderText="Name oder Lerngruppe suchen…" Margin="16,10,16,4"/>
<DataGrid ItemsSource="{Binding Students}"
SelectedItem="{Binding SelectedStudent}"
AutoGenerateColumns="False" IsReadOnly="True"
GridLinesVisibility="Horizontal"
CanUserReorderColumns="False" Margin="16,4">
<DataGrid.Columns>
<DataGridTemplateColumn Header="" Width="48">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:StudentListItem">
<Border Width="30" Height="30" CornerRadius="15" Background="{Binding AvatarColor}"
HorizontalAlignment="Center" VerticalAlignment="Center"
ToolTip.Tip="{Binding AvatarTooltip}">
<TextBlock Text="{Binding AvatarLabel}" Foreground="White" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/>
<DataGridTextColumn Header="Geburtsdatum" Binding="{Binding DateOfBirth}" Width="130"/>
</DataGrid.Columns>