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 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? OnNavigateToDetail { get; set; } [ObservableProperty] private string _searchText = ""; [ObservableProperty] private bool _showInactive; [ObservableProperty] private StudentListItem? _selectedStudent; public ObservableCollection 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 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? 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 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 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 GroupMemberships { get; } = []; public ObservableCollection Documentation { get; } = []; public ObservableCollection Contacts { get; } = []; public ObservableCollection GradeHistory { get; } = []; public bool HasNoContacts => Contacts.Count == 0; public Func>? OnEditContact { get; set; } public Action? OnViewAddress { get; set; } public Func>? OnEditDocumentation { get; set; } public Func>? OnConfirmDeleteDocumentation { get; set; } public Func? OnSaveExportFile { get; set; } public Func>? OnConductParentCall { get; set; } public Func>? OnManageStudent { get; set; } public Func? 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(); 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 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 GenderOptions => GenderAvatarDisplay.Options; public List RelationPresets { get; } = ["Schüler/in", "Mutter", "Vater", "Elternteil", "Erziehungsberechtigte/r", "Sonstige"]; public ObservableCollection 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 RelationPresets { get; } = ["Schüler/in", "Mutter", "Vater", "Elternteil", "Erziehungsberechtigte/r", "Sonstige"]; public List 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, }; }