- Dashboard: Fehlzeiten-Warnung lädt Mitarbeitssitzungen einmal vorab statt pro Schüler/Eintrag einzeln nachzuschlagen (N+1 vermieden); neues IParticipationSessionRepository.GetAll() dafür. - CI: .gitea/workflows/ci.yml baut und testet bei jedem Push/PR. Dabei fehlende Release|Any CPU-Konfiguration für 6 Projekte in der .sln behoben (LehrerApp.Data.Tests wurde bei Release-Builds der Solution bislang stillschweigend übersprungen). TreatWarningsAsErrors jetzt aktiv. - SettingsViewModel (1986 Zeilen) als partial class auf 20 Themen- dateien aufgeteilt, Verhalten unverändert. - Backup: optionaler zweiter Sicherungsordner (USB-Stick/Netzlaufwerk, best-effort) und Integritätsprüfung nach jedem Backup (DatabaseEncryptionService.CanOpenAndRead). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
252 lines
9.1 KiB
C#
252 lines
9.1 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using LehrerApp.Data;
|
|
using LehrerApp.Desktop.Services;
|
|
using LehrerApp.Desktop.ViewModels.Planning;
|
|
using LehrerApp.Sync;
|
|
using LehrerApp.Sync.Crypto;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Settings;
|
|
|
|
public partial class SettingsViewModel
|
|
{
|
|
// ── Kompetenzkatalog ──────────────────────────────────────────────────────
|
|
|
|
[ObservableProperty] private SubjectListItem? _catalogSubject;
|
|
[ObservableProperty] private int _catalogGradeLevel = 10;
|
|
[ObservableProperty] private string _newDomainName = "";
|
|
[ObservableProperty] private string _newDomainCode = "";
|
|
[ObservableProperty] private string _catalogValidation = "";
|
|
|
|
public ObservableCollection<DomainEditItem> Domains { get; } = [];
|
|
|
|
// ── Katalog: Laden ────────────────────────────────────────────────────────
|
|
|
|
partial void OnCatalogSubjectChanged(SubjectListItem? value) => LoadCatalog();
|
|
partial void OnCatalogGradeLevelChanged(int value) => LoadCatalog();
|
|
|
|
private void LoadCatalog()
|
|
{
|
|
Domains.Clear();
|
|
CatalogValidation = "";
|
|
if (CatalogSubject is null) return;
|
|
foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel))
|
|
Domains.Add(CreateDomainEditItem(d));
|
|
RefreshDomainMoveState();
|
|
}
|
|
|
|
// ── Katalog: Bereich hinzufügen / löschen ────────────────────────────────
|
|
|
|
[RelayCommand]
|
|
private void AddDomain()
|
|
{
|
|
if (CatalogSubject is null) { CatalogValidation = "Bitte zuerst ein Fach auswählen."; return; }
|
|
if (string.IsNullOrWhiteSpace(NewDomainName)) { CatalogValidation = "Bereichsname erforderlich."; return; }
|
|
|
|
var domain = new CompetencyDomain
|
|
{
|
|
SubjectId = CatalogSubject.Id,
|
|
GradeLevel = CatalogGradeLevel,
|
|
Name = NewDomainName.Trim(),
|
|
Code = NewDomainCode.Trim(),
|
|
SortOrder = Domains.Count,
|
|
};
|
|
_domainRepo.Save(domain);
|
|
Domains.Add(CreateDomainEditItem(domain));
|
|
RefreshDomainMoveState();
|
|
NewDomainName = ""; NewDomainCode = ""; CatalogValidation = "";
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void DeleteDomain(DomainEditItem? item)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ── DomainEditItem ────────────────────────────────────────────────────────────
|
|
|
|
public partial class DomainEditItem : ObservableObject
|
|
{
|
|
private readonly CompetencyDomain _domain;
|
|
private readonly ICompetencyDomainRepository _repo;
|
|
|
|
public Guid Id { get; }
|
|
public string Name { get; }
|
|
public string Code { get; }
|
|
public string DisplayName { get; }
|
|
|
|
[ObservableProperty] private string _newItemCode = "";
|
|
[ObservableProperty] private string _newItemDesc = "";
|
|
|
|
public ObservableCollection<CompetencyItemVm> Items { get; } = [];
|
|
public IRelayCommand MoveUpCommand { get; }
|
|
public IRelayCommand MoveDownCommand { get; }
|
|
|
|
public DomainEditItem(CompetencyDomain domain, ICompetencyDomainRepository repo,
|
|
Action<DomainEditItem>? onMoveUp = null, Action<DomainEditItem>? onMoveDown = null)
|
|
{
|
|
_domain = domain;
|
|
_repo = repo;
|
|
Id = domain.Id;
|
|
Name = domain.Name;
|
|
Code = domain.Code;
|
|
DisplayName = string.IsNullOrEmpty(domain.Code)
|
|
? domain.Name
|
|
: $"{domain.Name} ({domain.Code})";
|
|
|
|
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]
|
|
private void AddItem()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(NewItemDesc)) return;
|
|
var item = new CompetencyItem
|
|
{
|
|
Code = NewItemCode.Trim(),
|
|
Description = NewItemDesc.Trim(),
|
|
SortOrder = _domain.Items.Count,
|
|
};
|
|
_domain.Items.Add(item);
|
|
_repo.Save(_domain);
|
|
Items.Add(CreateItemViewModel(item));
|
|
RefreshItemMoveState();
|
|
NewItemCode = ""; NewItemDesc = "";
|
|
}
|
|
|
|
private void DeleteItem(CompetencyItemVm vm)
|
|
{
|
|
_domain.Items.RemoveAll(i => i.Id == vm.ItemId);
|
|
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);
|
|
}
|
|
}
|
|
|
|
// ── CompetencyItemVm ──────────────────────────────────────────────────────────
|
|
|
|
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,
|
|
Action<CompetencyItemVm>? onMoveUp = null, Action<CompetencyItemVm>? onMoveDown = null)
|
|
{
|
|
Model = item;
|
|
ItemId = item.Id;
|
|
Code = item.Code;
|
|
Description = item.Description;
|
|
Display = string.IsNullOrEmpty(item.Code)
|
|
? 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();
|
|
}
|
|
}
|
|
|
|
// ── GradingKeyTemplateEditItem (1.3.2) ─────────────────────────────────────────
|