Compare commits
2
Commits
fd715dd5a5
...
8cbd4475d7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cbd4475d7 | ||
|
|
21378bbebd |
@@ -11,6 +11,7 @@ public class Exam
|
|||||||
public List<ExamTask> Tasks { get; set; } = [];
|
public List<ExamTask> Tasks { get; set; } = [];
|
||||||
public List<GradingKeyEntry> GradingKey { get; set; } = [];
|
public List<GradingKeyEntry> GradingKey { get; set; } = [];
|
||||||
public ExamStatus Status { get; set; } = ExamStatus.Planned;
|
public ExamStatus Status { get; set; } = ExamStatus.Planned;
|
||||||
|
public DateOnly? ReturnedAt { get; set; }
|
||||||
public string? Notes { get; set; }
|
public string? Notes { get; set; }
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
@@ -21,6 +22,7 @@ public class ExamTask
|
|||||||
public string? Title { get; set; }
|
public string? Title { get; set; }
|
||||||
public double MaxPoints { get; set; }
|
public double MaxPoints { get; set; }
|
||||||
public double Weight { get; set; } = 1.0;
|
public double Weight { get; set; } = 1.0;
|
||||||
|
public List<string> CompetencyCodes { get; set; } = [];
|
||||||
}
|
}
|
||||||
public class GradingKeyEntry
|
public class GradingKeyEntry
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -89,7 +89,12 @@ public class ExamRepository(LiteDbContext db) : IExamRepository
|
|||||||
public List<Exam> GetByGroup(Guid groupId) =>
|
public List<Exam> GetByGroup(Guid groupId) =>
|
||||||
db.Exams.Find(e => e.GroupId == groupId).OrderByDescending(e => e.Date).ToList();
|
db.Exams.Find(e => e.GroupId == groupId).OrderByDescending(e => e.Date).ToList();
|
||||||
public void Save(Exam e) { e.UpdatedAt = DateTime.UtcNow; db.Exams.Upsert(e); }
|
public void Save(Exam e) { e.UpdatedAt = DateTime.UtcNow; db.Exams.Upsert(e); }
|
||||||
public void Delete(Guid id) => db.Exams.Delete(id);
|
public void Delete(Guid id)
|
||||||
|
{
|
||||||
|
foreach (var result in db.ExamResults.Find(r => r.ExamId == id).ToList())
|
||||||
|
db.ExamResults.Delete(result.Id);
|
||||||
|
db.Exams.Delete(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ExamResultRepository(LiteDbContext db) : IExamResultRepository
|
public class ExamResultRepository(LiteDbContext db) : IExamResultRepository
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
// ── Dialog: Klausur anlegen / bearbeiten / duplizieren ───────────────────────
|
||||||
|
|
||||||
|
public partial class ExamDialogViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||||
|
private readonly Guid _groupId;
|
||||||
|
private readonly Guid? _subjectId;
|
||||||
|
private readonly int _gradeLevel;
|
||||||
|
private readonly Exam? _editingExam;
|
||||||
|
private readonly List<GradingKeyEntry> _gradingKey;
|
||||||
|
private readonly string _subjectName;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _title = "";
|
||||||
|
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||||
|
[ObservableProperty] private int? _examNumber;
|
||||||
|
[ObservableProperty] private string _notes = "";
|
||||||
|
[ObservableProperty] private string _returnedAtText = "";
|
||||||
|
[ObservableProperty] private string _validationMessage = "";
|
||||||
|
[ObservableProperty] private double _totalPoints;
|
||||||
|
[ObservableProperty] private bool _hasCompetencyCatalog;
|
||||||
|
[ObservableProperty] private bool _useWeighting;
|
||||||
|
|
||||||
|
public bool TotalPointsWarning => TotalPoints <= 0;
|
||||||
|
public string TotalPointsDisplay =>
|
||||||
|
$"{TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkte gesamt";
|
||||||
|
|
||||||
|
public ObservableCollection<ExamTaskEditItem> Tasks { get; } = [];
|
||||||
|
|
||||||
|
public Exam? Result { get; private set; }
|
||||||
|
public string DialogTitle => _editingExam is null ? "Neue Klausur anlegen" : "Klausur bearbeiten";
|
||||||
|
public string SaveButtonText => _editingExam is null ? "Anlegen" : "Speichern";
|
||||||
|
|
||||||
|
/// Fach kommt von der Lerngruppe, nicht editierbar (jede Gruppe unterrichtet ein Fach).
|
||||||
|
public string SubjectDisplay => string.IsNullOrWhiteSpace(_subjectName)
|
||||||
|
? "Kein Fach hinterlegt (siehe Lerngruppe)" : $"Fach: {_subjectName}";
|
||||||
|
|
||||||
|
public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains,
|
||||||
|
Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem,
|
||||||
|
string defaultSubjectName, Exam? editingExam, Exam? duplicateSource)
|
||||||
|
{
|
||||||
|
_exams = exams; _competencyDomains = competencyDomains;
|
||||||
|
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
|
||||||
|
_editingExam = editingExam;
|
||||||
|
_subjectName = defaultSubjectName;
|
||||||
|
|
||||||
|
HasCompetencyCatalog = subjectId.HasValue
|
||||||
|
&& _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0;
|
||||||
|
|
||||||
|
var source = editingExam ?? duplicateSource;
|
||||||
|
if (source is not null)
|
||||||
|
{
|
||||||
|
var isDuplicate = duplicateSource is not null;
|
||||||
|
Title = isDuplicate ? $"{source.Title} (Kopie)" : source.Title;
|
||||||
|
DateText = isDuplicate
|
||||||
|
? DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy")
|
||||||
|
: source.Date.ToString("dd.MM.yyyy");
|
||||||
|
ExamNumber = source.ExamNumber;
|
||||||
|
Notes = source.Notes ?? "";
|
||||||
|
ReturnedAtText = isDuplicate ? "" : source.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
|
||||||
|
_gradingKey = source.GradingKey
|
||||||
|
.Select(k => new GradingKeyEntry { Grade = k.Grade, MinPercent = k.MinPercent }).ToList();
|
||||||
|
foreach (var t in source.Tasks.OrderBy(t => t.Nr))
|
||||||
|
AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.CompetencyCodes]);
|
||||||
|
UseWeighting = Tasks.Any(t => Math.Abs(t.Weight - 1.0) > 0.0001);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_gradingKey = gradingSystem == GradingSystem.Grades1To6
|
||||||
|
? GradingService.DefaultKey1To6() : GradingService.DefaultKey0To15();
|
||||||
|
}
|
||||||
|
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
|
||||||
|
RecomputeTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnUseWeightingChanged(bool value)
|
||||||
|
{
|
||||||
|
foreach (var t in Tasks) t.ShowWeight = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddTask()
|
||||||
|
{
|
||||||
|
AddTaskInternal(null, 0, 1.0, []);
|
||||||
|
Tasks[^1].ShowWeight = UseWeighting;
|
||||||
|
RecomputeTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTaskInternal(string? title, double maxPoints, double weight, List<string> competencyCodes)
|
||||||
|
{
|
||||||
|
var item = new ExamTaskEditItem(title, maxPoints, weight, competencyCodes,
|
||||||
|
BuildCompetencyTagGroups(competencyCodes))
|
||||||
|
{
|
||||||
|
OnChanged = RecomputeTotals,
|
||||||
|
OnRemove = RemoveTask,
|
||||||
|
OnMoveUp = MoveTaskUp,
|
||||||
|
OnMoveDown = MoveTaskDown,
|
||||||
|
};
|
||||||
|
Tasks.Add(item);
|
||||||
|
RenumberTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CompetencyTagGroup> BuildCompetencyTagGroups(List<string> selectedCodes)
|
||||||
|
{
|
||||||
|
var groups = new List<CompetencyTagGroup>();
|
||||||
|
if (!_subjectId.HasValue) return groups;
|
||||||
|
var selected = selectedCodes.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))
|
||||||
|
group.Items.Add(new CompetencyTag(item.Code, item.Description, selected.Contains(item.Code)));
|
||||||
|
if (group.Items.Count > 0) groups.Add(group);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveTask(ExamTaskEditItem item)
|
||||||
|
{
|
||||||
|
Tasks.Remove(item);
|
||||||
|
RenumberTasks();
|
||||||
|
RecomputeTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveTaskUp(ExamTaskEditItem item)
|
||||||
|
{
|
||||||
|
var idx = Tasks.IndexOf(item);
|
||||||
|
if (idx <= 0) return;
|
||||||
|
Tasks.Move(idx, idx - 1);
|
||||||
|
RenumberTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveTaskDown(ExamTaskEditItem item)
|
||||||
|
{
|
||||||
|
var idx = Tasks.IndexOf(item);
|
||||||
|
if (idx < 0 || idx >= Tasks.Count - 1) return;
|
||||||
|
Tasks.Move(idx, idx + 1);
|
||||||
|
RenumberTasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenumberTasks()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Tasks.Count; i++) Tasks[i].Nr = i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeTotals()
|
||||||
|
{
|
||||||
|
TotalPoints = Tasks.Sum(t => t.MaxPoints);
|
||||||
|
OnPropertyChanged(nameof(TotalPointsWarning));
|
||||||
|
OnPropertyChanged(nameof(TotalPointsDisplay));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Save()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Title)) { ValidationMessage = "Titel erforderlich."; return; }
|
||||||
|
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||||
|
{
|
||||||
|
ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DateOnly? returnedAt = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(ReturnedAtText))
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(ReturnedAtText, "dd.MM.yyyy", null, DateTimeStyles.None, out var r))
|
||||||
|
{
|
||||||
|
ValidationMessage = "Rückgabedatum im Format TT.MM.JJJJ eingeben.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
returnedAt = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result = _editingExam ?? new Exam { GroupId = _groupId };
|
||||||
|
Result.Title = Title.Trim();
|
||||||
|
Result.Date = date;
|
||||||
|
Result.Subject = _subjectName.Trim();
|
||||||
|
Result.ExamNumber = ExamNumber;
|
||||||
|
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
|
||||||
|
Result.ReturnedAt = returnedAt;
|
||||||
|
Result.Tasks = Tasks.Select(t => t.ToModel()).ToList();
|
||||||
|
Result.GradingKey = _gradingKey;
|
||||||
|
_exams.Save(Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Zeile im Aufgaben-Editor (1.2.1 / 1.2.2 / 1.2.4) ─────────────────────────
|
||||||
|
|
||||||
|
public partial class ExamTaskEditItem : ObservableObject
|
||||||
|
{
|
||||||
|
[ObservableProperty] private int _nr;
|
||||||
|
[ObservableProperty] private string _title = "";
|
||||||
|
[ObservableProperty] private double _maxPoints;
|
||||||
|
[ObservableProperty] private double _weight = 1.0;
|
||||||
|
[ObservableProperty] private bool _isCompetencyPanelOpen;
|
||||||
|
[ObservableProperty] private bool _showWeight;
|
||||||
|
|
||||||
|
public List<string> CompetencyCodes { get; }
|
||||||
|
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
|
||||||
|
|
||||||
|
public string CompetencySummary => CompetencyCodes.Count == 0
|
||||||
|
? "Keine Kompetenzen"
|
||||||
|
: $"{CompetencyCodes.Count} Kompetenz(en)";
|
||||||
|
|
||||||
|
public Action? OnChanged { get; set; }
|
||||||
|
public Action<ExamTaskEditItem>? OnRemove { get; set; }
|
||||||
|
public Action<ExamTaskEditItem>? OnMoveUp { get; set; }
|
||||||
|
public Action<ExamTaskEditItem>? OnMoveDown { get; set; }
|
||||||
|
|
||||||
|
public ExamTaskEditItem(string? title, double maxPoints, double weight,
|
||||||
|
List<string> competencyCodes, List<CompetencyTagGroup> tagGroups)
|
||||||
|
{
|
||||||
|
_title = title ?? "";
|
||||||
|
_maxPoints = maxPoints;
|
||||||
|
_weight = weight;
|
||||||
|
CompetencyCodes = competencyCodes;
|
||||||
|
foreach (var g in tagGroups)
|
||||||
|
{
|
||||||
|
foreach (var tag in g.Items) tag.OnChanged = OnCompetencyToggled;
|
||||||
|
CompetencyTagGroups.Add(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCompetencyToggled(string code, bool selected)
|
||||||
|
{
|
||||||
|
if (selected) { if (!CompetencyCodes.Contains(code)) CompetencyCodes.Add(code); }
|
||||||
|
else CompetencyCodes.Remove(code);
|
||||||
|
OnPropertyChanged(nameof(CompetencySummary));
|
||||||
|
OnChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private void ToggleCompetencyPanel() => IsCompetencyPanelOpen = !IsCompetencyPanelOpen;
|
||||||
|
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
||||||
|
[RelayCommand] private void MoveUp() => OnMoveUp?.Invoke(this);
|
||||||
|
[RelayCommand] private void MoveDown() => OnMoveDown?.Invoke(this);
|
||||||
|
|
||||||
|
partial void OnMaxPointsChanged(double value) => OnChanged?.Invoke();
|
||||||
|
|
||||||
|
public ExamTask ToModel() => new()
|
||||||
|
{
|
||||||
|
Nr = Nr,
|
||||||
|
Title = string.IsNullOrWhiteSpace(Title) ? null : Title.Trim(),
|
||||||
|
MaxPoints = MaxPoints,
|
||||||
|
Weight = Weight,
|
||||||
|
CompetencyCodes = CompetencyCodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -165,12 +165,17 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
[ObservableProperty] private int _studentCount;
|
[ObservableProperty] private int _studentCount;
|
||||||
[ObservableProperty] private int _activeTabIndex = 0;
|
[ObservableProperty] private int _activeTabIndex = 0;
|
||||||
[ObservableProperty] private StudentSummary? _selectedStudent;
|
[ObservableProperty] private StudentSummary? _selectedStudent;
|
||||||
|
[ObservableProperty] private ExamSummary? _selectedExam;
|
||||||
|
|
||||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||||
|
|
||||||
public ParticipationTabViewModel ParticipationTab { get; }
|
public ParticipationTabViewModel ParticipationTab { get; }
|
||||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
|
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||||
|
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
|
||||||
|
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
||||||
|
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
|
||||||
|
|
||||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
||||||
@@ -190,9 +195,17 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
||||||
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
||||||
LoadStudents();
|
LoadStudents();
|
||||||
|
ReloadExams();
|
||||||
|
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReloadExams()
|
||||||
|
{
|
||||||
|
if (Group is null) return;
|
||||||
|
var selectedId = SelectedExam?.Id;
|
||||||
Exams.Clear();
|
Exams.Clear();
|
||||||
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
|
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
|
||||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadStudents()
|
public void LoadStudents()
|
||||||
@@ -240,7 +253,91 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
|
|
||||||
private bool HasSelectedStudent() => SelectedStudent is not null;
|
private bool HasSelectedStudent() => SelectedStudent is not null;
|
||||||
|
|
||||||
[RelayCommand] private void AddExam() { /* TODO */ }
|
[RelayCommand]
|
||||||
|
private async Task AddExam()
|
||||||
|
{
|
||||||
|
if (Group is null || OnAddExam is null) return;
|
||||||
|
var saved = await OnAddExam(Group.Id);
|
||||||
|
if (saved) ReloadExams();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
|
private async Task EditExam()
|
||||||
|
{
|
||||||
|
if (SelectedExam is null || OnEditExam is null) return;
|
||||||
|
var exam = _exams.GetById(SelectedExam.Id);
|
||||||
|
if (exam is null) return;
|
||||||
|
var id = exam.Id;
|
||||||
|
var saved = await OnEditExam(exam);
|
||||||
|
if (saved)
|
||||||
|
{
|
||||||
|
ReloadExams();
|
||||||
|
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
|
private async Task DuplicateExam()
|
||||||
|
{
|
||||||
|
if (SelectedExam is null || OnDuplicateExam is null) return;
|
||||||
|
var exam = _exams.GetById(SelectedExam.Id);
|
||||||
|
if (exam is null) return;
|
||||||
|
var saved = await OnDuplicateExam(exam);
|
||||||
|
if (saved) ReloadExams();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
|
private async Task DeleteExam()
|
||||||
|
{
|
||||||
|
if (SelectedExam is null || OnConfirmDeleteExam is null) return;
|
||||||
|
var selected = SelectedExam;
|
||||||
|
if (!await OnConfirmDeleteExam(selected)) return;
|
||||||
|
_exams.Delete(selected.Id);
|
||||||
|
SelectedExam = null;
|
||||||
|
ReloadExams();
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
|
private void AdvanceExamStatus()
|
||||||
|
{
|
||||||
|
if (SelectedExam is null) return;
|
||||||
|
SetExamStatus(SelectedExam.Status switch
|
||||||
|
{
|
||||||
|
ExamStatus.Planned => ExamStatus.Conducted,
|
||||||
|
ExamStatus.Conducted => ExamStatus.Graded,
|
||||||
|
ExamStatus.Graded => ExamStatus.Returned,
|
||||||
|
_ => SelectedExam.Status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||||
|
private void SetExamStatus(ExamStatus status)
|
||||||
|
{
|
||||||
|
if (SelectedExam is null) return;
|
||||||
|
var exam = _exams.GetById(SelectedExam.Id);
|
||||||
|
if (exam is null) return;
|
||||||
|
exam.Status = status;
|
||||||
|
if (status == ExamStatus.Returned)
|
||||||
|
exam.ReturnedAt ??= DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
else
|
||||||
|
exam.ReturnedAt = null;
|
||||||
|
_exams.Save(exam);
|
||||||
|
var id = exam.Id;
|
||||||
|
ReloadExams();
|
||||||
|
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnSelectedExamChanged(ExamSummary? value)
|
||||||
|
{
|
||||||
|
EditExamCommand.NotifyCanExecuteChanged();
|
||||||
|
DuplicateExamCommand.NotifyCanExecuteChanged();
|
||||||
|
DeleteExamCommand.NotifyCanExecuteChanged();
|
||||||
|
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
|
||||||
|
SetExamStatusCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasSelectedExam() => SelectedExam is not null;
|
||||||
|
|
||||||
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
|
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,10 +374,15 @@ public class ExamSummary
|
|||||||
public Guid Id { get; }
|
public Guid Id { get; }
|
||||||
public string Title { get; }
|
public string Title { get; }
|
||||||
public string Date { get; }
|
public string Date { get; }
|
||||||
|
public ExamStatus Status { get; }
|
||||||
public string StatusLabel { get; }
|
public string StatusLabel { get; }
|
||||||
|
public string StatusColorHex { get; }
|
||||||
|
public string ReturnedAtDisplay { get; }
|
||||||
|
|
||||||
public ExamSummary(Core.Models.Exam e)
|
public ExamSummary(Core.Models.Exam e)
|
||||||
{
|
{
|
||||||
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy");
|
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status;
|
||||||
|
ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
|
||||||
StatusLabel = e.Status switch
|
StatusLabel = e.Status switch
|
||||||
{
|
{
|
||||||
ExamStatus.Planned => "Geplant",
|
ExamStatus.Planned => "Geplant",
|
||||||
@@ -289,6 +391,14 @@ public class ExamSummary
|
|||||||
ExamStatus.Returned => "Zurückgegeben",
|
ExamStatus.Returned => "Zurückgegeben",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
|
StatusColorHex = e.Status switch
|
||||||
|
{
|
||||||
|
ExamStatus.Planned => "#9E9E9E",
|
||||||
|
ExamStatus.Conducted => "#FB8C00",
|
||||||
|
ExamStatus.Graded => "#43A047",
|
||||||
|
ExamStatus.Returned => "#1E88E5",
|
||||||
|
_ => "#9E9E9E",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -465,7 +575,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
|||||||
if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; }
|
if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; }
|
||||||
if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 1–13."; return; }
|
if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 1–13."; return; }
|
||||||
|
|
||||||
string? subjectName = IsKurs && !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null;
|
string? subjectName = !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null;
|
||||||
Guid? subjectId = null;
|
Guid? subjectId = null;
|
||||||
|
|
||||||
if (subjectName is not null)
|
if (subjectName is not null)
|
||||||
|
|||||||
@@ -25,8 +25,9 @@
|
|||||||
<TextBox Text="{Binding Name}" PlaceholderText="{Binding NameHint}"/>
|
<TextBox Text="{Binding Name}" PlaceholderText="{Binding NameHint}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Fach – nur bei Kurs, AutoComplete aus bekannten Fächern -->
|
<!-- Fach – AutoComplete aus bekannten Fächern; auch bei Klasse, da eine
|
||||||
<StackPanel Spacing="4" IsVisible="{Binding IsKurs}">
|
Lerngruppe genau ein Fach unterrichtet (bei zwei Fächern: zwei Gruppen anlegen) -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Fach (optional)" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Fach (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
<AutoCompleteBox Text="{Binding Subject}"
|
<AutoCompleteBox Text="{Binding Subject}"
|
||||||
ItemsSource="{Binding KnownSubjectNames}"
|
ItemsSource="{Binding KnownSubjectNames}"
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.DeleteExamDialog"
|
||||||
|
x:CompileBindings="False"
|
||||||
|
Title="Klausur löschen"
|
||||||
|
Width="430" SizeToContent="Height"
|
||||||
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<StackPanel Grid.Row="0" Spacing="12">
|
||||||
|
<TextBlock Text="Klausur wirklich löschen?" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding}" FontSize="15" FontWeight="SemiBold" TextWrapping="Wrap"/>
|
||||||
|
<TextBlock Text="Dabei werden auch alle erfassten Ergebnisse dieser Klausur dauerhaft gelöscht."
|
||||||
|
TextWrapping="Wrap" Opacity="0.7"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="Endgültig löschen" HorizontalAlignment="Stretch" Click="OnDelete"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class DeleteExamDialog : Window
|
||||||
|
{
|
||||||
|
public DeleteExamDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnDelete(object? sender, RoutedEventArgs e) => Close(true);
|
||||||
|
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.ExamDialog"
|
||||||
|
x:DataType="vm:ExamDialogViewModel"
|
||||||
|
Title="{Binding DialogTitle}"
|
||||||
|
Width="640" Height="680" MinWidth="560" MinHeight="420"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<ScrollViewer Grid.Row="0">
|
||||||
|
<StackPanel Spacing="14" Margin="0,0,12,0">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock Text="{Binding DialogTitle}" FontSize="18" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding SubjectDisplay}" FontSize="12" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Title}" PlaceholderText="z.B. 1. Klausur Kinetik"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,12,*,12,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/>
|
||||||
|
<NumericUpDown Value="{Binding ExamNumber}" Minimum="1" Maximum="20" FormatString="0"
|
||||||
|
ShowButtonSpinner="False"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="4" Spacing="4">
|
||||||
|
<TextBlock Text="Rückgabedatum" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding ReturnedAtText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Notizen" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding Notes}" AcceptsReturn="True" Height="56" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="Aufgaben" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Content="+ Aufgabe" Command="{Binding AddTaskCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CheckBox Content="Gewichtung verwenden" IsChecked="{Binding UseWeighting}"/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding TotalPointsDisplay}" FontSize="12" Opacity="0.7"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Achtung: noch keine Punkte vergeben" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding TotalPointsWarning}" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding Tasks}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ExamTaskEditItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="4" Padding="8" Margin="0,0,0,6">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<Grid ColumnDefinitions="26,*,Auto,Auto,Auto,Auto,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding Nr}" VerticalAlignment="Center" Opacity="0.6"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding Title}" PlaceholderText="Titel der Aufgabe" Margin="0,0,6,0"/>
|
||||||
|
<NumericUpDown Grid.Column="2" Value="{Binding MaxPoints}" Minimum="0" FormatString="0.##"
|
||||||
|
Width="72" ShowButtonSpinner="False" Margin="0,0,6,0" ToolTip.Tip="Maximalpunkte"/>
|
||||||
|
<NumericUpDown Grid.Column="3" Value="{Binding Weight}" Minimum="0" FormatString="0.##"
|
||||||
|
Width="72" ShowButtonSpinner="False" Margin="0,0,6,0" ToolTip.Tip="Gewichtung"
|
||||||
|
IsVisible="{Binding ShowWeight}"/>
|
||||||
|
<Button Grid.Column="4" Content="↑" Command="{Binding MoveUpCommand}" Padding="6,2"
|
||||||
|
ToolTip.Tip="Nach oben"/>
|
||||||
|
<Button Grid.Column="5" Content="↓" Command="{Binding MoveDownCommand}" Padding="6,2"
|
||||||
|
ToolTip.Tip="Nach unten" Margin="4,0,0,0"/>
|
||||||
|
<Button Grid.Column="6" Content="✕" Command="{Binding RemoveCommand}" Padding="6,2"
|
||||||
|
ToolTip.Tip="Entfernen" Margin="4,0,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Button Content="{Binding CompetencySummary}" Command="{Binding ToggleCompetencyPanelCommand}"
|
||||||
|
HorizontalAlignment="Left" FontSize="11" Padding="6,2"/>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding CompetencyTagGroups}" IsVisible="{Binding IsCompetencyPanelOpen}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:CompetencyTagGroup">
|
||||||
|
<StackPanel Margin="12,2">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" FontSize="11" Opacity="0.6"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding Items}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:CompetencyTag">
|
||||||
|
<ToggleButton Content="{Binding Display}" IsChecked="{Binding IsSelected}"
|
||||||
|
Margin="0,2,6,2" FontSize="11" Padding="6,2"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
<Button Grid.Column="2" Content="{Binding SaveButtonText}"
|
||||||
|
HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class ExamDialog : Window
|
||||||
|
{
|
||||||
|
public ExamDialog() => InitializeComponent();
|
||||||
|
|
||||||
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is ExamDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
if (vm.Result is not null) Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Groups"
|
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Groups"
|
||||||
|
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.GroupDetailView"
|
x:Class="LehrerApp.Desktop.Views.Groups.GroupDetailView"
|
||||||
x:DataType="vm:GroupDetailViewModel">
|
x:DataType="vm:GroupDetailViewModel">
|
||||||
|
|
||||||
@@ -66,7 +67,29 @@
|
|||||||
|
|
||||||
<!-- Tab: Klausuren -->
|
<!-- Tab: Klausuren -->
|
||||||
<ContentPage Header="Klausuren">
|
<ContentPage Header="Klausuren">
|
||||||
<DataGrid ItemsSource="{Binding Exams}"
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8"
|
||||||
|
IsVisible="{Binding SelectedExam, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
|
||||||
|
<Button Content="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
|
||||||
|
<SplitButton Content="Status ▸" Command="{Binding AdvanceExamStatusCommand}">
|
||||||
|
<SplitButton.Flyout>
|
||||||
|
<MenuFlyout Placement="BottomEdgeAlignedLeft">
|
||||||
|
<MenuItem Header="Geplant" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Planned}"/>
|
||||||
|
<MenuItem Header="Durchgeführt" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Conducted}"/>
|
||||||
|
<MenuItem Header="Korrigiert" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Graded}"/>
|
||||||
|
<MenuItem Header="Zurückgegeben" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Returned}"/>
|
||||||
|
</MenuFlyout>
|
||||||
|
</SplitButton.Flyout>
|
||||||
|
</SplitButton>
|
||||||
|
<Button Content="Löschen" Command="{Binding DeleteExamCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<DataGrid Grid.Row="1" ItemsSource="{Binding Exams}"
|
||||||
|
SelectedItem="{Binding SelectedExam}"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
GridLinesVisibility="Horizontal"
|
GridLinesVisibility="Horizontal"
|
||||||
@@ -74,9 +97,40 @@
|
|||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Datum" Binding="{Binding Date}" Width="110"/>
|
<DataGridTextColumn Header="Datum" Binding="{Binding Date}" Width="110"/>
|
||||||
<DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/>
|
<DataGridTextColumn Header="Titel" Binding="{Binding Title}" Width="*"/>
|
||||||
<DataGridTextColumn Header="Status" Binding="{Binding StatusLabel}" Width="130"/>
|
<DataGridTemplateColumn Header="Status" Width="150">
|
||||||
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ExamSummary">
|
||||||
|
<Border Background="{Binding StatusColorHex}" CornerRadius="4"
|
||||||
|
Padding="8,2" HorizontalAlignment="Left">
|
||||||
|
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
<DataGridTextColumn Header="Rückgabe" Binding="{Binding ReturnedAtDisplay}" Width="100"/>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
|
<DataGrid.ContextMenu>
|
||||||
|
<ContextMenu>
|
||||||
|
<MenuItem Header="Bearbeiten" Command="{Binding EditExamCommand}"/>
|
||||||
|
<MenuItem Header="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
|
||||||
|
<MenuItem Header="Status">
|
||||||
|
<MenuItem Header="Weiter" Command="{Binding AdvanceExamStatusCommand}"/>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="Geplant" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Planned}"/>
|
||||||
|
<MenuItem Header="Durchgeführt" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Conducted}"/>
|
||||||
|
<MenuItem Header="Korrigiert" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Graded}"/>
|
||||||
|
<MenuItem Header="Zurückgegeben" Command="{Binding SetExamStatusCommand}"
|
||||||
|
CommandParameter="{x:Static models:ExamStatus.Returned}"/>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="Löschen" Command="{Binding DeleteExamCommand}"/>
|
||||||
|
</ContextMenu>
|
||||||
|
</DataGrid.ContextMenu>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Noten -->
|
<!-- Tab: Noten -->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -13,7 +14,13 @@ public partial class GroupDetailView : UserControl
|
|||||||
{
|
{
|
||||||
base.OnDataContextChanged(e);
|
base.OnDataContextChanged(e);
|
||||||
if (DataContext is GroupDetailViewModel vm)
|
if (DataContext is GroupDetailViewModel vm)
|
||||||
|
{
|
||||||
vm.OnAddStudent = ShowAddStudentDialog;
|
vm.OnAddStudent = ShowAddStudentDialog;
|
||||||
|
vm.OnAddExam = ShowAddExamDialog;
|
||||||
|
vm.OnEditExam = ShowEditExamDialog;
|
||||||
|
vm.OnDuplicateExam = ShowDuplicateExamDialog;
|
||||||
|
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> ShowAddStudentDialog()
|
private async Task<bool> ShowAddStudentDialog()
|
||||||
@@ -32,4 +39,37 @@ public partial class GroupDetailView : UserControl
|
|||||||
|
|
||||||
return await dialog.ShowDialog<bool>(owner);
|
return await dialog.ShowDialog<bool>(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task<bool> ShowAddExamDialog(Guid groupId) =>
|
||||||
|
ShowExamDialog(groupId, editingExam: null, duplicateSource: null);
|
||||||
|
|
||||||
|
private Task<bool> ShowEditExamDialog(Exam exam) =>
|
||||||
|
ShowExamDialog(exam.GroupId, editingExam: exam, duplicateSource: null);
|
||||||
|
|
||||||
|
private Task<bool> ShowDuplicateExamDialog(Exam exam) =>
|
||||||
|
ShowExamDialog(exam.GroupId, editingExam: null, duplicateSource: exam);
|
||||||
|
|
||||||
|
private async Task<bool> ShowExamDialog(Guid groupId, Exam? editingExam, Exam? duplicateSource)
|
||||||
|
{
|
||||||
|
if (DataContext is not GroupDetailViewModel vm || vm.Group is null) return false;
|
||||||
|
|
||||||
|
var dialogVm = new ExamDialogViewModel(
|
||||||
|
App.Services.GetRequiredService<IExamRepository>(),
|
||||||
|
App.Services.GetRequiredService<ICompetencyDomainRepository>(),
|
||||||
|
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem,
|
||||||
|
vm.Group.Subject ?? "", editingExam, duplicateSource);
|
||||||
|
|
||||||
|
var dialog = new ExamDialog { DataContext = dialogVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return false;
|
||||||
|
|
||||||
|
return await dialog.ShowDialog<bool>(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowDeleteExamDialog(ExamSummary exam)
|
||||||
|
{
|
||||||
|
var dialog = new DeleteExamDialog { DataContext = exam.Title };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,20 +32,21 @@ Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits i
|
|||||||
[Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt.
|
[Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt.
|
||||||
|
|
||||||
### 1.1 Klausur anlegen und verwalten
|
### 1.1 Klausur anlegen und verwalten
|
||||||
- [ ] **1.1.1** Dialog `AddExamDialog` — Titel, Datum, Fach, Klausurnummer, Notizen.
|
- [x] **1.1.1** Dialog `AddExamDialog` — Titel, Datum, Fach, Klausurnummer, Notizen.
|
||||||
Ersetzt den TODO-Stub `AddExam()` in [GroupViewModels.cs:189](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs#L189).
|
Ersetzt den TODO-Stub `AddExam()` in [GroupViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs).
|
||||||
- [ ] **1.1.2** Klausur bearbeiten/löschen (Kontextmenü im Klausuren-Tab, Löschen mit Rückfrage).
|
Umgesetzt als `ExamDialog`/`ExamDialogViewModel`.
|
||||||
- [ ] **1.1.3** Statuswechsel `Planned → Conducted → Graded → Returned` per Button,
|
- [x] **1.1.2** Klausur bearbeiten/löschen (Kontextmenü im Klausuren-Tab, Löschen mit Rückfrage).
|
||||||
|
- [x] **1.1.3** Statuswechsel `Planned → Conducted → Graded → Returned` per Button,
|
||||||
inkl. Farbcodierung des Status im DataGrid.
|
inkl. Farbcodierung des Status im DataGrid.
|
||||||
- [ ] **1.1.4** Klausur aus bestehender Klausur duplizieren (Aufgaben + Notenschlüssel übernehmen,
|
- [x] **1.1.4** Klausur aus bestehender Klausur duplizieren (Aufgaben + Notenschlüssel übernehmen,
|
||||||
neues Datum) — für Parallelkurse.
|
neues Datum) — für Parallelkurse.
|
||||||
|
|
||||||
### 1.2 Aufgabenstruktur (`ExamTask`)
|
### 1.2 Aufgabenstruktur (`ExamTask`)
|
||||||
- [ ] **1.2.1** Editor für die Aufgabenliste: Nr., Titel, Maximalpunkte, Gewichtung.
|
- [x] **1.2.1** Editor für die Aufgabenliste: Nr., Titel, Maximalpunkte, Gewichtung.
|
||||||
Zeilen hinzufügen/entfernen/umsortieren.
|
Zeilen hinzufügen/entfernen/umsortieren. Umgesetzt im `ExamDialog` (Teil von 1.1.1).
|
||||||
- [ ] **1.2.2** Automatische Anzeige der Gesamtpunktzahl, Warnung bei 0 Punkten.
|
- [x] **1.2.2** Automatische Anzeige der Gesamtpunktzahl, Warnung bei 0 Punkten.
|
||||||
- [ ] **1.2.3** Optional: Teilaufgaben (a/b/c) — erfordert Modellerweiterung, vorher entscheiden.
|
- [ ] **1.2.3** Optional: Teilaufgaben (a/b/c) — erfordert Modellerweiterung, vorher entscheiden.
|
||||||
- [ ] **1.2.4** Zuordnung von Kompetenzen (`CompetencyItem`) zu einzelnen Aufgaben —
|
- [x] **1.2.4** Zuordnung von Kompetenzen (`CompetencyItem`) zu einzelnen Aufgaben —
|
||||||
Voraussetzung für die Kompetenzauswertung in 8.3.
|
Voraussetzung für die Kompetenzauswertung in 8.3.
|
||||||
|
|
||||||
### 1.3 Notenschlüssel
|
### 1.3 Notenschlüssel
|
||||||
|
|||||||
Reference in New Issue
Block a user