From b2211270b4b2d46a0d519d393cb1d42e568d4909 Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Tue, 23 Jun 2026 10:12:52 +0200 Subject: [PATCH] =?UTF-8?q?Mitarbeit=20bewerten=20begonnen,=20Sch=C3=BCler?= =?UTF-8?q?daten,=20Gruppen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LehrerApp.Core/Interfaces/IRepositories.cs | 23 + LehrerApp.Core/Models/Participation.cs | 54 ++ LehrerApp.Core/Models/Student.cs | 18 + LehrerApp.Data/LiteDbContext.cs | 12 +- .../Repositories/AllRepositories.cs | 46 ++ LehrerApp.Desktop/App.axaml.cs | 12 +- LehrerApp.Desktop/AppBootstrapper.cs | 11 +- .../ViewModels/Groups/GroupViewModels.cs | 122 ++++- .../Groups/ParticipationViewModels.cs | 463 ++++++++++++++++++ .../ViewModels/MainWindowViewModel.cs | 14 +- .../ViewModels/Students/StudentViewModels.cs | 326 +++++++++++- .../Views/Groups/AddSessionDialog.axaml | 33 ++ .../Views/Groups/AddSessionDialog.axaml.cs | 27 + .../Groups/AddStudentToGroupDialog.axaml | 43 ++ .../Groups/AddStudentToGroupDialog.axaml.cs | 27 + .../Views/Groups/GroupDetailView.axaml | 18 +- .../Views/Groups/GroupDetailView.axaml.cs | 34 +- .../Views/Groups/GroupListView.axaml | 9 +- .../ParticipationQuickInputDialog.axaml | 57 +++ .../ParticipationQuickInputDialog.axaml.cs | 54 ++ .../Views/Groups/ParticipationTabView.axaml | 58 +++ .../Groups/ParticipationTabView.axaml.cs | 121 +++++ LehrerApp.Desktop/Views/MainWindow.axaml | 2 +- .../Views/Students/AddStudentDialog.axaml | 115 +++++ .../Views/Students/AddStudentDialog.axaml.cs | 27 + .../Views/Students/AddressViewerWindow.axaml | 32 ++ .../Students/AddressViewerWindow.axaml.cs | 18 + .../Views/Students/ContactEditDialog.axaml | 87 ++++ .../Views/Students/ContactEditDialog.axaml.cs | 25 + .../Views/Students/StudentDetailView.axaml | 68 +++ .../Views/Students/StudentDetailView.axaml.cs | 47 +- .../Views/SyncStatusBar.axaml.cs | 14 +- 32 files changed, 1968 insertions(+), 49 deletions(-) create mode 100644 LehrerApp.Core/Models/Participation.cs create mode 100644 LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs create mode 100644 LehrerApp.Desktop/Views/Groups/AddSessionDialog.axaml create mode 100644 LehrerApp.Desktop/Views/Groups/AddSessionDialog.axaml.cs create mode 100644 LehrerApp.Desktop/Views/Groups/AddStudentToGroupDialog.axaml create mode 100644 LehrerApp.Desktop/Views/Groups/AddStudentToGroupDialog.axaml.cs create mode 100644 LehrerApp.Desktop/Views/Groups/ParticipationQuickInputDialog.axaml create mode 100644 LehrerApp.Desktop/Views/Groups/ParticipationQuickInputDialog.axaml.cs create mode 100644 LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml create mode 100644 LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml.cs create mode 100644 LehrerApp.Desktop/Views/Students/AddStudentDialog.axaml create mode 100644 LehrerApp.Desktop/Views/Students/AddStudentDialog.axaml.cs create mode 100644 LehrerApp.Desktop/Views/Students/AddressViewerWindow.axaml create mode 100644 LehrerApp.Desktop/Views/Students/AddressViewerWindow.axaml.cs create mode 100644 LehrerApp.Desktop/Views/Students/ContactEditDialog.axaml create mode 100644 LehrerApp.Desktop/Views/Students/ContactEditDialog.axaml.cs diff --git a/LehrerApp.Core/Interfaces/IRepositories.cs b/LehrerApp.Core/Interfaces/IRepositories.cs index 9005d82..e227bc1 100644 --- a/LehrerApp.Core/Interfaces/IRepositories.cs +++ b/LehrerApp.Core/Interfaces/IRepositories.cs @@ -85,3 +85,26 @@ public interface ITimeEntryRepository void Save(TimeEntry entry); void Delete(Guid id); } +public interface IParticipationSessionRepository +{ + List GetByGroup(Guid groupId); + ParticipationSession? GetById(Guid id); + void Save(ParticipationSession session); + void Delete(Guid id); +} +public interface IParticipationRepository +{ + List GetBySession(Guid sessionId); + List GetByStudent(Guid studentId); + ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId); + void Save(ParticipationEntry entry); + void SaveMany(List entries); + void DeleteBySession(Guid sessionId); +} +public interface IParticipationAspectRepository +{ + List GetDefaults(); + List GetByGroup(Guid groupId); + void Save(ParticipationAspect aspect); + void Delete(Guid id); +} diff --git a/LehrerApp.Core/Models/Participation.cs b/LehrerApp.Core/Models/Participation.cs new file mode 100644 index 0000000..fb03436 --- /dev/null +++ b/LehrerApp.Core/Models/Participation.cs @@ -0,0 +1,54 @@ +namespace LehrerApp.Core.Models; + +public class ParticipationSession +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid GroupId { get; set; } + public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today); + public Guid? LessonId { get; set; } + public string? Comment { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} + +public class ParticipationEntry +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid SessionId { get; set; } + public Guid GroupId { get; set; } + public Guid StudentId { get; set; } + public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today); + public List Ratings { get; set; } = []; + public string? Note { get; set; } + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} + +public class AspectRating +{ + public string Key { get; set; } = ""; + public int Value { get; set; } +} + +public class ParticipationAspect +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid? GroupId { get; set; } + public string Key { get; set; } = ""; + public string Label { get; set; } = ""; + public AspectValueType ValueType { get; set; } = AspectValueType.Scale5; + public bool IsActive { get; set; } = true; + public int SortOrder { get; set; } + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} + +public enum AspectValueType { Scale5, Scale3, Binary, Points } + +public static class DefaultParticipationAspects +{ + public static readonly IReadOnlyList All = + [ + new() { Key = "quality", Label = "Qualität", ValueType = AspectValueType.Scale5, SortOrder = 0 }, + new() { Key = "quantity", Label = "Quantität", ValueType = AspectValueType.Scale5, SortOrder = 1 }, + new() { Key = "workphase", Label = "Arbeitsphase", ValueType = AspectValueType.Scale5, SortOrder = 2 }, + ]; +} diff --git a/LehrerApp.Core/Models/Student.cs b/LehrerApp.Core/Models/Student.cs index 4780030..6bd1481 100644 --- a/LehrerApp.Core/Models/Student.cs +++ b/LehrerApp.Core/Models/Student.cs @@ -16,9 +16,27 @@ public class Student } public class Contact { + public Guid Id { get; set; } = Guid.NewGuid(); public string Name { get; set; } = ""; public string Relation { get; set; } = ""; public string? Phone { get; set; } public string? Email { get; set; } + public string? Street { get; set; } + public string? PostalCode { get; set; } + public string? City { get; set; } + public DateOnly? InvalidSince { get; set; } + public ContactInvalidReason? InvalidReason { get; set; } + public string? InvalidReasonDetails { get; set; } } + +public enum ContactInvalidReason +{ + Moved, + NewPhoneNumber, + LostCustody, + NewEmailAddress, + NoLongerResponsible, + Other, +} + public enum Gender { M, W, D } diff --git a/LehrerApp.Data/LiteDbContext.cs b/LehrerApp.Data/LiteDbContext.cs index 3946c2b..0ba9a14 100644 --- a/LehrerApp.Data/LiteDbContext.cs +++ b/LehrerApp.Data/LiteDbContext.cs @@ -28,8 +28,11 @@ public class LiteDbContext : IDisposable public ILiteCollection Units => _db.GetCollection("units"); public ILiteCollection Lessons => _db.GetCollection("lessons"); public ILiteCollection Documentation => _db.GetCollection("documentation"); - public ILiteCollection Tasks => _db.GetCollection("tasks"); - public ILiteCollection TimeEntries => _db.GetCollection("time_entries"); + public ILiteCollection Tasks => _db.GetCollection("tasks"); + public ILiteCollection TimeEntries => _db.GetCollection("time_entries"); + public ILiteCollection ParticipationSessions => _db.GetCollection("participation_sessions"); + public ILiteCollection ParticipationEntries => _db.GetCollection("participation"); + public ILiteCollection ParticipationAspects => _db.GetCollection("participation_aspects"); public void Checkpoint() => _db.Checkpoint(); @@ -54,6 +57,11 @@ public class LiteDbContext : IDisposable Documentation.EnsureIndex(x => x.StudentId); Tasks.EnsureIndex(x => x.Status); TimeEntries.EnsureIndex(x => x.Date); + ParticipationSessions.EnsureIndex(x => x.GroupId); + ParticipationSessions.EnsureIndex(x => x.Date); + ParticipationEntries.EnsureIndex(x => x.SessionId); + ParticipationEntries.EnsureIndex(x => x.StudentId); + ParticipationAspects.EnsureIndex(x => x.GroupId); } public void Dispose() => _db.Dispose(); diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index afe2f7a..14288aa 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -133,3 +133,49 @@ public class TimeEntryRepository(LiteDbContext db) : ITimeEntryRepository public void Save(TimeEntry e) => db.TimeEntries.Upsert(e); public void Delete(Guid id) => db.TimeEntries.Delete(id); } + +public class ParticipationSessionRepository(LiteDbContext db) : IParticipationSessionRepository +{ + public List GetByGroup(Guid groupId) => + db.ParticipationSessions.Find(s => s.GroupId == groupId).OrderByDescending(s => s.Date).ToList(); + public ParticipationSession? GetById(Guid id) => db.ParticipationSessions.FindById(id); + public void Save(ParticipationSession s) { s.UpdatedAt = DateTime.UtcNow; db.ParticipationSessions.Upsert(s); } + public void Delete(Guid id) + { + db.ParticipationSessions.Delete(id); + foreach (var e in db.ParticipationEntries.Find(e => e.SessionId == id).ToList()) + db.ParticipationEntries.Delete(e.Id); + } +} + +public class ParticipationRepository(LiteDbContext db) : IParticipationRepository +{ + public List GetBySession(Guid sessionId) => + db.ParticipationEntries.Find(e => e.SessionId == sessionId).ToList(); + public List GetByStudent(Guid studentId) => + db.ParticipationEntries.Find(e => e.StudentId == studentId).ToList(); + public ParticipationEntry? GetBySessionAndStudent(Guid sessionId, Guid studentId) => + db.ParticipationEntries.FindOne(e => e.SessionId == sessionId && e.StudentId == studentId); + public void Save(ParticipationEntry e) { e.UpdatedAt = DateTime.UtcNow; db.ParticipationEntries.Upsert(e); } + public void SaveMany(List entries) + { + var now = DateTime.UtcNow; + foreach (var e in entries) e.UpdatedAt = now; + db.ParticipationEntries.Upsert(entries); + } + public void DeleteBySession(Guid sessionId) + { + foreach (var e in db.ParticipationEntries.Find(e => e.SessionId == sessionId).ToList()) + db.ParticipationEntries.Delete(e.Id); + } +} + +public class ParticipationAspectRepository(LiteDbContext db) : IParticipationAspectRepository +{ + public List GetDefaults() => + db.ParticipationAspects.Find(a => a.GroupId == null && a.IsActive).OrderBy(a => a.SortOrder).ToList(); + public List GetByGroup(Guid groupId) => + db.ParticipationAspects.Find(a => a.GroupId == groupId && a.IsActive).OrderBy(a => a.SortOrder).ToList(); + public void Save(ParticipationAspect a) { a.UpdatedAt = DateTime.UtcNow; db.ParticipationAspects.Upsert(a); } + public void Delete(Guid id) => db.ParticipationAspects.Delete(id); +} diff --git a/LehrerApp.Desktop/App.axaml.cs b/LehrerApp.Desktop/App.axaml.cs index 6772be1..2827807 100644 --- a/LehrerApp.Desktop/App.axaml.cs +++ b/LehrerApp.Desktop/App.axaml.cs @@ -40,9 +40,19 @@ public class App : Application var dash = Services.GetRequiredService(); dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id); - // StudentList → StudentDetail + // StudentList → StudentDetail + Anlegen var sl = Services.GetRequiredService(); sl.OnNavigateToDetail = id => main.NavigateToStudent(id); + sl.OnAddStudent = () => ShowAddStudentDialog(); + } + + private static async Task ShowAddStudentDialog() + { + var vm = new ViewModels.Students.AddStudentDialogViewModel( + Services.GetRequiredService()); + var dialog = new Views.Students.AddStudentDialog { DataContext = vm }; + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) + await dialog.ShowDialog(owner); } private static async void ShowAddGroupDialog(GroupListViewModel groupList) diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index 7e32192..59050ac 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -50,8 +50,11 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // ── Services ────────────────────────────────────────────────────────── services.AddSingleton(); @@ -95,13 +98,15 @@ public static class AppBootstrapper // Singleton: einmal erstellt, überall dieselbe Instanz services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(sp => + new SyncStatusViewModel(sp.GetService())); services.AddSingleton(); services.AddSingleton(); // Transient: neue Instanz pro Navigation (für Detailseiten) services.AddTransient(); services.AddTransient(); + services.AddTransient(); return services.BuildServiceProvider(); } diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index 7cbb0ea..4c7ad4b 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -21,6 +21,9 @@ public partial class GroupListViewModel : ObservableObject [ObservableProperty] private string _searchText = ""; [ObservableProperty] private GroupListItem? _selectedGroup; + public string SelectedGroupDisplayName => SelectedGroup?.DisplayName ?? ""; + public string SelectedGroupSubtitle => SelectedGroup?.Subtitle ?? ""; + public ObservableCollection SchoolYears { get; } = []; public ObservableCollection Groups { get; } = []; @@ -33,8 +36,12 @@ public partial class GroupListViewModel : ObservableObject partial void OnSelectedSchoolYearChanged(string value) => LoadGroups(); partial void OnSearchTextChanged(string value) => LoadGroups(); - partial void OnSelectedGroupChanged(GroupListItem? value) => + partial void OnSelectedGroupChanged(GroupListItem? value) + { + OnPropertyChanged(nameof(SelectedGroupDisplayName)); + OnPropertyChanged(nameof(SelectedGroupSubtitle)); NavigateToSectionCommand.NotifyCanExecuteChanged(); + } public void LoadGroups() { @@ -88,6 +95,7 @@ public partial class GroupDetailViewModel : ObservableObject { private readonly IGroupRepository _groups; private readonly IStudentRepository _students; + private readonly IEnrollmentRepository _enrollments; private readonly IExamRepository _exams; private readonly IGradeRepository _grades; @@ -96,14 +104,21 @@ public partial class GroupDetailViewModel : ObservableObject [ObservableProperty] private string _groupSubtitle = ""; [ObservableProperty] private int _studentCount; [ObservableProperty] private int _activeTabIndex = 0; + [ObservableProperty] private StudentSummary? _selectedStudent; public ObservableCollection Students { get; } = []; public ObservableCollection Exams { get; } = []; + public ParticipationTabViewModel ParticipationTab { get; } + public Func>? OnAddStudent { get; set; } + public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students, - IExamRepository exams, IGradeRepository grades) + IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades, + ParticipationTabViewModel participationTab) { - _groups = groups; _students = students; _exams = exams; _grades = grades; + _groups = groups; _students = students; _enrollments = enrollments; + _exams = exams; _grades = grades; + ParticipationTab = participationTab; } public void LoadGroup(Guid id) @@ -114,19 +129,48 @@ public partial class GroupDetailViewModel : ObservableObject GroupSubtitle = $"{Group.SchoolYear} · " + $"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " + $"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}"; + LoadStudents(); + Exams.Clear(); + foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e)); + ParticipationTab.Initialize(Group.Id, Group.SchoolYear); + } + public void LoadStudents() + { + if (Group is null) return; Students.Clear(); var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear); StudentCount = enrolled.Count; foreach (var s in enrolled) Students.Add(new StudentSummary(s)); - - Exams.Clear(); - foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e)); } - [RelayCommand] private void AddStudent() { /* TODO */ } - [RelayCommand] private void AddExam() { /* TODO */ } - [RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); } + [RelayCommand] + private async Task AddStudent() + { + if (OnAddStudent is null) return; + var confirmed = await OnAddStudent(); + if (confirmed) LoadStudents(); + } + + [RelayCommand(CanExecute = nameof(HasSelectedStudent))] + private void RemoveStudent() + { + if (Group is null || SelectedStudent is null) return; + var enrollment = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear) + .FirstOrDefault(e => e.StudentId == SelectedStudent.Id); + if (enrollment is null) return; + _enrollments.Delete(enrollment.Id); + LoadStudents(); + SelectedStudent = null; + } + + partial void OnSelectedStudentChanged(StudentSummary? value) => + RemoveStudentCommand.NotifyCanExecuteChanged(); + + private bool HasSelectedStudent() => SelectedStudent is not null; + + [RelayCommand] private void AddExam() { /* TODO */ } + [RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); } } public class StudentSummary @@ -156,6 +200,66 @@ public class ExamSummary } } +// ── Dialog: Schüler zur Gruppe hinzufügen ───────────────────────────────────── + +public partial class AddStudentToGroupDialogViewModel : ObservableObject +{ + private readonly IStudentRepository _students; + private readonly IEnrollmentRepository _enrollments; + private readonly Guid _groupId; + private readonly string _schoolYear; + + [ObservableProperty] private string _searchText = ""; + [ObservableProperty] private StudentPickerItem? _selectedStudent; + [ObservableProperty] private string _validationMessage = ""; + + public ObservableCollection AvailableStudents { get; } = []; + public Enrollment? Result { get; private set; } + + public AddStudentToGroupDialogViewModel(IStudentRepository students, + IEnrollmentRepository enrollments, Guid groupId, string schoolYear) + { + _students = students; _enrollments = enrollments; + _groupId = groupId; _schoolYear = schoolYear; + LoadAvailableStudents(); + } + + partial void OnSearchTextChanged(string value) => LoadAvailableStudents(); + + private void LoadAvailableStudents() + { + var alreadyEnrolled = _enrollments.GetByGroupAndYear(_groupId, _schoolYear) + .Select(e => e.StudentId).ToHashSet(); + var all = _students.GetAll(); + var available = all + .Where(s => !alreadyEnrolled.Contains(s.Id)) + .Where(s => string.IsNullOrWhiteSpace(SearchText) || + s.FullName.Contains(SearchText, StringComparison.OrdinalIgnoreCase)); + AvailableStudents.Clear(); + foreach (var s in available) AvailableStudents.Add(new StudentPickerItem(s)); + } + + [RelayCommand] + private void Save() + { + if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; } + Result = new Enrollment + { + StudentId = SelectedStudent.Id, + GroupId = _groupId, + SchoolYear = _schoolYear, + }; + _enrollments.Save(Result); + } +} + +public class StudentPickerItem +{ + public Guid Id { get; } + public string FullName { get; } + public StudentPickerItem(Student s) { Id = s.Id; FullName = s.FullName; } +} + // ── Dialog: Neue Lerngruppe anlegen ────────────────────────────────────────── public partial class AddGroupDialogViewModel : ObservableObject diff --git a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs new file mode 100644 index 0000000..7de794e --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs @@ -0,0 +1,463 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LehrerApp.Core.Interfaces; +using LehrerApp.Core.Models; +using System.Collections.ObjectModel; + +namespace LehrerApp.Desktop.ViewModels.Groups; + +// ── Tab-ViewModel ───────────────────────────────────────────────────────────── + +public partial class ParticipationTabViewModel : ObservableObject +{ + private readonly IParticipationSessionRepository _sessions; + private readonly IParticipationRepository _entries; + private readonly IParticipationAspectRepository _aspects; + private readonly IStudentRepository _students; + + private Guid _groupId; + private string _schoolYear = ""; + + [ObservableProperty] private ParticipationSessionItem? _selectedSession; + [ObservableProperty] private string _noDataText = "Keine Bewertungssitzung ausgewählt."; + + public string SelectedSessionDisplay => SelectedSession?.Display ?? ""; + + public ObservableCollection Sessions { get; } = []; + public ObservableCollection StudentRows { get; } = []; + public ObservableCollection Aspects { get; } = []; + + public Func>? OnAddSession { get; set; } + public Func? OnQuickInput { get; set; } + + public ParticipationTabViewModel( + IParticipationSessionRepository sessions, + IParticipationRepository entries, + IParticipationAspectRepository aspects, + IStudentRepository students) + { + _sessions = sessions; _entries = entries; + _aspects = aspects; _students = students; + } + + public void Initialize(Guid groupId, string schoolYear) + { + _groupId = groupId; + _schoolYear = schoolYear; + LoadAspects(); + LoadSessions(); + } + + private void LoadAspects() + { + Aspects.Clear(); + var defaults = _aspects.GetDefaults(); + var specific = _aspects.GetByGroup(_groupId); + var all = defaults.Concat(specific).ToList(); + + if (!all.Any()) + { + foreach (var a in DefaultParticipationAspects.All) + Aspects.Add(new AspectColumnDef(a)); + } + else + { + foreach (var a in all) Aspects.Add(new AspectColumnDef(a)); + } + } + + public void LoadSessions() + { + Sessions.Clear(); + foreach (var s in _sessions.GetByGroup(_groupId)) + Sessions.Add(new ParticipationSessionItem(s)); + if (SelectedSession is null && Sessions.Any()) + SelectedSession = Sessions[0]; + } + + partial void OnSelectedSessionChanged(ParticipationSessionItem? value) + { + OnPropertyChanged(nameof(SelectedSessionDisplay)); + if (value is null) { StudentRows.Clear(); QuickInputCommand.NotifyCanExecuteChanged(); return; } + LoadGrid(value.Id); + } + + private void LoadGrid(Guid sessionId) + { + StudentRows.Clear(); + var students = _students.GetByGroup(_groupId, _schoolYear); + var entries = _entries.GetBySession(sessionId); + + foreach (var s in students) + { + var entry = entries.FirstOrDefault(e => e.StudentId == s.Id) + ?? new ParticipationEntry { SessionId = sessionId, GroupId = _groupId, StudentId = s.Id }; + var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList()); + row.OnRatingChanged = (studentId, key, val) => SaveRating(sessionId, studentId, key, val); + StudentRows.Add(row); + } + QuickInputCommand.NotifyCanExecuteChanged(); + } + + private void SaveRating(Guid sessionId, Guid studentId, string key, int? value) + { + var session = _sessions.GetById(sessionId); + var entry = _entries.GetBySessionAndStudent(sessionId, studentId) + ?? new ParticipationEntry + { + SessionId = sessionId, + GroupId = _groupId, + StudentId = studentId, + Date = session?.Date ?? DateOnly.FromDateTime(DateTime.Today), + }; + + var existing = entry.Ratings.FirstOrDefault(r => r.Key == key); + if (value is null) + { + if (existing is not null) entry.Ratings.Remove(existing); + } + else + { + if (existing is null) entry.Ratings.Add(new AspectRating { Key = key, Value = value.Value }); + else existing.Value = value.Value; + } + _entries.Save(entry); + } + + [RelayCommand] + private async Task AddSession() + { + if (OnAddSession is null) return; + var session = await OnAddSession(); + if (session is null) return; + session.GroupId = _groupId; + _sessions.Save(session); + LoadSessions(); + SelectedSession = Sessions.FirstOrDefault(s => s.Id == session.Id); + } + + [RelayCommand(CanExecute = nameof(CanQuickInput))] + private async Task QuickInput() + { + if (OnQuickInput is null) return; + await OnQuickInput(this); + if (SelectedSession is not null) LoadGrid(SelectedSession.Id); + } + + private bool CanQuickInput() => SelectedSession is not null && StudentRows.Count > 0; + + [RelayCommand] + private void DeleteSession() + { + if (SelectedSession is null) return; + _sessions.Delete(SelectedSession.Id); + LoadSessions(); + } + + public void SaveNote(Guid sessionId, Guid studentId, string note) + { + var entry = _entries.GetBySessionAndStudent(sessionId, studentId); + if (entry is null) return; + entry.Note = note; + _entries.Save(entry); + } +} + +// ── Zeilendaten für das Bewertungsraster ───────────────────────────────────── + +public partial class ParticipationStudentRow : ObservableObject +{ + public Guid StudentId { get; } + public string Name { get; } + + private readonly ParticipationEntry _entry; + private readonly IReadOnlyList _aspectDefs; + + public ObservableCollection Cells { get; } = []; + public Action? OnRatingChanged { get; set; } + + public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry, List aspects) + { + StudentId = id; + Name = name; + _entry = entry; + _aspectDefs = aspects; + + foreach (var a in aspects) + { + var existing = entry.Ratings.FirstOrDefault(r => r.Key == a.Key); + var cell = new RatingCell(id, a.Key, existing?.Value); + cell.OnChanged = (sid, key, val) => OnRatingChanged?.Invoke(sid, key, val); + Cells.Add(cell); + } + } + + public int? GetRating(string key) => + _entry.Ratings.FirstOrDefault(r => r.Key == key)?.Value; + + public void SetRating(string key, int? value) + { + var cell = Cells.FirstOrDefault(c => c.AspectKey == key); + cell?.SetValue(value); + OnRatingChanged?.Invoke(StudentId, key, value); + } +} + +// ── Eine Bewertungszelle ────────────────────────────────────────────────────── + +public partial class RatingCell : ObservableObject +{ + public Guid StudentId { get; } + public string AspectKey { get; } + + [ObservableProperty] private int? _value; + [ObservableProperty] private string _displayLabel = ""; + + public Action? OnChanged { get; set; } + + public RatingCell(Guid studentId, string key, int? value) + { + StudentId = studentId; + AspectKey = key; + _value = value; + UpdateLabel(); + } + + public void SetValue(int? value) + { + Value = value; + UpdateLabel(); + OnChanged?.Invoke(StudentId, AspectKey, value); + } + + [RelayCommand] + private void CycleUp() + { + var next = Value is null ? -2 : Math.Min(2, Value.Value + 1); + SetValue(next); + } + + [RelayCommand] + private void CycleDown() + { + var next = Value is null ? 2 : Math.Max(-2, Value.Value - 1); + SetValue(next); + } + + [RelayCommand] + private void Clear() => SetValue(null); + + private void UpdateLabel() => DisplayLabel = Value switch + { + 2 => "++", + 1 => "+", + 0 => "~", + -1 => "−", + -2 => "−−", + _ => "", + }; +} + +// ── Hilfsklassen ────────────────────────────────────────────────────────────── + +public class AspectColumnDef +{ + public string Key { get; } + public string Label { get; } + public AspectColumnDef(ParticipationAspect a) { Key = a.Key; Label = a.Label; } +} + +public class ParticipationSessionItem +{ + public Guid Id { get; } + public string Display { get; } + public string Comment { get; } + public DateOnly Date { get; } + + public ParticipationSessionItem(ParticipationSession s) + { + Id = s.Id; + Date = s.Date; + Comment = s.Comment ?? ""; + Display = s.Comment is { Length: > 0 } + ? $"{s.Date:dd.MM.yyyy} – {s.Comment}" + : s.Date.ToString("dd.MM.yyyy"); + } +} + +// ── Dialog: Sitzung anlegen ─────────────────────────────────────────────────── + +public partial class AddSessionDialogViewModel : ObservableObject +{ + [ObservableProperty] private DateOnly _date = DateOnly.FromDateTime(DateTime.Today); + [ObservableProperty] private string _comment = ""; + [ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy"); + [ObservableProperty] private string _validationMessage = ""; + + public ParticipationSession? Result { get; private set; } + + [RelayCommand] + private void Save() + { + if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, + System.Globalization.DateTimeStyles.None, out var date)) + { + ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; + return; + } + Result = new ParticipationSession { Date = date, Comment = Comment.Trim() }; + } +} + +// ── Dialog: Schnelleingabe ──────────────────────────────────────────────────── + +public partial class QuickInputViewModel : ObservableObject +{ + private readonly List _rows; + private readonly List _aspects; + + [ObservableProperty] private int _studentIndex; + [ObservableProperty] private int _aspectIndex; + [ObservableProperty] private string _studentName = ""; + [ObservableProperty] private string _currentAspectLabel = ""; + [ObservableProperty] private string _currentValueLabel = ""; + [ObservableProperty] private string _progressText = ""; + + public ObservableCollection AspectRows { get; } = []; + + public QuickInputViewModel(List rows, List aspects) + { + _rows = rows; + _aspects = aspects; + if (rows.Any()) ShowStudent(0); + } + + private void ShowStudent(int index) + { + if (index < 0 || index >= _rows.Count) return; + StudentIndex = index; + var row = _rows[index]; + StudentName = row.Name; + ProgressText = $"{index + 1} / {_rows.Count}"; + + AspectRows.Clear(); + foreach (var (a, i) in _aspects.Select((a, i) => (a, i))) + { + var val = row.GetRating(a.Key); + AspectRows.Add(new QuickAspectRow(i, a.Label, val, i == AspectIndex)); + } + UpdateCurrentAspect(); + } + + private void UpdateCurrentAspect() + { + if (!_aspects.Any()) return; + var safeIdx = Math.Clamp(AspectIndex, 0, _aspects.Count - 1); + CurrentAspectLabel = _aspects[safeIdx].Label; + var row = AspectRows.ElementAtOrDefault(safeIdx); + CurrentValueLabel = row?.DisplayLabel ?? ""; + foreach (var r in AspectRows) r.IsActive = r.Index == safeIdx; + } + + public void SetRatingByNumber(int num) + { + // 1=−−, 2=−, 3=~, 4=+, 5=++ + var val = num switch { 1 => -2, 2 => -1, 3 => 0, 4 => 1, 5 => 2, _ => (int?)null }; + if (val is null) return; + ApplyRating(val.Value); + } + + public void IncrementRating() + { + var cell = GetCurrentCell(); + if (cell is null) return; + var next = cell.Value is null ? -2 : Math.Min(2, cell.Value.Value + 1); + ApplyRating(next); + } + + public void DecrementRating() + { + var cell = GetCurrentCell(); + if (cell is null) return; + var next = cell.Value is null ? 2 : Math.Max(-2, cell.Value.Value - 1); + ApplyRating(next); + } + + private void ApplyRating(int val) + { + if (_rows.Count == 0 || !_aspects.Any()) return; + var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key; + _rows[StudentIndex].SetRating(key, val); + var row = AspectRows.ElementAtOrDefault(AspectIndex); + if (row is not null) + { + row.Value = val; + row.UpdateLabel(); + } + CurrentValueLabel = RatingLabel(val); + } + + public void SelectAspect(int index) + { + if (index < 0 || index >= _aspects.Count) return; + AspectIndex = index; + UpdateCurrentAspect(); + } + + public void NextAspect() + { + AspectIndex = (AspectIndex + 1) % _aspects.Count; + UpdateCurrentAspect(); + } + + public void NextStudent() + { + if (StudentIndex >= _rows.Count - 1) return; + AspectIndex = 0; + ShowStudent(StudentIndex + 1); + } + + public void PreviousStudent() + { + if (StudentIndex <= 0) return; + AspectIndex = 0; + ShowStudent(StudentIndex - 1); + } + + public bool IsLastStudent => StudentIndex >= _rows.Count - 1; + + private RatingCell? GetCurrentCell() + { + if (_rows.Count == 0 || !_aspects.Any()) return null; + var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key; + return _rows[StudentIndex].Cells.FirstOrDefault(c => c.AspectKey == key); + } + + private static string RatingLabel(int? v) => v switch + { + 2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "", + }; +} + +public partial class QuickAspectRow : ObservableObject +{ + public int Index { get; } + public string Label { get; } + [ObservableProperty] private bool _isActive; + [ObservableProperty] private string _displayLabel = ""; + public int? Value { get; set; } + + public QuickAspectRow(int index, string label, int? value, bool isActive) + { + Index = index; + Label = label; + Value = value; + IsActive = isActive; + UpdateLabel(); + } + + public void UpdateLabel() => DisplayLabel = Value switch + { + 2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "·", + }; +} diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index 55cd491..3f5fc14 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -15,10 +15,14 @@ public partial class MainWindowViewModel : ObservableObject [ObservableProperty] private NavItem _activeNavItem = NavItem.Dashboard; [ObservableProperty] private string _currentSchoolYear = ""; + public SyncStatusViewModel SyncStatus { get; } + public MainWindowViewModel(IServiceProvider services, - DashboardViewModel dashboard, SchoolYearService sy) + DashboardViewModel dashboard, SchoolYearService sy, + SyncStatusViewModel syncStatus) { _services = services; + SyncStatus = syncStatus; CurrentSchoolYear = sy.CurrentSchoolYear(); CurrentPage = dashboard; } @@ -42,11 +46,11 @@ public partial class MainWindowViewModel : ObservableObject public void NavigateToGroupDetail(Guid groupId, int initialTab = 0) { - ActiveNavItem = NavItem.Groups; - var vm = _services.GetRequiredService(); - vm.LoadGroup(groupId); + ActiveNavItem = NavItem.Groups; + var vm = _services.GetRequiredService(); vm.ActiveTabIndex = initialTab; - CurrentPage = vm; + CurrentPage = vm; // View bindet zuerst → Callbacks werden registriert + vm.LoadGroup(groupId); // dann Daten laden } public void NavigateToStudent(Guid studentId) diff --git a/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs b/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs index fab019b..3454ed0 100644 --- a/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs @@ -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? 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 Enrollments { get; } = []; - public ObservableCollection Documentation { get; } = []; + public ObservableCollection Enrollments { 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, 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 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, + }; +} diff --git a/LehrerApp.Desktop/Views/Groups/AddSessionDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddSessionDialog.axaml new file mode 100644 index 0000000..00bc3b5 --- /dev/null +++ b/LehrerApp.Desktop/Views/Groups/AddSessionDialog.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + +