using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using System.Collections.ObjectModel; namespace LehrerApp.Desktop.ViewModels.Students; public partial class StudentListViewModel : ObservableObject { private readonly IStudentRepository _students; public Action? OnNavigateToDetail { get; set; } [ObservableProperty] private string _searchText = ""; [ObservableProperty] private bool _showInactive; [ObservableProperty] private StudentListItem? _selectedStudent; public ObservableCollection Students { get; } = []; public StudentListViewModel(IStudentRepository students) { _students = students; 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 f = string.IsNullOrWhiteSpace(SearchText) ? all : all.Where(s => s.LastName.Contains(SearchText, StringComparison.OrdinalIgnoreCase) || s.FirstName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)); foreach (var s in f) Students.Add(new StudentListItem(s)); } 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 StudentListItem(Student s) { Id = s.Id; FullName = s.FullName; DateOfBirth = s.DateOfBirth?.ToString("dd.MM.yyyy") ?? ""; } } 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; [ObservableProperty] private Student? _student; [ObservableProperty] private string _studentTitle = ""; [ObservableProperty] private bool _isEditing; [ObservableProperty] private string _editFirstName = ""; [ObservableProperty] private string _editLastName = ""; [ObservableProperty] private ContactItem? _selectedContact; public ObservableCollection GroupMemberships { get; } = []; public ObservableCollection Documentation { get; } = []; public ObservableCollection Contacts { get; } = []; public bool HasNoContacts => Contacts.Count == 0; public Func>? OnEditContact { get; set; } public Action? OnViewAddress { get; set; } public StudentDetailViewModel(IStudentRepository students, IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects, IDocumentationRepository docs) { _students = students; _memberships = memberships; _groups = groups; _subjects = subjects; _docs = docs; } public void LoadStudent(Guid id) { Student = _students.GetById(id); if (Student is null) return; StudentTitle = Student.FullName; EditFirstName = Student.FirstName; EditLastName = Student.LastName; GroupMemberships.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 }); } LoadContacts(); Documentation.Clear(); foreach (var d in _docs.GetByStudent(Student.Id)) Documentation.Add(new() { Date = d.Date.ToString("dd.MM.yyyy"), Title = d.Title, TypeLabel = d.Type switch { DocumentationType.Conversation => "Gespräch", DocumentationType.Incident => "Vorkommnis", DocumentationType.SupportPlan => "Förderplan", DocumentationType.Absence => "Fehlzeit", _ => "", }, IsConfidential = d.IsConfidential }); } [RelayCommand] private void StartEdit() => IsEditing = true; [RelayCommand] private void CancelEdit() { if (Student is null) return; EditFirstName = Student.FirstName; EditLastName = Student.LastName; IsEditing = false; } [RelayCommand] private void SaveEdit() { if (Student is null) return; Student.FirstName = EditFirstName; Student.LastName = EditLastName; _students.Save(Student); StudentTitle = Student.FullName; 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; } = ""; } public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } } public class ContactItem { public Contact Model { get; } public Guid Id => Model.Id; public string Name { get; } public string Relation { get; } public string? Phone { get; } public string? Email { get; } public string Address { get; } public bool HasPhone => !string.IsNullOrEmpty(Phone); 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 => Phone ?? ""; 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; Phone = c.Phone; Email = c.Email; Address = FormatAddress(c); CallCommand = new RelayCommand(() => OpenUri($"tel:{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 _validationMessage = ""; public List GenderOptions { get; } = ["", "M – männlich", "W – weiblich", "D – divers"]; 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() { if (string.IsNullOrWhiteSpace(LastName)) { ValidationMessage = "Nachname erforderlich."; return; } if (string.IsNullOrWhiteSpace(FirstName)) { ValidationMessage = "Vorname erforderlich."; return; } DateOnly? dob = null; if (!string.IsNullOrWhiteSpace(DateOfBirthText)) { if (!DateOnly.TryParseExact(DateOfBirthText, "dd.MM.yyyy", null, System.Globalization.DateTimeStyles.None, out var d)) { ValidationMessage = "Geburtsdatum im Format TT.MM.JJJJ."; return; } dob = d; } Result = new Student { 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, }, 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 _phone = ""; [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(), Phone = string.IsNullOrWhiteSpace(Phone) ? null : Phone.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 _phone = ""; [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 _validationMessage = ""; 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; Phone = source.Phone ?? ""; 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() { if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Name erforderlich."; return; } DateOnly? invalidSince = null; if (IsInvalid) { if (!DateOnly.TryParseExact(InvalidSinceText, "dd.MM.yyyy", null, System.Globalization.DateTimeStyles.None, out var parsedDate)) { ValidationMessage = "Ungültig seit im Format TT.MM.JJJJ angeben."; return; } invalidSince = parsedDate; } Result = new Contact { Id = _source?.Id ?? Guid.NewGuid(), Name = Name.Trim(), Relation = Relation.Trim(), Phone = NullIfEmpty(Phone), 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, }; }