Persönliche Klausurlast pro Woche, Mitarbeit-Reset und Sitzplan-Strichliste
CI / build-and-test (push) Canceled after 0s
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>
This commit is contained in:
@@ -32,6 +32,7 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private string _titleError = "";
|
||||
[ObservableProperty] private string _dateTextError = "";
|
||||
[ObservableProperty] private string _returnedAtTextError = "";
|
||||
[ObservableProperty] private string _weekLoadHint = "";
|
||||
[ObservableProperty] private double _totalPoints;
|
||||
[ObservableProperty] private bool _hasCompetencyCatalog;
|
||||
[ObservableProperty] private bool _useWeighting;
|
||||
@@ -108,6 +109,7 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
}
|
||||
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
|
||||
RecomputeTotals();
|
||||
UpdateWeekLoadHint();
|
||||
}
|
||||
|
||||
partial void OnUseWeightingChanged(bool value)
|
||||
@@ -115,6 +117,26 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
foreach (var t in Tasks) t.ShowWeight = value;
|
||||
}
|
||||
|
||||
partial void OnDateTextChanged(string value) => UpdateWeekLoadHint();
|
||||
|
||||
/// Nutzer-Feedback: persönliche Klausurlast pro Woche — die klassenbezogene
|
||||
/// Kollisionsprüfung übernimmt bereits der schulische Klausurplaner (siehe
|
||||
/// ExamWeekLoadService), hier geht es um die eigene Häufung über alle Kurse hinweg.
|
||||
private void UpdateWeekLoadHint()
|
||||
{
|
||||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||
{
|
||||
WeekLoadHint = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = ExamWeekLoadService.CountInSameWeek(_exams.GetAll(), date, _editingExam?.Id);
|
||||
var totalIfSaved = existing + 1;
|
||||
WeekLoadHint = totalIfSaved >= ExamWeekLoadService.WarningThreshold
|
||||
? $"Damit hättest du {totalIfSaved} Klausuren in dieser Woche."
|
||||
: "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddTask()
|
||||
{
|
||||
|
||||
@@ -824,7 +824,7 @@ public partial class QuickInputViewModel : ObservableObject
|
||||
_ => "1–5 bewerten",
|
||||
};
|
||||
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste/↓ nächster Aspekt · ↑ vorheriger Aspekt · " +
|
||||
"Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Esc schließen";
|
||||
"Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · Esc schließen";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,6 +902,13 @@ public partial class QuickInputViewModel : ObservableObject
|
||||
ApplyRating(steps[Math.Clamp(idx + 1, 0, steps.Count - 1)]);
|
||||
}
|
||||
|
||||
/// Nutzer-Feedback: eine aus Versehen gesetzte Bewertung (z.B. Qualität, obwohl der Schüler
|
||||
/// sich nie gemeldet hat) muss sich wieder auf "nicht bewertet" zurücksetzen lassen, ohne
|
||||
/// einen inhaltlich falschen Wert stehen lassen zu müssen. Bewusst eine eigene Aktion statt
|
||||
/// über 0/1 zu laufen — 0 ("--") ist bei Quantität ein echter, beobachteter Wert ("hat sich
|
||||
/// nicht gemeldet"), kein Platzhalter für "nicht ermittelt".
|
||||
public void ClearRating() => ApplyRating(null);
|
||||
|
||||
public void DecrementRating()
|
||||
{
|
||||
var cell = GetCurrentCell();
|
||||
@@ -918,7 +925,7 @@ public partial class QuickInputViewModel : ObservableObject
|
||||
ApplyRating(steps[Math.Clamp(idx - 1, 0, steps.Count - 1)]);
|
||||
}
|
||||
|
||||
private void ApplyRating(int val)
|
||||
private void ApplyRating(int? val)
|
||||
{
|
||||
if (_rows.Count == 0 || !_aspects.Any()) return;
|
||||
var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key;
|
||||
|
||||
@@ -227,7 +227,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
||||
?? StudentSeatOption.Empty;
|
||||
var isHidden = hiddenSeats.Any(h => h.Row == row && h.Column == column);
|
||||
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
|
||||
CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden));
|
||||
CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden, TallyParticipation));
|
||||
}
|
||||
UpdateAssignmentSummary();
|
||||
RefreshSeatLessonData();
|
||||
@@ -266,6 +266,22 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// Strichliste im Sitzplan (Nutzer-Feedback): "drangekommen" ist immer auch eine Meldung und
|
||||
/// erhöht daher beide Zähler — es gibt kein "drangekommen, ohne sich gemeldet zu haben".
|
||||
private void TallyParticipation(SeatCellViewModel seat, bool calledOn)
|
||||
{
|
||||
if (!IsEditable || seat.SelectedOption.StudentId is not Guid studentId) return;
|
||||
var session = EnsureTodaySession();
|
||||
if (session is null) return;
|
||||
|
||||
var entry = _participation.GetBySessionAndStudent(session.Id, studentId)
|
||||
?? new ParticipationEntry { SessionId = session.Id, StudentId = studentId };
|
||||
entry.RaisedHandCount++;
|
||||
if (calledOn) entry.CalledOnCount++;
|
||||
_participation.Save(entry);
|
||||
RefreshSeatLessonData();
|
||||
}
|
||||
|
||||
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
|
||||
{
|
||||
if (!IsEditable || _documentation is null ||
|
||||
@@ -531,6 +547,8 @@ public partial class SeatCellViewModel : ObservableObject
|
||||
[ObservableProperty] private double _lessonOpacity = 1.0;
|
||||
[ObservableProperty] private string _attendanceBadge = "";
|
||||
[ObservableProperty] private string _homeworkBadge = "";
|
||||
[ObservableProperty] private int _raisedHandCount;
|
||||
[ObservableProperty] private int _calledOnCount;
|
||||
public ObservableCollection<SituationTagChoice> SituationTags { get; } = [];
|
||||
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
|
||||
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
|
||||
@@ -555,10 +573,13 @@ public partial class SeatCellViewModel : ObservableObject
|
||||
/// die Abblendung für ausgeblendete Plätze hier statt per CSS-Klasse erfolgen.</summary>
|
||||
public double DisplayOpacity => IsHidden ? 0.4 : LessonOpacity;
|
||||
|
||||
private readonly Action<SeatCellViewModel, bool> _tally;
|
||||
|
||||
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
||||
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
|
||||
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false,
|
||||
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null)
|
||||
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null,
|
||||
Action<SeatCellViewModel, bool>? tally = null)
|
||||
{
|
||||
Row = row;
|
||||
Column = column;
|
||||
@@ -570,10 +591,18 @@ public partial class SeatCellViewModel : ObservableObject
|
||||
_canRecordLesson = canRecordLesson;
|
||||
_isHidden = isHidden;
|
||||
_toggleHidden = toggleHidden ?? (_ => { });
|
||||
_tally = tally ?? ((_, _) => { });
|
||||
foreach (var tag in SituationTagChoice.DefaultTags)
|
||||
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
|
||||
}
|
||||
|
||||
/// Strichliste im Sitzplan (Nutzer-Feedback): schnelles Mitzählen während des Unterrichts,
|
||||
/// ohne dafür den vollen Sitzplatz-Bewertungsdialog öffnen zu müssen — bewusst nur ein
|
||||
/// Hochzählen ohne Korrekturmöglichkeit hier direkt auf der Kachel; die Rohwerte bleiben über
|
||||
/// den Bewertungsdialog einsehbar und fließen dort als Quantitäts-Vorschlag ein.
|
||||
[RelayCommand] private void TallyRaisedHand() => _tally(this, false);
|
||||
[RelayCommand] private void TallyCalledOn() => _tally(this, true);
|
||||
|
||||
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsOccupied));
|
||||
@@ -615,6 +644,8 @@ public partial class SeatCellViewModel : ObservableObject
|
||||
var attendance = entry?.Attendance;
|
||||
AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
|
||||
HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
|
||||
RaisedHandCount = entry?.RaisedHandCount ?? 0;
|
||||
CalledOnCount = entry?.CalledOnCount ?? 0;
|
||||
LessonOpacity = attendance is not null and not AttendanceStatus.Present
|
||||
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
|
||||
var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -690,7 +721,10 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
||||
foreach (var (aspect, index) in aspectDefinitions.Select((a, i) => (a, i)))
|
||||
{
|
||||
var value = _entry?.Ratings.FirstOrDefault(r => r.Key == aspect.Key)?.Value;
|
||||
AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating));
|
||||
// Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback): nur für den
|
||||
// eingebauten "quantity"-Aspekt, siehe ParticipationCountSuggestion.
|
||||
var raisedHandCount = aspect.Key == "quantity" ? _entry?.RaisedHandCount : null;
|
||||
AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating, raisedHandCount));
|
||||
}
|
||||
|
||||
BuildAttendanceChoices();
|
||||
@@ -847,11 +881,22 @@ public partial class SeatAssessmentAspectRow : ObservableObject
|
||||
public string DisplayValue => ParticipationRatingScale.DisplayLabel(ValueType, Value);
|
||||
public ObservableCollection<SeatRatingChoice> Choices { get; } = [];
|
||||
|
||||
/// Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback), siehe
|
||||
/// ParticipationCountSuggestion — null, wenn keine Strichliste zu diesem Aspekt gehört.
|
||||
private readonly int? _raisedHandCount;
|
||||
public int? SuggestedValue { get; }
|
||||
public bool HasSuggestion => SuggestedValue.HasValue && SuggestedValue != Value;
|
||||
public string SuggestionLabel =>
|
||||
$"{_raisedHandCount}× gemeldet → Vorschlag: {ParticipationRatingScale.DisplayLabel(ValueType, SuggestedValue)}";
|
||||
|
||||
public SeatAssessmentAspectRow(int index, ParticipationAspect aspect, int? value,
|
||||
Action<string, int?> apply)
|
||||
Action<string, int?> apply, int? raisedHandCount = null)
|
||||
{
|
||||
Index = index; Key = aspect.Key; Label = aspect.Label; ValueType = aspect.ValueType;
|
||||
MaxPoints = aspect.MaxPoints; _value = value; _apply = apply;
|
||||
_raisedHandCount = raisedHandCount;
|
||||
SuggestedValue = raisedHandCount is int rhc
|
||||
? ParticipationCountSuggestion.SuggestQuantity(ValueType, rhc) : null;
|
||||
var steps = ValueType == AspectValueType.Points
|
||||
? Enumerable.Range(0, Math.Min(MaxPoints, 9) + 1).Select(v => (v, v.ToString())).ToList()
|
||||
: ParticipationRatingScale.Steps(ValueType).ToList();
|
||||
@@ -864,10 +909,13 @@ public partial class SeatAssessmentAspectRow : ObservableObject
|
||||
{
|
||||
Value = value;
|
||||
OnPropertyChanged(nameof(DisplayValue));
|
||||
OnPropertyChanged(nameof(HasSuggestion));
|
||||
foreach (var choice in Choices) choice.IsSelected = choice.Value == value;
|
||||
_apply(Key, value);
|
||||
}
|
||||
|
||||
[RelayCommand] private void ApplySuggestion() => ApplyValue(SuggestedValue);
|
||||
|
||||
public void Adjust(int delta)
|
||||
{
|
||||
if (ValueType == AspectValueType.Points)
|
||||
|
||||
Reference in New Issue
Block a user