Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs
T

249 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
using System.Text;
namespace LehrerApp.Desktop.ViewModels.Groups;
// ── Zeugnisnote (2.4) ─────────────────────────────────────────────────────────
public partial class ReportGradeDialogViewModel : ObservableObject
{
private readonly IGradeRepository _grades;
private readonly IExamRepository _exams;
private readonly IExamResultRepository _results;
private readonly IStudentRepository _students;
private readonly IGroupMembershipRepository _memberships;
private readonly IGradingSchemeRepository _schemes;
private readonly IReportGradeRepository _reportGrades;
private readonly GradingService _grading;
private readonly Guid _groupId;
private readonly GroupType _groupType;
private readonly GradingSystem _gradingSystem;
private readonly string _groupLabel;
private readonly string _schoolYear;
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
[ObservableProperty] private RoundingRule _roundingRule = RoundingRule.Commercial;
[ObservableProperty] private string _schemeSummary = "";
public string GroupLabel => _groupLabel;
public List<ParticipationPeriodOption> PeriodOptions { get; } =
[
new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"),
new(ParticipationPeriod.H1, "1. Halbjahr"),
new(ParticipationPeriod.H2, "2. Halbjahr"),
];
public string[] RoundingOptions { get; } = RoundingRuleDisplay.Options;
// Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens.
public string RoundingRuleName
{
get => RoundingRuleDisplay.Label(RoundingRule);
set => RoundingRule = RoundingRuleDisplay.FromLabel(value);
}
public ObservableCollection<ReportGradeRow> Rows { get; } = [];
public ReportGradeDialogViewModel(IGradeRepository grades, IExamRepository exams,
IExamResultRepository results, IStudentRepository students, IGroupMembershipRepository memberships,
IGradingSchemeRepository schemes, IReportGradeRepository reportGrades, GradingService grading,
Guid groupId, GroupType groupType, GradingSystem gradingSystem, string groupLabel, string schoolYear)
{
_grades = grades; _exams = exams; _results = results; _students = students;
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
_schoolYear = schoolYear;
_selectedPeriod = PeriodOptions[0];
Recompute();
}
partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute();
partial void OnRoundingRuleChanged(RoundingRule value) => Recompute();
private GradingScheme ResolveScheme() =>
_schemes.GetByGroup(_groupId)
?? _schemes.GetDefaultForType(_groupType)
?? new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
private void Recompute()
{
var scheme = ResolveScheme();
SchemeSummary = $"Klausuren {scheme.ExamsPercent:0.#} % · Mitarbeit {scheme.ParticipationPercent:0.#} % · " +
$"Sonstige {scheme.OtherPercent:0.#} %";
var period = SelectedPeriod.Period;
var periodTag = SelectedPeriod.Label;
var (periodFrom, periodTo) = GroupMembershipService.SchoolYearPeriod(_schoolYear, period switch
{
ParticipationPeriod.H1 => SchoolYearPeriodKind.H1,
ParticipationPeriod.H2 => SchoolYearPeriodKind.H2,
_ => SchoolYearPeriodKind.FullYear,
});
var students = _students.GetByGroup(_groupId);
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
var exams = _exams.GetByGroup(_groupId).Where(e => e.Date >= periodFrom && e.Date <= periodTo).ToList();
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
var allGrades = _grades.GetByGroup(_groupId).Where(g => g.Date >= periodFrom && g.Date <= periodTo).ToList();
Rows.Clear();
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
{
membershipsByStudent.TryGetValue(student.Id, out var membership);
if (membership is not null && !GroupMembershipService.Overlaps(membership, periodFrom, periodTo)) continue;
var existing = _reportGrades.GetByStudentGroupPeriod(student.Id, _groupId, periodTag);
if (existing is { IsLocked: true })
{
Rows.Add(ReportGradeRow.FromLocked(student.Id, student.FullName, existing, Save, ToggleLock));
continue;
}
var examGrades = new List<(string Grade, double Weight)>();
foreach (var exam in exams)
{
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, exam.Date)) continue;
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
if (!resultsByExam[exam.Id].TryGetValue(student.Id, out var result)) continue;
if (result.Absent || result.Grade is null) continue;
examGrades.Add((result.Grade, 1.0));
}
var participationGrades = allGrades
.Where(g => g.StudentId == student.Id && g.Category == GradeCategory.Participation
&& (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date)))
.Select(g => (g.Value, g.Weight)).ToList();
var otherGrades = allGrades
.Where(g => g.StudentId == student.Id && g.Category != GradeCategory.Participation
&& (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date)))
.Select(g => (g.Value, g.Weight)).ToList();
var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades,
scheme, _gradingSystem, RoundingRule);
Rows.Add(ReportGradeRow.FromCalculated(student.Id, student.FullName, calculated, existing, Save, ToggleLock));
}
}
private void Save(ReportGradeRow row)
{
if (!string.IsNullOrWhiteSpace(row.OverrideValue) && string.IsNullOrWhiteSpace(row.OverrideReason))
{
row.ValidationMessage = "Für ein manuelles Übersteuern ist eine Begründung Pflicht.";
return;
}
row.ValidationMessage = "";
var record = _reportGrades.GetByStudentGroupPeriod(row.StudentId, _groupId, SelectedPeriod.Label)
?? new ReportGrade { StudentId = row.StudentId, GroupId = _groupId, Period = SelectedPeriod.Label };
record.CalculatedValue = row.CalculatedValue ?? "";
record.OverrideValue = string.IsNullOrWhiteSpace(row.OverrideValue) ? null : row.OverrideValue.Trim();
record.OverrideReason = string.IsNullOrWhiteSpace(row.OverrideReason) ? null : row.OverrideReason.Trim();
_reportGrades.Save(record);
row.MarkSaved();
}
private void ToggleLock(ReportGradeRow row)
{
var record = _reportGrades.GetByStudentGroupPeriod(row.StudentId, _groupId, SelectedPeriod.Label);
if (record is null)
{
if (!row.IsLocked)
{
// Festschreiben ohne vorherigen Save: aktuellen Stand zuerst sichern.
Save(row);
record = _reportGrades.GetByStudentGroupPeriod(row.StudentId, _groupId, SelectedPeriod.Label);
if (record is null) return;
}
else return;
}
record.IsLocked = !record.IsLocked;
_reportGrades.Save(record);
Recompute();
}
public string ExportCsv()
{
var sb = new StringBuilder();
sb.AppendLine($"Zeugnisnoten;{_groupLabel};{SelectedPeriod.Label}");
sb.AppendLine("Schüler;Berechnet;Übersteuert;Begründung;Endnote;Gesperrt");
foreach (var r in Rows)
sb.AppendLine($"{r.Name};{r.CalculatedValue};{r.OverrideValue};{r.OverrideReason};{r.FinalDisplay};{(r.IsLocked ? "ja" : "")}");
return sb.ToString();
}
}
// ── Rundungsregel-Anzeige ─────────────────────────────────────────────────────
public static class RoundingRuleDisplay
{
public static string Label(RoundingRule r) => r switch
{
RoundingRule.Commercial => "Kaufmännisch",
RoundingRule.Pedagogical => "Pädagogisch",
_ => r.ToString(),
};
public static string[] Options { get; } = [Label(RoundingRule.Commercial), Label(RoundingRule.Pedagogical)];
public static RoundingRule FromLabel(string? label) =>
label == Label(RoundingRule.Pedagogical) ? RoundingRule.Pedagogical : RoundingRule.Commercial;
}
// ── Zeile: Zeugnisnote eines Schülers ────────────────────────────────────────
public partial class ReportGradeRow : ObservableObject
{
public Guid StudentId { get; }
public string Name { get; }
public string? CalculatedValue { get; private set; }
public bool IsLocked { get; private set; }
[ObservableProperty] private string? _overrideValue;
[ObservableProperty] private string? _overrideReason;
[ObservableProperty] private string _validationMessage = "";
public string CalculatedDisplay => CalculatedValue ?? "";
public string FinalDisplay => !string.IsNullOrWhiteSpace(OverrideValue) ? OverrideValue! : CalculatedDisplay;
public string LockLabel => IsLocked ? "Entsperren" : "Festschreiben";
public IRelayCommand SaveCommand { get; }
public IRelayCommand ToggleLockCommand { get; }
private ReportGradeRow(Guid studentId, string name, string? calculated, ReportGrade? existing,
bool locked, Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock)
{
StudentId = studentId;
Name = name;
CalculatedValue = calculated;
IsLocked = locked;
_overrideValue = existing?.OverrideValue;
_overrideReason = existing?.OverrideReason;
SaveCommand = new RelayCommand(() => onSave(this));
ToggleLockCommand = new RelayCommand(() => onToggleLock(this));
}
public static ReportGradeRow FromCalculated(Guid studentId, string name, string? calculated,
ReportGrade? existing, Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock) =>
new(studentId, name, calculated, existing, existing?.IsLocked ?? false, onSave, onToggleLock);
public static ReportGradeRow FromLocked(Guid studentId, string name, ReportGrade locked,
Action<ReportGradeRow> onSave, Action<ReportGradeRow> onToggleLock) =>
new(studentId, name, locked.CalculatedValue, locked, true, onSave, onToggleLock);
public void MarkSaved()
{
OnPropertyChanged(nameof(FinalDisplay));
}
partial void OnOverrideValueChanged(string? value) => OnPropertyChanged(nameof(FinalDisplay));
}