Notenverwaltung (Kapitel 2) und Mitarbeits-Assistent
Notenübersicht der Gruppe (Matrix, Gesamt-Spalte, Sortierung, Halbjahresfilter), Einzelnoten-Pflege samt Sammelerfassung, Gewichtungsschema mit Voreinstellung je Gruppentyp, Zeugnisnoten-Berechnung mit Übersteuern/Festschreiben/Export und Notenentwicklung im Schülerdetail. Dazu Anwesenheits-/Hausaufgaben-Tracking je Mitarbeit-Sitzung, ein neuer Mitarbeits-Assistent (Zeitleiste mit Abschnitten, automatischer Notenvorschlag, Zusammenzug zur Halbjahresnote) und eine Dashboard-Kachel für offene Entschuldigungen. Außerdem: verbliebene englische Begriffe in Auswahlfeldern und Buttons auf Deutsch umgestellt.
This commit is contained in:
@@ -2,7 +2,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Students;
|
||||
|
||||
@@ -72,6 +74,9 @@ public partial class StudentDetailViewModel : ObservableObject
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly IDocumentationRepository _docs;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IExamResultRepository _examResults;
|
||||
private readonly IGradeRepository _grades;
|
||||
|
||||
[ObservableProperty] private Student? _student;
|
||||
[ObservableProperty] private string _studentTitle = "";
|
||||
@@ -83,16 +88,19 @@ public partial class StudentDetailViewModel : ObservableObject
|
||||
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
|
||||
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
||||
public ObservableCollection<ContactItem> Contacts { get; } = [];
|
||||
public ObservableCollection<StudentGradeHistoryGroup> GradeHistory { get; } = [];
|
||||
public bool HasNoContacts => Contacts.Count == 0;
|
||||
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
|
||||
public Action<ContactItem>? OnViewAddress { get; set; }
|
||||
|
||||
public StudentDetailViewModel(IStudentRepository students,
|
||||
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
|
||||
IDocumentationRepository docs)
|
||||
IDocumentationRepository docs, IExamRepository exams, IExamResultRepository examResults,
|
||||
IGradeRepository grades)
|
||||
{
|
||||
_students = students; _memberships = memberships;
|
||||
_groups = groups; _subjects = subjects; _docs = docs;
|
||||
_exams = exams; _examResults = examResults; _grades = grades;
|
||||
}
|
||||
|
||||
public void LoadStudent(Guid id)
|
||||
@@ -104,12 +112,16 @@ public partial class StudentDetailViewModel : ObservableObject
|
||||
EditLastName = Student.LastName;
|
||||
|
||||
GroupMemberships.Clear();
|
||||
GradeHistory.Clear();
|
||||
foreach (var membership in _memberships.GetByStudent(Student.Id))
|
||||
{
|
||||
var g = _groups.GetById(membership.GroupId);
|
||||
if (g is null) continue;
|
||||
var subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
|
||||
GroupMemberships.Add(new() { SchoolYear = g.SchoolYear, GroupName = g.Name, Subject = subject });
|
||||
|
||||
var historyGroup = BuildGradeHistory(g, subject);
|
||||
if (historyGroup is not null) GradeHistory.Add(historyGroup);
|
||||
}
|
||||
|
||||
LoadContacts();
|
||||
@@ -128,6 +140,49 @@ public partial class StudentDetailViewModel : ObservableObject
|
||||
IsConfidential = d.IsConfidential });
|
||||
}
|
||||
|
||||
// ── Notenentwicklung (2.5) ────────────────────────────────────────────────
|
||||
|
||||
private StudentGradeHistoryGroup? BuildGradeHistory(LearningGroup g, string subject)
|
||||
{
|
||||
var label = string.IsNullOrEmpty(subject) ? g.Name : $"{g.Name} · {subject}";
|
||||
var entries = new List<(DateOnly Date, string PointLabel, string Value)>();
|
||||
|
||||
foreach (var exam in _exams.GetByGroup(g.Id))
|
||||
{
|
||||
var result = _examResults.GetByExamAndStudent(exam.Id, Student!.Id);
|
||||
if (result is null || result.Absent || result.Grade is null) continue;
|
||||
entries.Add((exam.Date, exam.Title, result.Grade));
|
||||
}
|
||||
foreach (var grade in _grades.GetByStudentAndGroup(Student!.Id, g.Id))
|
||||
entries.Add((grade.Date, GradeCategoryDisplay.Label(grade.Category), grade.Value));
|
||||
|
||||
var ordered = entries.OrderBy(e => e.Date).ToList();
|
||||
if (ordered.Count == 0) return null;
|
||||
|
||||
var historyGroup = new StudentGradeHistoryGroup(label);
|
||||
int? previousNoteEquivalent = null;
|
||||
foreach (var e in ordered)
|
||||
{
|
||||
int? noteEquivalent = int.TryParse(e.Value, out var raw)
|
||||
? (g.GradingSystem == GradingSystem.Grades1To6 ? raw : int.Parse(PointsNoteMapping.PointsToNote(raw)))
|
||||
: null;
|
||||
|
||||
var isFailing = noteEquivalent is >= 5;
|
||||
var isDrop = noteEquivalent.HasValue && previousNoteEquivalent.HasValue
|
||||
&& noteEquivalent.Value - previousNoteEquivalent.Value >= 1;
|
||||
|
||||
var warnings = new List<string>();
|
||||
if (isDrop) warnings.Add("Abfall um ≥ 1 Note");
|
||||
if (isFailing) warnings.Add("Versetzungsgefährdung");
|
||||
|
||||
historyGroup.Points.Add(new GradeHistoryPoint(e.Date, e.PointLabel, e.Value,
|
||||
noteEquivalent, warnings.Count > 0, string.Join(" · ", warnings)));
|
||||
|
||||
if (noteEquivalent.HasValue) previousNoteEquivalent = noteEquivalent;
|
||||
}
|
||||
return historyGroup;
|
||||
}
|
||||
|
||||
[RelayCommand] private void StartEdit() => IsEditing = true;
|
||||
[RelayCommand] private void CancelEdit()
|
||||
{
|
||||
@@ -200,6 +255,43 @@ public partial class StudentDetailViewModel : ObservableObject
|
||||
public class GroupMembershipEntry { public string SchoolYear { get; set; } = ""; public string GroupName { get; set; } = ""; public string Subject { get; set; } = ""; }
|
||||
public class DocEntry { public string Date { get; set; } = ""; public string Title { get; set; } = ""; public string TypeLabel { get; set; } = ""; public bool IsConfidential { get; set; } }
|
||||
|
||||
// ── Notenentwicklung (2.5) ──────────────────────────────────────────────────
|
||||
|
||||
public class StudentGradeHistoryGroup(string label)
|
||||
{
|
||||
public string Label { get; } = label;
|
||||
public ObservableCollection<GradeHistoryPoint> Points { get; } = [];
|
||||
public bool HasWarnings => Points.Any(p => p.IsWarning);
|
||||
}
|
||||
|
||||
public class GradeHistoryPoint
|
||||
{
|
||||
public string DateDisplay { get; }
|
||||
public string Label { get; }
|
||||
public string Value { get; }
|
||||
public double BarHeight { get; }
|
||||
public bool IsWarning { get; }
|
||||
public string WarningText { get; }
|
||||
public string TooltipText { get; }
|
||||
|
||||
public GradeHistoryPoint(DateOnly date, string label, string value, int? noteEquivalent,
|
||||
bool isWarning, string warningText)
|
||||
{
|
||||
DateDisplay = date.ToString("dd.MM.", CultureInfo.InvariantCulture);
|
||||
Label = label;
|
||||
Value = value;
|
||||
IsWarning = isWarning;
|
||||
WarningText = warningText;
|
||||
// Balkenhöhe nach Notenqualität (1 = beste Note) auf 6..60px, sonst neutrale Mindesthöhe.
|
||||
BarHeight = noteEquivalent.HasValue
|
||||
? 6 + Math.Clamp((6 - noteEquivalent.Value) / 5.0, 0, 1) * 54
|
||||
: 6;
|
||||
TooltipText = warningText.Length > 0
|
||||
? $"{date:dd.MM.yyyy} · {label}: {value} ⚠ {warningText}"
|
||||
: $"{date:dd.MM.yyyy} · {label}: {value}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ContactItem
|
||||
{
|
||||
public Contact Model { get; }
|
||||
|
||||
Reference in New Issue
Block a user