Files
LehrerApp/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs

838 lines
33 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using System.Collections.ObjectModel;
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 = "";
[ObservableProperty] private bool _showInactive;
[ObservableProperty] private StudentListItem? _selectedStudent;
public ObservableCollection<StudentListItem> Students { get; } = [];
public string CountSummary => $"{Students.Count} Schüler gesamt";
public StudentListViewModel(IStudentRepository students, IGroupRepository groups,
IGroupMembershipRepository memberships)
{
_students = students;
_groups = groups;
_memberships = memberships;
LoadStudents();
}
partial void OnSearchTextChanged(string value) => LoadStudents();
partial void OnShowInactiveChanged(bool value) => LoadStudents();
partial void OnSelectedStudentChanged(StudentListItem? value)
{
if (value is not null) OnNavigateToDetail?.Invoke(value.Id);
}
public void LoadStudents()
{
Students.Clear();
var all = _students.GetAll(ShowInactive);
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));
}
public Func<Task>? OnAddStudent { get; set; }
[RelayCommand]
private async Task AddStudent()
{
if (OnAddStudent is null) return;
await OnAddStudent();
LoadStudents();
}
[RelayCommand] private void Refresh() => LoadStudents();
}
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));
}
}
public partial class StudentDetailViewModel : ObservableObject
{
private readonly IStudentRepository _students;
private readonly IGroupMembershipRepository _memberships;
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly IDocumentationRepository _docs;
private readonly IExamRepository _exams;
private readonly IExamResultRepository _examResults;
private readonly IGradeRepository _grades;
private readonly IParticipationRepository _participation;
private readonly IParticipationSessionRepository _participationSessions;
private readonly AttendanceBalanceService _attendanceBalance;
private readonly PersonalDataExportService _export;
private readonly SchoolYearService _schoolYear;
[ObservableProperty] private Student? _student;
[ObservableProperty] private string _studentTitle = "";
[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; } = [];
public ObservableCollection<ContactItem> Contacts { get; } = [];
public ObservableCollection<StudentGradeHistoryGroup> GradeHistory { get; } = [];
public bool HasNoContacts => Contacts.Count == 0;
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
public Action<ContactItem>? OnViewAddress { get; set; }
public Func<Guid, Documentation?, Task<Documentation?>>? OnEditDocumentation { get; set; }
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,
IDocumentationRepository docs, IExamRepository exams, IExamResultRepository examResults,
IGradeRepository grades, IParticipationRepository participation,
IParticipationSessionRepository participationSessions, AttendanceBalanceService attendanceBalance,
PersonalDataExportService export, SchoolYearService schoolYear)
{
_students = students; _memberships = memberships;
_groups = groups; _subjects = subjects; _docs = docs;
_exams = exams; _examResults = examResults; _grades = grades;
_participation = participation; _participationSessions = participationSessions;
_attendanceBalance = attendanceBalance; _export = export; _schoolYear = schoolYear;
}
public void LoadStudent(Guid id)
{
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();
foreach (var membership in _memberships.GetByStudent(Student.Id))
{
var g = _groups.GetById(membership.GroupId);
if (g is null) continue;
var subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
GroupMemberships.Add(new() { SchoolYear = g.SchoolYear, GroupName = g.Name, Subject = subject });
var historyGroup = BuildGradeHistory(g, subject);
if (historyGroup is not null) GradeHistory.Add(historyGroup);
}
LoadContacts();
LoadDocumentation();
LoadAttendanceBalance();
}
// ── Dokumentation (5.1) ───────────────────────────────────────────────────
private void LoadDocumentation()
{
if (Student is null) return;
Documentation.Clear();
foreach (var d in _docs.GetByStudent(Student.Id))
Documentation.Add(new DocumentationItem(d));
}
[RelayCommand]
private async Task AddDocumentation()
{
if (Student is null || OnEditDocumentation is null) return;
var result = await OnEditDocumentation(Student.Id, null);
if (result is null) return;
_docs.Save(result);
LoadDocumentation();
}
[RelayCommand]
private async Task EditDocumentation(DocumentationItem? item)
{
if (Student is null || item is null || OnEditDocumentation is null) return;
var result = await OnEditDocumentation(Student.Id, item.Model);
if (result is null) return;
_docs.Save(result);
LoadDocumentation();
}
[RelayCommand]
private async Task DeleteDocumentation(DocumentationItem? item)
{
if (item is null) return;
if (OnConfirmDeleteDocumentation is not null && !await OnConfirmDeleteDocumentation(item)) return;
_docs.Delete(item.Model.Id);
LoadDocumentation();
}
[RelayCommand]
private async Task ConductParentCall(DocumentationItem? item)
{
if (Student is null || item is null || OnConductParentCall is null) return;
var result = await OnConductParentCall(item.Model, Student.FullName);
if (result is null) return;
_docs.Save(result);
LoadDocumentation();
}
// ── Fehlzeitenbilanz (5.2.2/5.2.3) ────────────────────────────────────────
private void LoadAttendanceBalance()
{
if (Student is null) return;
var schoolYear = _schoolYear.CurrentSchoolYear();
var from = _schoolYear.SchoolYearStart(schoolYear);
var to = _schoolYear.SchoolYearEnd(schoolYear);
var entries = _participation.GetByStudent(Student.Id)
.Select(e => _participationSessions.GetById(e.SessionId) is { } session
? ((DateOnly?)session.Date, e.Attendance) : (null, e.Attendance))
.Where(t => t.Item1.HasValue)
.Select(t => (t.Item1!.Value, t.Attendance));
Attendance = _attendanceBalance.Calculate(entries, from, to);
}
// ── Datenauskunft (5.4.3) ─────────────────────────────────────────────────
[RelayCommand]
private async Task ExportPersonalData()
{
if (Student is null || OnSaveExportFile is null) return;
var json = _export.ExportAsJson(Student.Id);
await OnSaveExportFile(json);
ExportStatus = "Datenauskunft exportiert.";
}
// ── Notenentwicklung (2.5) ────────────────────────────────────────────────
private StudentGradeHistoryGroup? BuildGradeHistory(LearningGroup g, string subject)
{
var label = string.IsNullOrEmpty(subject) ? g.Name : $"{g.Name} · {subject}";
var entries = new List<(DateOnly Date, string PointLabel, string Value)>();
foreach (var exam in _exams.GetByGroup(g.Id))
{
var result = _examResults.GetByExamAndStudent(exam.Id, Student!.Id);
if (result is null || result.Absent || result.Grade is null) continue;
entries.Add((exam.Date, exam.Title, result.Grade));
}
foreach (var grade in _grades.GetByStudentAndGroup(Student!.Id, g.Id))
entries.Add((grade.Date, GradeCategoryDisplay.Label(grade.Category), grade.Value));
var ordered = entries.OrderBy(e => e.Date).ToList();
if (ordered.Count == 0) return null;
var historyGroup = new StudentGradeHistoryGroup(label);
int? previousNoteEquivalent = null;
foreach (var e in ordered)
{
int? noteEquivalent = int.TryParse(e.Value, out var raw)
? (g.GradingSystem == GradingSystem.Grades1To6 ? raw : int.Parse(PointsNoteMapping.PointsToNote(raw)))
: null;
var isFailing = noteEquivalent is >= 5;
var isDrop = noteEquivalent.HasValue && previousNoteEquivalent.HasValue
&& noteEquivalent.Value - previousNoteEquivalent.Value >= 1;
var warnings = new List<string>();
if (isDrop) warnings.Add("Abfall um ≥ 1 Note");
if (isFailing) warnings.Add("Versetzungsgefährdung");
historyGroup.Points.Add(new GradeHistoryPoint(e.Date, e.PointLabel, e.Value,
noteEquivalent, warnings.Count > 0, string.Join(" · ", warnings)));
if (noteEquivalent.HasValue) previousNoteEquivalent = noteEquivalent;
}
return historyGroup;
}
[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;
}
private void LoadContacts(Guid? selectedId = null)
{
Contacts.Clear();
if (Student is null) return;
foreach (var c in Student.Contacts
.OrderBy(c => c.InvalidSince.HasValue)
.ThenBy(c => c.Name))
Contacts.Add(new ContactItem(c));
OnPropertyChanged(nameof(HasNoContacts));
SelectedContact = selectedId.HasValue
? Contacts.FirstOrDefault(c => c.Id == selectedId.Value)
: null;
}
[RelayCommand]
private async Task AddContact()
{
if (Student is null || OnEditContact is null) return;
var contact = await OnEditContact(null);
if (contact is null) return;
Student.Contacts.Add(contact);
_students.Save(Student);
LoadContacts(contact.Id);
}
[RelayCommand(CanExecute = nameof(HasSelectedContact))]
private async Task EditContact()
{
if (Student is null || SelectedContact is null || OnEditContact is null) return;
var edited = await OnEditContact(SelectedContact.Model);
if (edited is null) return;
var index = Student.Contacts.FindIndex(c => c.Id == edited.Id);
if (index >= 0) Student.Contacts[index] = edited;
_students.Save(Student);
LoadContacts(edited.Id);
}
[RelayCommand(CanExecute = nameof(CanViewSelectedAddress))]
private void ViewSelectedAddress()
{
if (SelectedContact is not null) OnViewAddress?.Invoke(SelectedContact);
}
partial void OnSelectedContactChanged(ContactItem? value)
{
EditContactCommand.NotifyCanExecuteChanged();
ViewSelectedAddressCommand.NotifyCanExecuteChanged();
}
private bool HasSelectedContact() => SelectedContact is not null;
private bool CanViewSelectedAddress() => SelectedContact?.HasAddress == true;
}
public class GroupMembershipEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
// ── Notenentwicklung (2.5) ──────────────────────────────────────────────────
public class StudentGradeHistoryGroup(string label)
{
public string Label { get; } = label;
public ObservableCollection<GradeHistoryPoint> Points { get; } = [];
public bool HasWarnings => Points.Any(p => p.IsWarning);
}
public class GradeHistoryPoint
{
public string DateDisplay { get; }
public string Label { get; }
public string Value { get; }
public double BarHeight { get; }
public bool IsWarning { get; }
public string WarningText { get; }
public string TooltipText { get; }
public GradeHistoryPoint(DateOnly date, string label, string value, int? noteEquivalent,
bool isWarning, string warningText)
{
DateDisplay = date.ToString("dd.MM.", CultureInfo.InvariantCulture);
Label = label;
Value = value;
IsWarning = isWarning;
WarningText = warningText;
// Balkenhöhe nach Notenqualität (1 = beste Note) auf 6..60px, sonst neutrale Mindesthöhe.
BarHeight = noteEquivalent.HasValue
? 6 + Math.Clamp((6 - noteEquivalent.Value) / 5.0, 0, 1) * 54
: 6;
TooltipText = warningText.Length > 0
? $"{date:dd.MM.yyyy} · {label}: {value} ⚠ {warningText}"
: $"{date:dd.MM.yyyy} · {label}: {value}";
}
}
public class ContactItem
{
public Contact Model { get; }
public Guid Id => Model.Id;
public string Name { get; }
public string Relation { get; }
public string LetterSalutation { get; }
public string? Phone { get; }
public string? MobilePhone { get; }
public string? Email { get; }
public string Address { get; }
public bool HasPhone => !string.IsNullOrEmpty(Phone) || !string.IsNullOrEmpty(MobilePhone);
public bool HasEmail => !string.IsNullOrEmpty(Email);
public bool HasAddress => !string.IsNullOrEmpty(Address);
public bool IsInvalid => Model.InvalidSince.HasValue;
public bool IsValid => !IsInvalid;
public string PhoneDisplay => string.Join(" · ", new[] { MobilePhone, Phone }
.Where(value => !string.IsNullOrWhiteSpace(value)));
public string EmailDisplay => Email ?? "";
public string StatusText => IsInvalid
? $"Ungültig seit {Model.InvalidSince:dd.MM.yyyy} · {InvalidReasonText(Model.InvalidReason)}"
: "Aktuell";
public IRelayCommand CallCommand { get; }
public IRelayCommand MailCommand { get; }
public ContactItem(Contact c)
{
Model = c;
Name = c.Name;
Relation = c.Relation;
LetterSalutation = c.LetterSalutation ?? "";
Phone = c.Phone;
MobilePhone = c.MobilePhone;
Email = c.Email;
Address = FormatAddress(c);
CallCommand = new RelayCommand(() => OpenUri($"tel:{MobilePhone ?? Phone}"), () => HasPhone);
MailCommand = new RelayCommand(() => OpenUri($"mailto:{Email}"), () => HasEmail);
}
public static string InvalidReasonText(ContactInvalidReason? reason) => reason switch
{
ContactInvalidReason.Moved => "Umzug",
ContactInvalidReason.NewPhoneNumber => "Telefonnummer neu",
ContactInvalidReason.LostCustody => "Sorgerecht verloren",
ContactInvalidReason.NewEmailAddress => "E-Mail-Adresse neu",
ContactInvalidReason.NoLongerResponsible => "Nicht mehr zuständig",
ContactInvalidReason.Other => "Sonstiges",
_ => "Kein Grund angegeben",
};
private static string FormatAddress(Contact c)
{
var cityLine = string.Join(" ", new[] { c.PostalCode, c.City }
.Where(value => !string.IsNullOrWhiteSpace(value)));
return string.Join(Environment.NewLine, new[] { c.Street, cityLine }
.Where(value => !string.IsNullOrWhiteSpace(value)));
}
private static void OpenUri(string uri) =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(uri) { UseShellExecute = true });
}
// ── Dialog: Schüler anlegen ───────────────────────────────────────────────────
public partial class AddStudentDialogViewModel : ObservableObject
{
private readonly IStudentRepository _students;
[ObservableProperty] private string _firstName = "";
[ObservableProperty] private string _lastName = "";
[ObservableProperty] private string _dateOfBirthText = "";
[ObservableProperty] private string _selectedGender = "";
[ObservableProperty] private string _notes = "";
[ObservableProperty] private string _lastNameError = "";
[ObservableProperty] private string _firstNameError = "";
[ObservableProperty] private string _dateOfBirthError = "";
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; } = [];
public Student? Result { get; private set; }
public AddStudentDialogViewModel(IStudentRepository students) => _students = students;
[RelayCommand]
private void AddContact() =>
Contacts.Add(new ContactEntryViewModel(this) { Relation = "Elternteil" });
public void RemoveContact(ContactEntryViewModel c) => Contacts.Remove(c);
[RelayCommand]
private void Save()
{
LastNameError = ""; FirstNameError = ""; DateOfBirthError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(LastName)) { LastNameError = "Nachname erforderlich."; valid = false; }
if (string.IsNullOrWhiteSpace(FirstName)) { FirstNameError = "Vorname erforderlich."; valid = false; }
DateOnly? dob = null;
if (!string.IsNullOrWhiteSpace(DateOfBirthText))
{
if (!DateOnly.TryParseExact(DateOfBirthText, "dd.MM.yyyy", null,
System.Globalization.DateTimeStyles.None, out var d))
{
DateOfBirthError = "Format TT.MM.JJJJ."; valid = false;
}
else dob = d;
}
if (!valid) return;
Result = new Student
{
FirstName = FirstName.Trim(),
LastName = LastName.Trim(),
DateOfBirth = dob,
Gender = GenderAvatarDisplay.FromOption(SelectedGender),
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
Contacts = Contacts.Select(c => c.ToModel()).Where(c => !string.IsNullOrWhiteSpace(c.Name)).ToList(),
};
_students.Save(Result);
}
}
public partial class ContactEntryViewModel : ObservableObject
{
private readonly AddStudentDialogViewModel _parent;
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _relation = "";
[ObservableProperty] private string _letterSalutation = "";
[ObservableProperty] private string _phone = "";
[ObservableProperty] private string _mobilePhone = "";
[ObservableProperty] private string _email = "";
[ObservableProperty] private string _street = "";
[ObservableProperty] private string _postalCode = "";
[ObservableProperty] private string _city = "";
public ContactEntryViewModel(AddStudentDialogViewModel parent) => _parent = parent;
[RelayCommand] private void Remove() => _parent.RemoveContact(this);
public Contact ToModel() => new()
{
Name = Name.Trim(),
Relation = Relation.Trim(),
LetterSalutation = string.IsNullOrWhiteSpace(LetterSalutation) ? null : LetterSalutation.Trim(),
Phone = string.IsNullOrWhiteSpace(Phone) ? null : Phone.Trim(),
MobilePhone = string.IsNullOrWhiteSpace(MobilePhone) ? null : MobilePhone.Trim(),
Email = string.IsNullOrWhiteSpace(Email) ? null : Email.Trim(),
Street = string.IsNullOrWhiteSpace(Street) ? null : Street.Trim(),
PostalCode = string.IsNullOrWhiteSpace(PostalCode) ? null : PostalCode.Trim(),
City = string.IsNullOrWhiteSpace(City) ? null : City.Trim(),
};
}
public partial class ContactEditDialogViewModel : ObservableObject
{
private readonly Contact? _source;
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _relation = "Elternteil";
[ObservableProperty] private string _letterSalutation = "";
[ObservableProperty] private string _phone = "";
[ObservableProperty] private string _mobilePhone = "";
[ObservableProperty] private string _email = "";
[ObservableProperty] private string _street = "";
[ObservableProperty] private string _postalCode = "";
[ObservableProperty] private string _city = "";
[ObservableProperty] private bool _isInvalid;
[ObservableProperty] private string _invalidSinceText = "";
[ObservableProperty] private string _selectedInvalidReason = "";
[ObservableProperty] private string _invalidReasonDetails = "";
[ObservableProperty] private string _nameError = "";
[ObservableProperty] private string _invalidSinceError = "";
public string DialogTitle => _source is null ? "Kontakt anlegen" : "Kontakt bearbeiten";
public List<string> RelationPresets { get; } =
["Schüler/in", "Mutter", "Vater", "Elternteil", "Erziehungsberechtigte/r", "Sonstige"];
public List<string> InvalidReasonOptions { get; } =
["Umzug", "Telefonnummer neu", "Sorgerecht verloren", "E-Mail-Adresse neu",
"Nicht mehr zuständig", "Sonstiges"];
public Contact? Result { get; private set; }
public ContactEditDialogViewModel(Contact? source)
{
_source = source;
if (source is null) return;
Name = source.Name;
Relation = source.Relation;
LetterSalutation = source.LetterSalutation ?? "";
Phone = source.Phone ?? "";
MobilePhone = source.MobilePhone ?? "";
Email = source.Email ?? "";
Street = source.Street ?? "";
PostalCode = source.PostalCode ?? "";
City = source.City ?? "";
IsInvalid = source.InvalidSince.HasValue;
InvalidSinceText = source.InvalidSince?.ToString("dd.MM.yyyy") ?? "";
SelectedInvalidReason = ContactItem.InvalidReasonText(source.InvalidReason);
InvalidReasonDetails = source.InvalidReasonDetails ?? "";
}
partial void OnIsInvalidChanged(bool value)
{
if (value && string.IsNullOrWhiteSpace(InvalidSinceText))
InvalidSinceText = DateTime.Today.ToString("dd.MM.yyyy");
if (value && string.IsNullOrWhiteSpace(SelectedInvalidReason))
SelectedInvalidReason = InvalidReasonOptions[0];
}
[RelayCommand]
private void Save()
{
NameError = ""; InvalidSinceError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Name))
{
NameError = "Name erforderlich.";
valid = false;
}
DateOnly? invalidSince = null;
if (IsInvalid)
{
if (!DateOnly.TryParseExact(InvalidSinceText, "dd.MM.yyyy", null,
System.Globalization.DateTimeStyles.None, out var parsedDate))
{
InvalidSinceError = "Format TT.MM.JJJJ.";
valid = false;
}
else invalidSince = parsedDate;
}
if (!valid) return;
Result = new Contact
{
Id = _source?.Id ?? Guid.NewGuid(),
Name = Name.Trim(),
Relation = Relation.Trim(),
LetterSalutation = NullIfEmpty(LetterSalutation),
Phone = NullIfEmpty(Phone),
MobilePhone = NullIfEmpty(MobilePhone),
Email = NullIfEmpty(Email),
Street = NullIfEmpty(Street),
PostalCode = NullIfEmpty(PostalCode),
City = NullIfEmpty(City),
InvalidSince = invalidSince,
InvalidReason = IsInvalid ? ParseInvalidReason(SelectedInvalidReason) : null,
InvalidReasonDetails = IsInvalid ? NullIfEmpty(InvalidReasonDetails) : null,
};
}
private static string? NullIfEmpty(string value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static ContactInvalidReason ParseInvalidReason(string value) => value switch
{
"Umzug" => ContactInvalidReason.Moved,
"Telefonnummer neu" => ContactInvalidReason.NewPhoneNumber,
"Sorgerecht verloren" => ContactInvalidReason.LostCustody,
"E-Mail-Adresse neu" => ContactInvalidReason.NewEmailAddress,
"Nicht mehr zuständig" => ContactInvalidReason.NoLongerResponsible,
_ => ContactInvalidReason.Other,
};
}