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
@@ -9,9 +9,56 @@ using System.Globalization;
namespace LehrerApp.Desktop.ViewModels.Students;
public static class GenderAvatarDisplay
{
public static List<string> Options { get; } = ["", "M männlich", "W weiblich", "D divers"];
public static string Label(Gender? gender) => gender switch
{
Gender.M => "M",
Gender.W => "W",
Gender.D => "D",
_ => "?",
};
public static string Color(Gender? gender) => gender switch
{
Gender.M => "#3B82F6",
Gender.W => "#A855F7",
Gender.D => "#0D9488",
_ => "#6B7280",
};
public static string Tooltip(Gender? gender) => gender switch
{
Gender.M => "Geschlecht: männlich",
Gender.W => "Geschlecht: weiblich",
Gender.D => "Geschlecht: divers",
_ => "Geschlecht nicht angegeben",
};
public static string ToOption(Gender? gender) => gender switch
{
Gender.M => "M männlich",
Gender.W => "W weiblich",
Gender.D => "D divers",
_ => "",
};
public static Gender? FromOption(string? option) => option switch
{
"M männlich" => Gender.M,
"W weiblich" => Gender.W,
"D divers" => Gender.D,
_ => null,
};
}
public partial class StudentListViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly IGroupRepository _groups;
private readonly IGroupMembershipRepository _memberships;
public Action<Guid>? OnNavigateToDetail { get; set; }
[ObservableProperty] private string _searchText = "";
@@ -21,9 +68,12 @@ public partial class StudentListViewModel : ObservableObject
public ObservableCollection<StudentListItem> Students { get; } = [];
public string CountSummary => $"{Students.Count} Schüler gesamt";
public StudentListViewModel(IStudentRepository students)
public StudentListViewModel(IStudentRepository students, IGroupRepository groups,
IGroupMembershipRepository memberships)
{
_students = students;
_groups = groups;
_memberships = memberships;
LoadStudents();
}
@@ -38,9 +88,21 @@ public partial class StudentListViewModel : ObservableObject
{
Students.Clear();
var all = _students.GetAll(ShowInactive);
var f = string.IsNullOrWhiteSpace(SearchText) ? all
: all.Where(s => s.LastName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| s.FirstName.Contains(SearchText, StringComparison.OrdinalIgnoreCase));
var query = SearchText.Trim();
IEnumerable<Student> f = all;
if (query.Length > 0)
{
var matchingStudentIds = _groups.GetAll(includeInactive: true)
.Where(g => g.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
.SelectMany(g => _memberships.GetByGroup(g.Id))
.Select(m => m.StudentId)
.ToHashSet();
f = all.Where(s => s.LastName.Contains(query, StringComparison.OrdinalIgnoreCase)
|| s.FirstName.Contains(query, StringComparison.OrdinalIgnoreCase)
|| s.FullName.Contains(query, StringComparison.OrdinalIgnoreCase)
|| matchingStudentIds.Contains(s.Id));
}
foreach (var s in f) Students.Add(new StudentListItem(s));
OnPropertyChanged(nameof(CountSummary));
}
@@ -63,10 +125,92 @@ public class StudentListItem
public Guid Id { get; }
public string FullName { get; }
public string DateOfBirth { get; }
public string AvatarLabel { get; }
public string AvatarColor { get; }
public string AvatarTooltip { get; }
public StudentListItem(Student s)
{
Id = s.Id; FullName = s.FullName;
DateOfBirth = s.DateOfBirth?.ToString("dd.MM.yyyy") ?? "";
AvatarLabel = GenderAvatarDisplay.Label(s.Gender);
AvatarColor = GenderAvatarDisplay.Color(s.Gender);
AvatarTooltip = GenderAvatarDisplay.Tooltip(s.Gender);
}
}
public enum StudentManagementResult { Cancelled, Deactivated, Reactivated, Deleted }
public class StudentReferenceItem(string label, int count)
{
public string Label { get; } = label;
public int Count { get; } = count;
public string CountDisplay => Count.ToString(CultureInfo.InvariantCulture);
}
public partial class ManageStudentDialogViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly Student _student;
[ObservableProperty] private string _errorMessage = "";
public string StudentName => _student.FullName;
public bool IsActive => _student.IsActive;
public bool IsInactive => !IsActive;
public StudentReferenceSummary References { get; }
public bool HasReferences => References.HasReferences;
public bool CanDelete => !HasReferences;
public ObservableCollection<StudentReferenceItem> ReferenceItems { get; } = [];
public StudentManagementResult Result { get; private set; }
public ManageStudentDialogViewModel(IStudentRepository students, Student student)
{
_students = students;
_student = student;
References = students.GetReferenceSummary(student.Id);
AddReference("Gruppenzuordnungen", References.Memberships);
AddReference("Klausurergebnisse", References.ExamResults);
AddReference("Einzelnoten", References.Grades);
AddReference("Zeugnisnoten", References.ReportGrades);
AddReference("Mitarbeitseinträge", References.ParticipationEntries);
AddReference("Dokumentationseinträge", References.DocumentationEntries);
}
[RelayCommand]
private void Deactivate()
{
_student.IsActive = false;
_students.Save(_student);
Result = StudentManagementResult.Deactivated;
}
[RelayCommand]
private void Reactivate()
{
_student.IsActive = true;
_students.Save(_student);
Result = StudentManagementResult.Reactivated;
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private void DeletePermanently()
{
ErrorMessage = "";
try
{
_students.Delete(_student.Id);
Result = StudentManagementResult.Deleted;
}
catch (InvalidOperationException ex)
{
ErrorMessage = ex.Message;
}
}
private void AddReference(string label, int count)
{
if (count > 0) ReferenceItems.Add(new StudentReferenceItem(label, count));
}
}
@@ -91,9 +235,17 @@ public partial class StudentDetailViewModel : ObservableObject
[ObservableProperty] private bool _isEditing;
[ObservableProperty] private string _editFirstName = "";
[ObservableProperty] private string _editLastName = "";
[ObservableProperty] private string _editGender = "";
[ObservableProperty] private ContactItem? _selectedContact;
[ObservableProperty] private AttendanceBalance? _attendance;
[ObservableProperty] private string _exportStatus = "";
[ObservableProperty] private bool _isStudentActive = true;
public string StudentStatus => IsStudentActive ? "Aktiv" : "Inaktiv";
public List<string> GenderOptions => GenderAvatarDisplay.Options;
public string StudentAvatarLabel => GenderAvatarDisplay.Label(Student?.Gender);
public string StudentAvatarColor => GenderAvatarDisplay.Color(Student?.Gender);
public string StudentAvatarTooltip => GenderAvatarDisplay.Tooltip(Student?.Gender);
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
public ObservableCollection<DocumentationItem> Documentation { get; } = [];
@@ -106,6 +258,11 @@ public partial class StudentDetailViewModel : ObservableObject
public Func<DocumentationItem, Task<bool>>? OnConfirmDeleteDocumentation { get; set; }
public Func<string, Task>? OnSaveExportFile { get; set; }
public Func<Documentation, string, Task<Documentation?>>? OnConductParentCall { get; set; }
public Func<Student, Task<StudentManagementResult>>? OnManageStudent { get; set; }
public Func<Task>? OnCreateLetter { get; set; }
public Action? OnReturnToStudentList { get; set; }
partial void OnIsStudentActiveChanged(bool value) => OnPropertyChanged(nameof(StudentStatus));
public StudentDetailViewModel(IStudentRepository students,
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
@@ -126,8 +283,13 @@ public partial class StudentDetailViewModel : ObservableObject
Student = _students.GetById(id);
if (Student is null) return;
StudentTitle = Student.FullName;
IsStudentActive = Student.IsActive;
EditFirstName = Student.FirstName;
EditLastName = Student.LastName;
EditGender = GenderAvatarDisplay.ToOption(Student.Gender);
OnPropertyChanged(nameof(StudentAvatarLabel));
OnPropertyChanged(nameof(StudentAvatarColor));
OnPropertyChanged(nameof(StudentAvatarTooltip));
GroupMemberships.Clear();
GradeHistory.Clear();
@@ -269,18 +431,44 @@ public partial class StudentDetailViewModel : ObservableObject
}
[RelayCommand] private void StartEdit() => IsEditing = true;
[RelayCommand]
private async Task CreateLetter()
{
if (Student is not null && OnCreateLetter is not null) await OnCreateLetter();
}
[RelayCommand]
private async Task ManageStudent()
{
if (Student is null || OnManageStudent is null) return;
var result = await OnManageStudent(Student);
if (result == StudentManagementResult.Deleted)
{
OnReturnToStudentList?.Invoke();
return;
}
if (result is StudentManagementResult.Deactivated or StudentManagementResult.Reactivated)
LoadStudent(Student.Id);
}
[RelayCommand] private void CancelEdit()
{
if (Student is null) return;
EditFirstName = Student.FirstName; EditLastName = Student.LastName;
EditGender = GenderAvatarDisplay.ToOption(Student.Gender);
IsEditing = false;
}
[RelayCommand] private void SaveEdit()
{
if (Student is null) return;
Student.FirstName = EditFirstName; Student.LastName = EditLastName;
Student.Gender = GenderAvatarDisplay.FromOption(EditGender);
_students.Save(Student);
StudentTitle = Student.FullName;
OnPropertyChanged(nameof(StudentAvatarLabel));
OnPropertyChanged(nameof(StudentAvatarColor));
OnPropertyChanged(nameof(StudentAvatarTooltip));
IsEditing = false;
}
@@ -382,6 +570,7 @@ public class ContactItem
public Guid Id => Model.Id;
public string Name { get; }
public string Relation { get; }
public string LetterSalutation { get; }
public string? Phone { get; }
public string? Email { get; }
public string Address { get; }
@@ -404,6 +593,7 @@ public class ContactItem
Model = c;
Name = c.Name;
Relation = c.Relation;
LetterSalutation = c.LetterSalutation ?? "";
Phone = c.Phone;
Email = c.Email;
Address = FormatAddress(c);
@@ -449,7 +639,7 @@ public partial class AddStudentDialogViewModel : ObservableObject
[ObservableProperty] private string _firstNameError = "";
[ObservableProperty] private string _dateOfBirthError = "";
public List<string> GenderOptions { get; } = ["", "M männlich", "W weiblich", "D divers"];
public List<string> GenderOptions => GenderAvatarDisplay.Options;
public List<string> RelationPresets { get; } = ["Schüler/in", "Mutter", "Vater", "Elternteil", "Erziehungsberechtigte/r", "Sonstige"];
public ObservableCollection<ContactEntryViewModel> Contacts { get; } = [];
@@ -490,13 +680,7 @@ public partial class AddStudentDialogViewModel : ObservableObject
FirstName = FirstName.Trim(),
LastName = LastName.Trim(),
DateOfBirth = dob,
Gender = SelectedGender switch
{
"M männlich" => Gender.M,
"W weiblich" => Gender.W,
"D divers" => Gender.D,
_ => (Gender?)null,
},
Gender = GenderAvatarDisplay.FromOption(SelectedGender),
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
Contacts = Contacts.Select(c => c.ToModel()).Where(c => !string.IsNullOrWhiteSpace(c.Name)).ToList(),
};
@@ -510,6 +694,7 @@ public partial class ContactEntryViewModel : ObservableObject
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _relation = "";
[ObservableProperty] private string _letterSalutation = "";
[ObservableProperty] private string _phone = "";
[ObservableProperty] private string _email = "";
[ObservableProperty] private string _street = "";
@@ -524,6 +709,7 @@ public partial class ContactEntryViewModel : ObservableObject
{
Name = Name.Trim(),
Relation = Relation.Trim(),
LetterSalutation = string.IsNullOrWhiteSpace(LetterSalutation) ? null : LetterSalutation.Trim(),
Phone = string.IsNullOrWhiteSpace(Phone) ? null : Phone.Trim(),
Email = string.IsNullOrWhiteSpace(Email) ? null : Email.Trim(),
Street = string.IsNullOrWhiteSpace(Street) ? null : Street.Trim(),
@@ -538,6 +724,7 @@ public partial class ContactEditDialogViewModel : ObservableObject
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _relation = "Elternteil";
[ObservableProperty] private string _letterSalutation = "";
[ObservableProperty] private string _phone = "";
[ObservableProperty] private string _email = "";
[ObservableProperty] private string _street = "";
@@ -564,6 +751,7 @@ public partial class ContactEditDialogViewModel : ObservableObject
if (source is null) return;
Name = source.Name;
Relation = source.Relation;
LetterSalutation = source.LetterSalutation ?? "";
Phone = source.Phone ?? "";
Email = source.Email ?? "";
Street = source.Street ?? "";
@@ -614,6 +802,7 @@ public partial class ContactEditDialogViewModel : ObservableObject
Id = _source?.Id ?? Guid.NewGuid(),
Name = Name.Trim(),
Relation = Relation.Trim(),
LetterSalutation = NullIfEmpty(LetterSalutation),
Phone = NullIfEmpty(Phone),
Email = NullIfEmpty(Email),
Street = NullIfEmpty(Street),