Schüler hinzufügen, Kompetenzen
This commit is contained in:
@@ -108,3 +108,19 @@ public interface IParticipationAspectRepository
|
||||
void Save(ParticipationAspect aspect);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface ISubjectRepository
|
||||
{
|
||||
List<Subject> GetAll();
|
||||
Subject? GetById(Guid id);
|
||||
Subject? GetByName(string name);
|
||||
void Save(Subject subject);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface ICompetencyDomainRepository
|
||||
{
|
||||
List<CompetencyDomain> GetBySubjectAndGrade(Guid subjectId, int gradeLevel);
|
||||
CompetencyDomain? GetById(Guid id);
|
||||
void Save(CompetencyDomain domain);
|
||||
void Delete(Guid id);
|
||||
void DeleteBySubjectAndGrade(Guid subjectId, int gradeLevel);
|
||||
}
|
||||
|
||||
@@ -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<CompetencyItem> 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; }
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<string> 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<AspectRating> Ratings { get; set; } = [];
|
||||
public List<AspectRating> Ratings { get; set; } = [];
|
||||
public List<CompetencyRating> 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; } = "";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -33,6 +33,8 @@ public class LiteDbContext : IDisposable
|
||||
public ILiteCollection<ParticipationSession> ParticipationSessions => _db.GetCollection<ParticipationSession>("participation_sessions");
|
||||
public ILiteCollection<ParticipationEntry> ParticipationEntries => _db.GetCollection<ParticipationEntry>("participation");
|
||||
public ILiteCollection<ParticipationAspect> ParticipationAspects => _db.GetCollection<ParticipationAspect>("participation_aspects");
|
||||
public ILiteCollection<Subject> Subjects => _db.GetCollection<Subject>("subjects");
|
||||
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("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();
|
||||
|
||||
@@ -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<Subject> 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<CompetencyDomain> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GroupListViewModel>();
|
||||
gl.OnNavigateToDetail = (id, tab) => main.NavigateToGroupDetail(id, tab);
|
||||
gl.OnAddGroup = () => ShowAddGroupDialog(gl);
|
||||
|
||||
// Dashboard → GroupDetail (Chips)
|
||||
var dash = Services.GetRequiredService<DashboardViewModel>();
|
||||
@@ -54,18 +53,4 @@ public class App : Application
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||
await dialog.ShowDialog<bool>(owner);
|
||||
}
|
||||
|
||||
private static async void ShowAddGroupDialog(GroupListViewModel groupList)
|
||||
{
|
||||
var vm = new ViewModels.Groups.AddGroupDialogViewModel(
|
||||
Services.GetRequiredService<Core.Interfaces.IGroupRepository>(),
|
||||
Services.GetRequiredService<Core.Services.SchoolYearService>());
|
||||
var dialog = new Views.Groups.AddGroupDialog { DataContext = vm };
|
||||
|
||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner })
|
||||
{
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
if (ok) groupList.LoadGroups();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IParticipationSessionRepository, ParticipationSessionRepository>();
|
||||
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
|
||||
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
|
||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
services.AddSingleton<GradingService>();
|
||||
@@ -107,6 +110,8 @@ public static class AppBootstrapper
|
||||
services.AddTransient<GroupDetailViewModel>();
|
||||
services.AddTransient<StudentDetailViewModel>();
|
||||
services.AddTransient<ParticipationTabViewModel>();
|
||||
services.AddTransient<AddGroupDialogViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public partial class GroupListViewModel : ObservableObject
|
||||
private readonly SchoolYearService _sy;
|
||||
|
||||
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
||||
public Action? OnAddGroup { get; set; }
|
||||
public Func<Task>? 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<StudentPickerItem> 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<Subject> _allSubjects = [];
|
||||
|
||||
public List<string> TypeOptions { get; } = ["Klasse", "Kurs"];
|
||||
[ObservableProperty] private string _selectedTypeName = "Kurs";
|
||||
@@ -289,12 +364,15 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private string _validationMessage = "";
|
||||
|
||||
public List<string> SchoolYears { get; }
|
||||
public List<string> 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,
|
||||
|
||||
@@ -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<string> ActiveCompetencyCodes { get; private set; } = [];
|
||||
|
||||
public ObservableCollection<ParticipationSessionItem> Sessions { get; } = [];
|
||||
public ObservableCollection<ParticipationStudentRow> StudentRows { get; } = [];
|
||||
public ObservableCollection<AspectColumnDef> Aspects { get; } = [];
|
||||
public ObservableCollection<ParticipationSessionItem> Sessions { get; } = [];
|
||||
public ObservableCollection<ParticipationStudentRow> StudentRows { get; } = [];
|
||||
public ObservableCollection<AspectColumnDef> Aspects { get; } = [];
|
||||
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
|
||||
|
||||
public Func<Task<ParticipationSession?>>? OnAddSession { get; set; }
|
||||
public Func<ParticipationTabViewModel, Task>? 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<AspectColumnDef> _aspectDefs;
|
||||
|
||||
public ObservableCollection<RatingCell> Cells { get; } = [];
|
||||
public Action<Guid, string, int?>? OnRatingChanged { get; set; }
|
||||
public ObservableCollection<RatingCell> Cells { get; } = [];
|
||||
public ObservableCollection<RatingCell> CompetencyCells { get; } = [];
|
||||
|
||||
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry, List<AspectColumnDef> aspects)
|
||||
public Action<Guid, string, int?>? OnRatingChanged { get; set; }
|
||||
public Action<Guid, string, int?>? OnCompetencyRatingChanged { get; set; }
|
||||
|
||||
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
|
||||
List<AspectColumnDef> aspects, List<string> 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<CompetencyTag> 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<string, bool>? 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
|
||||
|
||||
@@ -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<SettingsViewModel>(),
|
||||
_ => CurrentPage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<SubjectListItem> 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<DomainEditItem> 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<CatalogDto>(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<CompetencyItemVm> 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<CompetencyItemVm> 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<DomainDto>? Domains { get; set; }
|
||||
}
|
||||
|
||||
internal class DomainDto
|
||||
{
|
||||
[JsonPropertyName("name")] public string? Name { get; set; }
|
||||
[JsonPropertyName("code")] public string? Code { get; set; }
|
||||
[JsonPropertyName("competencies")] public List<CompetencyDto>? Competencies { get; set; }
|
||||
}
|
||||
|
||||
internal class CompetencyDto
|
||||
{
|
||||
[JsonPropertyName("code")] public string? Code { get; set; }
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
}
|
||||
@@ -25,10 +25,15 @@
|
||||
<TextBox Text="{Binding Name}" PlaceholderText="{Binding NameHint}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Fach – nur bei Kurs relevant -->
|
||||
<!-- Fach – nur bei Kurs, AutoComplete aus bekannten Fächern -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding IsKurs}">
|
||||
<TextBlock Text="Fach (optional)" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Subject}" PlaceholderText="z.B. Chemie, Mathematik"/>
|
||||
<AutoCompleteBox Text="{Binding Subject}"
|
||||
ItemsSource="{Binding KnownSubjectNames}"
|
||||
FilterMode="Contains"
|
||||
MinimumPrefixLength="0"
|
||||
PlaceholderText="z.B. Chemie, Mathematik"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,12,*">
|
||||
|
||||
@@ -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">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto" Margin="24">
|
||||
|
||||
<!-- Überschrift + Suche -->
|
||||
<StackPanel Grid.Row="0" Spacing="12" Margin="0,0,0,12">
|
||||
@@ -29,8 +29,32 @@
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Zeitraum-Auswahl -->
|
||||
<Border Grid.Row="2"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,10" Margin="0,10,0,0">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Einschreibungszeitraum" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<RadioButton Content="Ganzes Jahr" IsChecked="{Binding IsFullYear}"/>
|
||||
<RadioButton Content="Nur H1" IsChecked="{Binding IsH1Only}"/>
|
||||
<RadioButton Content="Nur H2" IsChecked="{Binding IsH2Only}"/>
|
||||
<RadioButton Content="Datum" IsChecked="{Binding IsCustom}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,12,*"
|
||||
IsVisible="{Binding IsCustomPeriod}">
|
||||
<TextBox Grid.Column="0" Text="{Binding JoinedAtText}"
|
||||
PlaceholderText="Eintr. TT.MM.JJJJ" FontSize="12"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding LeftAtText}"
|
||||
PlaceholderText="Austr. TT.MM.JJJJ" FontSize="12"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Validation + Buttons -->
|
||||
<StackPanel Grid.Row="2" Spacing="12" Margin="0,16,0,0">
|
||||
<StackPanel Grid.Row="3" Spacing="12" Margin="0,16,0,0">
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
|
||||
@@ -53,9 +53,8 @@
|
||||
CanUserResizeColumns="True"
|
||||
Margin="0">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Name"
|
||||
Binding="{Binding FullName}"
|
||||
Width="*"/>
|
||||
<DataGridTextColumn Header="Name" Binding="{Binding FullName}" Width="*"/>
|
||||
<DataGridTextColumn Header="Zeitraum" Binding="{Binding PeriodLabel}" Width="90"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</ContentPage>
|
||||
|
||||
@@ -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<AddGroupDialogViewModel>();
|
||||
var dialog = new AddGroupDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null)
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,15 +30,65 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Rechte Seite: Bewertungsraster (wird per Code-Behind aufgebaut) -->
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding SelectedSessionDisplay}"
|
||||
FontSize="13" FontWeight="SemiBold" Margin="12,10,12,4"
|
||||
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<!-- Rechte Seite -->
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*">
|
||||
|
||||
<!-- Header: Session-Titel + Toggle-Buttons -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="12,8,12,4"
|
||||
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding SelectedSessionDisplay}"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="{Binding HasCompetencyCatalog}">
|
||||
<ToggleButton Content="◇ Kompetenzen"
|
||||
IsChecked="{Binding CompetencyTagsVisible}"
|
||||
FontSize="11" Padding="8,3"/>
|
||||
<ToggleButton Content="◈ Schüler-Ratings"
|
||||
IsChecked="{Binding StudentCompetencyRatingsVisible}"
|
||||
FontSize="11" Padding="8,3"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Kompetenz-Tag-Panel -->
|
||||
<Border Grid.Row="1"
|
||||
IsVisible="{Binding CompetencyTagsVisible}"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="12,8">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" MaxHeight="200">
|
||||
<ItemsControl ItemsSource="{Binding CompetencyTagGroups}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:CompetencyTagGroup">
|
||||
<StackPanel Spacing="4" Margin="0,0,0,10">
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
FontSize="11" FontWeight="SemiBold" Opacity="0.55"/>
|
||||
<ItemsControl ItemsSource="{Binding Items}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:CompetencyTag">
|
||||
<ToggleButton IsChecked="{Binding IsSelected}"
|
||||
Content="{Binding Display}"
|
||||
FontSize="11"
|
||||
Padding="6,3"
|
||||
Margin="0,0,4,4"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- DataGrid: Spalten werden in ParticipationTabView.axaml.cs dynamisch erzeugt -->
|
||||
<DataGrid Grid.Row="1"
|
||||
<DataGrid Grid.Row="2"
|
||||
x:Name="RatingGrid"
|
||||
ItemsSource="{Binding StudentRows}"
|
||||
AutoGenerateColumns="False"
|
||||
@@ -48,7 +98,7 @@
|
||||
CanUserResizeColumns="True"
|
||||
IsVisible="{Binding SelectedSession, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding NoDataText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Opacity="0.35" FontSize="14"
|
||||
|
||||
@@ -21,6 +21,11 @@ public partial class ParticipationTabView : UserControl
|
||||
vm.OnAddSession = ShowAddSessionDialog;
|
||||
vm.OnQuickInput = ShowQuickInputDialog;
|
||||
vm.Aspects.CollectionChanged += (_, _) => 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<ParticipationStudentRow>((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);
|
||||
|
||||
@@ -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 @@
|
||||
<DataTemplate DataType="vms:StudentDetailViewModel">
|
||||
<vs:StudentDetailView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vmset:SettingsViewModel">
|
||||
<vset:SettingsView/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:PlaceholderViewModel">
|
||||
<views:PlaceholderView/>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||
x:Class="LehrerApp.Desktop.Views.Settings.SettingsView"
|
||||
x:DataType="vm:SettingsViewModel">
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,28" Spacing="32" MaxWidth="640">
|
||||
|
||||
<TextBlock Text="Einstellungen" FontSize="22" FontWeight="SemiBold"/>
|
||||
|
||||
<!-- ── FÄCHER ────────────────────────────────────────────────── -->
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="FÄCHER" FontSize="10" FontWeight="Bold" Opacity="0.4"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Subjects}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:SubjectListItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,7">
|
||||
<Grid ColumnDefinitions="*,80,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}"
|
||||
VerticalAlignment="Center" FontSize="14"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding ShortName}"
|
||||
VerticalAlignment="Center" Opacity="0.5" FontSize="13"/>
|
||||
<Button Grid.Column="2" Content="Löschen" FontSize="12" Padding="10,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteSubjectCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Neues Fach" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||
<Grid ColumnDefinitions="*,12,100,12,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewName}"
|
||||
PlaceholderText="Name (z.B. Chemie)"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding NewShort}"
|
||||
PlaceholderText="Kürzel (z.B. Ch)"/>
|
||||
<Button Grid.Column="4" Content="Hinzufügen"
|
||||
Command="{Binding AddSubjectCommand}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ── KOMPETENZKATALOGE ─────────────────────────────────────── -->
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="KOMPETENZKATALOGE" FontSize="10" FontWeight="Bold" Opacity="0.4"/>
|
||||
|
||||
<!-- Fach + Klassenstufe Auswahl + Import/Export -->
|
||||
<Grid ColumnDefinitions="*,12,120,12,Auto,8,Auto">
|
||||
<ComboBox Grid.Column="0"
|
||||
ItemsSource="{Binding Subjects}"
|
||||
SelectedItem="{Binding CatalogSubject}"
|
||||
PlaceholderText="Fach auswählen"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:SubjectListItem">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<NumericUpDown Grid.Column="2"
|
||||
Value="{Binding CatalogGradeLevel}"
|
||||
Minimum="1" Maximum="13" FormatString="0"/>
|
||||
<Button Grid.Column="4" Content="Import JSON" Click="OnImportClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<Button Grid.Column="6" Content="Export JSON" Click="OnExportClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Katalog-Validierungsmeldung -->
|
||||
<TextBlock Text="{Binding CatalogValidation}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding CatalogValidation, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
<!-- Leerer Zustand -->
|
||||
<TextBlock Text="Kein Fach ausgewählt." Opacity="0.4" FontSize="13"
|
||||
IsVisible="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNull}}"/>
|
||||
|
||||
<!-- Domain-Liste -->
|
||||
<ItemsControl ItemsSource="{Binding Domains}"
|
||||
IsVisible="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:DomainEditItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Margin="0,0,0,8" Padding="14,12">
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<!-- Bereichs-Header -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding DisplayName}"
|
||||
FontWeight="SemiBold" FontSize="14" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Bereich löschen" FontSize="12" Padding="10,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).DeleteDomainCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Kompetenz-Items -->
|
||||
<ItemsControl ItemsSource="{Binding Items}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:CompetencyItemVm">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Display}"
|
||||
TextWrapping="Wrap" VerticalAlignment="Center"
|
||||
FontSize="13"/>
|
||||
<Button Grid.Column="1" Content="×" Padding="7,2" FontSize="13"
|
||||
Command="{Binding DeleteCommand}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Kompetenz hinzufügen -->
|
||||
<Grid ColumnDefinitions="80,8,*,8,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewItemCode}"
|
||||
PlaceholderText="EG1" FontSize="12"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding NewItemDesc}"
|
||||
PlaceholderText="Kompetenz-Beschreibung" FontSize="12"/>
|
||||
<Button Grid.Column="4" Content="+"
|
||||
Command="{Binding AddItemCommand}"/>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Neuen Bereich hinzufügen -->
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,12"
|
||||
IsVisible="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Neuer Bereich" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||
<Grid ColumnDefinitions="*,12,90,12,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewDomainName}"
|
||||
PlaceholderText="Bereichsname (z.B. Erkenntnisgewinnung)"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding NewDomainCode}"
|
||||
PlaceholderText="Kürzel (z.B. EG)"/>
|
||||
<Button Grid.Column="4" Content="Hinzufügen"
|
||||
Command="{Binding AddDomainCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,45 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Settings;
|
||||
|
||||
public partial class SettingsView : UserControl
|
||||
{
|
||||
public SettingsView() => InitializeComponent();
|
||||
|
||||
private async void OnImportClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null || DataContext is not SettingsViewModel vm) return;
|
||||
|
||||
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Kompetenzkatalog importieren",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = [new FilePickerFileType("JSON-Dateien") { Patterns = ["*.json"] }],
|
||||
});
|
||||
|
||||
if (files.Count == 0) return;
|
||||
var json = await File.ReadAllTextAsync(files[0].Path.LocalPath);
|
||||
vm.ImportCatalog(json);
|
||||
}
|
||||
|
||||
private async void OnExportClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null || DataContext is not SettingsViewModel vm) return;
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Kompetenzkatalog exportieren",
|
||||
SuggestedFileName = $"Kompetenzkatalog_{vm.CatalogSubject?.Name}_{vm.CatalogGradeLevel}.json",
|
||||
FileTypeChoices = [new FilePickerFileType("JSON-Dateien") { Patterns = ["*.json"] }],
|
||||
});
|
||||
|
||||
if (file is null) return;
|
||||
var json = vm.ExportCatalog();
|
||||
await File.WriteAllTextAsync(file.Path.LocalPath, json);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
# Kompetenzkatalog mit KI erstellen
|
||||
|
||||
Diese Datei erklärt das JSON-Format für Kompetenzkataloge in der LehrerApp
|
||||
und enthält einen fertigen Prompt, den du in eine KI (z.B. Claude, ChatGPT)
|
||||
einfügen kannst.
|
||||
|
||||
---
|
||||
|
||||
## JSON-Format
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "Chemie",
|
||||
"gradeLevel": 10,
|
||||
"domains": [
|
||||
{
|
||||
"name": "Erkenntnisgewinnung",
|
||||
"code": "EG",
|
||||
"competencies": [
|
||||
{ "code": "EG1", "description": "Hypothesen zu chemischen Phänomenen formulieren und überprüfen" },
|
||||
{ "code": "EG2", "description": "Experimente zielgerichtet planen und sicher durchführen" },
|
||||
{ "code": "EG3", "description": "Messdaten systematisch erfassen, auswerten und interpretieren" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kommunikation",
|
||||
"code": "K",
|
||||
"competencies": [
|
||||
{ "code": "K1", "description": "Sachverhalte unter Verwendung der Fachsprache beschreiben" },
|
||||
{ "code": "K2", "description": "Ergebnisse adressatengerecht präsentieren" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Feldübersicht
|
||||
|
||||
| Feld | Typ | Bedeutung |
|
||||
|------|-----|-----------|
|
||||
| `subject` | String | Name des Fachs – muss mit dem in der App angelegten Fach übereinstimmen |
|
||||
| `gradeLevel` | Zahl | Klassenstufe (1–13) |
|
||||
| `domains[].name` | String | Name des Kompetenzbereichs (z.B. „Erkenntnisgewinnung") |
|
||||
| `domains[].code` | String | Kürzel für den Bereich (z.B. „EG") – wird in der App angezeigt |
|
||||
| `competencies[].code` | String | Kürzel der einzelnen Kompetenz (z.B. „EG1") |
|
||||
| `competencies[].description` | String | Beschreibung der Kompetenz als vollständiger Satz |
|
||||
|
||||
### Hinweise
|
||||
- `code` darf leer sein (`""`), ist aber für die spätere Zuordnung hilfreich
|
||||
- Die Reihenfolge der Bereiche und Kompetenzen im JSON wird beibehalten
|
||||
- Beim Import werden **alle bestehenden Einträge** für Fach + Klassenstufe ersetzt
|
||||
- Mehrere Klassenstufen = mehrere JSON-Dateien
|
||||
|
||||
---
|
||||
|
||||
## Fertiger KI-Prompt
|
||||
|
||||
Kopiere den folgenden Text in eine KI deiner Wahl und passe die
|
||||
markierten Stellen an:
|
||||
|
||||
---
|
||||
|
||||
> **Prompt:**
|
||||
>
|
||||
> Erstelle einen Kompetenzkatalog für das Fach **[FACH]**, Klassenstufe **[STUFE]**,
|
||||
> basierend auf dem Bildungsplan Baden-Württemberg (alternativ: Lehrplan [BUNDESLAND]).
|
||||
>
|
||||
> Gib das Ergebnis als valides JSON in genau diesem Format aus – kein Fließtext davor oder danach:
|
||||
>
|
||||
> ```json
|
||||
> {
|
||||
> "subject": "[FACH]",
|
||||
> "gradeLevel": [STUFE],
|
||||
> "domains": [
|
||||
> {
|
||||
> "name": "Bereichsname",
|
||||
> "code": "XX",
|
||||
> "competencies": [
|
||||
> { "code": "XX1", "description": "Kompetenz als vollständiger Satz im Schüler-Infinitiv" }
|
||||
> ]
|
||||
> }
|
||||
> ]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> Anforderungen:
|
||||
> - Verwende die prozessbezogenen Kompetenzbereiche des Bildungsplans als `domains`
|
||||
> (z.B. für Naturwissenschaften: Erkenntnisgewinnung, Kommunikation, Bewertung,
|
||||
> ggf. fachspezifische Inhaltsbereiche)
|
||||
> - Pro Bereich 4–8 konkrete, beobachtbare Kompetenzen
|
||||
> - Beschreibungen im Schüler-Infinitiv: „... formulieren", „... auswerten", „... beurteilen"
|
||||
> - Codes: Bereichskürzel + laufende Nummer (EG1, EG2, K1, K2, ...)
|
||||
> - Keine Duplikate, keine Leerzeilen im JSON, UTF-8
|
||||
|
||||
---
|
||||
|
||||
## Beispiele pro Fach
|
||||
|
||||
### Naturwissenschaften (Chemie, Physik, Biologie)
|
||||
Typische Bereiche nach Bildungsplan BW:
|
||||
- **EG** – Erkenntnisgewinnung
|
||||
- **K** – Kommunikation
|
||||
- **B** – Bewertung
|
||||
- **FW** – Fachwissen (inhaltliche Kompetenzen, ggf. nach Themenfeld aufgeteilt)
|
||||
|
||||
### Mathematik
|
||||
- **L** – Leitideen (Zahl, Messen, Raum & Form, Funktionaler Zusammenhang, Daten & Zufall)
|
||||
- **PK** – Prozessbezogene Kompetenzen (Modellieren, Problemlösen, Argumentieren, ...)
|
||||
|
||||
### Sprachen (Deutsch, Englisch, ...)
|
||||
- Kompetenzbereiche nach GER (A1–C2) oder nach Lehrplan:
|
||||
Sprechen, Schreiben, Lesen, Hören, Sprachmittlung, Sprachbewusstsein
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Fach in der App unter **Einstellungen → Fächer** anlegen (falls noch nicht vorhanden)
|
||||
2. KI-Prompt mit Fach und Klassenstufe anpassen und ausführen
|
||||
3. JSON aus der KI-Antwort kopieren und in eine `.json`-Datei speichern
|
||||
4. In der App: **Einstellungen → Kompetenzkataloge** → Fach + Klassenstufe wählen → **Import JSON**
|
||||
5. Nach dem Import erscheinen die Bereiche und Kompetenzen sofort in der Übersicht
|
||||
|
||||
---
|
||||
|
||||
## Vollständiges Beispiel: Chemie Klasse 10
|
||||
|
||||
```json
|
||||
{
|
||||
"subject": "Chemie",
|
||||
"gradeLevel": 10,
|
||||
"domains": [
|
||||
{
|
||||
"name": "Erkenntnisgewinnung",
|
||||
"code": "EG",
|
||||
"competencies": [
|
||||
{ "code": "EG1", "description": "Beobachtungen von Erklärungen unterscheiden und Hypothesen ableiten" },
|
||||
{ "code": "EG2", "description": "Experimente unter Einhaltung der Sicherheitsvorschriften planen und durchführen" },
|
||||
{ "code": "EG3", "description": "Messdaten tabellarisch erfassen und grafisch darstellen" },
|
||||
{ "code": "EG4", "description": "Ergebnisse mit der Ausgangsfragestellung verknüpfen und Schlussfolgerungen formulieren" },
|
||||
{ "code": "EG5", "description": "Modelle zur Erklärung chemischer Phänomene nutzen und deren Grenzen benennen" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kommunikation",
|
||||
"code": "K",
|
||||
"competencies": [
|
||||
{ "code": "K1", "description": "Fachbegriffe situationsgerecht und korrekt verwenden" },
|
||||
{ "code": "K2", "description": "Chemische Gleichungen lesen, beschreiben und erläutern" },
|
||||
{ "code": "K3", "description": "Arbeitsergebnisse strukturiert und verständlich präsentieren" },
|
||||
{ "code": "K4", "description": "Quellen fachgerecht zitieren und Informationen kritisch bewerten" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bewertung",
|
||||
"code": "B",
|
||||
"competencies": [
|
||||
{ "code": "B1", "description": "Chancen und Risiken chemischer Produkte und Verfahren abwägen" },
|
||||
{ "code": "B2", "description": "Eigene Urteile zu chemisch-gesellschaftlichen Fragen begründet vertreten" },
|
||||
{ "code": "B3", "description": "Perspektivwechsel einnehmen und unterschiedliche Interessen berücksichtigen" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Fachwissen",
|
||||
"code": "FW",
|
||||
"competencies": [
|
||||
{ "code": "FW1", "description": "Säure-Base-Reaktionen mit dem Protonenübertragungskonzept erklären" },
|
||||
{ "code": "FW2", "description": "Den pH-Wert berechnen und messtechnisch bestimmen" },
|
||||
{ "code": "FW3", "description": "Redoxreaktionen als Elektronenübertragungsreaktionen beschreiben" },
|
||||
{ "code": "FW4", "description": "Organische Verbindungen anhand funktioneller Gruppen einordnen" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user