Mitarbeit bewerten begonnen, Schülerdaten, Gruppen

This commit is contained in:
2026-06-23 10:12:52 +02:00
parent fdbaa9ef4d
commit b2211270b4
32 changed files with 1968 additions and 49 deletions
@@ -40,8 +40,17 @@ public partial class StudentListViewModel : ObservableObject
foreach (var s in f) Students.Add(new StudentListItem(s));
}
[RelayCommand] private void AddStudent() { /* TODO */ }
[RelayCommand] private void Refresh() => LoadStudents();
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
@@ -68,9 +77,14 @@ public partial class StudentDetailViewModel : ObservableObject
[ObservableProperty] private bool _isEditing;
[ObservableProperty] private string _editFirstName = "";
[ObservableProperty] private string _editLastName = "";
[ObservableProperty] private ContactItem? _selectedContact;
public ObservableCollection<EnrollmentEntry> Enrollments { get; } = [];
public ObservableCollection<DocEntry> Documentation { get; } = [];
public ObservableCollection<EnrollmentEntry> Enrollments { 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,
IEnrollmentRepository enrollments, IGroupRepository groups,
@@ -96,6 +110,8 @@ public partial class StudentDetailViewModel : ObservableObject
Enrollments.Add(new() { SchoolYear = e.SchoolYear, GroupName = g.Name, Subject = g.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,
@@ -125,7 +141,309 @@ public partial class StudentDetailViewModel : ObservableObject
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 EnrollmentEntry { 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,
};
}