CI / build-and-test (push) Canceled after 0s
- Klausuren: neuer ExamWeekLoadService zählt eigene Klausuren je ISO-Woche über alle Kurse hinweg (Klassen-Kollisionen deckt bereits der externe Klausurplaner ab). Hinweis direkt im ExamDialog beim Datum, zusätzlich neue Dashboard-Karte "Klausurwochen" für die 60-Tage-Vorausschau. - Mitarbeit: Schnelleingabe-Dialog kann eine Bewertung jetzt per Entf auf "nicht bewertet" zurücksetzen (bisher nur im Sitzplatz-Dialog möglich). - Sitzplan: Strichliste "gemeldet"/"gemeldet und drangekommen" direkt auf der Sitzplatz-Kachel (ParticipationEntry.RaisedHandCount/CalledOnCount); schlägt im Sitzplatz-Dialog eine Quantitätsbewertung vor (ein Klick zum Übernehmen, nie automatisch). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
195 lines
8.1 KiB
C#
195 lines
8.1 KiB
C#
namespace LehrerApp.Core.Models;
|
||
|
||
public class ParticipationSession
|
||
{
|
||
public Guid Id { get; set; } = Guid.NewGuid();
|
||
public Guid GroupId { get; set; }
|
||
public DateOnly Date { get; set; } = DateOnly.FromDateTime(DateTime.Today);
|
||
public Guid? LessonId { get; set; }
|
||
public string? Comment { get; set; }
|
||
public List<string> CompetencyCodes { get; set; } = [];
|
||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||
}
|
||
|
||
public class ParticipationEntry
|
||
{
|
||
public Guid Id { get; set; } = Guid.NewGuid();
|
||
public Guid SessionId { get; set; }
|
||
public Guid StudentId { get; set; }
|
||
public List<AspectRating> Ratings { get; set; } = [];
|
||
public List<CompetencyRating> CompetencyRatings { get; set; } = [];
|
||
public string? Note { get; set; }
|
||
public HomeworkStatus? Homework { get; set; }
|
||
// Bleibt für bereits gespeicherte Daten erhalten. Neue Schreibvorgänge setzen beide Felder.
|
||
public bool HomeworkMissing { get; set; }
|
||
public AttendanceStatus? Attendance { get; set; }
|
||
// Strichliste im Sitzplan (Nutzer-Feedback): RaisedHandCount zählt jede beobachtete Meldung,
|
||
// CalledOnCount die Teilmenge davon, bei der der Schüler auch drangekommen ist —
|
||
// "drangekommen" ist immer auch eine Meldung und erhöht daher immer beide Zähler zugleich,
|
||
// nie CalledOnCount allein. Ergibt nebenbei eine Aufrufgerechtigkeits-Quote
|
||
// (CalledOnCount / RaisedHandCount) und schlägt die Quantitätsbewertung vor, siehe
|
||
// ParticipationCountSuggestion.
|
||
public int RaisedHandCount { get; set; }
|
||
public int CalledOnCount { get; set; }
|
||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Hausaufgabenstatus einer Sitzung; null bedeutet, dass für diesen Termin keine
|
||
/// Hausaufgabe erfasst wurde. MissingOpen kann später in MissingOverdue oder SubmittedLate
|
||
/// überführt werden.
|
||
/// </summary>
|
||
public enum HomeworkStatus
|
||
{
|
||
Completed,
|
||
MissingOpen,
|
||
MissingOverdue,
|
||
SubmittedLate,
|
||
// Hinten angefügt, damit bestehende LiteDB-Werte numerisch stabil bleiben.
|
||
PartiallyCompleted,
|
||
PartialSubmittedLate,
|
||
PartialMissingOverdue,
|
||
}
|
||
|
||
/// <summary>
|
||
/// Anwesenheitsstatus einer Sitzung; null bedeutet, dass die Anwesenheit für diesen Termin
|
||
/// noch nicht kontrolliert wurde. <see cref="Present"/> bestätigt die Anwesenheit ausdrücklich.
|
||
/// <see cref="ExcusePending"/> ist ein bewusster Zwischenzustand, da die Entschuldigung meist
|
||
/// erst später eintrifft. <see cref="Truant"/> bezeichnet gesichertes Schwänzen;
|
||
/// <see cref="OtherSchoolEvent"/> eine schulisch veranlasste Abwesenheit.
|
||
/// </summary>
|
||
public enum AttendanceStatus
|
||
{
|
||
ExcusePending,
|
||
Excused,
|
||
Unexcused,
|
||
Truant,
|
||
OtherSchoolEvent,
|
||
// Hinten angefügt, damit die numerischen Werte bereits gespeicherter LiteDB-Einträge stabil bleiben.
|
||
Present,
|
||
Late,
|
||
SignificantlyLate,
|
||
LeftDuringClass,
|
||
LearningIsland,
|
||
Suspended,
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ein Bewertungsabschnitt einer Lerngruppe (z.B. alle 4–7 Wochen), an dessen Ende eine
|
||
/// Abschnittsnote Mitarbeit vergeben wird. Nur abgeschlossene Abschnitte werden gespeichert;
|
||
/// der aktuell laufende Zeitraum ergibt sich aus dem Ende des letzten Abschnitts bis heute.
|
||
/// </summary>
|
||
public class ParticipationSection
|
||
{
|
||
public Guid Id { get; set; } = Guid.NewGuid();
|
||
public Guid GroupId { get; set; }
|
||
public string Label { get; set; } = "";
|
||
public DateOnly StartDate { get; set; }
|
||
public DateOnly EndDate { get; set; }
|
||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||
}
|
||
|
||
public class CompetencyRating
|
||
{
|
||
public string Code { get; set; } = "";
|
||
public int Value { get; set; }
|
||
}
|
||
|
||
public class AspectRating
|
||
{
|
||
public string Key { get; set; } = "";
|
||
public int Value { get; set; }
|
||
}
|
||
|
||
public class ParticipationAspect
|
||
{
|
||
public Guid Id { get; set; } = Guid.NewGuid();
|
||
public Guid? GroupId { get; set; }
|
||
public string Key { get; set; } = "";
|
||
public string Label { get; set; } = "";
|
||
public AspectValueType ValueType { get; set; } = AspectValueType.Scale5;
|
||
// Nur bei ValueType.Points relevant: die Rohbewertung ist dann ein echter Punktwert
|
||
// 0..MaxPoints (nicht auf der -2..+2-Achse), siehe ParticipationRatingScale.
|
||
public int MaxPoints { get; set; } = 5;
|
||
public double Weight { get; set; } = 1.0;
|
||
public bool IsActive { get; set; } = true;
|
||
public int SortOrder { get; set; }
|
||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||
}
|
||
|
||
public enum AspectValueType { Scale5, Scale3, Binary, Points }
|
||
|
||
public static class DefaultParticipationAspects
|
||
{
|
||
public static readonly IReadOnlyList<ParticipationAspect> All =
|
||
[
|
||
new() { Key = "quality", Label = "Qualität", ValueType = AspectValueType.Scale5, SortOrder = 0 },
|
||
new() { Key = "quantity", Label = "Quantität", ValueType = AspectValueType.Scale5, SortOrder = 1 },
|
||
new() { Key = "workphase", Label = "Arbeitsphase", ValueType = AspectValueType.Scale5, SortOrder = 2 },
|
||
];
|
||
}
|
||
|
||
/// <summary>
|
||
/// Definiert je <see cref="AspectValueType"/>, welche Rohwerte eine Bewertung annehmen kann und
|
||
/// wie sie sich auf die gemeinsame Qualitätsachse (-2..+2, wie <see cref="AspectValueType.Scale5"/>)
|
||
/// abbilden, auf der die Gewichtung/Mittelwertbildung zur Mitarbeitsnote (3.2) rechnet.
|
||
/// Scale3/Binary liegen bewusst direkt AUF dieser Achse (nur mit weniger Zwischenschritten)
|
||
/// statt einer eigenen Wertemenge zu bekommen — dadurch bleibt die bestehende Aggregation
|
||
/// unverändert kompatibel, ohne pro Bewertung erst umrechnen zu müssen. Nur "Points" hat eine
|
||
/// eigene, von der Lehrkraft festgelegte Obergrenze (<see cref="ParticipationAspect.MaxPoints"/>)
|
||
/// und wird für die Aggregation linear auf dieselbe Achse normiert (0 → -2, MaxPoints → +2).
|
||
/// </summary>
|
||
public static class ParticipationRatingScale
|
||
{
|
||
/// Feste Rohwert/Label-Paare für Typen mit einer kleinen, festen Stufenzahl — Grundlage für
|
||
/// Raster-Buttons und Schnelleingabe-Zifferntasten. Leer für Points (freie Zahleneingabe).
|
||
public static IReadOnlyList<(int Value, string Label)> Steps(AspectValueType type) => type switch
|
||
{
|
||
AspectValueType.Scale5 => [(-2, "−−"), (-1, "−"), (0, "∼"), (1, "+"), (2, "++")],
|
||
AspectValueType.Scale3 => [(-2, "−"), (0, "∼"), (2, "+")],
|
||
AspectValueType.Binary => [(-2, "Nein"), (2, "Ja")],
|
||
_ => [],
|
||
};
|
||
|
||
public static string DisplayLabel(AspectValueType type, int? value)
|
||
{
|
||
if (value is null) return "·";
|
||
if (type == AspectValueType.Points) return value.Value.ToString();
|
||
var match = Steps(type).FirstOrDefault(s => s.Value == value.Value);
|
||
return match.Label ?? "·";
|
||
}
|
||
|
||
/// Bildet einen Rohwert auf die gemeinsame -2..+2-Achse ab, für die Mittelwertbildung in 3.2.
|
||
public static double Normalize(AspectValueType type, int value, int maxPoints) => type switch
|
||
{
|
||
AspectValueType.Points when maxPoints > 0 => Math.Clamp(value, 0, maxPoints) / (double)maxPoints * 4.0 - 2.0,
|
||
AspectValueType.Points => 0, // keine sinnvolle Obergrenze konfiguriert -> neutral werten
|
||
_ => value,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Nutzer-Feedback: die Strichliste im Sitzplan (<see cref="ParticipationEntry.RaisedHandCount"/>)
|
||
/// schlägt direkt eine Quantitätsbewertung vor — wie oft sich jemand meldet, ist ja bereits die
|
||
/// Quantität. Nur ein Richtwert, kein fester Automatismus: die Lehrkraft übernimmt den Vorschlag
|
||
/// per Klick oder ignoriert ihn. Bewusst nur für <see cref="AspectValueType.Scale5"/> (der
|
||
/// Standardtyp des "quantity"-Aspekts) — für individuell umkonfigurierte Aspekttypen gäbe es keine
|
||
/// sinnvolle, unmissverständliche Abbildung.
|
||
/// </summary>
|
||
public static class ParticipationCountSuggestion
|
||
{
|
||
public static int? SuggestQuantity(AspectValueType type, int raisedHandCount)
|
||
{
|
||
if (type != AspectValueType.Scale5) return null;
|
||
return raisedHandCount switch
|
||
{
|
||
0 => -2,
|
||
1 => -1,
|
||
2 or 3 => 0,
|
||
4 or 5 => 1,
|
||
_ => 2,
|
||
};
|
||
}
|
||
}
|