Mitarbeits Wizzard und neue Statuslabels für Anwesenheit und Hausaufgaben

This commit is contained in:
2026-08-13 01:49:40 +02:00
parent febcb0855a
commit 741586846a
10 changed files with 1160 additions and 144 deletions
@@ -40,6 +40,15 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
[ObservableProperty] private string _sectionValidationMessage = "";
[ObservableProperty] private ParticipationPeriodOption _rollupPeriod;
[ObservableProperty] private string _rollupStatusMessage = "";
[ObservableProperty] private double _timelineZoom = 1.0;
[ObservableProperty] private bool _showQuality = true;
[ObservableProperty] private bool _showQuantity = true;
[ObservableProperty] private bool _showWorkphase = true;
[ObservableProperty] private bool _showDataPoints = true;
[ObservableProperty] private bool _showWeightedTrend = true;
[ObservableProperty] private bool _showQualityTrend;
[ObservableProperty] private bool _showQuantityTrend;
[ObservableProperty] private bool _showWorkphaseTrend;
public List<ParticipationPeriodOption> RollupPeriodOptions { get; } =
[
@@ -68,8 +77,10 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
GroupLabel = groupLabel;
_aspectWeights = aspects.GetDefaults().Concat(aspects.GetByGroup(groupId))
.ToDictionary(a => a.Key, a => a.Weight);
_aspectWeights = aspects.GetDefaults()
.Concat(aspects.GetByGroup(groupId))
.GroupBy(a => a.Key)
.ToDictionary(g => g.Key, g => g.Last().Weight);
_students = students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToList();
_allSessions = sessions.GetByGroup(groupId).OrderBy(s => s.Date).ToList();
_sectionList = sectionRepo.GetByGroup(groupId).OrderBy(s => s.StartDate).ToList();
@@ -120,6 +131,11 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
if (result is null) continue;
points.Add((exam.Date, BuildExamPoint(exam, result)));
}
foreach (var grade in _grades.GetByStudentAndGroup(studentId, _groupId)
.Where(g => g.Category != GradeCategory.Participation))
{
points.Add((grade.Date, BuildOtherGradePoint(grade)));
}
points = points.OrderBy(p => p.Date).ToList();
Timeline.Clear();
@@ -140,7 +156,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
private WizardTimelinePoint BuildSessionPoint(ParticipationSession session, ParticipationEntry? entry, Guid studentId)
{
var ratingLabel = entry is not null ? WeightedRatingLabel(entry) : "";
var weightedValue = entry is not null ? WeightedRating(entry) : null;
var ratingLabel = weightedValue is not null ? RatingLabel((int)Math.Round(weightedValue.Value, MidpointRounding.AwayFromZero)) : "";
var note = entry?.Note;
var tooltip = $"{session.Date:dd.MM.yyyy}" +
(ratingLabel.Length > 0 ? $" · {ratingLabel}" : "") +
@@ -149,9 +166,14 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
var point = new WizardTimelinePoint(session.Date, isExam: false, ratingLabel, examLabel: "",
hasNote: !string.IsNullOrWhiteSpace(note), tooltip: tooltip)
{
HasHomework = entry?.HomeworkMissing ?? false,
Homework = entry is null ? null : HomeworkDisplay.Effective(entry),
AttendanceIcon = AttendanceDisplay.ShortLabel(entry?.Attendance),
AttendanceTooltip = AttendanceDisplay.Label(entry?.Attendance),
AttendanceColor = AttendanceDisplay.Color(entry?.Attendance),
AspectValues = entry?.Ratings.ToDictionary(r => r.Key, r => r.Value)
?? new Dictionary<string, int>(),
WeightedValue = weightedValue,
OverallGrade = weightedValue is null ? "" : _grading.ParticipationGrade(weightedValue.Value, _gradingSystem),
};
point.ToggleHomeworkCommand = new RelayCommand(() => ToggleHomeworkAt(session.Id, studentId, point));
point.CycleAttendanceCommand = new RelayCommand(() => CycleAttendanceAt(session.Id, studentId, point));
@@ -165,9 +187,24 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
hasNote: false, tooltip: $"{exam.Date:dd.MM.yyyy} · Klausur {examLabel}");
}
private string WeightedRatingLabel(ParticipationEntry entry)
private static WizardTimelinePoint BuildOtherGradePoint(Grade grade)
{
if (entry.Ratings.Count == 0) return "";
var category = grade.Category switch
{
GradeCategory.Oral => "Mündlich",
GradeCategory.Homework => "Hausaufgabe",
GradeCategory.Project => "Projekt",
_ => "Sonstige Leistung",
};
var detail = string.IsNullOrWhiteSpace(grade.Note) ? category : grade.Note.Trim();
var label = $"{detail}: {grade.Value}";
return new WizardTimelinePoint(grade.Date, isExam: true, ratingLabel: "", examLabel: label,
hasNote: false, tooltip: $"{grade.Date:dd.MM.yyyy} · {category} · {label}");
}
private double? WeightedRating(ParticipationEntry entry)
{
if (entry.Ratings.Count == 0) return null;
var weightSum = 0.0; var valueSum = 0.0;
foreach (var r in entry.Ratings)
{
@@ -175,8 +212,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
if (w <= 0) continue;
valueSum += r.Value * w; weightSum += w;
}
if (weightSum <= 0) return "";
return RatingLabel((int)Math.Round(valueSum / weightSum, MidpointRounding.AwayFromZero));
return weightSum <= 0 ? null : valueSum / weightSum;
}
private static string RatingLabel(int v) => v switch
@@ -188,9 +224,10 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
{
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
entry.HomeworkMissing = !entry.HomeworkMissing;
entry.Homework = HomeworkDisplay.Next(HomeworkDisplay.Effective(entry));
entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(entry.Homework);
_entries.Save(entry);
point.HasHomework = entry.HomeworkMissing;
point.Homework = entry.Homework;
}
private void CycleAttendanceAt(Guid sessionId, Guid studentId, WizardTimelinePoint point)
@@ -199,15 +236,21 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
entry.Attendance = entry.Attendance switch
{
null => AttendanceStatus.ExcusePending,
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
AttendanceStatus.Unexcused => null,
_ => null,
null => AttendanceStatus.Present,
AttendanceStatus.Present => AttendanceStatus.ExcusePending,
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
AttendanceStatus.Unexcused => AttendanceStatus.Truant,
AttendanceStatus.Truant => AttendanceStatus.OtherSchoolEvent,
AttendanceStatus.OtherSchoolEvent => null,
_ => null,
};
_entries.Save(entry);
point.AttendanceIcon = AttendanceDisplay.ShortLabel(entry.Attendance);
// Das Setzen jeder ObservableProperty kann unmittelbar ein Neuzeichnen auslösen.
// Deshalb muss die zum aktiven Symbol gehörende Farbe zuerst bereitstehen.
point.AttendanceColor = AttendanceDisplay.Color(entry.Attendance);
point.AttendanceTooltip = AttendanceDisplay.Label(entry.Attendance);
point.AttendanceIcon = AttendanceDisplay.ShortLabel(entry.Attendance);
}
// ── Abschnitte ────────────────────────────────────────────────────────────
@@ -364,19 +407,36 @@ public class WizardSectionGroup(string bandLabel, bool isOpen)
public partial class WizardTimelinePoint : ObservableObject
{
public DateOnly Date { get; }
public string DateDisplay { get; }
public bool IsExam { get; }
public string RatingLabel { get; }
public string ExamLabel { get; }
public bool HasNote { get; }
public string TooltipText { get; }
public IReadOnlyDictionary<string, int> AspectValues { get; init; } = new Dictionary<string, int>();
public double? WeightedValue { get; init; }
public string OverallGrade { get; init; } = "";
[ObservableProperty] private bool _hasHomework;
[ObservableProperty] private HomeworkStatus? _homework;
[ObservableProperty] private string _attendanceIcon = "";
[ObservableProperty] private string _attendanceTooltip = "Anwesend";
[ObservableProperty] private string _attendanceColor = "";
public bool IsAbsent => AttendanceIcon.Length > 0;
public string AttendanceButtonLabel => AttendanceIcon.Length > 0 ? AttendanceIcon : "Anw";
public string AttendanceButtonLabel => AttendanceIcon.Length > 0 ? AttendanceIcon : "·";
public bool HasHomeworkStatus => Homework is not null;
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
public string HomeworkColor => HomeworkDisplay.Color(Homework);
partial void OnHomeworkChanged(HomeworkStatus? value)
{
OnPropertyChanged(nameof(HasHomeworkStatus));
OnPropertyChanged(nameof(HomeworkSymbol));
OnPropertyChanged(nameof(HomeworkTooltip));
OnPropertyChanged(nameof(HomeworkColor));
}
partial void OnAttendanceIconChanged(string value)
{
@@ -390,6 +450,7 @@ public partial class WizardTimelinePoint : ObservableObject
public WizardTimelinePoint(DateOnly date, bool isExam, string ratingLabel, string examLabel,
bool hasNote, string tooltip)
{
Date = date;
DateDisplay = date.ToString("dd.MM.", CultureInfo.InvariantCulture);
IsExam = isExam;
RatingLabel = ratingLabel;