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:
@@ -56,6 +56,20 @@ public interface IGradeRepository
|
||||
void Save(Grade grade);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface IGradingSchemeRepository
|
||||
{
|
||||
GradingScheme? GetByGroup(Guid groupId);
|
||||
GradingScheme? GetDefaultForType(GroupType type);
|
||||
void Save(GradingScheme scheme);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface IReportGradeRepository
|
||||
{
|
||||
List<ReportGrade> GetByGroup(Guid groupId);
|
||||
ReportGrade? GetByStudentGroupPeriod(Guid studentId, Guid groupId, string period);
|
||||
void Save(ReportGrade grade);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface IUnitRepository
|
||||
{
|
||||
Unit? GetById(Guid id);
|
||||
@@ -116,6 +130,12 @@ public interface IParticipationAspectRepository
|
||||
void Save(ParticipationAspect aspect);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface IParticipationSectionRepository
|
||||
{
|
||||
List<ParticipationSection> GetByGroup(Guid groupId);
|
||||
void Save(ParticipationSection section);
|
||||
void Delete(Guid id);
|
||||
}
|
||||
public interface ISubjectRepository
|
||||
{
|
||||
List<Subject> GetAll();
|
||||
|
||||
@@ -20,9 +20,34 @@ public class ParticipationEntry
|
||||
public List<AspectRating> Ratings { get; set; } = [];
|
||||
public List<CompetencyRating> CompetencyRatings { get; set; } = [];
|
||||
public string? Note { get; set; }
|
||||
public bool HomeworkMissing { get; set; }
|
||||
public AttendanceStatus? Attendance { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anwesenheitsstatus einer Sitzung; null (Standard) bedeutet anwesend.
|
||||
/// <see cref="ExcusePending"/> ist ein bewusster Zwischenzustand, da die Entschuldigung meist
|
||||
/// erst später eintrifft — er wird beim Erfassen des Fehltags gesetzt und danach manuell auf
|
||||
/// <see cref="Excused"/> oder <see cref="Unexcused"/> nachgetragen.
|
||||
/// </summary>
|
||||
public enum AttendanceStatus { ExcusePending, Excused, Unexcused }
|
||||
|
||||
/// <summary>
|
||||
/// Ein Bewertungsabschnitt einer Lerngruppe (z.B. alle 4–7 Wochen), an dessen Ende eine
|
||||
/// Abschnittsnote Mitarbeit vergeben wird. Nur abgeschlossene Abschnitte werden gespeichert;
|
||||
/// der aktuell laufende Zeitraum ergibt sich aus dem Ende des letzten Abschnitts bis heute.
|
||||
/// </summary>
|
||||
public class ParticipationSection
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid GroupId { get; set; }
|
||||
public string Label { get; set; } = "";
|
||||
public DateOnly StartDate { get; set; }
|
||||
public DateOnly EndDate { get; set; }
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class CompetencyRating
|
||||
{
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
@@ -14,6 +14,22 @@ public class Grade
|
||||
}
|
||||
public enum GradeCategory { Oral, Homework, Participation, Project, Other }
|
||||
|
||||
/// <summary>
|
||||
/// Prozentuale Gewichtung von Klausuren/Mitarbeit/sonstigen Leistungen für die Zeugnisnote.
|
||||
/// Entweder gruppenspezifisch (<see cref="GroupId"/> gesetzt) oder als Voreinstellung je
|
||||
/// Gruppentyp (<see cref="GroupType"/> gesetzt, <see cref="GroupId"/> null).
|
||||
/// </summary>
|
||||
public class GradingScheme
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid? GroupId { get; set; }
|
||||
public GroupType? GroupType { get; set; }
|
||||
public double ExamsPercent { get; set; }
|
||||
public double ParticipationPercent { get; set; }
|
||||
public double OtherPercent { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public class Unit
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
@@ -45,3 +61,22 @@ public class Lesson
|
||||
}
|
||||
public enum UnitStatus { Planned, Active, Completed }
|
||||
public enum LessonStatus { Planned, Conducted }
|
||||
|
||||
/// <summary>
|
||||
/// Zeugnisnote eines Schülers in einer Lerngruppe für einen Zeitraum (Halbjahr/Gesamtjahr).
|
||||
/// <see cref="CalculatedValue"/> ist das zuletzt berechnete Ergebnis; <see cref="OverrideValue"/>
|
||||
/// überschreibt es bei pädagogischem Ermessen (erfordert <see cref="OverrideReason"/>).
|
||||
/// Nach dem Festschreiben (<see cref="IsLocked"/>) wird der Datensatz nicht mehr neu berechnet.
|
||||
/// </summary>
|
||||
public class ReportGrade
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid StudentId { get; set; }
|
||||
public Guid GroupId { get; set; }
|
||||
public string Period { get; set; } = "";
|
||||
public string CalculatedValue { get; set; } = "";
|
||||
public string? OverrideValue { get; set; }
|
||||
public string? OverrideReason { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,16 @@ public class GradingService
|
||||
return CalculateGrade(percent, 100, key);
|
||||
}
|
||||
|
||||
public string? ValidateGradingScheme(GradingScheme scheme)
|
||||
{
|
||||
if (scheme.ExamsPercent < 0 || scheme.ParticipationPercent < 0 || scheme.OtherPercent < 0)
|
||||
return "Anteile dürfen nicht negativ sein.";
|
||||
var sum = scheme.ExamsPercent + scheme.ParticipationPercent + scheme.OtherPercent;
|
||||
return Math.Abs(sum - 100.0) > 0.01
|
||||
? $"Die Anteile müssen zusammen 100 % ergeben (aktuell {sum.ToString("0.#")} %)."
|
||||
: null;
|
||||
}
|
||||
|
||||
public double WeightedAverage(List<(string Grade, double Weight)> grades)
|
||||
{
|
||||
var numeric = grades
|
||||
@@ -73,4 +83,51 @@ public class GradingService
|
||||
var total = numeric.Sum(g => g.Weight);
|
||||
return total == 0 ? 0 : numeric.Sum(g => g.Value!.Value * g.Weight) / total;
|
||||
}
|
||||
|
||||
/// Rundet einen rechnerischen Notenwert auf eine ganze Note/Punktzahl.
|
||||
/// "Kaufmännisch" rundet bei genau 0,5 immer vom Nullpunkt weg (Standard).
|
||||
/// "Pädagogisch" rundet bei genau 0,5 in Richtung der besseren Note
|
||||
/// (Grades1To6: kleinere Zahl ist besser → abrunden; Points0To15: größere Zahl ist besser → aufrunden).
|
||||
public string RoundToGrade(double value, GradingSystem system, RoundingRule rule)
|
||||
{
|
||||
int rounded;
|
||||
var floor = Math.Floor(value);
|
||||
var isExactHalf = Math.Abs(value - floor - 0.5) < 0.0001;
|
||||
|
||||
if (rule == RoundingRule.Pedagogical && isExactHalf)
|
||||
{
|
||||
var betterIsLower = system == GradingSystem.Grades1To6;
|
||||
rounded = betterIsLower ? (int)floor : (int)floor + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
rounded = (int)Math.Round(value, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
var (min, max) = system == GradingSystem.Grades1To6 ? (1, 6) : (0, 15);
|
||||
return Math.Clamp(rounded, min, max).ToString();
|
||||
}
|
||||
|
||||
/// Berechnet die Zeugnisnote (2.4.1) aus den drei Leistungsbereichen gemäß Gewichtungsschema.
|
||||
/// Bereiche ohne Werte werden ausgelassen; die verbleibenden Prozentanteile werden neu normiert.
|
||||
/// Liefert null, wenn in keinem Bereich Werte vorliegen.
|
||||
public string? CalculateReportGrade(
|
||||
List<(string Grade, double Weight)> examGrades,
|
||||
List<(string Grade, double Weight)> participationGrades,
|
||||
List<(string Grade, double Weight)> otherGrades,
|
||||
GradingScheme scheme, GradingSystem system, RoundingRule rounding)
|
||||
{
|
||||
var buckets = new List<(double Avg, double Percent)>();
|
||||
if (examGrades.Count > 0) buckets.Add((WeightedAverage(examGrades), scheme.ExamsPercent));
|
||||
if (participationGrades.Count > 0) buckets.Add((WeightedAverage(participationGrades), scheme.ParticipationPercent));
|
||||
if (otherGrades.Count > 0) buckets.Add((WeightedAverage(otherGrades), scheme.OtherPercent));
|
||||
|
||||
var totalPercent = buckets.Sum(b => b.Percent);
|
||||
if (buckets.Count == 0 || totalPercent <= 0) return null;
|
||||
|
||||
var weighted = buckets.Sum(b => b.Avg * b.Percent) / totalPercent;
|
||||
return RoundToGrade(weighted, system, rounding);
|
||||
}
|
||||
}
|
||||
|
||||
public enum RoundingRule { Commercial, Pedagogical }
|
||||
|
||||
@@ -26,6 +26,8 @@ public class LiteDbContext : IDisposable
|
||||
public ILiteCollection<Exam> Exams => _db.GetCollection<Exam>("exams");
|
||||
public ILiteCollection<ExamResult> ExamResults => _db.GetCollection<ExamResult>("exam_results");
|
||||
public ILiteCollection<Grade> Grades => _db.GetCollection<Grade>("grades");
|
||||
public ILiteCollection<GradingScheme> GradingSchemes => _db.GetCollection<GradingScheme>("grading_schemes");
|
||||
public ILiteCollection<ReportGrade> ReportGrades => _db.GetCollection<ReportGrade>("report_grades");
|
||||
public ILiteCollection<GradingKeyTemplate> GradingKeyTemplates => _db.GetCollection<GradingKeyTemplate>("grading_key_templates");
|
||||
public ILiteCollection<Unit> Units => _db.GetCollection<Unit>("units");
|
||||
public ILiteCollection<Lesson> Lessons => _db.GetCollection<Lesson>("lessons");
|
||||
@@ -35,6 +37,7 @@ 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<ParticipationSection> ParticipationSections => _db.GetCollection<ParticipationSection>("participation_sections");
|
||||
public ILiteCollection<Subject> Subjects => _db.GetCollection<Subject>("subjects");
|
||||
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
||||
|
||||
@@ -152,6 +155,12 @@ public class LiteDbContext : IDisposable
|
||||
BsonExpression.Create("STRING($.ExamId) + ':' + STRING($.StudentId)"), unique: true);
|
||||
Grades.EnsureIndex(x => x.StudentId);
|
||||
Grades.EnsureIndex(x => x.GroupId);
|
||||
GradingSchemes.EnsureIndex(x => x.GroupId);
|
||||
GradingSchemes.EnsureIndex(x => x.GroupType);
|
||||
ReportGrades.EnsureIndex(x => x.GroupId);
|
||||
ReportGrades.EnsureIndex(x => x.StudentId);
|
||||
ReportGrades.EnsureIndex("ux_student_group_period",
|
||||
BsonExpression.Create("STRING($.StudentId) + ':' + STRING($.GroupId) + ':' + $.Period"), unique: true);
|
||||
GradingKeyTemplates.EnsureIndex(x => x.GradingSystem);
|
||||
Units.EnsureIndex(x => x.GroupId);
|
||||
Lessons.EnsureIndex(x => x.UnitId);
|
||||
@@ -167,6 +176,7 @@ public class LiteDbContext : IDisposable
|
||||
ParticipationEntries.EnsureIndex("ux_session_student",
|
||||
BsonExpression.Create("STRING($.SessionId) + ':' + STRING($.StudentId)"), unique: true);
|
||||
ParticipationAspects.EnsureIndex(x => x.GroupId);
|
||||
ParticipationSections.EnsureIndex(x => x.GroupId);
|
||||
Subjects.EnsureIndex(x => x.Name);
|
||||
Subjects.EnsureIndex("ux_subject_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
|
||||
CompetencyDomains.EnsureIndex(x => x.SubjectId);
|
||||
|
||||
@@ -147,6 +147,24 @@ public class GradeRepository(LiteDbContext db) : IGradeRepository
|
||||
public void Delete(Guid id) => db.Grades.Delete(id);
|
||||
}
|
||||
|
||||
public class GradingSchemeRepository(LiteDbContext db) : IGradingSchemeRepository
|
||||
{
|
||||
public GradingScheme? GetByGroup(Guid groupId) => db.GradingSchemes.FindOne(s => s.GroupId == groupId);
|
||||
public GradingScheme? GetDefaultForType(GroupType type) =>
|
||||
db.GradingSchemes.FindOne(s => s.GroupId == null && s.GroupType == type);
|
||||
public void Save(GradingScheme s) { s.UpdatedAt = DateTime.UtcNow; db.GradingSchemes.Upsert(s); }
|
||||
public void Delete(Guid id) => db.GradingSchemes.Delete(id);
|
||||
}
|
||||
|
||||
public class ReportGradeRepository(LiteDbContext db) : IReportGradeRepository
|
||||
{
|
||||
public List<ReportGrade> GetByGroup(Guid groupId) => db.ReportGrades.Find(r => r.GroupId == groupId).ToList();
|
||||
public ReportGrade? GetByStudentGroupPeriod(Guid studentId, Guid groupId, string period) =>
|
||||
db.ReportGrades.FindOne(r => r.StudentId == studentId && r.GroupId == groupId && r.Period == period);
|
||||
public void Save(ReportGrade r) { r.UpdatedAt = DateTime.UtcNow; db.ReportGrades.Upsert(r); }
|
||||
public void Delete(Guid id) => db.ReportGrades.Delete(id);
|
||||
}
|
||||
|
||||
public class UnitRepository(LiteDbContext db) : IUnitRepository
|
||||
{
|
||||
public Unit? GetById(Guid id) => db.Units.FindById(id);
|
||||
@@ -248,6 +266,14 @@ public class ParticipationAspectRepository(LiteDbContext db) : IParticipationAsp
|
||||
public void Delete(Guid id) => db.ParticipationAspects.Delete(id);
|
||||
}
|
||||
|
||||
public class ParticipationSectionRepository(LiteDbContext db) : IParticipationSectionRepository
|
||||
{
|
||||
public List<ParticipationSection> GetByGroup(Guid groupId) =>
|
||||
db.ParticipationSections.Find(s => s.GroupId == groupId).OrderBy(s => s.StartDate).ToList();
|
||||
public void Save(ParticipationSection s) => db.ParticipationSections.Upsert(s);
|
||||
public void Delete(Guid id) => db.ParticipationSections.Delete(id);
|
||||
}
|
||||
|
||||
public class SubjectRepository(LiteDbContext db) : ISubjectRepository
|
||||
{
|
||||
public List<Subject> GetAll() => db.Subjects.FindAll().OrderBy(s => s.Name).ToList();
|
||||
|
||||
@@ -48,6 +48,8 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IExamRepository, ExamRepository>();
|
||||
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
|
||||
services.AddSingleton<IGradeRepository, GradeRepository>();
|
||||
services.AddSingleton<IGradingSchemeRepository, GradingSchemeRepository>();
|
||||
services.AddSingleton<IReportGradeRepository, ReportGradeRepository>();
|
||||
services.AddSingleton<IGradingKeyTemplateRepository, GradingKeyTemplateRepository>();
|
||||
services.AddSingleton<IUnitRepository, UnitRepository>();
|
||||
services.AddSingleton<ILessonRepository, LessonRepository>();
|
||||
@@ -57,6 +59,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<IParticipationSessionRepository, ParticipationSessionRepository>();
|
||||
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
|
||||
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
|
||||
services.AddSingleton<IParticipationSectionRepository, ParticipationSectionRepository>();
|
||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||
|
||||
@@ -111,6 +114,7 @@ public static class AppBootstrapper
|
||||
services.AddTransient<GroupDetailViewModel>();
|
||||
services.AddTransient<StudentDetailViewModel>();
|
||||
services.AddTransient<ParticipationTabViewModel>();
|
||||
services.AddTransient<GradeOverviewTabViewModel>();
|
||||
services.AddTransient<AddGroupDialogViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
|
||||
@@ -17,8 +17,13 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
private readonly IParticipationSessionRepository _participationSessions;
|
||||
private readonly IParticipationRepository _participationEntries;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly SchoolYearService _sy;
|
||||
|
||||
private const int OpenExcuseMaxAgeDays = 21;
|
||||
|
||||
[ObservableProperty] private string _greeting = "";
|
||||
[ObservableProperty] private string _currentDate = "";
|
||||
[ObservableProperty] private string _currentSchoolYear = "";
|
||||
@@ -30,15 +35,19 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
|
||||
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
|
||||
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
||||
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
|
||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||
|
||||
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||
IExamRepository exams, IWorkTaskRepository tasks, SchoolYearService sy)
|
||||
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
|
||||
IParticipationRepository participationEntries, IStudentRepository students, SchoolYearService sy)
|
||||
{
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks; _sy = sy;
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||
_participationSessions = participationSessions; _participationEntries = participationEntries;
|
||||
_students = students; _sy = sy;
|
||||
Load();
|
||||
}
|
||||
|
||||
@@ -80,6 +89,41 @@ public partial class DashboardViewModel : ObservableObject
|
||||
|
||||
CalendarMonth = FirstOfMonth(now);
|
||||
LoadCalendar();
|
||||
LoadOpenExcuses(groups.Values.ToList(), today);
|
||||
}
|
||||
|
||||
private void LoadOpenExcuses(List<LearningGroup> groups, DateOnly today)
|
||||
{
|
||||
OpenExcuses.Clear();
|
||||
var cutoff = today.AddDays(-OpenExcuseMaxAgeDays);
|
||||
|
||||
var items = new List<OpenExcuseItem>();
|
||||
foreach (var g in groups)
|
||||
{
|
||||
foreach (var session in _participationSessions.GetByGroup(g.Id).Where(s => s.Date >= cutoff && s.Date <= today))
|
||||
{
|
||||
foreach (var entry in _participationEntries.GetBySession(session.Id)
|
||||
.Where(e => e.Attendance == AttendanceStatus.ExcusePending))
|
||||
{
|
||||
var student = _students.GetById(entry.StudentId);
|
||||
if (student is null) continue;
|
||||
var item = new OpenExcuseItem(session.Id, entry.StudentId, student.FullName, g.Name, session.Date);
|
||||
item.OnResolve = ResolveExcuse;
|
||||
items.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var item in items.OrderBy(i => i.Date))
|
||||
OpenExcuses.Add(item);
|
||||
}
|
||||
|
||||
private void ResolveExcuse(OpenExcuseItem item, AttendanceStatus status)
|
||||
{
|
||||
var entry = _participationEntries.GetBySessionAndStudent(item.SessionId, item.StudentId);
|
||||
if (entry is null) return;
|
||||
entry.Attendance = status;
|
||||
_participationEntries.Save(entry);
|
||||
OpenExcuses.Remove(item);
|
||||
}
|
||||
|
||||
private void LoadCalendar()
|
||||
@@ -172,6 +216,33 @@ public class LessonItem { public string GroupName { get; set; } = ""; public str
|
||||
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } }
|
||||
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
|
||||
|
||||
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
|
||||
|
||||
public partial class OpenExcuseItem : ObservableObject
|
||||
{
|
||||
public Guid SessionId { get; }
|
||||
public Guid StudentId { get; }
|
||||
public string StudentName { get; }
|
||||
public string GroupName { get; }
|
||||
public DateOnly Date { get; }
|
||||
public string DateDisplay { get; }
|
||||
|
||||
public Action<OpenExcuseItem, AttendanceStatus>? OnResolve { get; set; }
|
||||
|
||||
public OpenExcuseItem(Guid sessionId, Guid studentId, string studentName, string groupName, DateOnly date)
|
||||
{
|
||||
SessionId = sessionId;
|
||||
StudentId = studentId;
|
||||
StudentName = studentName;
|
||||
GroupName = groupName;
|
||||
Date = date;
|
||||
DateDisplay = date.ToString("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
[RelayCommand] private void MarkExcused() => OnResolve?.Invoke(this, AttendanceStatus.Excused);
|
||||
[RelayCommand] private void MarkUnexcused() => OnResolve?.Invoke(this, AttendanceStatus.Unexcused);
|
||||
}
|
||||
|
||||
public class CalendarDayCell
|
||||
{
|
||||
public int DayNumber { get; }
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
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;
|
||||
|
||||
// ── Notenübersicht der Gruppe (2.1) ──────────────────────────────────────────
|
||||
|
||||
public partial class GradeOverviewTabViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IExamResultRepository _results;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IGroupMembershipRepository _memberships;
|
||||
private readonly GradingService _grading;
|
||||
|
||||
private Guid _groupId;
|
||||
private GradingSystem _gradingSystem;
|
||||
private GroupType _groupType;
|
||||
private string _groupLabel = "";
|
||||
private bool _sortByTotal;
|
||||
private bool _sortDescending;
|
||||
|
||||
public Guid GroupId => _groupId;
|
||||
public GradingSystem GradingSystem => _gradingSystem;
|
||||
public GroupType GroupType => _groupType;
|
||||
public string GroupLabel => _groupLabel;
|
||||
|
||||
[ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
|
||||
[ObservableProperty] private bool _showAsPoints = true;
|
||||
[ObservableProperty] private GradeOverviewRow? _selectedRow;
|
||||
[ObservableProperty] private int _rebuildColumnsSignal;
|
||||
|
||||
public bool CanTogglePointsView => _gradingSystem == GradingSystem.Points0To15;
|
||||
|
||||
public List<ParticipationPeriodOption> PeriodOptions { get; } =
|
||||
[
|
||||
new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"),
|
||||
new(ParticipationPeriod.H1, "1. Halbjahr"),
|
||||
new(ParticipationPeriod.H2, "2. Halbjahr"),
|
||||
];
|
||||
|
||||
public ObservableCollection<GradeOverviewColumnDef> Columns { get; } = [];
|
||||
public ObservableCollection<GradeOverviewRow> Rows { get; } = [];
|
||||
|
||||
public Func<GradeOverviewRow, Task>? OnManageStudentGrades { get; set; }
|
||||
public Func<Task>? OnCollectiveGrade { get; set; }
|
||||
public Func<Task>? OnReportGrades { get; set; }
|
||||
|
||||
public GradeOverviewTabViewModel(IGradeRepository grades, IExamRepository exams,
|
||||
IExamResultRepository results, IStudentRepository students,
|
||||
IGroupMembershipRepository memberships, GradingService grading)
|
||||
{
|
||||
_grades = grades; _exams = exams; _results = results;
|
||||
_students = students; _memberships = memberships; _grading = grading;
|
||||
_selectedPeriod = PeriodOptions[0];
|
||||
}
|
||||
|
||||
public void Initialize(Guid groupId, GradingSystem gradingSystem, GroupType groupType, string groupLabel)
|
||||
{
|
||||
_groupId = groupId;
|
||||
_gradingSystem = gradingSystem;
|
||||
_groupType = groupType;
|
||||
_groupLabel = groupLabel;
|
||||
ShowAsPoints = gradingSystem == GradingSystem.Points0To15;
|
||||
OnPropertyChanged(nameof(CanTogglePointsView));
|
||||
Recompute();
|
||||
}
|
||||
|
||||
public void Refresh() => Recompute();
|
||||
|
||||
partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute();
|
||||
partial void OnShowAsPointsChanged(bool value) => Recompute();
|
||||
|
||||
[RelayCommand]
|
||||
private void SortByName()
|
||||
{
|
||||
if (!_sortByTotal) _sortDescending = !_sortDescending;
|
||||
else { _sortByTotal = false; _sortDescending = false; }
|
||||
ApplySort();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SortByTotal()
|
||||
{
|
||||
if (_sortByTotal) _sortDescending = !_sortDescending;
|
||||
else { _sortByTotal = true; _sortDescending = false; }
|
||||
ApplySort();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ManageStudentGrades()
|
||||
{
|
||||
if (SelectedRow is null || OnManageStudentGrades is null) return;
|
||||
await OnManageStudentGrades(SelectedRow);
|
||||
Recompute();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CollectiveGrade()
|
||||
{
|
||||
if (OnCollectiveGrade is null) return;
|
||||
await OnCollectiveGrade();
|
||||
Recompute();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ReportGrades()
|
||||
{
|
||||
if (OnReportGrades is null) return;
|
||||
await OnReportGrades();
|
||||
}
|
||||
|
||||
private void Recompute()
|
||||
{
|
||||
var period = SelectedPeriod.Period;
|
||||
|
||||
var students = _students.GetByGroup(_groupId);
|
||||
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
||||
|
||||
var exams = _exams.GetByGroup(_groupId)
|
||||
.Where(e => InPeriod(e.Date, period))
|
||||
.OrderBy(e => e.Date)
|
||||
.ToList();
|
||||
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
||||
|
||||
var otherGrades = _grades.GetByGroup(_groupId)
|
||||
.Where(g => InPeriod(g.Date, period))
|
||||
.ToList();
|
||||
var gradeColumnKeys = otherGrades
|
||||
.Select(g => (g.Category, Note: g.Note ?? "", g.Date))
|
||||
.Distinct()
|
||||
.OrderBy(k => k.Date)
|
||||
.ToList();
|
||||
|
||||
Columns.Clear();
|
||||
foreach (var exam in exams)
|
||||
Columns.Add(new GradeOverviewColumnDef($"{exam.Date:dd.MM.} {exam.Title}"));
|
||||
foreach (var key in gradeColumnKeys)
|
||||
{
|
||||
var header = string.IsNullOrWhiteSpace(key.Note)
|
||||
? $"{GradeCategoryDisplay.Label(key.Category)} {key.Date:dd.MM.}"
|
||||
: key.Note;
|
||||
Columns.Add(new GradeOverviewColumnDef(header));
|
||||
}
|
||||
|
||||
Rows.Clear();
|
||||
foreach (var student in students)
|
||||
{
|
||||
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
||||
if (!StudentActiveInPeriod(membership, period)) continue;
|
||||
|
||||
var cells = new List<string>();
|
||||
var numeric = new List<(string Grade, double Weight)>();
|
||||
|
||||
foreach (var exam in exams)
|
||||
{
|
||||
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) { cells.Add(""); continue; }
|
||||
resultsByExam[exam.Id].TryGetValue(student.Id, out var result);
|
||||
if (result is null) { cells.Add(""); continue; }
|
||||
if (result.Absent) { cells.Add("abwesend"); continue; }
|
||||
var display = FormatValue(result.Grade);
|
||||
cells.Add(display);
|
||||
if (result.Grade is not null) numeric.Add((result.Grade, 1.0));
|
||||
}
|
||||
|
||||
foreach (var key in gradeColumnKeys)
|
||||
{
|
||||
var grade = otherGrades.FirstOrDefault(g =>
|
||||
g.StudentId == student.Id && g.Category == key.Category &&
|
||||
(g.Note ?? "") == key.Note && g.Date == key.Date);
|
||||
if (grade is null) { cells.Add(""); continue; }
|
||||
cells.Add(FormatValue(grade.Value));
|
||||
numeric.Add((grade.Value, grade.Weight));
|
||||
}
|
||||
|
||||
var total = numeric.Count == 0 ? (double?)null : _grading.WeightedAverage(numeric);
|
||||
var totalDisplay = total is null ? "–" : total.Value.ToString("0.00", CultureInfo.InvariantCulture);
|
||||
|
||||
Rows.Add(new GradeOverviewRow(student.Id, student.FullName, cells, totalDisplay, total));
|
||||
}
|
||||
|
||||
ApplySort();
|
||||
RebuildColumnsSignal++;
|
||||
}
|
||||
|
||||
private string FormatValue(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return "";
|
||||
if (!ShowAsPoints && _gradingSystem == GradingSystem.Points0To15 && int.TryParse(raw, out var points))
|
||||
return PointsNoteMapping.PointsToNote(points);
|
||||
return raw;
|
||||
}
|
||||
|
||||
private void ApplySort()
|
||||
{
|
||||
var sorted = _sortByTotal
|
||||
? Rows.OrderBy(r => r.TotalSortValue is null).ThenBy(r => r.TotalSortValue).ToList()
|
||||
: Rows.OrderBy(r => r.Name).ToList();
|
||||
if (_sortDescending) sorted.Reverse();
|
||||
for (var i = 0; i < sorted.Count; i++) Rows.Move(Rows.IndexOf(sorted[i]), i);
|
||||
}
|
||||
|
||||
private static bool InPeriod(DateOnly date, ParticipationPeriod period) => period switch
|
||||
{
|
||||
ParticipationPeriod.H1 => date.Month >= 8 || date.Month <= 1,
|
||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
private static bool StudentActiveInPeriod(GroupMembership? m, ParticipationPeriod period)
|
||||
{
|
||||
if (period == ParticipationPeriod.FullYear || m is null) return true;
|
||||
return m.Period switch
|
||||
{
|
||||
MembershipPeriod.H1Only => period == ParticipationPeriod.H1,
|
||||
MembershipPeriod.H2Only => period == ParticipationPeriod.H2,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class GradeOverviewColumnDef(string header)
|
||||
{
|
||||
public string Header { get; } = header;
|
||||
}
|
||||
|
||||
public class GradeOverviewRow(Guid studentId, string name, List<string> cells, string totalDisplay, double? totalSortValue)
|
||||
{
|
||||
public Guid StudentId { get; } = studentId;
|
||||
public string Name { get; } = name;
|
||||
public List<string> Cells { get; } = cells;
|
||||
public string TotalDisplay { get; } = totalDisplay;
|
||||
public double? TotalSortValue { get; } = totalSortValue;
|
||||
}
|
||||
|
||||
// ── Kategorie-Anzeige & Punkte/Noten-Umrechnung ──────────────────────────────
|
||||
|
||||
public static class GradeCategoryDisplay
|
||||
{
|
||||
public static string Label(GradeCategory c) => c switch
|
||||
{
|
||||
GradeCategory.Oral => "Mündlich",
|
||||
GradeCategory.Homework => "Hausaufgaben",
|
||||
GradeCategory.Participation => "Mitarbeit",
|
||||
GradeCategory.Project => "Projekt",
|
||||
GradeCategory.Other => "Sonstiges",
|
||||
_ => c.ToString(),
|
||||
};
|
||||
|
||||
public static string[] Options { get; } = Enum.GetValues<GradeCategory>().Select(Label).ToArray();
|
||||
|
||||
public static GradeCategory FromLabel(string? label) =>
|
||||
Enum.GetValues<GradeCategory>().FirstOrDefault(c => Label(c) == label, GradeCategory.Other);
|
||||
}
|
||||
|
||||
public static class PointsNoteMapping
|
||||
{
|
||||
// Grobe, standardübliche Punkte-Noten-Umrechnung (Oberstufe), nur für die Anzeige.
|
||||
public static string PointsToNote(int points) => points switch
|
||||
{
|
||||
>= 13 => "1",
|
||||
>= 10 => "2",
|
||||
>= 7 => "3",
|
||||
>= 4 => "4",
|
||||
>= 1 => "5",
|
||||
_ => "6",
|
||||
};
|
||||
}
|
||||
|
||||
// ── Note hinzufügen/bearbeiten für einen Schüler (2.2.1, 2.2.2) ──────────────
|
||||
|
||||
public partial class StudentGradesDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly Guid _studentId;
|
||||
private readonly Guid _groupId;
|
||||
|
||||
public string StudentName { get; }
|
||||
|
||||
public ObservableCollection<GradeEditItem> Entries { get; } = [];
|
||||
|
||||
public StudentGradesDialogViewModel(IGradeRepository grades, Guid studentId, Guid groupId, string studentName)
|
||||
{
|
||||
_grades = grades; _studentId = studentId; _groupId = groupId;
|
||||
StudentName = studentName;
|
||||
Load();
|
||||
}
|
||||
|
||||
private void Load()
|
||||
{
|
||||
Entries.Clear();
|
||||
foreach (var g in _grades.GetByStudentAndGroup(_studentId, _groupId).OrderByDescending(g => g.Date))
|
||||
Entries.Add(new GradeEditItem(g) { OnSave = Save, OnDelete = Delete });
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddEntry()
|
||||
{
|
||||
var item = new GradeEditItem(new Grade
|
||||
{
|
||||
StudentId = _studentId,
|
||||
GroupId = _groupId,
|
||||
Category = GradeCategory.Other,
|
||||
Date = DateOnly.FromDateTime(DateTime.Today),
|
||||
})
|
||||
{ OnSave = Save, OnDelete = Delete, IsNew = true };
|
||||
Entries.Insert(0, item);
|
||||
}
|
||||
|
||||
private void Save(GradeEditItem item)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(item.Value)) { item.ValidationMessage = "Wert darf nicht leer sein."; return; }
|
||||
if (!DateOnly.TryParseExact(item.DateText, "dd.MM.yyyy", null,
|
||||
System.Globalization.DateTimeStyles.None, out _))
|
||||
{ item.ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; }
|
||||
item.ValidationMessage = "";
|
||||
var grade = item.ToModel();
|
||||
_grades.Save(grade);
|
||||
item.IsNew = false;
|
||||
item.MarkSaved(grade.CreatedAt);
|
||||
}
|
||||
|
||||
private void Delete(GradeEditItem item)
|
||||
{
|
||||
if (!item.IsNew) _grades.Delete(item.Id);
|
||||
Entries.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class GradeEditItem : ObservableObject
|
||||
{
|
||||
public Guid Id { get; }
|
||||
private readonly Guid _studentId;
|
||||
private readonly Guid _groupId;
|
||||
public bool IsNew { get; set; }
|
||||
|
||||
[ObservableProperty] private GradeCategory _category;
|
||||
[ObservableProperty] private string _value;
|
||||
[ObservableProperty] private string _dateText;
|
||||
[ObservableProperty] private double _weight;
|
||||
[ObservableProperty] private string? _note;
|
||||
[ObservableProperty] private string _validationMessage = "";
|
||||
|
||||
// Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens.
|
||||
public string CategoryName
|
||||
{
|
||||
get => GradeCategoryDisplay.Label(Category);
|
||||
set => Category = GradeCategoryDisplay.FromLabel(value);
|
||||
}
|
||||
|
||||
public string CreatedAtDisplay { get; private set; }
|
||||
|
||||
public Action<GradeEditItem>? OnSave { get; set; }
|
||||
public Action<GradeEditItem>? OnDelete { get; set; }
|
||||
|
||||
public GradeEditItem(Grade g)
|
||||
{
|
||||
Id = g.Id;
|
||||
_studentId = g.StudentId;
|
||||
_groupId = g.GroupId;
|
||||
_category = g.Category;
|
||||
_value = g.Value;
|
||||
_dateText = g.Date.ToString("dd.MM.yyyy");
|
||||
_weight = g.Weight;
|
||||
_note = g.Note;
|
||||
CreatedAtDisplay = $"erfasst am {g.CreatedAt.ToLocalTime():dd.MM.yyyy HH:mm}";
|
||||
}
|
||||
|
||||
public void MarkSaved(DateTime createdAt) => CreatedAtDisplay = $"erfasst am {createdAt.ToLocalTime():dd.MM.yyyy HH:mm}";
|
||||
|
||||
public Grade ToModel() => new()
|
||||
{
|
||||
Id = Id,
|
||||
StudentId = _studentId,
|
||||
GroupId = _groupId,
|
||||
Category = Category,
|
||||
Value = Value.Trim(),
|
||||
Date = DateOnly.ParseExact(DateText, "dd.MM.yyyy"),
|
||||
Weight = Weight,
|
||||
Note = string.IsNullOrWhiteSpace(Note) ? null : Note.Trim(),
|
||||
};
|
||||
|
||||
[RelayCommand]
|
||||
private void Save() => OnSave?.Invoke(this);
|
||||
|
||||
[RelayCommand]
|
||||
private void Delete() => OnDelete?.Invoke(this);
|
||||
}
|
||||
|
||||
// ── Sammelerfassung für die ganze Gruppe (2.2.3) ─────────────────────────────
|
||||
|
||||
public partial class CollectiveGradeDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly Guid _groupId;
|
||||
|
||||
[ObservableProperty] private GradeCategory _category = GradeCategory.Other;
|
||||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||
[ObservableProperty] private double _weight = 1.0;
|
||||
[ObservableProperty] private string? _note;
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
[ObservableProperty] private string _validationMessage = "";
|
||||
|
||||
// Für die ComboBox: deutsche Anzeige statt des rohen Enum-Namens.
|
||||
public string CategoryName
|
||||
{
|
||||
get => GradeCategoryDisplay.Label(Category);
|
||||
set => Category = GradeCategoryDisplay.FromLabel(value);
|
||||
}
|
||||
|
||||
public ObservableCollection<CollectiveGradeStudentRow> Rows { get; } = [];
|
||||
|
||||
public CollectiveGradeDialogViewModel(IGradeRepository grades, IStudentRepository students, Guid groupId)
|
||||
{
|
||||
_grades = grades; _groupId = groupId;
|
||||
foreach (var s in students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||
Rows.Add(new CollectiveGradeStudentRow(s.Id, s.FullName));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveAll()
|
||||
{
|
||||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null,
|
||||
System.Globalization.DateTimeStyles.None, out var date))
|
||||
{ ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; }
|
||||
ValidationMessage = "";
|
||||
|
||||
var count = 0;
|
||||
foreach (var row in Rows)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(row.Value)) continue;
|
||||
_grades.Save(new Grade
|
||||
{
|
||||
StudentId = row.StudentId,
|
||||
GroupId = _groupId,
|
||||
Category = Category,
|
||||
Value = row.Value.Trim(),
|
||||
Date = date,
|
||||
Weight = Weight,
|
||||
Note = string.IsNullOrWhiteSpace(Note) ? null : Note.Trim(),
|
||||
});
|
||||
count++;
|
||||
}
|
||||
StatusMessage = count == 0
|
||||
? "Keine Werte eingegeben."
|
||||
: $"{count} Note(n) gespeichert.";
|
||||
}
|
||||
}
|
||||
|
||||
public partial class CollectiveGradeStudentRow(Guid studentId, string name) : ObservableObject
|
||||
{
|
||||
public Guid StudentId { get; } = studentId;
|
||||
public string Name { get; } = name;
|
||||
[ObservableProperty] private string _value = "";
|
||||
}
|
||||
@@ -182,6 +182,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||
|
||||
public ParticipationTabViewModel ParticipationTab { get; }
|
||||
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
|
||||
@@ -193,11 +194,12 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||
IExamRepository exams, IGradeRepository grades,
|
||||
ParticipationTabViewModel participationTab)
|
||||
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab)
|
||||
{
|
||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||
_exams = exams; _grades = grades;
|
||||
ParticipationTab = participationTab;
|
||||
GradeOverviewTab = gradeOverviewTab;
|
||||
}
|
||||
|
||||
public void LoadGroup(Guid id)
|
||||
@@ -214,6 +216,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
LoadStudents();
|
||||
ReloadExams();
|
||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle);
|
||||
}
|
||||
|
||||
private void ReloadExams()
|
||||
|
||||
@@ -28,6 +28,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
public Guid GroupId => _groupId;
|
||||
public string SchoolYear => _schoolYear;
|
||||
public GradingSystem GradingSystem => _gradingSystem;
|
||||
public string GroupLabel { get; private set; } = "";
|
||||
|
||||
[ObservableProperty] private ParticipationSessionItem? _selectedSession;
|
||||
[ObservableProperty] private string _noDataText = "Keine Bewertungssitzung ausgewählt.";
|
||||
@@ -47,6 +48,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
public Func<Task<ParticipationSession?>>? OnAddSession { get; set; }
|
||||
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
|
||||
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
||||
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
|
||||
|
||||
public ParticipationTabViewModel(
|
||||
IParticipationSessionRepository sessions,
|
||||
@@ -72,6 +74,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
_subjectId = group?.SubjectId;
|
||||
_gradeLevel = group?.GradeLevel ?? 0;
|
||||
_gradingSystem = group?.GradingSystem ?? GradingSystem.Grades1To6;
|
||||
GroupLabel = group?.Name ?? "";
|
||||
|
||||
HasCompetencyCatalog = _subjectId.HasValue
|
||||
&& _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel).Count > 0;
|
||||
@@ -143,6 +146,8 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
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);
|
||||
row.HomeworkChangedCallback = (sid, val) => SaveHomework(sessionId, sid, val);
|
||||
row.AttendanceChangedCallback = (sid, val) => SaveAttendance(sessionId, sid, val);
|
||||
StudentRows.Add(row);
|
||||
}
|
||||
QuickInputCommand.NotifyCanExecuteChanged();
|
||||
@@ -185,6 +190,22 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
|
||||
}
|
||||
|
||||
private void SaveHomework(Guid sessionId, Guid studentId, bool value)
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||
entry.HomeworkMissing = value;
|
||||
_entries.Save(entry);
|
||||
}
|
||||
|
||||
private void SaveAttendance(Guid sessionId, Guid studentId, AttendanceStatus? value)
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||
entry.Attendance = value;
|
||||
_entries.Save(entry);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible;
|
||||
|
||||
@@ -259,6 +280,13 @@ public partial class ParticipationTabViewModel : ObservableObject
|
||||
await OnComputeGrade(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenWizard()
|
||||
{
|
||||
if (OnOpenWizard is null) return;
|
||||
await OnOpenWizard(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteSession()
|
||||
{
|
||||
@@ -310,8 +338,16 @@ public partial class ParticipationStudentRow : ObservableObject
|
||||
public ObservableCollection<RatingCell> Cells { get; } = [];
|
||||
public ObservableCollection<RatingCell> CompetencyCells { get; } = [];
|
||||
|
||||
[ObservableProperty] private bool _homeworkMissing;
|
||||
[ObservableProperty] private AttendanceStatus? _attendance;
|
||||
|
||||
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
||||
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
||||
|
||||
public Action<Guid, string, int?>? OnRatingChanged { get; set; }
|
||||
public Action<Guid, string, int?>? OnCompetencyRatingChanged { get; set; }
|
||||
public Action<Guid, bool>? HomeworkChangedCallback { get; set; }
|
||||
public Action<Guid, AttendanceStatus?>? AttendanceChangedCallback { get; set; }
|
||||
|
||||
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
|
||||
List<AspectColumnDef> aspects, List<string> competencyCodes)
|
||||
@@ -320,6 +356,8 @@ public partial class ParticipationStudentRow : ObservableObject
|
||||
Name = name;
|
||||
_entry = entry;
|
||||
_aspectDefs = aspects;
|
||||
_homeworkMissing = entry.HomeworkMissing;
|
||||
_attendance = entry.Attendance;
|
||||
|
||||
foreach (var a in aspects)
|
||||
{
|
||||
@@ -347,6 +385,61 @@ public partial class ParticipationStudentRow : ObservableObject
|
||||
cell?.SetValue(value);
|
||||
OnRatingChanged?.Invoke(StudentId, key, value);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ToggleHomework()
|
||||
{
|
||||
HomeworkMissing = !HomeworkMissing;
|
||||
HomeworkChangedCallback?.Invoke(StudentId, HomeworkMissing);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CycleAttendance()
|
||||
{
|
||||
Attendance = Attendance switch
|
||||
{
|
||||
null => AttendanceStatus.ExcusePending,
|
||||
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
|
||||
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
|
||||
AttendanceStatus.Unexcused => null,
|
||||
_ => null,
|
||||
};
|
||||
OnPropertyChanged(nameof(AttendanceLabel));
|
||||
OnPropertyChanged(nameof(AttendanceTooltip));
|
||||
AttendanceChangedCallback?.Invoke(StudentId, Attendance);
|
||||
}
|
||||
|
||||
// Direktes Setzen (z.B. aus dem Grading-Wizard heraus), ohne den Zyklus zu durchlaufen.
|
||||
public void SetAttendance(AttendanceStatus? value)
|
||||
{
|
||||
Attendance = value;
|
||||
OnPropertyChanged(nameof(AttendanceLabel));
|
||||
OnPropertyChanged(nameof(AttendanceTooltip));
|
||||
AttendanceChangedCallback?.Invoke(StudentId, value);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Anwesenheits-Anzeige ──────────────────────────────────────────────────────
|
||||
|
||||
public static class AttendanceDisplay
|
||||
{
|
||||
public static string Label(AttendanceStatus? s) => s switch
|
||||
{
|
||||
null => "Anwesend",
|
||||
AttendanceStatus.ExcusePending => "Krank (Entschuldigung offen)",
|
||||
AttendanceStatus.Excused => "Krank, entschuldigt",
|
||||
AttendanceStatus.Unexcused => "Krank, unentschuldigt",
|
||||
_ => "Anwesend",
|
||||
};
|
||||
|
||||
public static string ShortLabel(AttendanceStatus? s) => s switch
|
||||
{
|
||||
null => "",
|
||||
AttendanceStatus.ExcusePending => "K ?",
|
||||
AttendanceStatus.Excused => "K ✓",
|
||||
AttendanceStatus.Unexcused => "K ✗",
|
||||
_ => "",
|
||||
};
|
||||
}
|
||||
|
||||
// ── Eine Bewertungszelle ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
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;
|
||||
|
||||
// ── Grading-Wizard: Mitarbeit als Zeitleiste mit Abschnitten ────────────────
|
||||
|
||||
public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
{
|
||||
private const string AbschnittPrefix = "Abschnitt: ";
|
||||
|
||||
private readonly IParticipationSessionRepository _sessions;
|
||||
private readonly IParticipationRepository _entries;
|
||||
private readonly IParticipationSectionRepository _sectionRepo;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IExamResultRepository _results;
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly GradingService _grading;
|
||||
private readonly Guid _groupId;
|
||||
private readonly string _schoolYear;
|
||||
private readonly GradingSystem _gradingSystem;
|
||||
|
||||
private readonly List<Student> _students;
|
||||
private readonly List<ParticipationSession> _allSessions;
|
||||
private readonly List<ParticipationSection> _sectionList;
|
||||
private readonly Dictionary<string, double> _aspectWeights;
|
||||
|
||||
public string GroupLabel { get; }
|
||||
|
||||
[ObservableProperty] private int _studentIndex;
|
||||
[ObservableProperty] private string _studentName = "";
|
||||
[ObservableProperty] private string _progressText = "";
|
||||
[ObservableProperty] private string _newSectionLabel = "";
|
||||
[ObservableProperty] private string _newSectionEndDateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||
[ObservableProperty] private string _sectionValidationMessage = "";
|
||||
[ObservableProperty] private ParticipationPeriodOption _rollupPeriod;
|
||||
[ObservableProperty] private string _rollupStatusMessage = "";
|
||||
|
||||
public List<ParticipationPeriodOption> RollupPeriodOptions { get; } =
|
||||
[
|
||||
new(ParticipationPeriod.H1, "1. Halbjahr"),
|
||||
new(ParticipationPeriod.H2, "2. Halbjahr"),
|
||||
new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"),
|
||||
];
|
||||
|
||||
public ObservableCollection<WizardSectionGroup> Timeline { get; } = [];
|
||||
public ObservableCollection<WizardSectionRow> Sections { get; } = [];
|
||||
|
||||
public bool CanGoPrevious => StudentIndex > 0;
|
||||
public bool CanGoNext => StudentIndex < _students.Count - 1;
|
||||
|
||||
private Guid CurrentStudentId => _students[StudentIndex].Id;
|
||||
|
||||
public ParticipationWizardDialogViewModel(
|
||||
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
||||
IStudentRepository students, IExamRepository exams, IExamResultRepository results,
|
||||
IGradeRepository grades, GradingService grading,
|
||||
Guid groupId, string schoolYear, GradingSystem gradingSystem, string groupLabel)
|
||||
{
|
||||
_sessions = sessions; _entries = entries; _sectionRepo = sectionRepo;
|
||||
_exams = exams; _results = results; _grades = grades; _grading = grading;
|
||||
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
||||
GroupLabel = groupLabel;
|
||||
|
||||
_aspectWeights = aspects.GetDefaults().Concat(aspects.GetByGroup(groupId))
|
||||
.ToDictionary(a => a.Key, a => a.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();
|
||||
|
||||
_rollupPeriod = RollupPeriodOptions[0];
|
||||
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
||||
|
||||
if (_students.Count > 0) ShowStudent(0);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanGoPrevious))]
|
||||
private void PreviousStudent()
|
||||
{
|
||||
if (StudentIndex > 0) ShowStudent(StudentIndex - 1);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanGoNext))]
|
||||
private void NextStudent()
|
||||
{
|
||||
if (StudentIndex < _students.Count - 1) ShowStudent(StudentIndex + 1);
|
||||
}
|
||||
|
||||
private void ShowStudent(int index)
|
||||
{
|
||||
StudentIndex = index;
|
||||
StudentName = _students[index].FullName;
|
||||
ProgressText = $"{index + 1} / {_students.Count}";
|
||||
PreviousStudentCommand.NotifyCanExecuteChanged();
|
||||
NextStudentCommand.NotifyCanExecuteChanged();
|
||||
BuildTimeline(CurrentStudentId);
|
||||
BuildSections(CurrentStudentId);
|
||||
}
|
||||
|
||||
// ── Zeitleiste ────────────────────────────────────────────────────────────
|
||||
|
||||
private void BuildTimeline(Guid studentId)
|
||||
{
|
||||
var points = new List<(DateOnly Date, WizardTimelinePoint Point)>();
|
||||
|
||||
foreach (var session in _allSessions)
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
||||
points.Add((session.Date, BuildSessionPoint(session, entry, studentId)));
|
||||
}
|
||||
foreach (var exam in _exams.GetByGroup(_groupId).OrderBy(e => e.Date))
|
||||
{
|
||||
var result = _results.GetByExamAndStudent(exam.Id, studentId);
|
||||
if (result is null) continue;
|
||||
points.Add((exam.Date, BuildExamPoint(exam, result)));
|
||||
}
|
||||
points = points.OrderBy(p => p.Date).ToList();
|
||||
|
||||
Timeline.Clear();
|
||||
foreach (var section in _sectionList)
|
||||
{
|
||||
var group = new WizardSectionGroup($"{section.Label}\n{section.StartDate:dd.MM.}–{section.EndDate:dd.MM.}", isOpen: false);
|
||||
foreach (var (date, point) in points.Where(p => p.Date >= section.StartDate && p.Date <= section.EndDate))
|
||||
group.Points.Add(point);
|
||||
Timeline.Add(group);
|
||||
}
|
||||
|
||||
var openStart = ComputeOpenStart();
|
||||
var openGroup = new WizardSectionGroup($"läuft seit {openStart:dd.MM.}", isOpen: true);
|
||||
foreach (var (date, point) in points.Where(p => p.Date >= openStart))
|
||||
openGroup.Points.Add(point);
|
||||
Timeline.Add(openGroup);
|
||||
}
|
||||
|
||||
private WizardTimelinePoint BuildSessionPoint(ParticipationSession session, ParticipationEntry? entry, Guid studentId)
|
||||
{
|
||||
var ratingLabel = entry is not null ? WeightedRatingLabel(entry) : "";
|
||||
var note = entry?.Note;
|
||||
var tooltip = $"{session.Date:dd.MM.yyyy}" +
|
||||
(ratingLabel.Length > 0 ? $" · {ratingLabel}" : "") +
|
||||
(string.IsNullOrWhiteSpace(note) ? "" : $" · {note}");
|
||||
|
||||
var point = new WizardTimelinePoint(session.Date, isExam: false, ratingLabel, examLabel: "",
|
||||
hasNote: !string.IsNullOrWhiteSpace(note), tooltip: tooltip)
|
||||
{
|
||||
HasHomework = entry?.HomeworkMissing ?? false,
|
||||
AttendanceIcon = AttendanceDisplay.ShortLabel(entry?.Attendance),
|
||||
AttendanceTooltip = AttendanceDisplay.Label(entry?.Attendance),
|
||||
};
|
||||
point.ToggleHomeworkCommand = new RelayCommand(() => ToggleHomeworkAt(session.Id, studentId, point));
|
||||
point.CycleAttendanceCommand = new RelayCommand(() => CycleAttendanceAt(session.Id, studentId, point));
|
||||
return point;
|
||||
}
|
||||
|
||||
private WizardTimelinePoint BuildExamPoint(Exam exam, ExamResult result)
|
||||
{
|
||||
var examLabel = result.Absent ? $"{exam.Title}: abw." : $"{exam.Title}: {result.Grade}";
|
||||
return new WizardTimelinePoint(exam.Date, isExam: true, ratingLabel: "", examLabel: examLabel,
|
||||
hasNote: false, tooltip: $"{exam.Date:dd.MM.yyyy} · Klausur {examLabel}");
|
||||
}
|
||||
|
||||
private string WeightedRatingLabel(ParticipationEntry entry)
|
||||
{
|
||||
if (entry.Ratings.Count == 0) return "";
|
||||
var weightSum = 0.0; var valueSum = 0.0;
|
||||
foreach (var r in entry.Ratings)
|
||||
{
|
||||
var w = _aspectWeights.TryGetValue(r.Key, out var aw) ? aw : 1.0;
|
||||
if (w <= 0) continue;
|
||||
valueSum += r.Value * w; weightSum += w;
|
||||
}
|
||||
if (weightSum <= 0) return "";
|
||||
return RatingLabel((int)Math.Round(valueSum / weightSum, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
|
||||
private static string RatingLabel(int v) => v switch
|
||||
{
|
||||
>= 2 => "++", 1 => "+", 0 => "~", -1 => "−", _ => "−−",
|
||||
};
|
||||
|
||||
private void ToggleHomeworkAt(Guid sessionId, Guid studentId, WizardTimelinePoint point)
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||
entry.HomeworkMissing = !entry.HomeworkMissing;
|
||||
_entries.Save(entry);
|
||||
point.HasHomework = entry.HomeworkMissing;
|
||||
}
|
||||
|
||||
private void CycleAttendanceAt(Guid sessionId, Guid studentId, WizardTimelinePoint point)
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||
?? 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,
|
||||
};
|
||||
_entries.Save(entry);
|
||||
point.AttendanceIcon = AttendanceDisplay.ShortLabel(entry.Attendance);
|
||||
point.AttendanceTooltip = AttendanceDisplay.Label(entry.Attendance);
|
||||
}
|
||||
|
||||
// ── Abschnitte ────────────────────────────────────────────────────────────
|
||||
|
||||
private DateOnly ComputeOpenStart() =>
|
||||
_sectionList.Count > 0 ? _sectionList.Max(s => s.EndDate).AddDays(1)
|
||||
: (_allSessions.Count > 0 ? _allSessions.Min(s => s.Date) : DateOnly.FromDateTime(DateTime.Today));
|
||||
|
||||
private void BuildSections(Guid studentId)
|
||||
{
|
||||
Sections.Clear();
|
||||
var studentGrades = _grades.GetByStudentAndGroup(studentId, _groupId)
|
||||
.Where(g => g.Category == GradeCategory.Participation && g.Note is not null && g.Note.StartsWith(AbschnittPrefix))
|
||||
.ToList();
|
||||
|
||||
foreach (var section in _sectionList)
|
||||
{
|
||||
var grade = studentGrades.FirstOrDefault(g => g.Note == AbschnittPrefix + section.Label);
|
||||
var row = new WizardSectionRow(section.Label, section.StartDate, section.EndDate,
|
||||
grade?.Value ?? "", isOpen: false);
|
||||
row.OnSave = SaveSectionGrade;
|
||||
Sections.Add(row);
|
||||
}
|
||||
|
||||
var openStart = ComputeOpenStart();
|
||||
var suggestion = ComputeSuggestion(studentId, openStart, DateOnly.FromDateTime(DateTime.Today));
|
||||
Sections.Add(new WizardSectionRow("(läuft)", openStart, DateOnly.FromDateTime(DateTime.Today),
|
||||
suggestion ?? "", isOpen: true));
|
||||
}
|
||||
|
||||
private string? ComputeSuggestion(Guid studentId, DateOnly start, DateOnly end)
|
||||
{
|
||||
var points = new List<double>();
|
||||
foreach (var session in _allSessions.Where(s => s.Date >= start && s.Date <= end))
|
||||
{
|
||||
var entry = _entries.GetBySessionAndStudent(session.Id, studentId);
|
||||
if (entry is null || entry.Ratings.Count == 0) continue;
|
||||
var weightSum = 0.0; var valueSum = 0.0;
|
||||
foreach (var r in entry.Ratings)
|
||||
{
|
||||
var w = _aspectWeights.TryGetValue(r.Key, out var aw) ? aw : 1.0;
|
||||
if (w <= 0) continue;
|
||||
valueSum += r.Value * w; weightSum += w;
|
||||
}
|
||||
if (weightSum > 0) points.Add(valueSum / weightSum);
|
||||
}
|
||||
return points.Count == 0 ? null : _grading.ParticipationGrade(points.Average(), _gradingSystem);
|
||||
}
|
||||
|
||||
private void SaveSectionGrade(WizardSectionRow row)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(row.Value)) { row.StatusMessage = "Wert darf nicht leer sein."; return; }
|
||||
var noteTag = AbschnittPrefix + row.Label;
|
||||
var record = _grades.GetByStudentAndGroup(CurrentStudentId, _groupId)
|
||||
.FirstOrDefault(g => g.Category == GradeCategory.Participation && g.Note == noteTag)
|
||||
?? new Grade
|
||||
{
|
||||
StudentId = CurrentStudentId,
|
||||
GroupId = _groupId,
|
||||
Category = GradeCategory.Participation,
|
||||
Note = noteTag,
|
||||
Date = row.EndDate,
|
||||
};
|
||||
record.Value = row.Value.Trim();
|
||||
_grades.Save(record);
|
||||
row.StatusMessage = "Gespeichert.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CloseSection()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewSectionLabel)) { SectionValidationMessage = "Bezeichnung erforderlich."; return; }
|
||||
if (!DateOnly.TryParseExact(NewSectionEndDateText, "dd.MM.yyyy", null,
|
||||
DateTimeStyles.None, out var end))
|
||||
{ SectionValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; return; }
|
||||
|
||||
var start = ComputeOpenStart();
|
||||
if (end < start) { SectionValidationMessage = "Enddatum liegt vor Abschnittsbeginn."; return; }
|
||||
|
||||
var section = new ParticipationSection { GroupId = _groupId, Label = NewSectionLabel.Trim(), StartDate = start, EndDate = end };
|
||||
_sectionRepo.Save(section);
|
||||
_sectionList.Add(section);
|
||||
|
||||
foreach (var student in _students)
|
||||
{
|
||||
var suggestion = ComputeSuggestion(student.Id, start, end);
|
||||
if (suggestion is null) continue;
|
||||
_grades.Save(new Grade
|
||||
{
|
||||
StudentId = student.Id,
|
||||
GroupId = _groupId,
|
||||
Category = GradeCategory.Participation,
|
||||
Note = AbschnittPrefix + section.Label,
|
||||
Value = suggestion,
|
||||
Date = end,
|
||||
Weight = 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
SectionValidationMessage = "";
|
||||
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
||||
BuildTimeline(CurrentStudentId);
|
||||
BuildSections(CurrentStudentId);
|
||||
}
|
||||
|
||||
// ── Halbjahresnote aus Abschnitten ────────────────────────────────────────
|
||||
|
||||
[RelayCommand]
|
||||
private void ApplyRollup()
|
||||
{
|
||||
var periodTag = $"Mitarbeit {RollupPeriod.Label} {_schoolYear}";
|
||||
var applied = 0;
|
||||
foreach (var student in _students)
|
||||
{
|
||||
var sectionGrades = _grades.GetByStudentAndGroup(student.Id, _groupId)
|
||||
.Where(g => g.Category == GradeCategory.Participation && g.Note is not null && g.Note.StartsWith(AbschnittPrefix))
|
||||
.Where(g => InPeriod(g.Date, RollupPeriod.Period))
|
||||
.Select(g => (g.Value, g.Weight))
|
||||
.ToList();
|
||||
if (sectionGrades.Count == 0) continue;
|
||||
|
||||
var average = _grading.WeightedAverage(sectionGrades);
|
||||
var rounded = _grading.RoundToGrade(average, _gradingSystem, RoundingRule.Commercial);
|
||||
|
||||
var record = _grades.GetByStudentAndGroup(student.Id, _groupId)
|
||||
.FirstOrDefault(g => g.Category == GradeCategory.Participation && g.Note == periodTag)
|
||||
?? new Grade { StudentId = student.Id, GroupId = _groupId, Category = GradeCategory.Participation, Note = periodTag };
|
||||
record.Value = rounded;
|
||||
record.Date = DateOnly.FromDateTime(DateTime.Today);
|
||||
_grades.Save(record);
|
||||
applied++;
|
||||
}
|
||||
RollupStatusMessage = applied == 0
|
||||
? "Keine Abschnittsnoten im gewählten Zeitraum."
|
||||
: $"{applied} Halbjahresnote(n) aus Abschnitten übernommen.";
|
||||
}
|
||||
|
||||
private static bool InPeriod(DateOnly date, ParticipationPeriod period) => period switch
|
||||
{
|
||||
ParticipationPeriod.H1 => date.Month >= 8 || date.Month <= 1,
|
||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Zeitleisten-Bausteine ────────────────────────────────────────────────────
|
||||
|
||||
public class WizardSectionGroup(string bandLabel, bool isOpen)
|
||||
{
|
||||
public string BandLabel { get; } = bandLabel;
|
||||
public bool IsOpen { get; } = isOpen;
|
||||
public ObservableCollection<WizardTimelinePoint> Points { get; } = [];
|
||||
}
|
||||
|
||||
public partial class WizardTimelinePoint : ObservableObject
|
||||
{
|
||||
public string DateDisplay { get; }
|
||||
public bool IsExam { get; }
|
||||
public string RatingLabel { get; }
|
||||
public string ExamLabel { get; }
|
||||
public bool HasNote { get; }
|
||||
public string TooltipText { get; }
|
||||
|
||||
[ObservableProperty] private bool _hasHomework;
|
||||
[ObservableProperty] private string _attendanceIcon = "";
|
||||
[ObservableProperty] private string _attendanceTooltip = "Anwesend";
|
||||
|
||||
public bool IsAbsent => AttendanceIcon.Length > 0;
|
||||
public string AttendanceButtonLabel => AttendanceIcon.Length > 0 ? AttendanceIcon : "Anw";
|
||||
|
||||
partial void OnAttendanceIconChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsAbsent));
|
||||
OnPropertyChanged(nameof(AttendanceButtonLabel));
|
||||
}
|
||||
|
||||
public IRelayCommand? ToggleHomeworkCommand { get; set; }
|
||||
public IRelayCommand? CycleAttendanceCommand { get; set; }
|
||||
|
||||
public WizardTimelinePoint(DateOnly date, bool isExam, string ratingLabel, string examLabel,
|
||||
bool hasNote, string tooltip)
|
||||
{
|
||||
DateDisplay = date.ToString("dd.MM.", CultureInfo.InvariantCulture);
|
||||
IsExam = isExam;
|
||||
RatingLabel = ratingLabel;
|
||||
ExamLabel = examLabel;
|
||||
HasNote = hasNote;
|
||||
TooltipText = tooltip;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Abschnittsnote-Zeile ─────────────────────────────────────────────────────
|
||||
|
||||
public partial class WizardSectionRow : ObservableObject
|
||||
{
|
||||
public string Label { get; }
|
||||
public string RangeDisplay { get; }
|
||||
public DateOnly EndDate { get; }
|
||||
public bool IsOpen { get; }
|
||||
|
||||
[ObservableProperty] private string _value;
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
|
||||
public Action<WizardSectionRow>? OnSave { get; set; }
|
||||
|
||||
public WizardSectionRow(string label, DateOnly start, DateOnly end, string value, bool isOpen)
|
||||
{
|
||||
Label = label;
|
||||
RangeDisplay = $"{start:dd.MM.yyyy} – {end:dd.MM.yyyy}";
|
||||
EndDate = end;
|
||||
IsOpen = isOpen;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save() => OnSave?.Invoke(this);
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
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;
|
||||
|
||||
[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)
|
||||
{
|
||||
_grades = grades; _exams = exams; _results = results; _students = students;
|
||||
_memberships = memberships; _schemes = schemes; _reportGrades = reportGrades; _grading = grading;
|
||||
_groupId = groupId; _groupType = groupType; _gradingSystem = gradingSystem; _groupLabel = groupLabel;
|
||||
|
||||
_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 students = _students.GetByGroup(_groupId);
|
||||
var membershipsByStudent = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
|
||||
var exams = _exams.GetByGroup(_groupId).Where(e => InPeriod(e.Date, period)).ToList();
|
||||
var resultsByExam = exams.ToDictionary(e => e.Id, e => _results.GetByExam(e.Id).ToDictionary(r => r.StudentId));
|
||||
var allGrades = _grades.GetByGroup(_groupId).Where(g => InPeriod(g.Date, period)).ToList();
|
||||
|
||||
Rows.Clear();
|
||||
foreach (var student in students.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
||||
{
|
||||
membershipsByStudent.TryGetValue(student.Id, out var membership);
|
||||
if (!StudentActiveInPeriod(membership, period)) 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 (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)
|
||||
.Select(g => (g.Value, g.Weight)).ToList();
|
||||
var otherGrades = allGrades
|
||||
.Where(g => g.StudentId == student.Id && g.Category != GradeCategory.Participation)
|
||||
.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();
|
||||
}
|
||||
|
||||
private static bool InPeriod(DateOnly date, ParticipationPeriod period) => period switch
|
||||
{
|
||||
ParticipationPeriod.H1 => date.Month >= 8 || date.Month <= 1,
|
||||
ParticipationPeriod.H2 => date.Month >= 2 && date.Month <= 7,
|
||||
_ => true,
|
||||
};
|
||||
|
||||
private static bool StudentActiveInPeriod(GroupMembership? m, ParticipationPeriod period)
|
||||
{
|
||||
if (period == ParticipationPeriod.FullYear || m is null) return true;
|
||||
return m.Period switch
|
||||
{
|
||||
MembershipPeriod.H1Only => period == ParticipationPeriod.H1,
|
||||
MembershipPeriod.H2Only => period == ParticipationPeriod.H2,
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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));
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ICompetencyDomainRepository _domainRepo;
|
||||
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
|
||||
private readonly IGradingSchemeRepository _gradingSchemes;
|
||||
private readonly GradingService _grading;
|
||||
|
||||
// ── Fächer ────────────────────────────────────────────────────────────────
|
||||
@@ -45,17 +46,39 @@ public partial class SettingsViewModel : ObservableObject
|
||||
public List<string> GradingSystemOptions { get; } = ["Noten 1–6", "Punkte 0–15"];
|
||||
public ObservableCollection<GradingKeyTemplateEditItem> GradingKeyTemplateList { get; } = [];
|
||||
|
||||
// ── Gewichtungsschema-Voreinstellungen (2.3.3) ───────────────────────────
|
||||
|
||||
[ObservableProperty] private GradingSchemeEditItem _classScheme = null!;
|
||||
[ObservableProperty] private GradingSchemeEditItem _courseScheme = null!;
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading)
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||
GradingService grading)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
_gradingKeyTemplates = gradingKeyTemplates;
|
||||
_gradingSchemes = gradingSchemes;
|
||||
_grading = grading;
|
||||
LoadSubjects();
|
||||
LoadGradingKeyTemplates();
|
||||
LoadGradingSchemes();
|
||||
}
|
||||
|
||||
// ── Gewichtungsschema-Voreinstellungen: Laden ────────────────────────────
|
||||
|
||||
private void LoadGradingSchemes()
|
||||
{
|
||||
ClassScheme = new GradingSchemeEditItem(
|
||||
_gradingSchemes.GetDefaultForType(GroupType.Class)
|
||||
?? new GradingScheme { GroupType = GroupType.Class, ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 },
|
||||
"Klassen", _gradingSchemes, _grading);
|
||||
CourseScheme = new GradingSchemeEditItem(
|
||||
_gradingSchemes.GetDefaultForType(GroupType.Course)
|
||||
?? new GradingScheme { GroupType = GroupType.Course, ExamsPercent = 60, ParticipationPercent = 30, OtherPercent = 10 },
|
||||
"Kurse", _gradingSchemes, _grading);
|
||||
}
|
||||
|
||||
// ── Notenschlüssel-Vorlagen: Laden / Hinzufügen / Löschen ────────────────
|
||||
@@ -407,6 +430,47 @@ public class GradingKeyEntryVm
|
||||
}
|
||||
}
|
||||
|
||||
// ── GradingSchemeEditItem (2.3) ────────────────────────────────────────────────
|
||||
|
||||
public partial class GradingSchemeEditItem : ObservableObject
|
||||
{
|
||||
private readonly GradingScheme _scheme;
|
||||
private readonly IGradingSchemeRepository _repo;
|
||||
private readonly GradingService _grading;
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
[ObservableProperty] private double _examsPercent;
|
||||
[ObservableProperty] private double _participationPercent;
|
||||
[ObservableProperty] private double _otherPercent;
|
||||
[ObservableProperty] private string _validationMessage = "";
|
||||
[ObservableProperty] private string _statusMessage = "";
|
||||
|
||||
public GradingSchemeEditItem(GradingScheme scheme, string label, IGradingSchemeRepository repo, GradingService grading)
|
||||
{
|
||||
_scheme = scheme; _repo = repo; _grading = grading;
|
||||
Label = label;
|
||||
_examsPercent = scheme.ExamsPercent;
|
||||
_participationPercent = scheme.ParticipationPercent;
|
||||
_otherPercent = scheme.OtherPercent;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
_scheme.ExamsPercent = ExamsPercent;
|
||||
_scheme.ParticipationPercent = ParticipationPercent;
|
||||
_scheme.OtherPercent = OtherPercent;
|
||||
|
||||
var error = _grading.ValidateGradingScheme(_scheme);
|
||||
if (error is not null) { ValidationMessage = error; StatusMessage = ""; return; }
|
||||
|
||||
ValidationMessage = "";
|
||||
_repo.Save(_scheme);
|
||||
StatusMessage = "Gespeichert.";
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfklassen ───────────────────────────────────────────────────────────────
|
||||
|
||||
public class SubjectListItem(Subject s)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -167,6 +167,39 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
|
||||
<Border Grid.Column="1" Grid.Row="1" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="OFFENE ENTSCHULDIGUNGEN" FontSize="11" FontWeight="Bold"
|
||||
Opacity="0.5" Margin="0,0,0,10"/>
|
||||
<ItemsControl ItemsSource="{Binding OpenExcuses}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:OpenExcuseItem">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,4">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock FontSize="11" Opacity="0.6">
|
||||
<Run Text="{Binding GroupName}"/>
|
||||
<Run Text=" · "/>
|
||||
<Run Text="{Binding DateDisplay}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Entschuldigt" FontSize="11" Padding="7,3"
|
||||
Command="{Binding MarkExcusedCommand}" Margin="0,0,4,0"/>
|
||||
<Button Grid.Column="2" Content="Unentschuldigt" FontSize="11" Padding="7,3"
|
||||
Command="{Binding MarkUnexcusedCommand}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine offenen Entschuldigungen." Opacity="0.4" FontSize="13"
|
||||
IsVisible="{Binding !OpenExcuses.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Meine Lerngruppen: wächst mit der Zeit, deshalb ganz unten und volle Breite -->
|
||||
<Border Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<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.CollectiveGradeDialog"
|
||||
x:DataType="vm:CollectiveGradeDialogViewModel"
|
||||
Title="Sammelnote erfassen"
|
||||
Width="480" Height="640" MinWidth="400" MinHeight="360"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="24">
|
||||
|
||||
<TextBlock Grid.Row="0" Text="Sammelnote erfassen" FontSize="18" FontWeight="SemiBold"/>
|
||||
|
||||
<StackPanel Grid.Row="1" Spacing="6" Margin="0,12,0,10">
|
||||
<Grid ColumnDefinitions="120,*,90" ColumnSpacing="6">
|
||||
<ComboBox Grid.Column="0" ItemsSource="{x:Static vm:GradeCategoryDisplay.Options}"
|
||||
SelectedItem="{Binding CategoryName}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<NumericUpDown Grid.Column="2" Value="{Binding Weight}" Minimum="0" Maximum="10"
|
||||
Increment="0.1" FormatString="0.#" ShowButtonSpinner="False"
|
||||
ToolTip.Tip="Gewichtung"/>
|
||||
</Grid>
|
||||
<TextBox Text="{Binding Note}" PlaceholderText="Notiz (z.B. Hausaufgabenkontrolle)"/>
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<TextBlock Text="Werte pro Schüler unten eintragen. Leere Felder werden nicht gespeichert."
|
||||
FontSize="11" Opacity="0.5" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CollectiveGradeStudentRow">
|
||||
<Grid ColumnDefinitions="*,100" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" FontSize="13"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Value}" PlaceholderText="Wert"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="*,Auto,Auto" Margin="0,16,0,0">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" Foreground="Green" FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Grid.Column="1" Content="Speichern" Command="{Binding SaveAllCommand}" Margin="0,0,8,0"/>
|
||||
<Button Grid.Column="2" Content="Fertig" Click="OnClose"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class CollectiveGradeDialog : Window
|
||||
{
|
||||
public CollectiveGradeDialog() => InitializeComponent();
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<UserControl 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.GradeOverviewTabView"
|
||||
x:DataType="vm:GradeOverviewTabViewModel">
|
||||
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
|
||||
<TextBlock Text="Zeitraum:" VerticalAlignment="Center"/>
|
||||
<ComboBox ItemsSource="{Binding PeriodOptions}" SelectedItem="{Binding SelectedPeriod}" MinWidth="170"/>
|
||||
|
||||
<ToggleButton Content="Punkte anzeigen" IsChecked="{Binding ShowAsPoints}"
|
||||
IsVisible="{Binding CanTogglePointsView}" Margin="12,0,0,0"/>
|
||||
|
||||
<Button Content="Sortierung: Name" Command="{Binding SortByNameCommand}" Margin="12,0,0,0"/>
|
||||
<Button Content="Sortierung: Gesamt" Command="{Binding SortByTotalCommand}"/>
|
||||
|
||||
<Button Content="Noten verwalten" Command="{Binding ManageStudentGradesCommand}" Margin="12,0,0,0"
|
||||
IsVisible="{Binding SelectedRow, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<Button Content="+ Sammelnote" Command="{Binding CollectiveGradeCommand}"/>
|
||||
<Button Content="Zeugnisnoten" Command="{Binding ReportGradesCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid Grid.Row="1"
|
||||
x:Name="GradesGrid"
|
||||
ItemsSource="{Binding Rows}"
|
||||
SelectedItem="{Binding SelectedRow}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="All"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeColumns="True"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="Keine Schüler in dieser Gruppe."
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Opacity="0.35" FontSize="14"
|
||||
IsVisible="{Binding !Rows.Count}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,107 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class GradeOverviewTabView : UserControl
|
||||
{
|
||||
private GradeOverviewTabViewModel? _vm;
|
||||
|
||||
public GradeOverviewTabView() => InitializeComponent();
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
|
||||
if (DataContext is GradeOverviewTabViewModel vm)
|
||||
{
|
||||
_vm = vm;
|
||||
vm.OnManageStudentGrades = ShowStudentGradesDialog;
|
||||
vm.OnCollectiveGrade = ShowCollectiveGradeDialog;
|
||||
vm.OnReportGrades = ShowReportGradesDialog;
|
||||
vm.PropertyChanged += (_, pe) =>
|
||||
{
|
||||
if (pe.PropertyName == nameof(GradeOverviewTabViewModel.RebuildColumnsSignal))
|
||||
BuildColumns();
|
||||
};
|
||||
BuildColumns();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowStudentGradesDialog(GradeOverviewRow row)
|
||||
{
|
||||
var dialogVm = new StudentGradesDialogViewModel(
|
||||
App.Services.GetRequiredService<IGradeRepository>(),
|
||||
row.StudentId, _vm!.GroupId, row.Name);
|
||||
|
||||
var dialog = new StudentGradesDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null) await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task ShowCollectiveGradeDialog()
|
||||
{
|
||||
var dialogVm = new CollectiveGradeDialogViewModel(
|
||||
App.Services.GetRequiredService<IGradeRepository>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
_vm!.GroupId);
|
||||
|
||||
var dialog = new CollectiveGradeDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null) await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task ShowReportGradesDialog()
|
||||
{
|
||||
var dialogVm = new ReportGradeDialogViewModel(
|
||||
App.Services.GetRequiredService<IGradeRepository>(),
|
||||
App.Services.GetRequiredService<IExamRepository>(),
|
||||
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IGradingSchemeRepository>(),
|
||||
App.Services.GetRequiredService<IReportGradeRepository>(),
|
||||
App.Services.GetRequiredService<GradingService>(),
|
||||
_vm!.GroupId, _vm.GroupType, _vm.GradingSystem, _vm.GroupLabel);
|
||||
|
||||
var dialog = new ReportGradeDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null) await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private void BuildColumns()
|
||||
{
|
||||
var grid = this.FindControl<DataGrid>("GradesGrid");
|
||||
if (grid is null || _vm is null) return;
|
||||
|
||||
grid.Columns.Clear();
|
||||
grid.Columns.Add(new DataGridTextColumn
|
||||
{
|
||||
Header = "Schüler",
|
||||
Binding = new Binding("Name"),
|
||||
Width = new DataGridLength(160, DataGridLengthUnitType.Pixel),
|
||||
});
|
||||
|
||||
foreach (var (col, i) in _vm.Columns.Select((c, i) => (c, i)))
|
||||
{
|
||||
var idx = i;
|
||||
grid.Columns.Add(new DataGridTextColumn
|
||||
{
|
||||
Header = col.Header,
|
||||
Binding = new Binding($"Cells[{idx}]"),
|
||||
Width = new DataGridLength(120, DataGridLengthUnitType.Pixel),
|
||||
});
|
||||
}
|
||||
|
||||
grid.Columns.Add(new DataGridTextColumn
|
||||
{
|
||||
Header = "Gesamt",
|
||||
Binding = new Binding("TotalDisplay"),
|
||||
Width = new DataGridLength(80, DataGridLengthUnitType.Pixel),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -151,12 +151,7 @@
|
||||
|
||||
<!-- Tab: Noten -->
|
||||
<ContentPage Header="Noten">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="Notenübersicht" FontSize="16" Opacity="0.4"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Wird implementiert." FontSize="12" Opacity="0.3"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<views:GradeOverviewTabView DataContext="{Binding GradeOverviewTab}"/>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Planung -->
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<DataGridTextColumn Header="Schüler" Binding="{Binding Name}" Width="2*"/>
|
||||
<DataGridTextColumn Header="Sitzungen" Binding="{Binding SessionCount}" Width="Auto"/>
|
||||
<DataGridTextColumn Header="Ø Bewertung" Binding="{Binding AverageDisplay}" Width="Auto"/>
|
||||
<DataGridTextColumn Header="Trend" Binding="{Binding TrendSymbol}" Width="Auto"/>
|
||||
<DataGridTextColumn Header="Entwicklung" Binding="{Binding TrendSymbol}" Width="Auto"/>
|
||||
<DataGridTextColumn Header="Note" Binding="{Binding GradeDisplay}" Width="Auto"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<Border Grid.Column="0"
|
||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,1,0">
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*">
|
||||
|
||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="6" Margin="10,10,10,6">
|
||||
<Button Content="+ Sitzung" Command="{Binding AddSessionCommand}" HorizontalAlignment="Stretch"/>
|
||||
@@ -20,7 +20,10 @@
|
||||
<Button Grid.Row="1" Content="Ø Mitarbeitsnote" Command="{Binding ComputeGradeCommand}"
|
||||
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
||||
|
||||
<ListBox Grid.Row="2"
|
||||
<Button Grid.Row="2" Content="Mitarbeits-Assistent" Command="{Binding OpenWizardCommand}"
|
||||
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
||||
|
||||
<ListBox Grid.Row="3"
|
||||
ItemsSource="{Binding Sessions}"
|
||||
SelectedItem="{Binding SelectedSession}"
|
||||
BorderThickness="0">
|
||||
@@ -48,7 +51,7 @@
|
||||
<ToggleButton Content="◇ Kompetenzen"
|
||||
IsChecked="{Binding CompetencyTagsVisible}"
|
||||
FontSize="11" Padding="8,3"/>
|
||||
<ToggleButton Content="◈ Schüler-Ratings"
|
||||
<ToggleButton Content="◈ Schüler-Bewertungen"
|
||||
IsChecked="{Binding StudentCompetencyRatingsVisible}"
|
||||
FontSize="11" Padding="8,3"/>
|
||||
</StackPanel>
|
||||
|
||||
@@ -24,6 +24,7 @@ public partial class ParticipationTabView : UserControl
|
||||
vm.OnAddSession = ShowAddSessionDialog;
|
||||
vm.OnQuickInput = ShowQuickInputDialog;
|
||||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||||
vm.OnOpenWizard = ShowWizardDialog;
|
||||
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||||
vm.PropertyChanged += (_, pe) =>
|
||||
{
|
||||
@@ -60,6 +61,19 @@ public partial class ParticipationTabView : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
grid.Columns.Add(new DataGridTemplateColumn
|
||||
{
|
||||
Header = "HA",
|
||||
Width = new DataGridLength(50, DataGridLengthUnitType.Pixel),
|
||||
CellTemplate = BuildHomeworkCellTemplate(),
|
||||
});
|
||||
grid.Columns.Add(new DataGridTemplateColumn
|
||||
{
|
||||
Header = "Anwesenheit",
|
||||
Width = new DataGridLength(80, DataGridLengthUnitType.Pixel),
|
||||
CellTemplate = BuildAttendanceCellTemplate(),
|
||||
});
|
||||
|
||||
// Kompetenz-Spalten (opt-in)
|
||||
if (_vm.StudentCompetencyRatingsVisible && _vm.ActiveCompetencyCodes.Count > 0)
|
||||
{
|
||||
@@ -119,6 +133,66 @@ public partial class ParticipationTabView : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
private static IDataTemplate BuildHomeworkCellTemplate()
|
||||
{
|
||||
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||||
{
|
||||
if (row is null) return new TextBlock();
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Content = "HA vergessen",
|
||||
FontSize = 10,
|
||||
Padding = new Avalonia.Thickness(5, 1),
|
||||
Opacity = row.HomeworkMissing ? 1.0 : 0.25,
|
||||
Command = row.ToggleHomeworkCommand,
|
||||
};
|
||||
ToolTip.SetTip(btn, "Hausaufgaben vergessen (an/aus)");
|
||||
row.PropertyChanged += (_, pe) =>
|
||||
{
|
||||
if (pe.PropertyName == nameof(ParticipationStudentRow.HomeworkMissing))
|
||||
btn.Opacity = row.HomeworkMissing ? 1.0 : 0.25;
|
||||
};
|
||||
return new StackPanel
|
||||
{
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
Children = { btn },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static IDataTemplate BuildAttendanceCellTemplate()
|
||||
{
|
||||
return new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||||
{
|
||||
if (row is null) return new TextBlock();
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Content = string.IsNullOrEmpty(row.AttendanceLabel) ? "anwesend" : row.AttendanceLabel,
|
||||
FontSize = 10,
|
||||
Padding = new Avalonia.Thickness(5, 1),
|
||||
Command = row.CycleAttendanceCommand,
|
||||
};
|
||||
ToolTip.SetTip(btn, row.AttendanceTooltip);
|
||||
row.PropertyChanged += (_, pe) =>
|
||||
{
|
||||
if (pe.PropertyName == nameof(ParticipationStudentRow.AttendanceLabel))
|
||||
{
|
||||
btn.Content = string.IsNullOrEmpty(row.AttendanceLabel) ? "anwesend" : row.AttendanceLabel;
|
||||
ToolTip.SetTip(btn, row.AttendanceTooltip);
|
||||
}
|
||||
};
|
||||
return new StackPanel
|
||||
{
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||
Children = { btn },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private static string AspectShortcut(int i) => i switch
|
||||
{
|
||||
0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => ""
|
||||
@@ -163,4 +237,24 @@ public partial class ParticipationTabView : UserControl
|
||||
if (owner is not null)
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async Task ShowWizardDialog(ParticipationTabViewModel tabVm)
|
||||
{
|
||||
var dialogVm = new ParticipationWizardDialogViewModel(
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationAspectRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSectionRepository>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IExamRepository>(),
|
||||
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||
App.Services.GetRequiredService<IGradeRepository>(),
|
||||
App.Services.GetRequiredService<GradingService>(),
|
||||
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem, tabVm.GroupLabel);
|
||||
|
||||
var dialog = new ParticipationWizardDialog { DataContext = dialogVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null)
|
||||
await dialog.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<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.ParticipationWizardDialog"
|
||||
x:DataType="vm:ParticipationWizardDialogViewModel"
|
||||
Title="Mitarbeits-Assistent"
|
||||
Width="920" Height="760" MinWidth="720" MinHeight="520"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto,Auto" Margin="24">
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="2">
|
||||
<TextBlock Text="{Binding GroupLabel}" FontSize="13" Opacity="0.5"/>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StudentName}" FontSize="18" FontWeight="SemiBold"/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="◂ Zurück" Command="{Binding PreviousStudentCommand}"/>
|
||||
<TextBlock Text="{Binding ProgressText}" VerticalAlignment="Center" Opacity="0.6" FontSize="12"/>
|
||||
<Button Content="Weiter ▸" Command="{Binding NextStudentCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="1" Text="Zeitleiste" FontSize="13" FontWeight="SemiBold" Margin="0,14,0,6"/>
|
||||
|
||||
<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Timeline}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="Border.sectionband">
|
||||
<Setter Property="Background" Value="#E1F5EE"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
</Style>
|
||||
<Style Selector="Border.sectionband.open">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseMediumBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Button.haicon">
|
||||
<Setter Property="Opacity" Value="0.2"/>
|
||||
</Style>
|
||||
<Style Selector="Button.haicon.active">
|
||||
<Setter Property="Opacity" Value="1"/>
|
||||
<Setter Property="Background" Value="#E24B4A"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Style>
|
||||
<Style Selector="Button.attendanceicon">
|
||||
<Setter Property="Opacity" Value="0.2"/>
|
||||
</Style>
|
||||
<Style Selector="Button.attendanceicon.active">
|
||||
<Setter Property="Opacity" Value="1"/>
|
||||
<Setter Property="Background" Value="#EF9F27"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WizardSectionGroup">
|
||||
<Border Classes="sectionband" Classes.open="{Binding IsOpen}" CornerRadius="6" Padding="8,8" VerticalAlignment="Top">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding BandLabel}" FontSize="10" Opacity="0.6" TextAlignment="Center"/>
|
||||
<ItemsControl ItemsSource="{Binding Points}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WizardTimelinePoint">
|
||||
<StackPanel Width="54" ToolTip.Tip="{Binding TooltipText}">
|
||||
<TextBlock Text="{Binding DateDisplay}" FontSize="9" Opacity="0.5" HorizontalAlignment="Center"/>
|
||||
|
||||
<Border IsVisible="{Binding !IsExam}" Background="{DynamicResource SystemControlBackgroundAccentBrush}"
|
||||
CornerRadius="3" Padding="4,1" HorizontalAlignment="Center" Margin="0,2">
|
||||
<TextBlock Text="{Binding RatingLabel}" FontSize="11" Foreground="White" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border IsVisible="{Binding IsExam}" Background="#7F77DD"
|
||||
CornerRadius="3" Padding="3,1" HorizontalAlignment="Center" Margin="0,2">
|
||||
<TextBlock Text="{Binding ExamLabel}" FontSize="9" Foreground="White"
|
||||
HorizontalAlignment="Center" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="2"
|
||||
IsVisible="{Binding !IsExam}">
|
||||
<Button Classes="haicon" Classes.active="{Binding HasHomework}"
|
||||
Content="HA" FontSize="8" Padding="3,0" Command="{Binding ToggleHomeworkCommand}"
|
||||
ToolTip.Tip="Hausaufgaben vergessen (an/aus)"/>
|
||||
<Button Classes="attendanceicon" Classes.active="{Binding IsAbsent}"
|
||||
Content="{Binding AttendanceButtonLabel}" FontSize="8" Padding="3,0"
|
||||
Command="{Binding CycleAttendanceCommand}"
|
||||
ToolTip.Tip="{Binding AttendanceTooltip}"/>
|
||||
<TextBlock Text="💬" FontSize="9" IsVisible="{Binding HasNote}" ToolTip.Tip="Bemerkung"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Grid.Row="3" Spacing="4" Margin="0,14,0,0">
|
||||
<TextBlock Text="Abschnittsnoten dieses Schülers" FontSize="13" FontWeight="SemiBold"/>
|
||||
<ItemsControl ItemsSource="{Binding Sections}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WizardSectionRow">
|
||||
<Grid ColumnDefinitions="140,170,100,Auto,*" Margin="0,3">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Label}" VerticalAlignment="Center" FontSize="12"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding RangeDisplay}" VerticalAlignment="Center" FontSize="11" Opacity="0.6"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding Value}" IsEnabled="{Binding !IsOpen}"
|
||||
PlaceholderText="Note" FontSize="12"/>
|
||||
<Button Grid.Column="3" Content="Speichern" Command="{Binding SaveCommand}"
|
||||
IsVisible="{Binding !IsOpen}" Margin="6,0,0,0" FontSize="11" Padding="8,3"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding StatusMessage}" Foreground="Green" FontSize="11"
|
||||
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="4" ColumnDefinitions="*,*" Margin="0,14,0,0">
|
||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="10" Margin="0,0,6,0">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Abschnitt abschließen" FontSize="12" FontWeight="SemiBold"/>
|
||||
<Grid ColumnDefinitions="*,100" ColumnSpacing="6">
|
||||
<TextBox Grid.Column="0" Text="{Binding NewSectionLabel}" PlaceholderText="Bezeichnung"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding NewSectionEndDateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding SectionValidationMessage}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding SectionValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Content="Abschnitt für alle Schüler abschließen" Command="{Binding CloseSectionCommand}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="1" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="10" Margin="6,0,0,0">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Halbjahresnote aus Abschnitten" FontSize="12" FontWeight="SemiBold"/>
|
||||
<ComboBox ItemsSource="{Binding RollupPeriodOptions}" SelectedItem="{Binding RollupPeriod}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Text="{Binding RollupStatusMessage}" Foreground="Green" FontSize="11"
|
||||
IsVisible="{Binding RollupStatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Content="Für alle Schüler übernehmen" Command="{Binding ApplyRollupCommand}"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Button Grid.Row="5" Content="Schließen" HorizontalAlignment="Right" Margin="0,14,0,0" Click="OnClose"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class ParticipationWizardDialog : Window
|
||||
{
|
||||
public ParticipationWizardDialog() => InitializeComponent();
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<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.ReportGradeDialog"
|
||||
x:DataType="vm:ReportGradeDialogViewModel"
|
||||
Title="Zeugnisnoten"
|
||||
Width="820" Height="680" MinWidth="640" MinHeight="440"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="24">
|
||||
|
||||
<StackPanel Grid.Row="0" Spacing="2">
|
||||
<TextBlock Text="{Binding GroupLabel}" FontSize="18" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding SchemeSummary}" FontSize="12" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="12" Margin="0,12,0,10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="Zeitraum:" VerticalAlignment="Center"/>
|
||||
<ComboBox ItemsSource="{Binding PeriodOptions}" SelectedItem="{Binding SelectedPeriod}" MinWidth="170"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="Rundung:" VerticalAlignment="Center"/>
|
||||
<ComboBox ItemsSource="{Binding RoundingOptions}" SelectedItem="{Binding RoundingRuleName}" MinWidth="140"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ReportGradeRow">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,8">
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="180,90,*,120,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center" FontSize="13"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding CalculatedDisplay}" VerticalAlignment="Center"
|
||||
FontSize="13" Opacity="0.7" ToolTip.Tip="Berechnet"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding OverrideValue}" PlaceholderText="Übersteuern (optional)"
|
||||
IsEnabled="{Binding !IsLocked}"/>
|
||||
<TextBox Grid.Column="3" Text="{Binding OverrideReason}" PlaceholderText="Begründung"
|
||||
IsEnabled="{Binding !IsLocked}" Margin="6,0,0,0"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding FinalDisplay}" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" Margin="10,0" FontSize="14"/>
|
||||
<StackPanel Grid.Column="5" Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="Speichern" Command="{Binding SaveCommand}" IsEnabled="{Binding !IsLocked}"/>
|
||||
<Button Content="{Binding LockLabel}" Command="{Binding ToggleLockCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="Auto,*,Auto" Margin="0,16,0,0">
|
||||
<Button Grid.Column="0" Content="Als CSV exportieren" Click="OnExportClick"/>
|
||||
<Button Grid.Column="2" Content="Schließen" Click="OnClose"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class ReportGradeDialog : Window
|
||||
{
|
||||
public ReportGradeDialog() => InitializeComponent();
|
||||
|
||||
private async void OnExportClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not ReportGradeDialogViewModel vm) return;
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is null) return;
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Zeugnisnoten exportieren",
|
||||
SuggestedFileName = $"Zeugnisnoten_{vm.GroupLabel}.csv",
|
||||
FileTypeChoices = [new FilePickerFileType("CSV-Dateien") { Patterns = ["*.csv"] }],
|
||||
});
|
||||
|
||||
if (file is null) return;
|
||||
await File.WriteAllTextAsync(file.Path.LocalPath, vm.ExportCsv());
|
||||
}
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<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.StudentGradesDialog"
|
||||
x:DataType="vm:StudentGradesDialogViewModel"
|
||||
Title="{Binding StudentName}"
|
||||
Width="560" Height="600" MinWidth="480" MinHeight="360"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="24">
|
||||
|
||||
<TextBlock Grid.Row="0" Text="{Binding StudentName}" FontSize="18" FontWeight="SemiBold"/>
|
||||
|
||||
<Button Grid.Row="1" Content="+ Note hinzufügen" Command="{Binding AddEntryCommand}"
|
||||
HorizontalAlignment="Left" Margin="0,10,0,10"/>
|
||||
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ItemsControl ItemsSource="{Binding Entries}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GradeEditItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,8">
|
||||
<StackPanel Spacing="6">
|
||||
<Grid ColumnDefinitions="120,*,90,90" ColumnSpacing="6">
|
||||
<ComboBox Grid.Column="0" ItemsSource="{x:Static vm:GradeCategoryDisplay.Options}"
|
||||
SelectedItem="{Binding CategoryName}"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding Value}" PlaceholderText="Wert (z.B. 2 oder 11)"/>
|
||||
<TextBox Grid.Column="2" Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
|
||||
<NumericUpDown Grid.Column="3" Value="{Binding Weight}" Minimum="0" Maximum="10"
|
||||
Increment="0.1" FormatString="0.#" ShowButtonSpinner="False"
|
||||
ToolTip.Tip="Gewichtung"/>
|
||||
</Grid>
|
||||
<TextBox Text="{Binding Note}" PlaceholderText="Notiz (optional)" FontSize="12"/>
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding CreatedAtDisplay}" FontSize="11" Opacity="0.5"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Speichern" Command="{Binding SaveCommand}" Margin="0,0,6,0"/>
|
||||
<Button Grid.Column="2" Content="Löschen" Command="{Binding DeleteCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="Noch keine Noten erfasst." Opacity="0.4" FontSize="13"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,20,0,0"
|
||||
IsVisible="{Binding !Entries.Count}"/>
|
||||
|
||||
<Button Grid.Row="3" Content="Fertig" HorizontalAlignment="Stretch" Margin="0,16,0,0" Click="OnClose"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class StudentGradesDialog : Window
|
||||
{
|
||||
public StudentGradesDialog() => InitializeComponent();
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -77,9 +77,9 @@
|
||||
<NumericUpDown Grid.Column="2"
|
||||
Value="{Binding CatalogGradeLevel}"
|
||||
Minimum="1" Maximum="13" FormatString="0"/>
|
||||
<Button Grid.Column="4" Content="Import JSON" Click="OnImportClick"
|
||||
<Button Grid.Column="4" Content="JSON importieren" Click="OnImportClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<Button Grid.Column="6" Content="Export JSON" Click="OnExportClick"
|
||||
<Button Grid.Column="6" Content="JSON exportieren" Click="OnExportClick"
|
||||
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -222,6 +222,50 @@
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: Notenschema -->
|
||||
<ContentPage Header="Notenschema">
|
||||
<ContentPage.Resources>
|
||||
<DataTemplate x:Key="GradingSchemeTemplate" DataType="vm:GradingSchemeEditItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding Label}" FontWeight="SemiBold" FontSize="14"/>
|
||||
<Grid ColumnDefinitions="Auto,90,Auto,90,Auto,90" ColumnSpacing="6">
|
||||
<TextBlock Grid.Column="0" Text="Klausuren %" VerticalAlignment="Center" FontSize="12"/>
|
||||
<NumericUpDown Grid.Column="1" Value="{Binding ExamsPercent}" Minimum="0" Maximum="100"
|
||||
FormatString="0.#" ShowButtonSpinner="False"/>
|
||||
<TextBlock Grid.Column="2" Text="Mitarbeit %" VerticalAlignment="Center" FontSize="12"/>
|
||||
<NumericUpDown Grid.Column="3" Value="{Binding ParticipationPercent}" Minimum="0" Maximum="100"
|
||||
FormatString="0.#" ShowButtonSpinner="False"/>
|
||||
<TextBlock Grid.Column="4" Text="Sonstige %" VerticalAlignment="Center" FontSize="12"/>
|
||||
<NumericUpDown Grid.Column="5" Value="{Binding OtherPercent}" Minimum="0" Maximum="100"
|
||||
FormatString="0.#" ShowButtonSpinner="False"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" Foreground="Green" FontSize="12"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Grid.Column="1" Content="Speichern" Command="{Binding SaveCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ContentPage.Resources>
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||
|
||||
<TextBlock Text="Voreinstellung der Gewichtung für die Zeugnisnote (Klausuren / Mitarbeit / Sonstige), je Gruppentyp. Kann pro Lerngruppe überschrieben werden."
|
||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||
|
||||
<ContentControl Content="{Binding ClassScheme}" ContentTemplate="{StaticResource GradingSchemeTemplate}"/>
|
||||
<ContentControl Content="{Binding CourseScheme}" ContentTemplate="{StaticResource GradingSchemeTemplate}"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -129,10 +129,62 @@
|
||||
</ContentPage>
|
||||
|
||||
<ContentPage Header="Noten">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock Text="Notenübersicht" FontSize="16" Opacity="0.4" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Wird implementiert." FontSize="12" Opacity="0.3" HorizontalAlignment="Center"/>
|
||||
<ScrollViewer Padding="20">
|
||||
<StackPanel Spacing="20">
|
||||
<TextBlock Text="Notenentwicklung" FontSize="15" FontWeight="SemiBold"/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding GradeHistory}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:StudentGradeHistoryGroup">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="14,12" Margin="0,0,0,12">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Label}" FontWeight="SemiBold" FontSize="13"/>
|
||||
<Border Grid.Column="1" Background="#E53935" CornerRadius="4" Padding="6,1"
|
||||
IsVisible="{Binding HasWarnings}">
|
||||
<TextBlock Text="Auffälligkeiten" FontSize="10" Foreground="White"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding Points}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:GradeHistoryPoint">
|
||||
<StackPanel Width="46" VerticalAlignment="Bottom" ToolTip.Tip="{Binding TooltipText}">
|
||||
<Border Classes="historybar" Height="{Binding BarHeight}" Width="16" CornerRadius="2"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||
Classes.warning="{Binding IsWarning}"/>
|
||||
<TextBlock Text="{Binding Value}" FontSize="11" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" Margin="0,3,0,0"/>
|
||||
<TextBlock Text="{Binding DateDisplay}" FontSize="9" Opacity="0.5"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="Border.historybar">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border.historybar.warning">
|
||||
<Setter Property="Background" Value="#E53935"/>
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Text="Noch keine Noten für diesen Schüler erfasst." Opacity="0.4"
|
||||
IsVisible="{Binding !GradeHistory.Count}"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<ContentPage Header="Dokumentation">
|
||||
|
||||
@@ -102,36 +102,46 @@ pragmatisch über "Abwesend" bei den übrigen Schülern statt über eine eigene
|
||||
## 2. Noten & Zeugnisnoten
|
||||
|
||||
Modell `Grade` existiert in [Planning.cs](LehrerApp.Core/Models/Planning.cs), `GradeRepository` ebenfalls.
|
||||
Der Tab "Noten" in [GroupDetailView.axaml:83](LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml#L83) ist ein Platzhalter.
|
||||
Der Tab "Noten" in [GroupDetailView.axaml](LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml) ist jetzt
|
||||
vollständig umgesetzt (siehe unten), ebenso der gleichnamige Tab im Schülerdetail (2.5).
|
||||
|
||||
### 2.1 Notenübersicht der Gruppe
|
||||
- [ ] **2.1.1** Matrix: Zeilen = Schüler, Spalten = alle Leistungen (Klausuren, Mitarbeit je Halbjahr,
|
||||
- [x] **2.1.1** Matrix: Zeilen = Schüler, Spalten = alle Leistungen (Klausuren, Mitarbeit je Halbjahr,
|
||||
sonstige Noten). Zelle zeigt Note/Punkte.
|
||||
- [ ] **2.1.2** Spalte "Gesamt" mit gewichtetem Durchschnitt über `GradingService.WeightedAverage()`.
|
||||
- [ ] **2.1.3** Sortierung nach Name / Gesamtnote, Umschalten Noten ↔ Punkte.
|
||||
- [ ] **2.1.4** Halbjahresfilter (H1 / H2 / Gesamtjahr), berücksichtigt `GroupMembership.Period`.
|
||||
- [x] **2.1.2** Spalte "Gesamt" mit gewichtetem Durchschnitt über `GradingService.WeightedAverage()`.
|
||||
- [x] **2.1.3** Sortierung nach Name / Gesamtnote, Umschalten Noten ↔ Punkte.
|
||||
- [x] **2.1.4** Halbjahresfilter (H1 / H2 / Gesamtjahr), berücksichtigt `GroupMembership.Period`.
|
||||
|
||||
### 2.2 Einzelnoten pflegen
|
||||
- [ ] **2.2.1** Dialog "Note hinzufügen": Kategorie (`GradeCategory`), Wert, Datum, Gewichtung, Notiz.
|
||||
- [ ] **2.2.2** Note bearbeiten / löschen mit Historie (wer/wann geändert) — mindestens `CreatedAt` sichtbar.
|
||||
- [ ] **2.2.3** Sammelerfassung: eine Note (z.B. Hausaufgabenkontrolle) für die ganze Gruppe auf einmal.
|
||||
- [x] **2.2.1** Dialog "Note hinzufügen": Kategorie (`GradeCategory`), Wert, Datum, Gewichtung, Notiz.
|
||||
- [x] **2.2.2** Note bearbeiten / löschen mit Historie (wer/wann geändert) — mindestens `CreatedAt` sichtbar.
|
||||
- [x] **2.2.3** Sammelerfassung: eine Note (z.B. Hausaufgabenkontrolle) für die ganze Gruppe auf einmal.
|
||||
|
||||
### 2.3 Gewichtungsschema
|
||||
- [ ] **2.3.1** Neues Modell `GradingScheme` je Gruppe: prozentuale Anteile von
|
||||
- [x] **2.3.1** Neues Modell `GradingScheme` je Gruppe: prozentuale Anteile von
|
||||
Klausuren / Mitarbeit / sonstige Leistungen (z.B. 50/40/10).
|
||||
- [ ] **2.3.2** UI zur Bearbeitung, Validierung auf Summe 100 %.
|
||||
- [ ] **2.3.3** Voreinstellung je Gruppentyp (`Class` vs. `Course`) in den Einstellungen.
|
||||
- [x] **2.3.2** UI zur Bearbeitung, Validierung auf Summe 100 %.
|
||||
- [x] **2.3.3** Voreinstellung je Gruppentyp (`Class` vs. `Course`) in den Einstellungen.
|
||||
|
||||
### 2.4 Zeugnisnote
|
||||
- [ ] **2.4.1** Berechnung der Zeugnisnote aus Schema (2.3) + allen Teilnoten,
|
||||
- [x] **2.4.1** Berechnung der Zeugnisnote aus Schema (2.3) + allen Teilnoten,
|
||||
Rundungsregel konfigurierbar (kaufmännisch / pädagogisch abweichbar).
|
||||
- [ ] **2.4.2** Manuelles Übersteuern mit Pflicht-Begründung (pädagogischer Spielraum).
|
||||
- [ ] **2.4.3** Zeugnisnoten-Ansicht mit Sperren/Festschreiben zum Konferenztermin.
|
||||
- [ ] **2.4.4** Export der Zeugnisnotenliste (siehe 11.2).
|
||||
- [x] **2.4.2** Manuelles Übersteuern mit Pflicht-Begründung (pädagogischer Spielraum).
|
||||
- [x] **2.4.3** Zeugnisnoten-Ansicht mit Sperren/Festschreiben zum Konferenztermin.
|
||||
- [x] **2.4.4** Export der Zeugnisnotenliste (siehe 11.2).
|
||||
|
||||
### 2.5 Notenentwicklung
|
||||
- [ ] **2.5.1** Verlaufsdiagramm pro Schüler über das Schuljahr (im Schülerdetail).
|
||||
- [ ] **2.5.2** Auffälligkeiten markieren: Abfall um ≥ 1 Note, Versetzungsgefährdung (Note 5/6).
|
||||
- [x] **2.5.1** Verlaufsdiagramm pro Schüler über das Schuljahr (im Schülerdetail).
|
||||
- [x] **2.5.2** Auffälligkeiten markieren: Abfall um ≥ 1 Note, Versetzungsgefährdung (Note 5/6).
|
||||
|
||||
Umgesetzt über [GradeOverviewViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs)
|
||||
(Matrix-Tab, Einzelnoten-Dialoge), [ReportGradeViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs)
|
||||
(Zeugnisnoten-Dialog) und die neuen Modelle `GradingScheme`/`ReportGrade` in
|
||||
[Planning.cs](LehrerApp.Core/Models/Planning.cs). Das Gewichtungsschema wird pro Gruppe gesucht, sonst
|
||||
die Voreinstellung des Gruppentyps (Einstellungen → Notenschema), sonst ein Fallback 50/40/10 verwendet;
|
||||
Bereiche ohne Werte werden bei der Berechnung ausgelassen und die verbleibenden Prozentanteile neu normiert
|
||||
(`GradingService.CalculateReportGrade()`). Notenentwicklung im Schülerdetail zeigt ein einfaches
|
||||
Balken-Sparkline je Lerngruppe über alle Klausur- und Einzelnoten-Einträge chronologisch.
|
||||
|
||||
---
|
||||
|
||||
@@ -170,10 +180,40 @@ Mitarbeit-Note für denselben Zeitraum statt sie zu duplizieren (erkannt über d
|
||||
### 3.3 Sitzungen
|
||||
- [ ] **3.3.1** Sitzung automatisch aus einer geplanten `Lesson` erzeugen
|
||||
(Datum + Thema übernehmen) — Abhängigkeit zu 4.2.
|
||||
- [ ] **3.3.2** Anwesenheit in der Sitzung erfassen (fehlend/verspätet), Verknüpfung mit 5.2.
|
||||
- [x] **3.3.2** Anwesenheit in der Sitzung erfassen — als `ParticipationEntry.Attendance`
|
||||
(krank: Entschuldigung offen/entschuldigt/unentschuldigt), nicht als "verspätet"; siehe unten.
|
||||
Vorgriff auf 5.2, ersetzt dessen 5.2.1/5.2.4 aber nicht vollständig (kein Fehlzeitenmodul
|
||||
über alle Stundentypen hinweg, nur innerhalb von Mitarbeit-Sitzungen).
|
||||
- [ ] **3.3.3** Sitzung bearbeiten/löschen inkl. Rückfrage bei vorhandenen Bewertungen.
|
||||
- [ ] **3.3.4** Sitzungen mehrerer Gruppen im Kalenderüberblick.
|
||||
|
||||
### Abschnittsnoten & Mitarbeits-Assistent (nicht aus dieser Liste, eigener Workflow-Bedarf)
|
||||
|
||||
Ergänzt den Workflow "regelmäßig (alle 4–7 Wochen) mündliche Noten zu einer Abschnittsnote
|
||||
zusammenziehen, daraus die Halbjahresnote bilden":
|
||||
|
||||
- `ParticipationEntry.HomeworkMissing` (bool) und `.Attendance` (`AttendanceStatus?`:
|
||||
`ExcusePending`/`Excused`/`Unexcused`) — editierbar direkt im Bewertungsraster
|
||||
([ParticipationTabView.axaml](LehrerApp.Desktop/Views/Groups/ParticipationTabView.axaml), Spalten
|
||||
"HA"/"Anwesenheit") und im neuen Mitarbeits-Assistenten. `ExcusePending` ist bewusst ein
|
||||
Zwischenzustand, da die Entschuldigung meist erst später eintrifft.
|
||||
- Neues Modell `ParticipationSection` (Abschnitt: Start/Ende/Bezeichnung je Gruppe) — nur
|
||||
abgeschlossene Abschnitte werden gespeichert, der laufende Zeitraum ergibt sich aus dem Ende
|
||||
des letzten Abschnitts bis heute.
|
||||
- Neues Fenster **Mitarbeits-Assistent**
|
||||
([ParticipationWizardViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/ParticipationWizardViewModels.cs),
|
||||
[ParticipationWizardDialog.axaml](LehrerApp.Desktop/Views/Groups/ParticipationWizardDialog.axaml)):
|
||||
Zeitleiste je Schüler (Sitzungen als Bewertungs-Badges, Klausurtermine, Symbole für Hausaufgaben/
|
||||
Bemerkung/Anwesenheit), gruppiert nach Abschnitten. "Abschnitt abschließen" berechnet für alle
|
||||
Schüler einen Notenvorschlag aus den Sitzungen im Zeitraum und speichert ihn als `Grade`
|
||||
(`Category = Participation`, `Note = "Abschnitt: <Bezeichnung>"`), individuell nachjustierbar.
|
||||
"Halbjahresnote aus Abschnitten übernehmen" mittelt die Abschnittsnoten eines Zeitraums und
|
||||
speichert sie über denselben `Grade.Note`-Tag wie der bestehende "Ø Mitarbeitsnote"-Dialog (3.2) —
|
||||
beide Wege sind kompatibel, der Assistent baut auf 3.2 auf statt es zu ersetzen.
|
||||
- Dashboard-Kachel "Offene Entschuldigungen": listet alle `ExcusePending`-Einträge der letzten
|
||||
21 Tage gruppenübergreifend mit Direktauflösung; ältere Einträge werden ausgeblendet statt
|
||||
automatisch entschieden (pädagogische Entscheidung bleibt bei der Lehrkraft).
|
||||
|
||||
---
|
||||
|
||||
## 4. Unterrichtsplanung
|
||||
|
||||
@@ -37,6 +37,26 @@ ein eigenes Feld wie `Student.SchoolEntryDate` zu verwenden.
|
||||
Pro Kombination aus Schüler und Lerngruppe darf es höchstens eine Zuordnung
|
||||
geben.
|
||||
|
||||
## Gewichtungsschema (`GradingScheme`)
|
||||
|
||||
Legt die prozentuale Gewichtung von Klausuren, Mitarbeit und sonstigen Leistungen für die
|
||||
Zeugnisnote fest (Kapitel 2.3/2.4). Ein Datensatz ist entweder:
|
||||
|
||||
- gruppenspezifisch (`GroupId` gesetzt, `GroupType` leer), oder
|
||||
- eine Voreinstellung je Gruppentyp (`GroupType` gesetzt, `GroupId` leer, in den Einstellungen
|
||||
gepflegt).
|
||||
|
||||
Bei der Zeugnisnotenberechnung wird zuerst nach einem gruppenspezifischen Schema gesucht, sonst
|
||||
nach der Voreinstellung des Gruppentyps, sonst greift ein fest codierter Fallback (50/40/10).
|
||||
|
||||
## Zeugnisnote (`ReportGrade`)
|
||||
|
||||
Eine Zeugnisnote gehört zu genau einem Schüler, einer Lerngruppe und einem Zeitraum
|
||||
(`Period`: "Gesamtes Schuljahr" / "1. Halbjahr" / "2. Halbjahr" — freier Text, keine Verknüpfung
|
||||
zu `MembershipPeriod`). `CalculatedValue` ist das zuletzt berechnete Ergebnis, `OverrideValue`
|
||||
überschreibt es bei pädagogischem Ermessen und erfordert `OverrideReason`. Nach dem Festschreiben
|
||||
(`IsLocked`) wird der Datensatz bei einer Neuberechnung nicht mehr verändert.
|
||||
|
||||
## Bewusst gespeicherte Momentaufnahmen
|
||||
|
||||
Einige berechnete Werte bleiben absichtlich gespeichert:
|
||||
@@ -65,6 +85,7 @@ Die Datenbank schützt folgende Kombinationen mit eindeutigen Indizes:
|
||||
- Gruppenzuordnung: `StudentId + GroupId`
|
||||
- Klausurergebnis: `ExamId + StudentId`
|
||||
- Mitarbeitseintrag: `SessionId + StudentId`
|
||||
- Zeugnisnote: `StudentId + GroupId + Period`
|
||||
- Fach: normalisierter Fachname
|
||||
|
||||
Altdaten werden beim Öffnen der Datenbank automatisch migriert. Die Migration
|
||||
|
||||
Reference in New Issue
Block a user