Files
LehrerApp/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs
T
2026-08-12 17:45:02 +02:00

452 lines
18 KiB
C#
Raw 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 System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Students;
public partial class StudentListViewModel : ObservableObject
{
private readonly IStudentRepository _students;
public Action<Guid>? OnNavigateToDetail { get; set; }
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private bool _showInactive;
[ObservableProperty] private StudentListItem? _selectedStudent;
public ObservableCollection<StudentListItem> 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<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 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<GroupMembershipEntry> GroupMemberships { get; } = [];
public ObservableCollection<DocEntry> Documentation { get; } = [];
public ObservableCollection<ContactItem> Contacts { get; } = [];
public bool HasNoContacts => Contacts.Count == 0;
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
public Action<ContactItem>? 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<string> GenderOptions { get; } = ["", "M männlich", "W weiblich", "D divers"];
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()
{
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<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;
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,
};
}