Zentrale Exception-Behandlung (Dispatcher.UIThread.UnhandledException, AppDomain, TaskScheduler) verhindert Abstürze und protokolliert Fehler über AppLogger in eine rotierende Log-Datei im App-Datenverzeichnis. Toast-Benachrichtigungen zeigen Erfolg/Fehler global an. Alle Dialoge mit Formularfeldern zeigen Validierungsmeldungen jetzt direkt am betroffenen Feld statt in einem Sammel-Label. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
496 lines
22 KiB
C#
496 lines
22 KiB
C#
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 _newSectionLabelError = "";
|
||
[ObservableProperty] private string _newSectionEndDateTextError = "";
|
||
[ObservableProperty] private ParticipationPeriodOption _rollupPeriod;
|
||
[ObservableProperty] private string _rollupStatusMessage = "";
|
||
[ObservableProperty] private double _timelineZoom = 1.0;
|
||
[ObservableProperty] private bool _showQuality = true;
|
||
[ObservableProperty] private bool _showQuantity = true;
|
||
[ObservableProperty] private bool _showWorkphase = true;
|
||
[ObservableProperty] private bool _showDataPoints = true;
|
||
[ObservableProperty] private bool _showWeightedTrend = true;
|
||
[ObservableProperty] private bool _showQualityTrend;
|
||
[ObservableProperty] private bool _showQuantityTrend;
|
||
[ObservableProperty] private bool _showWorkphaseTrend;
|
||
|
||
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))
|
||
.GroupBy(a => a.Key)
|
||
.ToDictionary(g => g.Key, g => g.Last().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)));
|
||
}
|
||
foreach (var grade in _grades.GetByStudentAndGroup(studentId, _groupId)
|
||
.Where(g => g.Category != GradeCategory.Participation))
|
||
{
|
||
points.Add((grade.Date, BuildOtherGradePoint(grade)));
|
||
}
|
||
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 weightedValue = entry is not null ? WeightedRating(entry) : null;
|
||
var ratingLabel = weightedValue is not null ? RatingLabel((int)Math.Round(weightedValue.Value, MidpointRounding.AwayFromZero)) : "";
|
||
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)
|
||
{
|
||
Homework = entry is null ? null : HomeworkDisplay.Effective(entry),
|
||
AttendanceIcon = AttendanceDisplay.ShortLabel(entry?.Attendance),
|
||
AttendanceTooltip = AttendanceDisplay.Label(entry?.Attendance),
|
||
AttendanceColor = AttendanceDisplay.Color(entry?.Attendance),
|
||
AspectValues = entry?.Ratings.ToDictionary(r => r.Key, r => r.Value)
|
||
?? new Dictionary<string, int>(),
|
||
WeightedValue = weightedValue,
|
||
OverallGrade = weightedValue is null ? "" : _grading.ParticipationGrade(weightedValue.Value, _gradingSystem),
|
||
};
|
||
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 static WizardTimelinePoint BuildOtherGradePoint(Grade grade)
|
||
{
|
||
var category = grade.Category switch
|
||
{
|
||
GradeCategory.Oral => "Mündlich",
|
||
GradeCategory.Homework => "Hausaufgabe",
|
||
GradeCategory.Project => "Projekt",
|
||
_ => "Sonstige Leistung",
|
||
};
|
||
var detail = string.IsNullOrWhiteSpace(grade.Note) ? category : grade.Note.Trim();
|
||
var label = $"{detail}: {grade.Value}";
|
||
return new WizardTimelinePoint(grade.Date, isExam: true, ratingLabel: "", examLabel: label,
|
||
hasNote: false, tooltip: $"{grade.Date:dd.MM.yyyy} · {category} · {label}");
|
||
}
|
||
|
||
private double? WeightedRating(ParticipationEntry entry)
|
||
{
|
||
if (entry.Ratings.Count == 0) return null;
|
||
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;
|
||
}
|
||
return weightSum <= 0 ? null : valueSum / weightSum;
|
||
}
|
||
|
||
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.Homework = HomeworkDisplay.Next(HomeworkDisplay.Effective(entry));
|
||
entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(entry.Homework);
|
||
_entries.Save(entry);
|
||
point.Homework = entry.Homework;
|
||
}
|
||
|
||
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.Present,
|
||
AttendanceStatus.Present => AttendanceStatus.ExcusePending,
|
||
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
|
||
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
|
||
AttendanceStatus.Unexcused => AttendanceStatus.Truant,
|
||
AttendanceStatus.Truant => AttendanceStatus.OtherSchoolEvent,
|
||
AttendanceStatus.OtherSchoolEvent => null,
|
||
_ => null,
|
||
};
|
||
_entries.Save(entry);
|
||
// Das Setzen jeder ObservableProperty kann unmittelbar ein Neuzeichnen auslösen.
|
||
// Deshalb muss die zum aktiven Symbol gehörende Farbe zuerst bereitstehen.
|
||
point.AttendanceColor = AttendanceDisplay.Color(entry.Attendance);
|
||
point.AttendanceTooltip = AttendanceDisplay.Label(entry.Attendance);
|
||
point.AttendanceIcon = AttendanceDisplay.ShortLabel(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()
|
||
{
|
||
NewSectionLabelError = ""; NewSectionEndDateTextError = "";
|
||
var valid = true;
|
||
|
||
if (string.IsNullOrWhiteSpace(NewSectionLabel)) { NewSectionLabelError = "Bezeichnung erforderlich."; valid = false; }
|
||
|
||
DateOnly end = default;
|
||
if (!DateOnly.TryParseExact(NewSectionEndDateText, "dd.MM.yyyy", null,
|
||
DateTimeStyles.None, out end))
|
||
{ NewSectionEndDateTextError = "Format TT.MM.JJJJ."; valid = false; }
|
||
|
||
if (!valid) return;
|
||
|
||
var start = ComputeOpenStart();
|
||
if (end < start) { NewSectionEndDateTextError = "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,
|
||
});
|
||
}
|
||
|
||
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 DateOnly Date { get; }
|
||
public string DateDisplay { get; }
|
||
public bool IsExam { get; }
|
||
public string RatingLabel { get; }
|
||
public string ExamLabel { get; }
|
||
public bool HasNote { get; }
|
||
public string TooltipText { get; }
|
||
public IReadOnlyDictionary<string, int> AspectValues { get; init; } = new Dictionary<string, int>();
|
||
public double? WeightedValue { get; init; }
|
||
public string OverallGrade { get; init; } = "";
|
||
|
||
[ObservableProperty] private HomeworkStatus? _homework;
|
||
[ObservableProperty] private string _attendanceIcon = "";
|
||
[ObservableProperty] private string _attendanceTooltip = "Anwesend";
|
||
[ObservableProperty] private string _attendanceColor = "";
|
||
|
||
public bool IsAbsent => AttendanceIcon.Length > 0;
|
||
public string AttendanceButtonLabel => AttendanceIcon.Length > 0 ? AttendanceIcon : "·";
|
||
public bool HasHomeworkStatus => Homework is not null;
|
||
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
|
||
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
|
||
public string HomeworkColor => HomeworkDisplay.Color(Homework);
|
||
|
||
partial void OnHomeworkChanged(HomeworkStatus? value)
|
||
{
|
||
OnPropertyChanged(nameof(HasHomeworkStatus));
|
||
OnPropertyChanged(nameof(HomeworkSymbol));
|
||
OnPropertyChanged(nameof(HomeworkTooltip));
|
||
OnPropertyChanged(nameof(HomeworkColor));
|
||
}
|
||
|
||
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)
|
||
{
|
||
Date = date;
|
||
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);
|
||
}
|