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 Save(Grade grade);
|
||||||
void Delete(Guid id);
|
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
|
public interface IUnitRepository
|
||||||
{
|
{
|
||||||
Unit? GetById(Guid id);
|
Unit? GetById(Guid id);
|
||||||
@@ -116,6 +130,12 @@ public interface IParticipationAspectRepository
|
|||||||
void Save(ParticipationAspect aspect);
|
void Save(ParticipationAspect aspect);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
}
|
}
|
||||||
|
public interface IParticipationSectionRepository
|
||||||
|
{
|
||||||
|
List<ParticipationSection> GetByGroup(Guid groupId);
|
||||||
|
void Save(ParticipationSection section);
|
||||||
|
void Delete(Guid id);
|
||||||
|
}
|
||||||
public interface ISubjectRepository
|
public interface ISubjectRepository
|
||||||
{
|
{
|
||||||
List<Subject> GetAll();
|
List<Subject> GetAll();
|
||||||
|
|||||||
@@ -20,9 +20,34 @@ public class ParticipationEntry
|
|||||||
public List<AspectRating> Ratings { get; set; } = [];
|
public List<AspectRating> Ratings { get; set; } = [];
|
||||||
public List<CompetencyRating> CompetencyRatings { get; set; } = [];
|
public List<CompetencyRating> CompetencyRatings { get; set; } = [];
|
||||||
public string? Note { get; set; }
|
public string? Note { get; set; }
|
||||||
|
public bool HomeworkMissing { get; set; }
|
||||||
|
public AttendanceStatus? Attendance { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
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 class CompetencyRating
|
||||||
{
|
{
|
||||||
public string Code { get; set; } = "";
|
public string Code { get; set; } = "";
|
||||||
|
|||||||
@@ -14,6 +14,22 @@ public class Grade
|
|||||||
}
|
}
|
||||||
public enum GradeCategory { Oral, Homework, Participation, Project, Other }
|
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 class Unit
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
@@ -45,3 +61,22 @@ public class Lesson
|
|||||||
}
|
}
|
||||||
public enum UnitStatus { Planned, Active, Completed }
|
public enum UnitStatus { Planned, Active, Completed }
|
||||||
public enum LessonStatus { Planned, Conducted }
|
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);
|
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)
|
public double WeightedAverage(List<(string Grade, double Weight)> grades)
|
||||||
{
|
{
|
||||||
var numeric = grades
|
var numeric = grades
|
||||||
@@ -73,4 +83,51 @@ public class GradingService
|
|||||||
var total = numeric.Sum(g => g.Weight);
|
var total = numeric.Sum(g => g.Weight);
|
||||||
return total == 0 ? 0 : numeric.Sum(g => g.Value!.Value * g.Weight) / total;
|
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<Exam> Exams => _db.GetCollection<Exam>("exams");
|
||||||
public ILiteCollection<ExamResult> ExamResults => _db.GetCollection<ExamResult>("exam_results");
|
public ILiteCollection<ExamResult> ExamResults => _db.GetCollection<ExamResult>("exam_results");
|
||||||
public ILiteCollection<Grade> Grades => _db.GetCollection<Grade>("grades");
|
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<GradingKeyTemplate> GradingKeyTemplates => _db.GetCollection<GradingKeyTemplate>("grading_key_templates");
|
||||||
public ILiteCollection<Unit> Units => _db.GetCollection<Unit>("units");
|
public ILiteCollection<Unit> Units => _db.GetCollection<Unit>("units");
|
||||||
public ILiteCollection<Lesson> Lessons => _db.GetCollection<Lesson>("lessons");
|
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<ParticipationSession> ParticipationSessions => _db.GetCollection<ParticipationSession>("participation_sessions");
|
||||||
public ILiteCollection<ParticipationEntry> ParticipationEntries => _db.GetCollection<ParticipationEntry>("participation");
|
public ILiteCollection<ParticipationEntry> ParticipationEntries => _db.GetCollection<ParticipationEntry>("participation");
|
||||||
public ILiteCollection<ParticipationAspect> ParticipationAspects => _db.GetCollection<ParticipationAspect>("participation_aspects");
|
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<Subject> Subjects => _db.GetCollection<Subject>("subjects");
|
||||||
public ILiteCollection<CompetencyDomain> CompetencyDomains => _db.GetCollection<CompetencyDomain>("competency_domains");
|
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);
|
BsonExpression.Create("STRING($.ExamId) + ':' + STRING($.StudentId)"), unique: true);
|
||||||
Grades.EnsureIndex(x => x.StudentId);
|
Grades.EnsureIndex(x => x.StudentId);
|
||||||
Grades.EnsureIndex(x => x.GroupId);
|
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);
|
GradingKeyTemplates.EnsureIndex(x => x.GradingSystem);
|
||||||
Units.EnsureIndex(x => x.GroupId);
|
Units.EnsureIndex(x => x.GroupId);
|
||||||
Lessons.EnsureIndex(x => x.UnitId);
|
Lessons.EnsureIndex(x => x.UnitId);
|
||||||
@@ -167,6 +176,7 @@ public class LiteDbContext : IDisposable
|
|||||||
ParticipationEntries.EnsureIndex("ux_session_student",
|
ParticipationEntries.EnsureIndex("ux_session_student",
|
||||||
BsonExpression.Create("STRING($.SessionId) + ':' + STRING($.StudentId)"), unique: true);
|
BsonExpression.Create("STRING($.SessionId) + ':' + STRING($.StudentId)"), unique: true);
|
||||||
ParticipationAspects.EnsureIndex(x => x.GroupId);
|
ParticipationAspects.EnsureIndex(x => x.GroupId);
|
||||||
|
ParticipationSections.EnsureIndex(x => x.GroupId);
|
||||||
Subjects.EnsureIndex(x => x.Name);
|
Subjects.EnsureIndex(x => x.Name);
|
||||||
Subjects.EnsureIndex("ux_subject_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
|
Subjects.EnsureIndex("ux_subject_name", BsonExpression.Create("LOWER(TRIM($.Name))"), unique: true);
|
||||||
CompetencyDomains.EnsureIndex(x => x.SubjectId);
|
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 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 class UnitRepository(LiteDbContext db) : IUnitRepository
|
||||||
{
|
{
|
||||||
public Unit? GetById(Guid id) => db.Units.FindById(id);
|
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 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 class SubjectRepository(LiteDbContext db) : ISubjectRepository
|
||||||
{
|
{
|
||||||
public List<Subject> GetAll() => db.Subjects.FindAll().OrderBy(s => s.Name).ToList();
|
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<IExamRepository, ExamRepository>();
|
||||||
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
|
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
|
||||||
services.AddSingleton<IGradeRepository, GradeRepository>();
|
services.AddSingleton<IGradeRepository, GradeRepository>();
|
||||||
|
services.AddSingleton<IGradingSchemeRepository, GradingSchemeRepository>();
|
||||||
|
services.AddSingleton<IReportGradeRepository, ReportGradeRepository>();
|
||||||
services.AddSingleton<IGradingKeyTemplateRepository, GradingKeyTemplateRepository>();
|
services.AddSingleton<IGradingKeyTemplateRepository, GradingKeyTemplateRepository>();
|
||||||
services.AddSingleton<IUnitRepository, UnitRepository>();
|
services.AddSingleton<IUnitRepository, UnitRepository>();
|
||||||
services.AddSingleton<ILessonRepository, LessonRepository>();
|
services.AddSingleton<ILessonRepository, LessonRepository>();
|
||||||
@@ -57,6 +59,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton<IParticipationSessionRepository, ParticipationSessionRepository>();
|
services.AddSingleton<IParticipationSessionRepository, ParticipationSessionRepository>();
|
||||||
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
|
services.AddSingleton<IParticipationRepository, ParticipationRepository>();
|
||||||
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
|
services.AddSingleton<IParticipationAspectRepository, ParticipationAspectRepository>();
|
||||||
|
services.AddSingleton<IParticipationSectionRepository, ParticipationSectionRepository>();
|
||||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||||
|
|
||||||
@@ -111,6 +114,7 @@ public static class AppBootstrapper
|
|||||||
services.AddTransient<GroupDetailViewModel>();
|
services.AddTransient<GroupDetailViewModel>();
|
||||||
services.AddTransient<StudentDetailViewModel>();
|
services.AddTransient<StudentDetailViewModel>();
|
||||||
services.AddTransient<ParticipationTabViewModel>();
|
services.AddTransient<ParticipationTabViewModel>();
|
||||||
|
services.AddTransient<GradeOverviewTabViewModel>();
|
||||||
services.AddTransient<AddGroupDialogViewModel>();
|
services.AddTransient<AddGroupDialogViewModel>();
|
||||||
services.AddTransient<SettingsViewModel>();
|
services.AddTransient<SettingsViewModel>();
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,13 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
private readonly ILessonRepository _lessons;
|
private readonly ILessonRepository _lessons;
|
||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IWorkTaskRepository _tasks;
|
private readonly IWorkTaskRepository _tasks;
|
||||||
|
private readonly IParticipationSessionRepository _participationSessions;
|
||||||
|
private readonly IParticipationRepository _participationEntries;
|
||||||
|
private readonly IStudentRepository _students;
|
||||||
private readonly SchoolYearService _sy;
|
private readonly SchoolYearService _sy;
|
||||||
|
|
||||||
|
private const int OpenExcuseMaxAgeDays = 21;
|
||||||
|
|
||||||
[ObservableProperty] private string _greeting = "";
|
[ObservableProperty] private string _greeting = "";
|
||||||
[ObservableProperty] private string _currentDate = "";
|
[ObservableProperty] private string _currentDate = "";
|
||||||
[ObservableProperty] private string _currentSchoolYear = "";
|
[ObservableProperty] private string _currentSchoolYear = "";
|
||||||
@@ -30,15 +35,19 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
|
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
|
||||||
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
|
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
|
||||||
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
|
||||||
|
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||||
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
|
||||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
||||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||||
|
|
||||||
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
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();
|
Load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +89,41 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
|
|
||||||
CalendarMonth = FirstOfMonth(now);
|
CalendarMonth = FirstOfMonth(now);
|
||||||
LoadCalendar();
|
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()
|
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 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; } = ""; }
|
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 class CalendarDayCell
|
||||||
{
|
{
|
||||||
public int DayNumber { get; }
|
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 ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||||
|
|
||||||
public ParticipationTabViewModel ParticipationTab { get; }
|
public ParticipationTabViewModel ParticipationTab { get; }
|
||||||
|
public GradeOverviewTabViewModel GradeOverviewTab { get; }
|
||||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||||
public Func<Exam, Task<bool>>? OnEditExam { 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,
|
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||||
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
IGroupMembershipRepository memberships, ISubjectRepository subjects,
|
||||||
IExamRepository exams, IGradeRepository grades,
|
IExamRepository exams, IGradeRepository grades,
|
||||||
ParticipationTabViewModel participationTab)
|
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab)
|
||||||
{
|
{
|
||||||
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
|
||||||
_exams = exams; _grades = grades;
|
_exams = exams; _grades = grades;
|
||||||
ParticipationTab = participationTab;
|
ParticipationTab = participationTab;
|
||||||
|
GradeOverviewTab = gradeOverviewTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadGroup(Guid id)
|
public void LoadGroup(Guid id)
|
||||||
@@ -214,6 +216,7 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
LoadStudents();
|
LoadStudents();
|
||||||
ReloadExams();
|
ReloadExams();
|
||||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||||
|
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReloadExams()
|
private void ReloadExams()
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
public Guid GroupId => _groupId;
|
public Guid GroupId => _groupId;
|
||||||
public string SchoolYear => _schoolYear;
|
public string SchoolYear => _schoolYear;
|
||||||
public GradingSystem GradingSystem => _gradingSystem;
|
public GradingSystem GradingSystem => _gradingSystem;
|
||||||
|
public string GroupLabel { get; private set; } = "";
|
||||||
|
|
||||||
[ObservableProperty] private ParticipationSessionItem? _selectedSession;
|
[ObservableProperty] private ParticipationSessionItem? _selectedSession;
|
||||||
[ObservableProperty] private string _noDataText = "Keine Bewertungssitzung ausgewählt.";
|
[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<Task<ParticipationSession?>>? OnAddSession { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
||||||
|
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
|
||||||
|
|
||||||
public ParticipationTabViewModel(
|
public ParticipationTabViewModel(
|
||||||
IParticipationSessionRepository sessions,
|
IParticipationSessionRepository sessions,
|
||||||
@@ -72,6 +74,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
_subjectId = group?.SubjectId;
|
_subjectId = group?.SubjectId;
|
||||||
_gradeLevel = group?.GradeLevel ?? 0;
|
_gradeLevel = group?.GradeLevel ?? 0;
|
||||||
_gradingSystem = group?.GradingSystem ?? GradingSystem.Grades1To6;
|
_gradingSystem = group?.GradingSystem ?? GradingSystem.Grades1To6;
|
||||||
|
GroupLabel = group?.Name ?? "";
|
||||||
|
|
||||||
HasCompetencyCatalog = _subjectId.HasValue
|
HasCompetencyCatalog = _subjectId.HasValue
|
||||||
&& _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel).Count > 0;
|
&& _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);
|
var row = new ParticipationStudentRow(s.Id, s.FullName, entry, Aspects.ToList(), ActiveCompetencyCodes);
|
||||||
row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val);
|
row.OnRatingChanged = (sid, key, val) => SaveRating(sessionId, sid, key, val);
|
||||||
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, 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);
|
StudentRows.Add(row);
|
||||||
}
|
}
|
||||||
QuickInputCommand.NotifyCanExecuteChanged();
|
QuickInputCommand.NotifyCanExecuteChanged();
|
||||||
@@ -185,6 +190,22 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
|
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]
|
[RelayCommand]
|
||||||
private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible;
|
private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible;
|
||||||
|
|
||||||
@@ -259,6 +280,13 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
await OnComputeGrade(this);
|
await OnComputeGrade(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task OpenWizard()
|
||||||
|
{
|
||||||
|
if (OnOpenWizard is null) return;
|
||||||
|
await OnOpenWizard(this);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void DeleteSession()
|
private void DeleteSession()
|
||||||
{
|
{
|
||||||
@@ -310,8 +338,16 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
public ObservableCollection<RatingCell> Cells { get; } = [];
|
public ObservableCollection<RatingCell> Cells { get; } = [];
|
||||||
public ObservableCollection<RatingCell> CompetencyCells { 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?>? OnRatingChanged { get; set; }
|
||||||
public Action<Guid, string, int?>? OnCompetencyRatingChanged { 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,
|
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
|
||||||
List<AspectColumnDef> aspects, List<string> competencyCodes)
|
List<AspectColumnDef> aspects, List<string> competencyCodes)
|
||||||
@@ -320,6 +356,8 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
Name = name;
|
Name = name;
|
||||||
_entry = entry;
|
_entry = entry;
|
||||||
_aspectDefs = aspects;
|
_aspectDefs = aspects;
|
||||||
|
_homeworkMissing = entry.HomeworkMissing;
|
||||||
|
_attendance = entry.Attendance;
|
||||||
|
|
||||||
foreach (var a in aspects)
|
foreach (var a in aspects)
|
||||||
{
|
{
|
||||||
@@ -347,6 +385,61 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
cell?.SetValue(value);
|
cell?.SetValue(value);
|
||||||
OnRatingChanged?.Invoke(StudentId, key, 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 ──────────────────────────────────────────────────────
|
// ── 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 ISubjectRepository _subjects;
|
||||||
private readonly ICompetencyDomainRepository _domainRepo;
|
private readonly ICompetencyDomainRepository _domainRepo;
|
||||||
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
|
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
|
||||||
|
private readonly IGradingSchemeRepository _gradingSchemes;
|
||||||
private readonly GradingService _grading;
|
private readonly GradingService _grading;
|
||||||
|
|
||||||
// ── Fächer ────────────────────────────────────────────────────────────────
|
// ── Fächer ────────────────────────────────────────────────────────────────
|
||||||
@@ -45,17 +46,39 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
public List<string> GradingSystemOptions { get; } = ["Noten 1–6", "Punkte 0–15"];
|
public List<string> GradingSystemOptions { get; } = ["Noten 1–6", "Punkte 0–15"];
|
||||||
public ObservableCollection<GradingKeyTemplateEditItem> GradingKeyTemplateList { get; } = [];
|
public ObservableCollection<GradingKeyTemplateEditItem> GradingKeyTemplateList { get; } = [];
|
||||||
|
|
||||||
|
// ── Gewichtungsschema-Voreinstellungen (2.3.3) ───────────────────────────
|
||||||
|
|
||||||
|
[ObservableProperty] private GradingSchemeEditItem _classScheme = null!;
|
||||||
|
[ObservableProperty] private GradingSchemeEditItem _courseScheme = null!;
|
||||||
|
|
||||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||||
IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading)
|
IGradingKeyTemplateRepository gradingKeyTemplates, IGradingSchemeRepository gradingSchemes,
|
||||||
|
GradingService grading)
|
||||||
{
|
{
|
||||||
_subjects = subjects;
|
_subjects = subjects;
|
||||||
_domainRepo = domainRepo;
|
_domainRepo = domainRepo;
|
||||||
_gradingKeyTemplates = gradingKeyTemplates;
|
_gradingKeyTemplates = gradingKeyTemplates;
|
||||||
|
_gradingSchemes = gradingSchemes;
|
||||||
_grading = grading;
|
_grading = grading;
|
||||||
LoadSubjects();
|
LoadSubjects();
|
||||||
LoadGradingKeyTemplates();
|
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 ────────────────
|
// ── 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 ───────────────────────────────────────────────────────────────
|
// ── Hilfklassen ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public class SubjectListItem(Subject s)
|
public class SubjectListItem(Subject s)
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Students;
|
namespace LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
|
||||||
@@ -72,6 +74,9 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly ISubjectRepository _subjects;
|
private readonly ISubjectRepository _subjects;
|
||||||
private readonly IDocumentationRepository _docs;
|
private readonly IDocumentationRepository _docs;
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly IExamResultRepository _examResults;
|
||||||
|
private readonly IGradeRepository _grades;
|
||||||
|
|
||||||
[ObservableProperty] private Student? _student;
|
[ObservableProperty] private Student? _student;
|
||||||
[ObservableProperty] private string _studentTitle = "";
|
[ObservableProperty] private string _studentTitle = "";
|
||||||
@@ -83,16 +88,19 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
|
public ObservableCollection<GroupMembershipEntry> GroupMemberships { get; } = [];
|
||||||
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
public ObservableCollection<DocEntry> Documentation { get; } = [];
|
||||||
public ObservableCollection<ContactItem> Contacts { get; } = [];
|
public ObservableCollection<ContactItem> Contacts { get; } = [];
|
||||||
|
public ObservableCollection<StudentGradeHistoryGroup> GradeHistory { get; } = [];
|
||||||
public bool HasNoContacts => Contacts.Count == 0;
|
public bool HasNoContacts => Contacts.Count == 0;
|
||||||
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
|
public Func<Contact?, Task<Contact?>>? OnEditContact { get; set; }
|
||||||
public Action<ContactItem>? OnViewAddress { get; set; }
|
public Action<ContactItem>? OnViewAddress { get; set; }
|
||||||
|
|
||||||
public StudentDetailViewModel(IStudentRepository students,
|
public StudentDetailViewModel(IStudentRepository students,
|
||||||
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
|
IGroupMembershipRepository memberships, IGroupRepository groups, ISubjectRepository subjects,
|
||||||
IDocumentationRepository docs)
|
IDocumentationRepository docs, IExamRepository exams, IExamResultRepository examResults,
|
||||||
|
IGradeRepository grades)
|
||||||
{
|
{
|
||||||
_students = students; _memberships = memberships;
|
_students = students; _memberships = memberships;
|
||||||
_groups = groups; _subjects = subjects; _docs = docs;
|
_groups = groups; _subjects = subjects; _docs = docs;
|
||||||
|
_exams = exams; _examResults = examResults; _grades = grades;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadStudent(Guid id)
|
public void LoadStudent(Guid id)
|
||||||
@@ -104,12 +112,16 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
EditLastName = Student.LastName;
|
EditLastName = Student.LastName;
|
||||||
|
|
||||||
GroupMemberships.Clear();
|
GroupMemberships.Clear();
|
||||||
|
GradeHistory.Clear();
|
||||||
foreach (var membership in _memberships.GetByStudent(Student.Id))
|
foreach (var membership in _memberships.GetByStudent(Student.Id))
|
||||||
{
|
{
|
||||||
var g = _groups.GetById(membership.GroupId);
|
var g = _groups.GetById(membership.GroupId);
|
||||||
if (g is null) continue;
|
if (g is null) continue;
|
||||||
var subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
|
var subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "";
|
||||||
GroupMemberships.Add(new() { SchoolYear = g.SchoolYear, GroupName = g.Name, Subject = subject });
|
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();
|
LoadContacts();
|
||||||
@@ -128,6 +140,49 @@ public partial class StudentDetailViewModel : ObservableObject
|
|||||||
IsConfidential = d.IsConfidential });
|
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 StartEdit() => IsEditing = true;
|
||||||
[RelayCommand] private void CancelEdit()
|
[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 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; } }
|
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 class ContactItem
|
||||||
{
|
{
|
||||||
public Contact Model { get; }
|
public Contact Model { get; }
|
||||||
|
|||||||
@@ -167,6 +167,39 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</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 -->
|
<!-- Meine Lerngruppen: wächst mit der Zeit, deshalb ganz unten und volle Breite -->
|
||||||
<Border Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2"
|
<Border Grid.Column="0" Grid.Row="2" Grid.ColumnSpan="2"
|
||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
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 -->
|
<!-- Tab: Noten -->
|
||||||
<ContentPage Header="Noten">
|
<ContentPage Header="Noten">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<views:GradeOverviewTabView DataContext="{Binding GradeOverviewTab}"/>
|
||||||
<TextBlock Text="Notenübersicht" FontSize="16" Opacity="0.4"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
<TextBlock Text="Wird implementiert." FontSize="12" Opacity="0.3"
|
|
||||||
HorizontalAlignment="Center"/>
|
|
||||||
</StackPanel>
|
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Planung -->
|
<!-- Tab: Planung -->
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
<DataGridTextColumn Header="Schüler" Binding="{Binding Name}" Width="2*"/>
|
<DataGridTextColumn Header="Schüler" Binding="{Binding Name}" Width="2*"/>
|
||||||
<DataGridTextColumn Header="Sitzungen" Binding="{Binding SessionCount}" Width="Auto"/>
|
<DataGridTextColumn Header="Sitzungen" Binding="{Binding SessionCount}" Width="Auto"/>
|
||||||
<DataGridTextColumn Header="Ø Bewertung" Binding="{Binding AverageDisplay}" 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"/>
|
<DataGridTextColumn Header="Note" Binding="{Binding GradeDisplay}" Width="Auto"/>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<Border Grid.Column="0"
|
<Border Grid.Column="0"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
BorderThickness="0,0,1,0">
|
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">
|
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="6" Margin="10,10,10,6">
|
||||||
<Button Content="+ Sitzung" Command="{Binding AddSessionCommand}" HorizontalAlignment="Stretch"/>
|
<Button Content="+ Sitzung" Command="{Binding AddSessionCommand}" HorizontalAlignment="Stretch"/>
|
||||||
@@ -20,7 +20,10 @@
|
|||||||
<Button Grid.Row="1" Content="Ø Mitarbeitsnote" Command="{Binding ComputeGradeCommand}"
|
<Button Grid.Row="1" Content="Ø Mitarbeitsnote" Command="{Binding ComputeGradeCommand}"
|
||||||
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
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}"
|
ItemsSource="{Binding Sessions}"
|
||||||
SelectedItem="{Binding SelectedSession}"
|
SelectedItem="{Binding SelectedSession}"
|
||||||
BorderThickness="0">
|
BorderThickness="0">
|
||||||
@@ -48,7 +51,7 @@
|
|||||||
<ToggleButton Content="◇ Kompetenzen"
|
<ToggleButton Content="◇ Kompetenzen"
|
||||||
IsChecked="{Binding CompetencyTagsVisible}"
|
IsChecked="{Binding CompetencyTagsVisible}"
|
||||||
FontSize="11" Padding="8,3"/>
|
FontSize="11" Padding="8,3"/>
|
||||||
<ToggleButton Content="◈ Schüler-Ratings"
|
<ToggleButton Content="◈ Schüler-Bewertungen"
|
||||||
IsChecked="{Binding StudentCompetencyRatingsVisible}"
|
IsChecked="{Binding StudentCompetencyRatingsVisible}"
|
||||||
FontSize="11" Padding="8,3"/>
|
FontSize="11" Padding="8,3"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
vm.OnAddSession = ShowAddSessionDialog;
|
vm.OnAddSession = ShowAddSessionDialog;
|
||||||
vm.OnQuickInput = ShowQuickInputDialog;
|
vm.OnQuickInput = ShowQuickInputDialog;
|
||||||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||||||
|
vm.OnOpenWizard = ShowWizardDialog;
|
||||||
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||||||
vm.PropertyChanged += (_, pe) =>
|
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)
|
// Kompetenz-Spalten (opt-in)
|
||||||
if (_vm.StudentCompetencyRatingsVisible && _vm.ActiveCompetencyCodes.Count > 0)
|
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
|
private static string AspectShortcut(int i) => i switch
|
||||||
{
|
{
|
||||||
0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => ""
|
0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => ""
|
||||||
@@ -163,4 +237,24 @@ public partial class ParticipationTabView : UserControl
|
|||||||
if (owner is not null)
|
if (owner is not null)
|
||||||
await dialog.ShowDialog(owner);
|
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"
|
<NumericUpDown Grid.Column="2"
|
||||||
Value="{Binding CatalogGradeLevel}"
|
Value="{Binding CatalogGradeLevel}"
|
||||||
Minimum="1" Maximum="13" FormatString="0"/>
|
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}}"/>
|
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}}"/>
|
IsEnabled="{Binding CatalogSubject, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@@ -222,6 +222,50 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</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>
|
</TabbedPage>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -129,10 +129,62 @@
|
|||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<ContentPage Header="Noten">
|
<ContentPage Header="Noten">
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
<ScrollViewer Padding="20">
|
||||||
<TextBlock Text="Notenübersicht" FontSize="16" Opacity="0.4" HorizontalAlignment="Center"/>
|
<StackPanel Spacing="20">
|
||||||
<TextBlock Text="Wird implementiert." FontSize="12" Opacity="0.3" HorizontalAlignment="Center"/>
|
<TextBlock Text="Notenentwicklung" FontSize="15" FontWeight="SemiBold"/>
|
||||||
</StackPanel>
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<ContentPage Header="Dokumentation">
|
<ContentPage Header="Dokumentation">
|
||||||
|
|||||||
@@ -102,36 +102,46 @@ pragmatisch über "Abwesend" bei den übrigen Schülern statt über eine eigene
|
|||||||
## 2. Noten & Zeugnisnoten
|
## 2. Noten & Zeugnisnoten
|
||||||
|
|
||||||
Modell `Grade` existiert in [Planning.cs](LehrerApp.Core/Models/Planning.cs), `GradeRepository` ebenfalls.
|
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 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.
|
sonstige Noten). Zelle zeigt Note/Punkte.
|
||||||
- [ ] **2.1.2** Spalte "Gesamt" mit gewichtetem Durchschnitt über `GradingService.WeightedAverage()`.
|
- [x] **2.1.2** Spalte "Gesamt" mit gewichtetem Durchschnitt über `GradingService.WeightedAverage()`.
|
||||||
- [ ] **2.1.3** Sortierung nach Name / Gesamtnote, Umschalten Noten ↔ Punkte.
|
- [x] **2.1.3** Sortierung nach Name / Gesamtnote, Umschalten Noten ↔ Punkte.
|
||||||
- [ ] **2.1.4** Halbjahresfilter (H1 / H2 / Gesamtjahr), berücksichtigt `GroupMembership.Period`.
|
- [x] **2.1.4** Halbjahresfilter (H1 / H2 / Gesamtjahr), berücksichtigt `GroupMembership.Period`.
|
||||||
|
|
||||||
### 2.2 Einzelnoten pflegen
|
### 2.2 Einzelnoten pflegen
|
||||||
- [ ] **2.2.1** Dialog "Note hinzufügen": Kategorie (`GradeCategory`), Wert, Datum, Gewichtung, Notiz.
|
- [x] **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.
|
- [x] **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.3** Sammelerfassung: eine Note (z.B. Hausaufgabenkontrolle) für die ganze Gruppe auf einmal.
|
||||||
|
|
||||||
### 2.3 Gewichtungsschema
|
### 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).
|
Klausuren / Mitarbeit / sonstige Leistungen (z.B. 50/40/10).
|
||||||
- [ ] **2.3.2** UI zur Bearbeitung, Validierung auf Summe 100 %.
|
- [x] **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.3** Voreinstellung je Gruppentyp (`Class` vs. `Course`) in den Einstellungen.
|
||||||
|
|
||||||
### 2.4 Zeugnisnote
|
### 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).
|
Rundungsregel konfigurierbar (kaufmännisch / pädagogisch abweichbar).
|
||||||
- [ ] **2.4.2** Manuelles Übersteuern mit Pflicht-Begründung (pädagogischer Spielraum).
|
- [x] **2.4.2** Manuelles Übersteuern mit Pflicht-Begründung (pädagogischer Spielraum).
|
||||||
- [ ] **2.4.3** Zeugnisnoten-Ansicht mit Sperren/Festschreiben zum Konferenztermin.
|
- [x] **2.4.3** Zeugnisnoten-Ansicht mit Sperren/Festschreiben zum Konferenztermin.
|
||||||
- [ ] **2.4.4** Export der Zeugnisnotenliste (siehe 11.2).
|
- [x] **2.4.4** Export der Zeugnisnotenliste (siehe 11.2).
|
||||||
|
|
||||||
### 2.5 Notenentwicklung
|
### 2.5 Notenentwicklung
|
||||||
- [ ] **2.5.1** Verlaufsdiagramm pro Schüler über das Schuljahr (im Schülerdetail).
|
- [x] **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.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 Sitzungen
|
||||||
- [ ] **3.3.1** Sitzung automatisch aus einer geplanten `Lesson` erzeugen
|
- [ ] **3.3.1** Sitzung automatisch aus einer geplanten `Lesson` erzeugen
|
||||||
(Datum + Thema übernehmen) — Abhängigkeit zu 4.2.
|
(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.3** Sitzung bearbeiten/löschen inkl. Rückfrage bei vorhandenen Bewertungen.
|
||||||
- [ ] **3.3.4** Sitzungen mehrerer Gruppen im Kalenderüberblick.
|
- [ ] **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
|
## 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
|
Pro Kombination aus Schüler und Lerngruppe darf es höchstens eine Zuordnung
|
||||||
geben.
|
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
|
## Bewusst gespeicherte Momentaufnahmen
|
||||||
|
|
||||||
Einige berechnete Werte bleiben absichtlich gespeichert:
|
Einige berechnete Werte bleiben absichtlich gespeichert:
|
||||||
@@ -65,6 +85,7 @@ Die Datenbank schützt folgende Kombinationen mit eindeutigen Indizes:
|
|||||||
- Gruppenzuordnung: `StudentId + GroupId`
|
- Gruppenzuordnung: `StudentId + GroupId`
|
||||||
- Klausurergebnis: `ExamId + StudentId`
|
- Klausurergebnis: `ExamId + StudentId`
|
||||||
- Mitarbeitseintrag: `SessionId + StudentId`
|
- Mitarbeitseintrag: `SessionId + StudentId`
|
||||||
|
- Zeugnisnote: `StudentId + GroupId + Period`
|
||||||
- Fach: normalisierter Fachname
|
- Fach: normalisierter Fachname
|
||||||
|
|
||||||
Altdaten werden beim Öffnen der Datenbank automatisch migriert. Die Migration
|
Altdaten werden beim Öffnen der Datenbank automatisch migriert. Die Migration
|
||||||
|
|||||||
Reference in New Issue
Block a user