diff --git a/LehrerApp.Core/Interfaces/IRepositories.cs b/LehrerApp.Core/Interfaces/IRepositories.cs index e227bc1..934e897 100644 --- a/LehrerApp.Core/Interfaces/IRepositories.cs +++ b/LehrerApp.Core/Interfaces/IRepositories.cs @@ -108,3 +108,19 @@ public interface IParticipationAspectRepository void Save(ParticipationAspect aspect); void Delete(Guid id); } +public interface ISubjectRepository +{ + List GetAll(); + Subject? GetById(Guid id); + Subject? GetByName(string name); + void Save(Subject subject); + void Delete(Guid id); +} +public interface ICompetencyDomainRepository +{ + List GetBySubjectAndGrade(Guid subjectId, int gradeLevel); + CompetencyDomain? GetById(Guid id); + void Save(CompetencyDomain domain); + void Delete(Guid id); + void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel); +} diff --git a/LehrerApp.Core/Models/Competency.cs b/LehrerApp.Core/Models/Competency.cs new file mode 100644 index 0000000..c46e033 --- /dev/null +++ b/LehrerApp.Core/Models/Competency.cs @@ -0,0 +1,21 @@ +namespace LehrerApp.Core.Models; + +public class CompetencyDomain +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid SubjectId { get; set; } + public int GradeLevel { get; set; } + public string Name { get; set; } = ""; + public string Code { get; set; } = ""; + public int SortOrder { get; set; } + public List Items { get; set; } = []; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} + +public class CompetencyItem +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Code { get; set; } = ""; + public string Description { get; set; } = ""; + public int SortOrder { get; set; } +} diff --git a/LehrerApp.Core/Models/LearningGroup.cs b/LehrerApp.Core/Models/LearningGroup.cs index 8e77cb6..d9dcc51 100644 --- a/LehrerApp.Core/Models/LearningGroup.cs +++ b/LehrerApp.Core/Models/LearningGroup.cs @@ -6,6 +6,7 @@ public class LearningGroup public string Name { get; set; } = ""; public GroupType Type { get; set; } public string? Subject { get; set; } + public Guid? SubjectId { get; set; } public string SchoolYear { get; set; } = ""; public int GradeLevel { get; set; } public GradingSystem GradingSystem { get; set; } @@ -15,12 +16,15 @@ public class LearningGroup } public class Enrollment { - public Guid Id { get; set; } = Guid.NewGuid(); - public Guid StudentId { get; set; } - public Guid GroupId { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); + public Guid StudentId { get; set; } + public Guid GroupId { get; set; } public string SchoolYear { get; set; } = ""; - public DateOnly EnrolledAt { get; set; } = DateOnly.FromDateTime(DateTime.Today); - public DateOnly? LeftAt { get; set; } + public DateOnly EnrolledAt { get; set; } = DateOnly.FromDateTime(DateTime.Today); + public EnrollmentPeriod Period { get; set; } = EnrollmentPeriod.FullYear; + public DateOnly? JoinedAt { get; set; } + public DateOnly? LeftAt { get; set; } } +public enum EnrollmentPeriod { FullYear, H1Only, H2Only, Custom } public enum GroupType { Class, Course } public enum GradingSystem { Grades1To6, Points0To15 } diff --git a/LehrerApp.Core/Models/Participation.cs b/LehrerApp.Core/Models/Participation.cs index fb03436..3018540 100644 --- a/LehrerApp.Core/Models/Participation.cs +++ b/LehrerApp.Core/Models/Participation.cs @@ -7,6 +7,7 @@ public class ParticipationSession public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today); public Guid? LessonId { get; set; } public string? Comment { get; set; } + public List CompetencyCodes { get; set; } = []; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } @@ -18,11 +19,18 @@ public class ParticipationEntry 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 List Ratings { get; set; } = []; + public List CompetencyRatings { get; set; } = []; public string? Note { get; set; } public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } +public class CompetencyRating +{ + public string Code { get; set; } = ""; + public int Value { get; set; } +} + public class AspectRating { public string Key { get; set; } = ""; diff --git a/LehrerApp.Core/Models/Subject.cs b/LehrerApp.Core/Models/Subject.cs new file mode 100644 index 0000000..0df4c43 --- /dev/null +++ b/LehrerApp.Core/Models/Subject.cs @@ -0,0 +1,9 @@ +namespace LehrerApp.Core.Models; + +public class Subject +{ + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = ""; + public string ShortName { get; set; } = ""; + public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; +} diff --git a/LehrerApp.Data/LiteDbContext.cs b/LehrerApp.Data/LiteDbContext.cs index 0ba9a14..e794b7c 100644 --- a/LehrerApp.Data/LiteDbContext.cs +++ b/LehrerApp.Data/LiteDbContext.cs @@ -33,6 +33,8 @@ public class LiteDbContext : IDisposable public ILiteCollection ParticipationSessions => _db.GetCollection("participation_sessions"); public ILiteCollection ParticipationEntries => _db.GetCollection("participation"); public ILiteCollection ParticipationAspects => _db.GetCollection("participation_aspects"); + public ILiteCollection Subjects => _db.GetCollection("subjects"); + public ILiteCollection CompetencyDomains => _db.GetCollection("competency_domains"); public void Checkpoint() => _db.Checkpoint(); @@ -62,6 +64,9 @@ public class LiteDbContext : IDisposable ParticipationEntries.EnsureIndex(x => x.SessionId); ParticipationEntries.EnsureIndex(x => x.StudentId); ParticipationAspects.EnsureIndex(x => x.GroupId); + Subjects.EnsureIndex(x => x.Name); + CompetencyDomains.EnsureIndex(x => x.SubjectId); + CompetencyDomains.EnsureIndex(x => x.GradeLevel); } public void Dispose() => _db.Dispose(); diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index 14288aa..329654a 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -179,3 +179,32 @@ public class ParticipationAspectRepository(LiteDbContext db) : IParticipationAsp public void Save(ParticipationAspect a) { a.UpdatedAt = DateTime.UtcNow; db.ParticipationAspects.Upsert(a); } public void Delete(Guid id) => db.ParticipationAspects.Delete(id); } + +public class SubjectRepository(LiteDbContext db) : ISubjectRepository +{ + public List GetAll() => db.Subjects.FindAll().OrderBy(s => s.Name).ToList(); + public Subject? GetById(Guid id) => db.Subjects.FindById(id); + public Subject? GetByName(string name) => + db.Subjects.FindOne(s => s.Name == name); + public void Save(Subject s) { s.UpdatedAt = DateTime.UtcNow; db.Subjects.Upsert(s); } + public void Delete(Guid id) => db.Subjects.Delete(id); +} + +public class CompetencyDomainRepository(LiteDbContext db) : ICompetencyDomainRepository +{ + public List GetBySubjectAndGrade(Guid subjectId, int gradeLevel) => + db.CompetencyDomains + .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel) + .OrderBy(d => d.SortOrder) + .ToList(); + public CompetencyDomain? GetById(Guid id) => db.CompetencyDomains.FindById(id); + public void Save(CompetencyDomain d) { d.UpdatedAt = DateTime.UtcNow; db.CompetencyDomains.Upsert(d); } + public void Delete(Guid id) => db.CompetencyDomains.Delete(id); + public void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel) + { + foreach (var d in db.CompetencyDomains + .Find(d => d.SubjectId == subjectId && d.GradeLevel == gradeLevel) + .ToList()) + db.CompetencyDomains.Delete(d.Id); + } +} diff --git a/LehrerApp.Desktop/App.axaml.cs b/LehrerApp.Desktop/App.axaml.cs index 2827807..74a5975 100644 --- a/LehrerApp.Desktop/App.axaml.cs +++ b/LehrerApp.Desktop/App.axaml.cs @@ -31,10 +31,9 @@ public class App : Application private static void WireCallbacks(MainWindowViewModel main) { - // GroupList → GroupDetail + // GroupList → GroupDetail (OnAddGroup wird in GroupListView.axaml.cs verdrahtet) var gl = Services.GetRequiredService(); gl.OnNavigateToDetail = (id, tab) => main.NavigateToGroupDetail(id, tab); - gl.OnAddGroup = () => ShowAddGroupDialog(gl); // Dashboard → GroupDetail (Chips) var dash = Services.GetRequiredService(); @@ -54,18 +53,4 @@ public class App : Application if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) await dialog.ShowDialog(owner); } - - private static async void ShowAddGroupDialog(GroupListViewModel groupList) - { - var vm = new ViewModels.Groups.AddGroupDialogViewModel( - Services.GetRequiredService(), - Services.GetRequiredService()); - var dialog = new Views.Groups.AddGroupDialog { DataContext = vm }; - - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) - { - var ok = await dialog.ShowDialog(owner); - if (ok) groupList.LoadGroups(); - } - } } diff --git a/LehrerApp.Desktop/AppBootstrapper.cs b/LehrerApp.Desktop/AppBootstrapper.cs index 59050ac..d9e587f 100644 --- a/LehrerApp.Desktop/AppBootstrapper.cs +++ b/LehrerApp.Desktop/AppBootstrapper.cs @@ -4,6 +4,7 @@ using LehrerApp.Data; using LehrerApp.Data.Repositories; using LehrerApp.Desktop.ViewModels; using LehrerApp.Desktop.ViewModels.Groups; +using LehrerApp.Desktop.ViewModels.Settings; using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Sync; using LehrerApp.Sync.Crypto; @@ -55,6 +56,8 @@ public static class AppBootstrapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); // ── Services ────────────────────────────────────────────────────────── services.AddSingleton(); @@ -107,6 +110,8 @@ public static class AppBootstrapper services.AddTransient(); services.AddTransient(); 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 4c7ad4b..65a7ab4 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -15,7 +15,7 @@ public partial class GroupListViewModel : ObservableObject private readonly SchoolYearService _sy; public Action? OnNavigateToDetail { get; set; } - public Action? OnAddGroup { get; set; } + public Func? OnAddGroup { get; set; } [ObservableProperty] private string _selectedSchoolYear = ""; [ObservableProperty] private string _searchText = ""; @@ -54,7 +54,13 @@ public partial class GroupListViewModel : ObservableObject Groups.Add(new GroupListItem(g)); } - [RelayCommand] private void AddGroup() => OnAddGroup?.Invoke(); + [RelayCommand] + private async Task AddGroup() + { + if (OnAddGroup is null) return; + await OnAddGroup(); + LoadGroups(); + } [RelayCommand] private void Refresh() => LoadGroups(); [RelayCommand(CanExecute = nameof(HasSelectedGroup))] @@ -139,9 +145,15 @@ public partial class GroupDetailViewModel : ObservableObject { if (Group is null) return; Students.Clear(); - var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear); + var enrolled = _students.GetByGroup(Group.Id, Group.SchoolYear); + var enrollments = _enrollments.GetByGroupAndYear(Group.Id, Group.SchoolYear) + .ToDictionary(e => e.StudentId); StudentCount = enrolled.Count; - foreach (var s in enrolled) Students.Add(new StudentSummary(s)); + foreach (var s in enrolled) + { + enrollments.TryGetValue(s.Id, out var enr); + Students.Add(new StudentSummary(s, enr)); + } } [RelayCommand] @@ -149,7 +161,11 @@ public partial class GroupDetailViewModel : ObservableObject { if (OnAddStudent is null) return; var confirmed = await OnAddStudent(); - if (confirmed) LoadStudents(); + if (confirmed) + { + LoadStudents(); + ParticipationTab.RefreshCurrentGrid(); + } } [RelayCommand(CanExecute = nameof(HasSelectedStudent))] @@ -162,6 +178,7 @@ public partial class GroupDetailViewModel : ObservableObject _enrollments.Delete(enrollment.Id); LoadStudents(); SelectedStudent = null; + ParticipationTab.RefreshCurrentGrid(); } partial void OnSelectedStudentChanged(StudentSummary? value) => @@ -175,9 +192,30 @@ public partial class GroupDetailViewModel : ObservableObject public class StudentSummary { - public Guid Id { get; } - public string FullName { get; } - public StudentSummary(Core.Models.Student s) { Id = s.Id; FullName = s.FullName; } + public Guid Id { get; } + public string FullName { get; } + public string PeriodLabel { get; } + + public StudentSummary(Core.Models.Student s, Enrollment? e) + { + Id = s.Id; + FullName = s.FullName; + PeriodLabel = e?.Period switch + { + EnrollmentPeriod.H1Only => "H1", + EnrollmentPeriod.H2Only => "H2", + EnrollmentPeriod.Custom => BuildCustomLabel(e), + _ => "", + }; + } + + private static string BuildCustomLabel(Enrollment e) + { + if (e.JoinedAt.HasValue && e.LeftAt.HasValue) return $"{e.JoinedAt:dd.MM.}–{e.LeftAt:dd.MM.}"; + if (e.JoinedAt.HasValue) return $"ab {e.JoinedAt:dd.MM.}"; + if (e.LeftAt.HasValue) return $"bis {e.LeftAt:dd.MM.}"; + return "Datum"; + } } public class ExamSummary @@ -204,14 +242,23 @@ public class ExamSummary public partial class AddStudentToGroupDialogViewModel : ObservableObject { - private readonly IStudentRepository _students; + private readonly IStudentRepository _students; private readonly IEnrollmentRepository _enrollments; - private readonly Guid _groupId; + private readonly Guid _groupId; private readonly string _schoolYear; - [ObservableProperty] private string _searchText = ""; + [ObservableProperty] private string _searchText = ""; [ObservableProperty] private StudentPickerItem? _selectedStudent; - [ObservableProperty] private string _validationMessage = ""; + [ObservableProperty] private string _validationMessage = ""; + [ObservableProperty] private EnrollmentPeriod _period = EnrollmentPeriod.FullYear; + [ObservableProperty] private string _joinedAtText = ""; + [ObservableProperty] private string _leftAtText = ""; + + public bool IsFullYear { get => Period == EnrollmentPeriod.FullYear; set { if (value) Period = EnrollmentPeriod.FullYear; } } + public bool IsH1Only { get => Period == EnrollmentPeriod.H1Only; set { if (value) Period = EnrollmentPeriod.H1Only; } } + public bool IsH2Only { get => Period == EnrollmentPeriod.H2Only; set { if (value) Period = EnrollmentPeriod.H2Only; } } + public bool IsCustom { get => Period == EnrollmentPeriod.Custom; set { if (value) Period = EnrollmentPeriod.Custom; } } + public bool IsCustomPeriod => Period == EnrollmentPeriod.Custom; public ObservableCollection AvailableStudents { get; } = []; public Enrollment? Result { get; private set; } @@ -224,6 +271,15 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject LoadAvailableStudents(); } + partial void OnPeriodChanged(EnrollmentPeriod value) + { + OnPropertyChanged(nameof(IsFullYear)); + OnPropertyChanged(nameof(IsH1Only)); + OnPropertyChanged(nameof(IsH2Only)); + OnPropertyChanged(nameof(IsCustom)); + OnPropertyChanged(nameof(IsCustomPeriod)); + } + partial void OnSearchTextChanged(string value) => LoadAvailableStudents(); private void LoadAvailableStudents() @@ -243,11 +299,28 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject private void Save() { if (SelectedStudent is null) { ValidationMessage = "Bitte einen Schüler auswählen."; return; } + + DateOnly? joinedAt = null, leftAt = null; + if (Period == EnrollmentPeriod.Custom) + { + if (!string.IsNullOrWhiteSpace(JoinedAtText) && + DateOnly.TryParseExact(JoinedAtText, "dd.MM.yyyy", + null, System.Globalization.DateTimeStyles.None, out var j)) + joinedAt = j; + if (!string.IsNullOrWhiteSpace(LeftAtText) && + DateOnly.TryParseExact(LeftAtText, "dd.MM.yyyy", + null, System.Globalization.DateTimeStyles.None, out var l)) + leftAt = l; + } + Result = new Enrollment { StudentId = SelectedStudent.Id, GroupId = _groupId, SchoolYear = _schoolYear, + Period = Period, + JoinedAt = joinedAt, + LeftAt = leftAt, }; _enrollments.Save(Result); } @@ -255,7 +328,7 @@ public partial class AddStudentToGroupDialogViewModel : ObservableObject public class StudentPickerItem { - public Guid Id { get; } + public Guid Id { get; } public string FullName { get; } public StudentPickerItem(Student s) { Id = s.Id; FullName = s.FullName; } } @@ -264,8 +337,10 @@ public class StudentPickerItem public partial class AddGroupDialogViewModel : ObservableObject { - private readonly IGroupRepository _groups; - private readonly SchoolYearService _sy; + private readonly IGroupRepository _groups; + private readonly ISubjectRepository _subjects; + private readonly SchoolYearService _sy; + private List _allSubjects = []; public List TypeOptions { get; } = ["Klasse", "Kurs"]; [ObservableProperty] private string _selectedTypeName = "Kurs"; @@ -289,12 +364,15 @@ public partial class AddGroupDialogViewModel : ObservableObject [ObservableProperty] private string _validationMessage = ""; public List SchoolYears { get; } + public List KnownSubjectNames { get; private set; } = []; public LearningGroup? Result { get; private set; } - public AddGroupDialogViewModel(IGroupRepository groups, SchoolYearService sy) + public AddGroupDialogViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy) { - _groups = groups; _sy = sy; - SchoolYears = sy.RecentSchoolYears(3); + _groups = groups; _subjects = subjects; _sy = sy; + _allSubjects = subjects.GetAll(); + KnownSubjectNames = _allSubjects.Select(s => s.Name).ToList(); + SchoolYears = sy.RecentSchoolYears(3); SelectedSchoolYear = sy.CurrentSchoolYear(); PropertyChanged += (_, e) => { @@ -309,10 +387,31 @@ public partial class AddGroupDialogViewModel : ObservableObject { if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; } if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 1–13."; return; } + + string? subjectName = IsKurs && !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null; + Guid? subjectId = null; + + if (subjectName is not null) + { + var existing = _allSubjects.FirstOrDefault( + s => s.Name.Equals(subjectName, StringComparison.OrdinalIgnoreCase)); + if (existing is not null) + { + subjectId = existing.Id; + } + else + { + var newSubject = new Subject { Name = subjectName }; + _subjects.Save(newSubject); + subjectId = newSubject.Id; + } + } + Result = new LearningGroup { Name = Name.Trim(), - Subject = IsKurs && !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null, + Subject = subjectName, + SubjectId = subjectId, Type = IsKurs ? GroupType.Course : GroupType.Class, GradeLevel = GradeLevel, GradingSystem = GradingSystem, diff --git a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs index 7de794e..ba25e04 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs @@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; using System.Collections.ObjectModel; +using System.Collections.Generic; namespace LehrerApp.Desktop.ViewModels.Groups; @@ -14,18 +15,29 @@ public partial class ParticipationTabViewModel : ObservableObject private readonly IParticipationRepository _entries; private readonly IParticipationAspectRepository _aspects; private readonly IStudentRepository _students; + private readonly IEnrollmentRepository _enrollments; + private readonly IGroupRepository _groups; + private readonly ICompetencyDomainRepository _competencyDomains; private Guid _groupId; private string _schoolYear = ""; + private Guid? _subjectId; + private int _gradeLevel; [ObservableProperty] private ParticipationSessionItem? _selectedSession; [ObservableProperty] private string _noDataText = "Keine Bewertungssitzung ausgewählt."; + [ObservableProperty] private bool _competencyTagsVisible; + [ObservableProperty] private bool _hasCompetencyCatalog; + [ObservableProperty] private bool _studentCompetencyRatingsVisible; + [ObservableProperty] private int _rebuildColumnsSignal; public string SelectedSessionDisplay => SelectedSession?.Display ?? ""; + public List ActiveCompetencyCodes { get; private set; } = []; - public ObservableCollection Sessions { get; } = []; - public ObservableCollection StudentRows { get; } = []; - public ObservableCollection Aspects { get; } = []; + public ObservableCollection Sessions { get; } = []; + public ObservableCollection StudentRows { get; } = []; + public ObservableCollection Aspects { get; } = []; + public ObservableCollection CompetencyTagGroups { get; } = []; public Func>? OnAddSession { get; set; } public Func? OnQuickInput { get; set; } @@ -34,16 +46,29 @@ public partial class ParticipationTabViewModel : ObservableObject IParticipationSessionRepository sessions, IParticipationRepository entries, IParticipationAspectRepository aspects, - IStudentRepository students) + IStudentRepository students, + IEnrollmentRepository enrollments, + IGroupRepository groups, + ICompetencyDomainRepository competencyDomains) { - _sessions = sessions; _entries = entries; - _aspects = aspects; _students = students; + _sessions = sessions; _entries = entries; + _aspects = aspects; _students = students; + _enrollments = enrollments; _groups = groups; + _competencyDomains = competencyDomains; } public void Initialize(Guid groupId, string schoolYear) { _groupId = groupId; _schoolYear = schoolYear; + + var group = _groups.GetById(groupId); + _subjectId = group?.SubjectId; + _gradeLevel = group?.GradeLevel ?? 0; + + HasCompetencyCatalog = _subjectId.HasValue + && _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel).Count > 0; + LoadAspects(); LoadSessions(); } @@ -78,27 +103,54 @@ public partial class ParticipationTabViewModel : ObservableObject partial void OnSelectedSessionChanged(ParticipationSessionItem? value) { OnPropertyChanged(nameof(SelectedSessionDisplay)); - if (value is null) { StudentRows.Clear(); QuickInputCommand.NotifyCanExecuteChanged(); return; } - LoadGrid(value.Id); + if (value is null) + { + StudentRows.Clear(); + CompetencyTagGroups.Clear(); + ActiveCompetencyCodes = []; + QuickInputCommand.NotifyCanExecuteChanged(); + RebuildColumnsSignal++; + return; + } + LoadCompetencyTags(value.Id); // sets ActiveCompetencyCodes first + LoadGrid(value.Id); // uses ActiveCompetencyCodes, fires RebuildColumnsSignal++ } private void LoadGrid(Guid sessionId) { StudentRows.Clear(); - var students = _students.GetByGroup(_groupId, _schoolYear); - var entries = _entries.GetBySession(sessionId); + var session = _sessions.GetById(sessionId); + var sessionDate = session?.Date ?? DateOnly.FromDateTime(DateTime.Today); + var students = _students.GetByGroup(_groupId, _schoolYear); + var enrollments = _enrollments.GetByGroupAndYear(_groupId, _schoolYear); + var entries = _entries.GetBySession(sessionId); foreach (var s in students) { + var enrollment = enrollments.FirstOrDefault(e => e.StudentId == s.Id); + if (enrollment is not null && !IsEnrolledAtDate(enrollment, sessionDate)) + continue; + 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); + var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList(), ActiveCompetencyCodes); + row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val); + row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val); StudentRows.Add(row); } QuickInputCommand.NotifyCanExecuteChanged(); + RebuildColumnsSignal++; } + private static bool IsEnrolledAtDate(Enrollment e, DateOnly date) => e.Period switch + { + EnrollmentPeriod.H1Only => date.Month >= 8 || date.Month <= 1, + EnrollmentPeriod.H2Only => date.Month >= 2 && date.Month <= 7, + EnrollmentPeriod.Custom => (e.JoinedAt is null || date >= e.JoinedAt.Value) + && (e.LeftAt is null || date <= e.LeftAt.Value), + _ => true, + }; + private void SaveRating(Guid sessionId, Guid studentId, string key, int? value) { var session = _sessions.GetById(sessionId); @@ -124,6 +176,53 @@ public partial class ParticipationTabViewModel : ObservableObject _entries.Save(entry); } + public void RefreshCurrentGrid() + { + if (SelectedSession is not null) LoadGrid(SelectedSession.Id); + } + + [RelayCommand] + private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible; + + partial void OnStudentCompetencyRatingsVisibleChanged(bool value) => RebuildColumnsSignal++; + + private void LoadCompetencyTags(Guid sessionId) + { + CompetencyTagGroups.Clear(); + var session = _sessions.GetById(sessionId); + ActiveCompetencyCodes = session?.CompetencyCodes?.ToList() ?? []; + + if (!_subjectId.HasValue) return; + var active = ActiveCompetencyCodes.ToHashSet(); + + foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel)) + { + var group = new CompetencyTagGroup(domain.Name, domain.Code); + foreach (var item in domain.Items.OrderBy(i => i.SortOrder)) + { + var tag = new CompetencyTag(item.Code, item.Description, active.Contains(item.Code)); + tag.OnChanged = (code, sel) => OnTagToggled(code, sel); + group.Items.Add(tag); + } + if (group.Items.Count > 0) + CompetencyTagGroups.Add(group); + } + } + + private void OnTagToggled(string code, bool selected) + { + if (SelectedSession is null) return; + var session = _sessions.GetById(SelectedSession.Id); + if (session is null) return; + if (selected) { if (!session.CompetencyCodes.Contains(code)) session.CompetencyCodes.Add(code); } + else { session.CompetencyCodes.Remove(code); } + _sessions.Save(session); + + ActiveCompetencyCodes = session.CompetencyCodes.ToList(); + if (StudentCompetencyRatingsVisible) + LoadGrid(SelectedSession.Id); // reloads rows with updated cells, fires RebuildColumnsSignal++ + } + [RelayCommand] private async Task AddSession() { @@ -132,7 +231,10 @@ public partial class ParticipationTabViewModel : ObservableObject if (session is null) return; session.GroupId = _groupId; _sessions.Save(session); - LoadSessions(); + + Sessions.Clear(); + foreach (var s in _sessions.GetByGroup(_groupId)) + Sessions.Add(new ParticipationSessionItem(s)); SelectedSession = Sessions.FirstOrDefault(s => s.Id == session.Id); } @@ -161,6 +263,30 @@ public partial class ParticipationTabViewModel : ObservableObject entry.Note = note; _entries.Save(entry); } + + private void SaveCompetencyRating(Guid sessionId, Guid studentId, string code, 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.CompetencyRatings.FirstOrDefault(r => r.Code == code); + if (value is null) + { + if (existing is not null) entry.CompetencyRatings.Remove(existing); + } + else + { + if (existing is null) entry.CompetencyRatings.Add(new CompetencyRating { Code = code, Value = value.Value }); + else existing.Value = value.Value; + } + _entries.Save(entry); + } } // ── Zeilendaten für das Bewertungsraster ───────────────────────────────────── @@ -170,17 +296,21 @@ public partial class ParticipationStudentRow : ObservableObject public Guid StudentId { get; } public string Name { get; } - private readonly ParticipationEntry _entry; + private readonly ParticipationEntry _entry; private readonly IReadOnlyList _aspectDefs; - public ObservableCollection Cells { get; } = []; - public Action? OnRatingChanged { get; set; } + public ObservableCollection Cells { get; } = []; + public ObservableCollection CompetencyCells { get; } = []; - public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry, List aspects) + public Action? OnRatingChanged { get; set; } + public Action? OnCompetencyRatingChanged { get; set; } + + public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry, + List aspects, List competencyCodes) { - StudentId = id; - Name = name; - _entry = entry; + StudentId = id; + Name = name; + _entry = entry; _aspectDefs = aspects; foreach (var a in aspects) @@ -190,6 +320,14 @@ public partial class ParticipationStudentRow : ObservableObject cell.OnChanged = (sid, key, val) => OnRatingChanged?.Invoke(sid, key, val); Cells.Add(cell); } + + foreach (var code in competencyCodes) + { + var existing = entry.CompetencyRatings.FirstOrDefault(r => r.Code == code); + var cell = new RatingCell(id, code, existing?.Value); + cell.OnChanged = (sid, key, val) => OnCompetencyRatingChanged?.Invoke(sid, key, val); + CompetencyCells.Add(cell); + } } public int? GetRating(string key) => @@ -258,6 +396,37 @@ public partial class RatingCell : ObservableObject }; } +// ── Kompetenz-Tags ──────────────────────────────────────────────────────────── + +public class CompetencyTagGroup(string name, string code) +{ + public string Name { get; } = name; + public string Code { get; } = code; + public string DisplayName { get; } = string.IsNullOrEmpty(code) ? name : $"{name} ({code})"; + public List Items { get; } = []; +} + +public partial class CompetencyTag : ObservableObject +{ + public string Code { get; } + public string Description { get; } + public string Display { get; } + + [ObservableProperty] private bool _isSelected; + + public Action? OnChanged { get; set; } + + public CompetencyTag(string code, string description, bool isSelected) + { + Code = code; + Description = description; + Display = string.IsNullOrEmpty(code) ? description : $"[{code}] {description}"; + _isSelected = isSelected; + } + + partial void OnIsSelectedChanged(bool value) => OnChanged?.Invoke(Code, value); +} + // ── Hilfsklassen ────────────────────────────────────────────────────────────── public class AspectColumnDef diff --git a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs index 3f5fc14..44c0745 100644 --- a/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/MainWindowViewModel.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Services; using LehrerApp.Desktop.ViewModels.Groups; +using LehrerApp.Desktop.ViewModels.Settings; using LehrerApp.Desktop.ViewModels.Students; using Microsoft.Extensions.DependencyInjection; @@ -39,7 +40,7 @@ public partial class MainWindowViewModel : ObservableObject NavItem.Exams => new PlaceholderViewModel { Title = "Klausuren", Icon = "📝" }, NavItem.Planner => new PlaceholderViewModel { Title = "Unterrichtsplanung", Icon = "📅" }, NavItem.Workload => new PlaceholderViewModel { Title = "Arbeitszeit", Icon = "⏱" }, - NavItem.Settings => new PlaceholderViewModel { Title = "Einstellungen", Icon = "⚙️" }, + NavItem.Settings => _services.GetRequiredService(), _ => CurrentPage, }; } diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs new file mode 100644 index 0000000..ae8a2fd --- /dev/null +++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.cs @@ -0,0 +1,285 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using LehrerApp.Core.Interfaces; +using LehrerApp.Core.Models; +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LehrerApp.Desktop.ViewModels.Settings; + +// ── Haupt-ViewModel ─────────────────────────────────────────────────────────── + +public partial class SettingsViewModel : ObservableObject +{ + private readonly ISubjectRepository _subjects; + private readonly ICompetencyDomainRepository _domainRepo; + + // ── Fächer ──────────────────────────────────────────────────────────────── + + [ObservableProperty] private string _newName = ""; + [ObservableProperty] private string _newShort = ""; + [ObservableProperty] private string _validationMessage = ""; + + public ObservableCollection Subjects { get; } = []; + + // ── Kompetenzkatalog ────────────────────────────────────────────────────── + + [ObservableProperty] private SubjectListItem? _catalogSubject; + [ObservableProperty] private int _catalogGradeLevel = 10; + [ObservableProperty] private string _newDomainName = ""; + [ObservableProperty] private string _newDomainCode = ""; + [ObservableProperty] private string _catalogValidation = ""; + + public ObservableCollection Domains { get; } = []; + + // ── Konstruktor ─────────────────────────────────────────────────────────── + + public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo) + { + _subjects = subjects; + _domainRepo = domainRepo; + LoadSubjects(); + } + + // ── Fächer: Laden / Hinzufügen / Löschen ───────────────────────────────── + + public void LoadSubjects() + { + Subjects.Clear(); + foreach (var s in _subjects.GetAll()) + Subjects.Add(new SubjectListItem(s)); + } + + [RelayCommand] + private void AddSubject() + { + if (string.IsNullOrWhiteSpace(NewName)) { ValidationMessage = "Name erforderlich."; return; } + _subjects.Save(new Subject { Name = NewName.Trim(), ShortName = NewShort.Trim() }); + NewName = ""; NewShort = ""; ValidationMessage = ""; + LoadSubjects(); + } + + [RelayCommand] + private void DeleteSubject(SubjectListItem? item) + { + if (item is null) return; + _subjects.Delete(item.Id); + if (CatalogSubject?.Id == item.Id) CatalogSubject = null; + LoadSubjects(); + } + + // ── Katalog: Laden ──────────────────────────────────────────────────────── + + partial void OnCatalogSubjectChanged(SubjectListItem? value) => LoadCatalog(); + partial void OnCatalogGradeLevelChanged(int value) => LoadCatalog(); + + private void LoadCatalog() + { + Domains.Clear(); + CatalogValidation = ""; + if (CatalogSubject is null) return; + foreach (var d in _domainRepo.GetBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel)) + Domains.Add(new DomainEditItem(d, _domainRepo)); + } + + // ── Katalog: Bereich hinzufügen / löschen ──────────────────────────────── + + [RelayCommand] + private void AddDomain() + { + if (CatalogSubject is null) { CatalogValidation = "Bitte zuerst ein Fach auswählen."; return; } + if (string.IsNullOrWhiteSpace(NewDomainName)) { CatalogValidation = "Bereichsname erforderlich."; return; } + + var domain = new CompetencyDomain + { + SubjectId = CatalogSubject.Id, + GradeLevel = CatalogGradeLevel, + Name = NewDomainName.Trim(), + Code = NewDomainCode.Trim(), + SortOrder = Domains.Count, + }; + _domainRepo.Save(domain); + Domains.Add(new DomainEditItem(domain, _domainRepo)); + NewDomainName = ""; NewDomainCode = ""; CatalogValidation = ""; + } + + [RelayCommand] + private void DeleteDomain(DomainEditItem? item) + { + if (item is null) return; + _domainRepo.Delete(item.Id); + Domains.Remove(item); + } + + // ── JSON Import / Export ────────────────────────────────────────────────── + + public void ImportCatalog(string json) + { + if (CatalogSubject is null) { CatalogValidation = "Bitte zuerst ein Fach auswählen."; return; } + try + { + var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + var dto = JsonSerializer.Deserialize(json, opts); + if (dto?.Domains is null) { CatalogValidation = "Ungültiges JSON-Format."; return; } + + _domainRepo.DeleteBySubjectAndGrade(CatalogSubject.Id, CatalogGradeLevel); + + for (int i = 0; i < dto.Domains.Count; i++) + { + var d = dto.Domains[i]; + var domain = new CompetencyDomain + { + SubjectId = CatalogSubject.Id, + GradeLevel = CatalogGradeLevel, + Name = d.Name ?? "", + Code = d.Code ?? "", + SortOrder = i, + Items = (d.Competencies ?? []) + .Select((c, j) => new CompetencyItem + { + Code = c.Code ?? "", + Description = c.Description ?? "", + SortOrder = j, + }).ToList(), + }; + _domainRepo.Save(domain); + } + CatalogValidation = ""; + LoadCatalog(); + } + catch + { + CatalogValidation = "Import fehlgeschlagen – bitte JSON-Format prüfen."; + } + } + + public string ExportCatalog() + { + var dto = new CatalogDto + { + Subject = CatalogSubject?.Name ?? "", + GradeLevel = CatalogGradeLevel, + Domains = Domains.Select(d => new DomainDto + { + Name = d.Name, + Code = d.Code, + Competencies = d.Items.Select(i => new CompetencyDto + { + Code = i.Code, + Description = i.Description, + }).ToList(), + }).ToList(), + }; + return JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true }); + } +} + +// ── DomainEditItem ──────────────────────────────────────────────────────────── + +public partial class DomainEditItem : ObservableObject +{ + private readonly CompetencyDomain _domain; + private readonly ICompetencyDomainRepository _repo; + + public Guid Id { get; } + public string Name { get; } + public string Code { get; } + public string DisplayName { get; } + + [ObservableProperty] private string _newItemCode = ""; + [ObservableProperty] private string _newItemDesc = ""; + + public ObservableCollection Items { get; } = []; + + public DomainEditItem(CompetencyDomain domain, ICompetencyDomainRepository repo) + { + _domain = domain; + _repo = repo; + Id = domain.Id; + Name = domain.Name; + Code = domain.Code; + DisplayName = string.IsNullOrEmpty(domain.Code) + ? domain.Name + : $"{domain.Name} ({domain.Code})"; + + foreach (var item in domain.Items.OrderBy(i => i.SortOrder)) + Items.Add(new CompetencyItemVm(item, DeleteItem)); + } + + [RelayCommand] + private void AddItem() + { + if (string.IsNullOrWhiteSpace(NewItemDesc)) return; + var item = new CompetencyItem + { + Code = NewItemCode.Trim(), + Description = NewItemDesc.Trim(), + SortOrder = _domain.Items.Count, + }; + _domain.Items.Add(item); + _repo.Save(_domain); + Items.Add(new CompetencyItemVm(item, DeleteItem)); + NewItemCode = ""; NewItemDesc = ""; + } + + private void DeleteItem(CompetencyItemVm vm) + { + _domain.Items.RemoveAll(i => i.Id == vm.ItemId); + _repo.Save(_domain); + Items.Remove(vm); + } +} + +// ── CompetencyItemVm ────────────────────────────────────────────────────────── + +public class CompetencyItemVm +{ + public Guid ItemId { get; } + public string Code { get; } + public string Description { get; } + public string Display { get; } + public IRelayCommand DeleteCommand { get; } + + public CompetencyItemVm(CompetencyItem item, Action onDelete) + { + ItemId = item.Id; + Code = item.Code; + Description = item.Description; + Display = string.IsNullOrEmpty(item.Code) + ? item.Description + : $"[{item.Code}] {item.Description}"; + DeleteCommand = new RelayCommand(() => onDelete(this)); + } +} + +// ── Hilfklassen ─────────────────────────────────────────────────────────────── + +public class SubjectListItem(Subject s) +{ + public Guid Id { get; } = s.Id; + public string Name { get; } = s.Name; + public string ShortName { get; } = s.ShortName; +} + +// ── JSON DTOs ───────────────────────────────────────────────────────────────── + +internal class CatalogDto +{ + [JsonPropertyName("subject")] public string? Subject { get; set; } + [JsonPropertyName("gradeLevel")] public int GradeLevel { get; set; } + [JsonPropertyName("domains")] public List? Domains { get; set; } +} + +internal class DomainDto +{ + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("code")] public string? Code { get; set; } + [JsonPropertyName("competencies")] public List? Competencies { get; set; } +} + +internal class CompetencyDto +{ + [JsonPropertyName("code")] public string? Code { get; set; } + [JsonPropertyName("description")] public string? Description { get; set; } +} diff --git a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml index 0e3958a..311c10b 100644 --- a/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddGroupDialog.axaml @@ -25,10 +25,15 @@ - + - + diff --git a/LehrerApp.Desktop/Views/Groups/AddStudentToGroupDialog.axaml b/LehrerApp.Desktop/Views/Groups/AddStudentToGroupDialog.axaml index 11fb310..2a96c7a 100644 --- a/LehrerApp.Desktop/Views/Groups/AddStudentToGroupDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AddStudentToGroupDialog.axaml @@ -4,10 +4,10 @@ x:Class="LehrerApp.Desktop.Views.Groups.AddStudentToGroupDialog" x:DataType="vm:AddStudentToGroupDialogViewModel" Title="Schüler hinzufügen" - Width="420" Height="500" + Width="420" Height="560" CanResize="False" WindowStartupLocation="CenterOwner"> - + @@ -29,8 +29,32 @@ + + + + + + + + + + + + + + + + + + + - + diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml index 42679cd..e91d1b2 100644 --- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml +++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml @@ -53,9 +53,8 @@ CanUserResizeColumns="True" Margin="0"> - + + diff --git a/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs b/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs index 1a5b794..19829bb 100644 --- a/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs +++ b/LehrerApp.Desktop/Views/Groups/GroupListView.axaml.cs @@ -1,3 +1,26 @@ using Avalonia.Controls; +using LehrerApp.Desktop.ViewModels.Groups; +using Microsoft.Extensions.DependencyInjection; + namespace LehrerApp.Desktop.Views.Groups; -public partial class GroupListView : UserControl { public GroupListView() => InitializeComponent(); } + +public partial class GroupListView : UserControl +{ + public GroupListView() => InitializeComponent(); + + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + if (DataContext is GroupListViewModel vm) + vm.OnAddGroup = ShowAddGroupDialog; + } + + private async Task ShowAddGroupDialog() + { + var dialogVm = App.Services.GetRequiredService(); + var dialog = new AddGroupDialog { DataContext = dialogVm }; + var owner = TopLevel.GetTopLevel(this) as Window; + if (owner is not null) + await dialog.ShowDialog(owner); + } +} diff --git a/LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml b/LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml index 373a2b6..cc7d6fd 100644 --- a/LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml +++ b/LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml @@ -30,15 +30,65 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - BuildColumns(); + vm.PropertyChanged += (_, pe) => + { + if (pe.PropertyName == nameof(ParticipationTabViewModel.RebuildColumnsSignal)) + BuildColumns(); + }; BuildColumns(); } } @@ -39,25 +44,43 @@ public partial class ParticipationTabView : UserControl Width = new DataGridLength(160, DataGridLengthUnitType.Pixel), }); + // Aspekt-Spalten foreach (var (aspect, i) in _vm.Aspects.Select((a, i) => (a, i))) { - var capturedIndex = i; + var idx = i; grid.Columns.Add(new DataGridTemplateColumn { - Header = $"{aspect.Label} [{AspectShortcut(i)}]", - Width = new DataGridLength(1, DataGridLengthUnitType.Star), - CellTemplate = BuildCellTemplate(capturedIndex), + Header = $"{aspect.Label} [{AspectShortcut(i)}]", + Width = new DataGridLength(1, DataGridLengthUnitType.Star), + CellTemplate = BuildCellTemplate(idx, forCompetency: false), }); } + + // Kompetenz-Spalten (opt-in) + if (_vm.StudentCompetencyRatingsVisible && _vm.ActiveCompetencyCodes.Count > 0) + { + foreach (var (code, i) in _vm.ActiveCompetencyCodes.Select((c, i) => (c, i))) + { + var idx = i; + grid.Columns.Add(new DataGridTemplateColumn + { + Header = code, + Width = new DataGridLength(80, DataGridLengthUnitType.Pixel), + CellTemplate = BuildCellTemplate(idx, forCompetency: true), + }); + } + } } - private static IDataTemplate BuildCellTemplate(int aspectIndex) + private static IDataTemplate BuildCellTemplate(int cellIndex, bool forCompetency) { return new FuncDataTemplate((row, _) => { if (row is null) return new TextBlock(); - var cell = row.Cells.ElementAtOrDefault(aspectIndex); + var cell = forCompetency + ? row.CompetencyCells.ElementAtOrDefault(cellIndex) + : row.Cells.ElementAtOrDefault(cellIndex); if (cell is null) return new TextBlock(); var panel = new StackPanel @@ -73,10 +96,10 @@ public partial class ParticipationTabView : UserControl { var btn = new Button { - Content = label, - Padding = new Avalonia.Thickness(5, 1), + Content = label, + Padding = new Avalonia.Thickness(5, 1), FontSize = 11, - Opacity = cell.Value == val ? 1.0 : 0.3, + Opacity = cell.Value == val ? 1.0 : 0.3, }; var capturedVal = val; btn.Click += (_, _) => cell.SetValue(capturedVal); diff --git a/LehrerApp.Desktop/Views/MainWindow.axaml b/LehrerApp.Desktop/Views/MainWindow.axaml index 9c5b160..8534392 100644 --- a/LehrerApp.Desktop/Views/MainWindow.axaml +++ b/LehrerApp.Desktop/Views/MainWindow.axaml @@ -7,6 +7,8 @@ xmlns:vd="clr-namespace:LehrerApp.Desktop.Views.Dashboard" xmlns:vg="clr-namespace:LehrerApp.Desktop.Views.Groups" xmlns:vs="clr-namespace:LehrerApp.Desktop.Views.Students" + xmlns:vset="clr-namespace:LehrerApp.Desktop.Views.Settings" + xmlns:vmset="clr-namespace:LehrerApp.Desktop.ViewModels.Settings" x:Class="LehrerApp.Desktop.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Title="LehrerApp" @@ -41,6 +43,9 @@ + + + diff --git a/LehrerApp.Desktop/Views/Settings/SettingsView.axaml b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml new file mode 100644 index 0000000..d44cc4e --- /dev/null +++ b/LehrerApp.Desktop/Views/Settings/SettingsView.axaml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + +