Kompetenzen fast fertig

This commit is contained in:
2026-08-16 02:32:12 +02:00
parent 7c7fe6dfcb
commit 4437f951d9
22 changed files with 979 additions and 29 deletions
+2
View File
@@ -146,6 +146,7 @@ public static class AppBootstrapper
// ── Services ──────────────────────────────────────────────────────────
services.AddSingleton<GradingService>();
services.AddSingleton<CompetencyAnalysisService>();
services.AddSingleton<SchoolYearService>();
services.AddSingleton<GroupRolloverService>();
services.AddSingleton<PublicHolidayService>();
@@ -214,6 +215,7 @@ public static class AppBootstrapper
services.AddTransient<ParticipationTabViewModel>();
services.AddTransient<GradeOverviewTabViewModel>();
services.AddTransient<PlanningTabViewModel>();
services.AddTransient<CompetencyOverviewTabViewModel>();
services.AddTransient<AddGroupDialogViewModel>();
services.AddTransient<SettingsViewModel>();
@@ -0,0 +1,174 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class CompetencyOverviewTabViewModel : ObservableObject
{
private readonly IUnitRepository _units;
private readonly IExamRepository _exams;
private readonly IExamResultRepository _results;
private readonly ICompetencyDomainRepository _catalog;
private readonly IStudentRepository _students;
private readonly CompetencyAnalysisService _analysis;
private LearningGroup? _group;
[ObservableProperty] private CompetencyStudentOption? _selectedStudent;
[ObservableProperty] private double _reviewThreshold = 60;
[ObservableProperty] private string _summary = "";
[ObservableProperty] private string _emptyMessage = "";
public ObservableCollection<CompetencyStudentOption> StudentOptions { get; } = [];
public ObservableCollection<CompetencyOverviewRow> Rows { get; } = [];
public bool HasRows => Rows.Count > 0;
public bool HasStudents => StudentOptions.Count > 0;
public string ThresholdLabel => $"Wiederholungsbedarf unter {ReviewThreshold:0} %";
public CompetencyOverviewTabViewModel(IUnitRepository units, IExamRepository exams,
IExamResultRepository results, ICompetencyDomainRepository catalog,
IStudentRepository students, CompetencyAnalysisService analysis)
{
_units = units;
_exams = exams;
_results = results;
_catalog = catalog;
_students = students;
_analysis = analysis;
}
public void Initialize(LearningGroup group)
{
_group = group;
Refresh();
}
[RelayCommand]
public void Refresh()
{
if (_group is null) return;
var selectedStudentId = SelectedStudent?.Id;
var domains = _group.SubjectId is Guid subjectId
? _catalog.GetBySubjectAndGrade(subjectId, _group.GradeLevel)
: [];
var units = _units.GetByGroup(_group.Id);
var exams = _exams.GetByGroup(_group.Id);
var results = exams.SelectMany(x => _results.GetByExam(x.Id)).ToList();
var analysis = _analysis.Analyze(domains, units, exams, results);
StudentOptions.Clear();
foreach (var student in _students.GetByGroup(_group.Id)
.OrderBy(x => x.LastName).ThenBy(x => x.FirstName))
StudentOptions.Add(new CompetencyStudentOption(student.Id, student.FullName));
SelectedStudent = StudentOptions.FirstOrDefault(x => x.Id == selectedStudentId)
?? StudentOptions.FirstOrDefault();
Rows.Clear();
foreach (var item in analysis)
Rows.Add(new CompetencyOverviewRow(item, SelectedStudent?.Id, ReviewThreshold));
var treated = analysis.Count(x => x.TaughtUnitCount > 0);
var assessed = analysis.Count(x => x.AssessedExamCount > 0);
var evaluated = analysis.Count(x => x.GroupScore is not null);
Summary = $"{treated} von {analysis.Count} behandelt · {assessed} geprüft · {evaluated} mit Ergebnisdaten";
EmptyMessage = analysis.Count == 0
? "Noch keine Kompetenzen im Katalog oder in Unterrichtseinheiten und Klausuren zugeordnet."
: "";
OnPropertyChanged(nameof(HasRows));
OnPropertyChanged(nameof(HasStudents));
}
partial void OnSelectedStudentChanged(CompetencyStudentOption? value)
{
foreach (var row in Rows) row.SetStudent(value?.Id);
}
partial void OnReviewThresholdChanged(double value)
{
OnPropertyChanged(nameof(ThresholdLabel));
foreach (var row in Rows) row.SetThreshold(value);
}
}
public sealed record CompetencyStudentOption(Guid Id, string DisplayName);
public partial class CompetencyOverviewRow : ObservableObject
{
private readonly CompetencyAnalysisItem _item;
private double _threshold;
[ObservableProperty] private string _studentResult = "";
[ObservableProperty] private string _recommendation = "Noch keine Bewertung";
[ObservableProperty] private string _recommendationColor = "#6B7280";
public string Domain => string.IsNullOrWhiteSpace(_item.DomainCode)
? _item.DomainName
: $"{_item.DomainName} ({_item.DomainCode})";
public string Code => string.IsNullOrWhiteSpace(_item.Code) ? "" : _item.Code;
public string Description => _item.Description;
public string InstructionCoverage => CoverageText(
_item.TaughtUnitCount, "behandelt", _item.PlannedUnitCount, "geplant");
public string ExamCoverage => CoverageText(
_item.AssessedExamCount, "geprüft", _item.PlannedExamCount, "geplant");
public string GroupResult => ScoreText(_item.GroupScore, includeParticipants: true);
public CompetencyOverviewRow(CompetencyAnalysisItem item, Guid? studentId, double threshold)
{
_item = item;
_threshold = threshold;
SetStudent(studentId);
UpdateRecommendation();
}
public void SetStudent(Guid? studentId)
{
StudentResult = studentId is Guid id && _item.StudentScores.TryGetValue(id, out var score)
? ScoreText(score, includeParticipants: false)
: "";
}
public void SetThreshold(double threshold)
{
_threshold = threshold;
UpdateRecommendation();
}
private void UpdateRecommendation()
{
if (_item.GroupScore is null)
{
Recommendation = "Noch keine Bewertung";
RecommendationColor = "#6B7280";
}
else if (_item.GroupScore.Percent < _threshold)
{
Recommendation = "Wiederholungsbedarf";
RecommendationColor = "#C62828";
}
else
{
Recommendation = "Stand solide";
RecommendationColor = "#2E7D32";
}
}
private static string CoverageText(int completed, string completedLabel, int planned, string plannedLabel)
{
if (completed == 0 && planned == 0) return "";
if (completed == 0) return $"{planned}× {plannedLabel}";
if (planned == 0) return $"{completed}× {completedLabel}";
return $"{completed}× {completedLabel} · {planned}× {plannedLabel}";
}
private static string ScoreText(CompetencyScore? score, bool includeParticipants)
{
if (score is null) return "";
var evidence = score.EvidenceCount == 1 ? "1 Aufgabenwert" : $"{score.EvidenceCount} Aufgabenwerte";
return includeParticipants
? $"{score.Percent:0.#} % · {score.ParticipantCount} Schüler · {evidence}"
: $"{score.Percent:0.#} % · {evidence}";
}
}
@@ -206,6 +206,10 @@ public partial class GroupDetailViewModel : ObservableObject
NotifyWriteCommands();
}
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
partial void OnActiveTabIndexChanged(int value)
{
if (value == 6) CompetencyOverviewTab.Refresh();
}
public ObservableCollection<StudentSummary> Students { get; } = [];
public ObservableCollection<ExamSummary> Exams { get; } = [];
@@ -213,6 +217,7 @@ public partial class GroupDetailViewModel : ObservableObject
public ParticipationTabViewModel ParticipationTab { get; }
public GradeOverviewTabViewModel GradeOverviewTab { get; }
public PlanningTabViewModel PlanningTab { get; }
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
public Func<Task<bool>>? OnAddStudent { get; set; }
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
@@ -227,13 +232,14 @@ public partial class GroupDetailViewModel : ObservableObject
IGroupMembershipRepository memberships, ISubjectRepository subjects,
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
PlanningTabViewModel planningTab)
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab)
{
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
_exams = exams; _grades = grades; _tasks = tasks;
ParticipationTab = participationTab;
GradeOverviewTab = gradeOverviewTab;
PlanningTab = planningTab;
CompetencyOverviewTab = competencyOverviewTab;
}
public void LoadGroup(Guid id)
@@ -252,6 +258,7 @@ public partial class GroupDetailViewModel : ObservableObject
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
PlanningTab.Initialize(Group.Id, IsReadOnly);
CompetencyOverviewTab.Initialize(Group);
}
private void ReloadExams()
@@ -261,6 +268,7 @@ public partial class GroupDetailViewModel : ObservableObject
Exams.Clear();
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
CompetencyOverviewTab.Refresh();
}
public void LoadStudents()
@@ -368,6 +376,7 @@ public partial class GroupDetailViewModel : ObservableObject
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
await OnGradeExam(exam);
CompetencyOverviewTab.Refresh();
}
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
@@ -0,0 +1,29 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace LehrerApp.Desktop.ViewModels.Settings;
public partial class CompetencyCatalogCopyTargetViewModel : ObservableObject
{
[ObservableProperty] private int _targetGradeLevel;
[ObservableProperty] private string _error = "";
public string SourceLabel { get; }
public int SourceGradeLevel { get; }
public CompetencyCatalogCopyTargetViewModel(string subjectName, int sourceGradeLevel)
{
SourceGradeLevel = sourceGradeLevel;
SourceLabel = $"{subjectName} · Klassenstufe {sourceGradeLevel}";
_targetGradeLevel = sourceGradeLevel < 13 ? sourceGradeLevel + 1 : sourceGradeLevel - 1;
}
public bool Validate()
{
Error = TargetGradeLevel == SourceGradeLevel
? "Quell- und Zielklassenstufe müssen verschieden sein."
: TargetGradeLevel is < 1 or > 13
? "Die Klassenstufe muss zwischen 1 und 13 liegen."
: "";
return Error.Length == 0;
}
}
@@ -16,6 +16,7 @@ public partial class CompetencyCatalogImportViewModel : ObservableObject
public IReadOnlyList<ImportModeOption> Modes { get; }
public ObservableCollection<CompetencyImportConflictItem> Conflicts { get; } = [];
public IReadOnlyList<string> Warnings => _preview.Warnings;
public string Heading { get; }
public string Target => $"{_preview.SubjectName} · Klassenstufe {_preview.GradeLevel}";
public string Summary =>
$"{_preview.NewDomains} neue Bereiche · {_preview.NewCompetencies} neue Kompetenzen · " +
@@ -26,10 +27,12 @@ public partial class CompetencyCatalogImportViewModel : ObservableObject
public bool IsReplace => SelectedMode.Mode == CompetencyCatalogImportMode.Replace;
public CompetencyCatalogImportViewModel(
SettingsViewModel settings, CompetencyCatalogImportPreview preview)
SettingsViewModel settings, CompetencyCatalogImportPreview preview,
string heading = "Import prüfen")
{
_settings = settings;
_preview = preview;
Heading = heading;
Modes =
[
new(CompetencyCatalogImportMode.Merge, "Zusammenführen (empfohlen)"),
@@ -733,7 +733,8 @@ public partial class SettingsViewModel : ObservableObject
CatalogValidation = "";
if (CatalogSubject is null) return;
foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel))
Domains.Add(new DomainEditItem(d, _domainRepo));
Domains.Add(CreateDomainEditItem(d));
RefreshDomainMoveState();
}
// ── Katalog: Bereich hinzufügen / löschen ────────────────────────────────
@@ -753,7 +754,8 @@ public partial class SettingsViewModel : ObservableObject
SortOrder = Domains.Count,
};
_domainRepo.Save(domain);
Domains.Add(new DomainEditItem(domain, _domainRepo));
Domains.Add(CreateDomainEditItem(domain));
RefreshDomainMoveState();
NewDomainName = ""; NewDomainCode = ""; CatalogValidation = "";
}
@@ -763,6 +765,31 @@ public partial class SettingsViewModel : ObservableObject
if (item is null) return;
_domainRepo.Delete(item.Id);
Domains.Remove(item);
PersistDomainOrder();
}
private DomainEditItem CreateDomainEditItem(CompetencyDomain domain) =>
new(domain, _domainRepo, item => MoveDomain(item, -1), item => MoveDomain(item, 1));
private void MoveDomain(DomainEditItem item, int offset)
{
var oldIndex = Domains.IndexOf(item);
var newIndex = oldIndex + offset;
if (oldIndex < 0 || newIndex < 0 || newIndex >= Domains.Count) return;
Domains.Move(oldIndex, newIndex);
PersistDomainOrder();
}
private void PersistDomainOrder()
{
for (var i = 0; i < Domains.Count; i++) Domains[i].SetSortOrder(i);
RefreshDomainMoveState();
}
private void RefreshDomainMoveState()
{
for (var i = 0; i < Domains.Count; i++)
Domains[i].SetMoveState(i > 0, i < Domains.Count - 1);
}
// ── JSON Import / Export ──────────────────────────────────────────────────
@@ -798,11 +825,40 @@ public partial class SettingsViewModel : ObservableObject
}
public string ExportCatalog()
=> SerializeCatalog(CatalogGradeLevel);
public CompetencyCatalogImportPreview? PrepareCatalogCopy(int targetGradeLevel)
{
if (CatalogSubject is null)
{
CatalogValidation = "Bitte zuerst ein Fach auswählen.";
return null;
}
if (Domains.Count == 0)
{
CatalogValidation = "Der ausgewählte Katalog enthält keine Bereiche.";
return null;
}
if (targetGradeLevel is < 1 or > 13 || targetGradeLevel == CatalogGradeLevel)
{
CatalogValidation = "Bitte eine andere Zielklassenstufe zwischen 1 und 13 auswählen.";
return null;
}
CatalogValidation = "";
return _catalogImport.Analyze(SerializeCatalog(targetGradeLevel), CatalogSubject.Id,
CatalogSubject.Name, targetGradeLevel);
}
public void SetCatalogCopyStatus(int targetGradeLevel) =>
CatalogValidation = $"Kompetenzkatalog wurde in Klassenstufe {targetGradeLevel} kopiert.";
private string SerializeCatalog(int gradeLevel)
{
var dto = new CatalogDto
{
Subject = CatalogSubject?.Name ?? "",
GradeLevel = CatalogGradeLevel,
GradeLevel = gradeLevel,
Domains = Domains.Select(d => new DomainDto
{
Name = d.Name,
@@ -834,8 +890,11 @@ public partial class DomainEditItem : ObservableObject
[ObservableProperty] private string _newItemDesc = "";
public ObservableCollection<CompetencyItemVm> Items { get; } = [];
public IRelayCommand MoveUpCommand { get; }
public IRelayCommand MoveDownCommand { get; }
public DomainEditItem(CompetencyDomain domain, ICompetencyDomainRepository repo)
public DomainEditItem(CompetencyDomain domain, ICompetencyDomainRepository repo,
Action<DomainEditItem>? onMoveUp = null, Action<DomainEditItem>? onMoveDown = null)
{
_domain = domain;
_repo = repo;
@@ -846,8 +905,31 @@ public partial class DomainEditItem : ObservableObject
? domain.Name
: $"{domain.Name} ({domain.Code})";
foreach (var item in domain.Items.OrderBy(i => i.SortOrder))
Items.Add(new CompetencyItemVm(item, DeleteItem));
MoveUpCommand = new RelayCommand(() => onMoveUp?.Invoke(this), () => _canMoveUp);
MoveDownCommand = new RelayCommand(() => onMoveDown?.Invoke(this), () => _canMoveDown);
_domain.Items = domain.Items.OrderBy(i => i.SortOrder).ToList();
foreach (var item in _domain.Items)
Items.Add(CreateItemViewModel(item));
RefreshItemMoveState();
}
private bool _canMoveUp;
private bool _canMoveDown;
internal void SetMoveState(bool canMoveUp, bool canMoveDown)
{
_canMoveUp = canMoveUp;
_canMoveDown = canMoveDown;
MoveUpCommand.NotifyCanExecuteChanged();
MoveDownCommand.NotifyCanExecuteChanged();
}
internal void SetSortOrder(int sortOrder)
{
if (_domain.SortOrder == sortOrder) return;
_domain.SortOrder = sortOrder;
_repo.Save(_domain);
}
[RelayCommand]
@@ -862,15 +944,42 @@ public partial class DomainEditItem : ObservableObject
};
_domain.Items.Add(item);
_repo.Save(_domain);
Items.Add(new CompetencyItemVm(item, DeleteItem));
Items.Add(CreateItemViewModel(item));
RefreshItemMoveState();
NewItemCode = ""; NewItemDesc = "";
}
private void DeleteItem(CompetencyItemVm vm)
{
_domain.Items.RemoveAll(i => i.Id == vm.ItemId);
_repo.Save(_domain);
Items.Remove(vm);
PersistItemOrder();
}
private CompetencyItemVm CreateItemViewModel(CompetencyItem item) =>
new(item, DeleteItem, vm => MoveItem(vm, -1), vm => MoveItem(vm, 1));
private void MoveItem(CompetencyItemVm item, int offset)
{
var oldIndex = Items.IndexOf(item);
var newIndex = oldIndex + offset;
if (oldIndex < 0 || newIndex < 0 || newIndex >= Items.Count) return;
Items.Move(oldIndex, newIndex);
PersistItemOrder();
}
private void PersistItemOrder()
{
_domain.Items = Items.Select(x => x.Model).ToList();
for (var i = 0; i < _domain.Items.Count; i++) _domain.Items[i].SortOrder = i;
_repo.Save(_domain);
RefreshItemMoveState();
}
private void RefreshItemMoveState()
{
for (var i = 0; i < Items.Count; i++)
Items[i].SetMoveState(i > 0, i < Items.Count - 1);
}
}
@@ -878,14 +987,19 @@ public partial class DomainEditItem : ObservableObject
public class CompetencyItemVm
{
internal CompetencyItem Model { get; }
public Guid ItemId { get; }
public string Code { get; }
public string Description { get; }
public string Display { get; }
public IRelayCommand DeleteCommand { get; }
public IRelayCommand MoveUpCommand { get; }
public IRelayCommand MoveDownCommand { get; }
public CompetencyItemVm(CompetencyItem item, Action<CompetencyItemVm> onDelete)
public CompetencyItemVm(CompetencyItem item, Action<CompetencyItemVm> onDelete,
Action<CompetencyItemVm>? onMoveUp = null, Action<CompetencyItemVm>? onMoveDown = null)
{
Model = item;
ItemId = item.Id;
Code = item.Code;
Description = item.Description;
@@ -893,6 +1007,19 @@ public class CompetencyItemVm
? item.Description
: $"[{item.Code}] {item.Description}";
DeleteCommand = new RelayCommand(() => onDelete(this));
MoveUpCommand = new RelayCommand(() => onMoveUp?.Invoke(this), () => _canMoveUp);
MoveDownCommand = new RelayCommand(() => onMoveDown?.Invoke(this), () => _canMoveDown);
}
private bool _canMoveUp;
private bool _canMoveDown;
internal void SetMoveState(bool canMoveUp, bool canMoveDown)
{
_canMoveUp = canMoveUp;
_canMoveDown = canMoveDown;
MoveUpCommand.NotifyCanExecuteChanged();
MoveDownCommand.NotifyCanExecuteChanged();
}
}
@@ -0,0 +1,68 @@
<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.CompetencyOverviewTabView"
x:DataType="vm:CompetencyOverviewTabViewModel">
<Grid RowDefinitions="Auto,*" Margin="20">
<StackPanel Grid.Row="0" Spacing="10" Margin="0,0,0,12">
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="3">
<TextBlock Text="Kompetenzübersicht" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.65"/>
</StackPanel>
<Button Grid.Column="1" Content="Aktualisieren" Command="{Binding RefreshCommand}"/>
</Grid>
<Grid ColumnDefinitions="*,16,210" IsVisible="{Binding HasRows}">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Schülerprofil" FontSize="12" Opacity="0.7"/>
<ComboBox ItemsSource="{Binding StudentOptions}" SelectedItem="{Binding SelectedStudent}"
PlaceholderText="Keine Schüler vorhanden" IsEnabled="{Binding HasStudents}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:CompetencyStudentOption">
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="{Binding ThresholdLabel}" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding ReviewThreshold}" Minimum="0" Maximum="100"
Increment="5" FormatString="0"/>
</StackPanel>
</Grid>
<TextBlock Text="Prozentwerte beruhen auf bewerteten Klausuraufgaben. Eine Aufgabe mit mehreren Kompetenzcodes zählt für jede dieser Kompetenzen."
TextWrapping="Wrap" FontSize="11" Opacity="0.55" IsVisible="{Binding HasRows}"/>
</StackPanel>
<Grid Grid.Row="1">
<TextBlock Text="{Binding EmptyMessage}" Classes="emptyhint"
HorizontalAlignment="Center" VerticalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520"
IsVisible="{Binding !HasRows}"/>
<DataGrid ItemsSource="{Binding Rows}" AutoGenerateColumns="False" IsReadOnly="True"
GridLinesVisibility="Horizontal" CanUserReorderColumns="False"
CanUserResizeColumns="True" IsVisible="{Binding HasRows}">
<DataGrid.Columns>
<DataGridTextColumn Header="Bereich" Binding="{Binding Domain}" Width="170"/>
<DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="75"/>
<DataGridTextColumn Header="Kompetenz" Binding="{Binding Description}" Width="2*"/>
<DataGridTextColumn Header="Unterricht" Binding="{Binding InstructionCoverage}" Width="145"/>
<DataGridTextColumn Header="Klausuren" Binding="{Binding ExamCoverage}" Width="135"/>
<DataGridTextColumn Header="Gruppenmittel" Binding="{Binding GroupResult}" Width="210"/>
<DataGridTextColumn Header="Ausgewählter Schüler" Binding="{Binding StudentResult}" Width="160"/>
<DataGridTemplateColumn Header="Hinweis" Width="155">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:CompetencyOverviewRow">
<TextBlock Text="{Binding Recommendation}" Foreground="{Binding RecommendationColor}"
FontWeight="SemiBold" VerticalAlignment="Center"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,8 @@
using Avalonia.Controls;
namespace LehrerApp.Desktop.Views.Groups;
public partial class CompetencyOverviewTabView : UserControl
{
public CompetencyOverviewTabView() => InitializeComponent();
}
@@ -191,6 +191,11 @@
<views:PlanningTabView DataContext="{Binding PlanningTab}"/>
</ContentPage>
<!-- Tab: Kompetenzen -->
<ContentPage Header="Kompetenzen">
<views:CompetencyOverviewTabView DataContext="{Binding CompetencyOverviewTab}"/>
</ContentPage>
<!-- Tab: Dokumentation -->
<ContentPage Header="Dokumentation">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
@@ -132,11 +132,16 @@
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="5"/>
<Button Content="📋 Dokumentation"
<Button Content="🎯 Kompetenzübersicht"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="6"/>
<Button Content="📋 Dokumentation"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
Padding="12,9"
Command="{Binding NavigateToSectionCommand}"
CommandParameter="7"/>
</StackPanel>
</StackPanel>
@@ -0,0 +1,29 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
x:Class="LehrerApp.Desktop.Views.Settings.CompetencyCatalogCopyTargetDialog"
x:DataType="vm:CompetencyCatalogCopyTargetViewModel"
Title="Kompetenzkatalog kopieren"
Width="460" Height="280" CanResize="False"
WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
<StackPanel Grid.Row="0" Spacing="8">
<TextBlock Text="Katalog kopieren" Classes="dialogtitle"/>
<TextBlock Text="{Binding SourceLabel}" FontWeight="SemiBold"/>
<TextBlock Text="Wähle die Zielklassenstufe. Anschließend kannst du vorhandene Einträge und Konflikte in einer Vorschau prüfen."
TextWrapping="Wrap" FontSize="12" Opacity="0.7"/>
</StackPanel>
<StackPanel Grid.Row="1" Spacing="5" Margin="0,16,0,0">
<TextBlock Text="Zielklassenstufe" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding TargetGradeLevel}" Minimum="1" Maximum="13" FormatString="0"/>
<TextBlock Text="{Binding Error}" Foreground="Red" FontSize="12"
IsVisible="{Binding Error, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<Grid Grid.Row="2" ColumnDefinitions="*,10,*" Margin="0,16,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Weiter zur Vorschau" HorizontalAlignment="Stretch" Click="OnContinue"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Settings;
namespace LehrerApp.Desktop.Views.Settings;
public partial class CompetencyCatalogCopyTargetDialog : Window
{
public CompetencyCatalogCopyTargetDialog() => InitializeComponent();
private void OnContinue(object? sender, RoutedEventArgs e)
{
if (DataContext is CompetencyCatalogCopyTargetViewModel vm && vm.Validate())
Close((int?)vm.TargetGradeLevel);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
}
@@ -3,12 +3,12 @@
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
x:Class="LehrerApp.Desktop.Views.Settings.CompetencyCatalogImportDialog"
x:DataType="vm:CompetencyCatalogImportViewModel"
Title="Kompetenzkatalog importieren"
Title="{Binding Heading}"
Width="760" Height="720" MinWidth="620" MinHeight="560"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
<StackPanel Grid.Row="0" Spacing="10">
<TextBlock Text="Import prüfen" Classes="dialogtitle"/>
<TextBlock Text="{Binding Heading}" Classes="dialogtitle"/>
<TextBlock Text="{Binding Target}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.7"/>
@@ -111,7 +111,7 @@
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="640">
<!-- Fach + Klassenstufe Auswahl + Import/Export -->
<Grid ColumnDefinitions="*,12,120,12,Auto,8,Auto">
<Grid ColumnDefinitions="*,12,100,12,Auto,8,Auto,8,Auto">
<ComboBox Grid.Column="0"
ItemsSource="{Binding Subjects}"
SelectedItem="{Binding CatalogSubject}"
@@ -130,6 +130,9 @@
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Button Grid.Column="6" Content="JSON exportieren" Click="OnExportClick"
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Button Grid.Column="8" Content="Kopieren…" Click="OnCopyCatalogClick"
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"
ToolTip.Tip="Katalog in eine andere Klassenstufe kopieren"/>
</Grid>
<!-- Katalog-Validierungsmeldung -->
@@ -150,10 +153,14 @@
<StackPanel Spacing="8">
<!-- Bereichs-Header -->
<Grid ColumnDefinitions="*,Auto">
<Grid ColumnDefinitions="*,Auto,4,Auto,8,Auto">
<TextBlock Grid.Column="0" Text="{Binding DisplayName}"
FontWeight="SemiBold" FontSize="14" VerticalAlignment="Center"/>
<Button Grid.Column="1" Content="Bereich löschen" FontSize="12" Padding="10,4"
<Button Grid.Column="1" Content="" FontSize="13" Padding="8,3"
Command="{Binding MoveUpCommand}" ToolTip.Tip="Bereich nach oben"/>
<Button Grid.Column="3" Content="↓" FontSize="13" Padding="8,3"
Command="{Binding MoveDownCommand}" ToolTip.Tip="Bereich nach unten"/>
<Button Grid.Column="5" Content="Bereich löschen" FontSize="12" Padding="10,4"
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteDomainCommand}"
CommandParameter="{Binding}"/>
</Grid>
@@ -162,11 +169,15 @@
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:CompetencyItemVm">
<Grid ColumnDefinitions="*,Auto" Margin="0,3">
<Grid ColumnDefinitions="*,Auto,4,Auto,6,Auto" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding Display}"
TextWrapping="Wrap" VerticalAlignment="Center"
FontSize="13"/>
<Button Grid.Column="1" Content="×" Padding="7,2" FontSize="13"
<Button Grid.Column="1" Content="" Padding="7,2" FontSize="12"
Command="{Binding MoveUpCommand}" ToolTip.Tip="Kompetenz nach oben"/>
<Button Grid.Column="3" Content="↓" Padding="7,2" FontSize="12"
Command="{Binding MoveDownCommand}" ToolTip.Tip="Kompetenz nach unten"/>
<Button Grid.Column="5" Content="×" Padding="7,2" FontSize="13"
Command="{Binding DeleteCommand}"/>
</Grid>
</DataTemplate>
@@ -93,6 +93,30 @@ public partial class SettingsView : UserControl
await File.WriteAllTextAsync(file.Path.LocalPath, json);
}
private async void OnCopyCatalogClick(object? sender, RoutedEventArgs e)
{
if (DataContext is not SettingsViewModel { CatalogSubject: not null } vm) return;
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return;
var targetDialog = new CompetencyCatalogCopyTargetDialog
{
DataContext = new CompetencyCatalogCopyTargetViewModel(
vm.CatalogSubject.Name, vm.CatalogGradeLevel),
};
var targetGradeLevel = await targetDialog.ShowDialog<int?>(owner);
if (targetGradeLevel is null) return;
var preview = vm.PrepareCatalogCopy(targetGradeLevel.Value);
if (preview is null) return;
var previewDialog = new CompetencyCatalogImportDialog
{
DataContext = new CompetencyCatalogImportViewModel(vm, preview, "Kopie prüfen"),
};
if (await previewDialog.ShowDialog<bool>(owner))
vm.SetCatalogCopyStatus(targetGradeLevel.Value);
}
private async void OnEditGradingKeyTemplateClick(object? sender, RoutedEventArgs e)
{
if (sender is not Button { Tag: GradingKeyTemplateEditItem item }) return;