using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using LehrerApp.Core.Models; using System.Collections.ObjectModel; namespace LehrerApp.Desktop.ViewModels.Groups; /// Ein Zwischenstand-Durchgang: Stichtag und Anlass gelten für alle gewählten Schüler. public sealed record InterimRunOptions(DateOnly Date, string Occasion, IReadOnlyCollection StudentIds); // ── Zwischenstand-Modus des Assistenten (2.7.5) ────────────────────────────── // // Mit InterimRunOptions gestartet, geht der Assistent nur die gewählten Schüler durch und // notiert je Schüler eine vorläufige schriftliche, mündliche und Gesamtnote zum Stichtag — // als InterimGrade, ohne die Zeugnisnote (ReportGrade) anzufassen. Vorgeschlagen wird der // Stand aus InterimGradeCalculator, also inklusive des laufenden Mitarbeits-Abschnitts. public partial class ReportGradeAssistantViewModel { [ObservableProperty] private string? _interimWritten; [ObservableProperty] private string? _interimOral; [ObservableProperty] private string? _interimOverall; [ObservableProperty] private string? _interimNote; [ObservableProperty] private string _interimWrittenError = ""; [ObservableProperty] private string _interimOralError = ""; [ObservableProperty] private string _interimOverallError = ""; [ObservableProperty] private bool _hasInterimEntry; /// Alle bisher notierten Zwischenstände des gewählten Schülers — in beiden Modi sichtbar. public ObservableCollection InterimEntries { get; } = []; public bool HasInterimEntries => InterimEntries.Count > 0; public bool IsInterimMode => _interim is not null; public bool IsReportMode => _interim is null; public string WindowTitle => IsInterimMode ? "Zwischenstand" : "Zeugnisnoten-Assistent"; public string InterimTitle => _interim is null ? "" : $"Zwischenstand {_interim.Date:dd.MM.yyyy} · {_interim.Occasion}"; /// Startet aus dem Zeugnisnoten-Modus einen Zwischenstand für den gezeigten Schüler; vom /// Fenster gesetzt, das den Startdialog öffnen kann. public Func? OnStartInterim { get; set; } private InterimGrade? CurrentInterim(Guid studentId) => _interim is null ? null : _interimGrades.GetByStudentAndGroup(studentId, _groupId) .FirstOrDefault(g => g.Date == _interim.Date && g.Occasion == _interim.Occasion); private void RefreshInterimItem(AssistantStudentItem item) { var existing = CurrentInterim(item.StudentId); if (existing is not null) item.Update(existing.OverallValue, AssistantGradeStatus.Noted); else item.Update(_interimCalculator.Calculate(item.StudentId, _interim!.Date, RoundingRule).Result.Rounded ?? "–", AssistantGradeStatus.Open); } private void ShowInterim(Guid studentId) { var interim = _interimCalculator.Calculate(studentId, _interim!.Date, RoundingRule); var result = interim.Result; _calculatedValue = result.Rounded; CalculatedDisplay = result.Rounded ?? "–"; var between = result.Value is { } value ? ReportGradeCalculator.BetweenGrades(value) : null; BorderlineHint = between is null ? "" : $"Liegt genau {between} — pädagogische Entscheidung."; CalculationDetail = result.Value is null ? "Noch keine Leistungen bis zum Stichtag." : $"Stand {_interim.Date:dd.MM.yyyy} · gewichtet {Format(result.Value.Value)} vor Rundung"; var scheme = _calculator.Scheme; Tiles.Clear(); Tiles.Add(BucketTile("Schriftlich", result.Written, scheme.ExamsPercent)); Tiles.Add(new AssistantTile("Mitarbeit", result.Participation.HasValue ? Format(result.Participation.Value) : "–", $"{scheme.ParticipationPercent:0.#} %" + (interim.RunningParticipation is null ? "" : $" · laufender Abschnitt: {interim.RunningParticipation}"))); Tiles.Add(BucketTile("Sonstige", result.Other, scheme.OtherPercent)); // Bereits notiert: der notierte Stand gilt. Sonst der berechnete Vorschlag zum Anpassen. var existing = CurrentInterim(studentId); HasInterimEntry = existing is not null; InterimWritten = existing is not null ? existing.WrittenValue : RoundOrNull(result.Written); InterimOral = existing is not null ? existing.OralValue : RoundOrNull(result.Participation); InterimOverall = existing is not null ? existing.OverallValue : result.Rounded; InterimNote = existing?.Note; } private string? RoundOrNull(double? value) => value.HasValue ? _calculator.Round(value.Value, RoundingRule) : null; private void LoadInterimEntries(Guid studentId) { InterimEntries.Clear(); foreach (var entry in _interimGrades.GetByStudentAndGroup(studentId, _groupId) .OrderByDescending(g => g.Date).ThenByDescending(g => g.UpdatedAt)) InterimEntries.Add(new AssistantInterimRow(entry)); OnPropertyChanged(nameof(HasInterimEntries)); } [RelayCommand] private void NoteInterim() => TryNoteInterim(); [RelayCommand] private void NoteInterimAndNext() { if (!TryNoteInterim()) return; if (CanGoNextStudent) NextStudent(); } private bool TryNoteInterim() { if (_interim is null || SelectedStudent is null) return false; ClearErrors(); var valid = true; if (!IsGradeOrEmpty(InterimWritten)) { InterimWrittenError = "Keine gültige Note."; valid = false; } if (!IsGradeOrEmpty(InterimOral)) { InterimOralError = "Keine gültige Note."; valid = false; } if (NullIfEmpty(InterimOverall) is null) { InterimOverallError = "Gesamtnote erforderlich."; valid = false; } else if (!IsGradeOrEmpty(InterimOverall)) { InterimOverallError = "Keine gültige Note."; valid = false; } if (!valid) return false; var studentId = SelectedStudent.StudentId; var entry = CurrentInterim(studentId) ?? new InterimGrade { StudentId = studentId, GroupId = _groupId, Date = _interim.Date, Occasion = _interim.Occasion, }; entry.WrittenValue = NullIfEmpty(InterimWritten); entry.OralValue = NullIfEmpty(InterimOral); entry.OverallValue = NullIfEmpty(InterimOverall)!; entry.Note = NullIfEmpty(InterimNote); _interimGrades.Save(entry); RefreshItem(SelectedStudent); ShowStudent(); StatusMessage = "Zwischenstand notiert."; return true; } [RelayCommand] private void RemoveInterim() { if (SelectedStudent is null || CurrentInterim(SelectedStudent.StudentId) is not { } entry) return; _interimGrades.Delete(entry.Id); RefreshItem(SelectedStudent); ShowStudent(); StatusMessage = "Zwischenstand entfernt."; } [RelayCommand] private async Task StartInterim() { if (OnStartInterim is null || SelectedStudent is null) return; await OnStartInterim(SelectedStudent.StudentId); ShowStudent(); } } public class AssistantInterimRow(InterimGrade entry) { public string Title { get; } = $"{entry.Date:dd.MM.yyyy} · {entry.Occasion}"; public string OverallValue { get; } = entry.OverallValue; public string Detail { get; } = string.Join(" · ", new[] { entry.WrittenValue is null ? null : $"schriftlich {entry.WrittenValue}", entry.OralValue is null ? null : $"mündlich {entry.OralValue}", }.Where(part => part is not null)); public string Note { get; } = entry.Note ?? ""; }