Mitarbeits Wizzard und neue Statuslabels für Anwesenheit und Hausaufgaben
This commit is contained in:
@@ -20,18 +20,47 @@ 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 HomeworkStatus? Homework { get; set; }
|
||||||
|
// Bleibt für bereits gespeicherte Daten erhalten. Neue Schreibvorgänge setzen beide Felder.
|
||||||
public bool HomeworkMissing { get; set; }
|
public bool HomeworkMissing { get; set; }
|
||||||
public AttendanceStatus? Attendance { get; set; }
|
public AttendanceStatus? Attendance { get; set; }
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Anwesenheitsstatus einer Sitzung; null (Standard) bedeutet anwesend.
|
/// Hausaufgabenstatus einer Sitzung; null bedeutet, dass für diesen Termin keine
|
||||||
/// <see cref="ExcusePending"/> ist ein bewusster Zwischenzustand, da die Entschuldigung meist
|
/// Hausaufgabe erfasst wurde. MissingOpen kann später in MissingOverdue oder SubmittedLate
|
||||||
/// erst später eintrifft — er wird beim Erfassen des Fehltags gesetzt und danach manuell auf
|
/// überführt werden.
|
||||||
/// <see cref="Excused"/> oder <see cref="Unexcused"/> nachgetragen.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum AttendanceStatus { ExcusePending, Excused, Unexcused }
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ein Bewertungsabschnitt einer Lerngruppe (z.B. alle 4–7 Wochen), an dessen Ende eine
|
/// Ein Bewertungsabschnitt einer Lerngruppe (z.B. alle 4–7 Wochen), an dessen Ende eine
|
||||||
|
|||||||
@@ -47,6 +47,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>? OnStatusQuickInput { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnOpenWizard { get; set; }
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
CompetencyTagGroups.Clear();
|
CompetencyTagGroups.Clear();
|
||||||
ActiveCompetencyCodes = [];
|
ActiveCompetencyCodes = [];
|
||||||
QuickInputCommand.NotifyCanExecuteChanged();
|
QuickInputCommand.NotifyCanExecuteChanged();
|
||||||
|
StatusQuickInputCommand.NotifyCanExecuteChanged();
|
||||||
RebuildColumnsSignal++;
|
RebuildColumnsSignal++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,6 +153,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
StudentRows.Add(row);
|
StudentRows.Add(row);
|
||||||
}
|
}
|
||||||
QuickInputCommand.NotifyCanExecuteChanged();
|
QuickInputCommand.NotifyCanExecuteChanged();
|
||||||
|
StatusQuickInputCommand.NotifyCanExecuteChanged();
|
||||||
RebuildColumnsSignal++;
|
RebuildColumnsSignal++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +193,12 @@ 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)
|
private void SaveHomework(Guid sessionId, Guid studentId, HomeworkStatus? value)
|
||||||
{
|
{
|
||||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||||
entry.HomeworkMissing = value;
|
entry.Homework = value;
|
||||||
|
entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(value);
|
||||||
_entries.Save(entry);
|
_entries.Save(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +277,14 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
|
|
||||||
private bool CanQuickInput() => SelectedSession is not null && StudentRows.Count > 0;
|
private bool CanQuickInput() => SelectedSession is not null && StudentRows.Count > 0;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanQuickInput))]
|
||||||
|
private async Task StatusQuickInput()
|
||||||
|
{
|
||||||
|
if (OnStatusQuickInput is null) return;
|
||||||
|
await OnStatusQuickInput(this);
|
||||||
|
if (SelectedSession is not null) LoadGrid(SelectedSession.Id);
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ComputeGrade()
|
private async Task ComputeGrade()
|
||||||
{
|
{
|
||||||
@@ -338,15 +350,17 @@ 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 HomeworkStatus? _homework;
|
||||||
[ObservableProperty] private AttendanceStatus? _attendance;
|
[ObservableProperty] private AttendanceStatus? _attendance;
|
||||||
|
|
||||||
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
|
||||||
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
||||||
|
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
|
||||||
|
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
|
||||||
|
|
||||||
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, HomeworkStatus?>? HomeworkChangedCallback { get; set; }
|
||||||
public Action<Guid, AttendanceStatus?>? AttendanceChangedCallback { 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,
|
||||||
@@ -356,7 +370,7 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
Name = name;
|
Name = name;
|
||||||
_entry = entry;
|
_entry = entry;
|
||||||
_aspectDefs = aspects;
|
_aspectDefs = aspects;
|
||||||
_homeworkMissing = entry.HomeworkMissing;
|
_homework = HomeworkDisplay.Effective(entry);
|
||||||
_attendance = entry.Attendance;
|
_attendance = entry.Attendance;
|
||||||
|
|
||||||
foreach (var a in aspects)
|
foreach (var a in aspects)
|
||||||
@@ -389,8 +403,18 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ToggleHomework()
|
private void ToggleHomework()
|
||||||
{
|
{
|
||||||
HomeworkMissing = !HomeworkMissing;
|
Homework = HomeworkDisplay.Next(Homework);
|
||||||
HomeworkChangedCallback?.Invoke(StudentId, HomeworkMissing);
|
OnPropertyChanged(nameof(HomeworkSymbol));
|
||||||
|
OnPropertyChanged(nameof(HomeworkTooltip));
|
||||||
|
HomeworkChangedCallback?.Invoke(StudentId, Homework);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetHomework(HomeworkStatus? value)
|
||||||
|
{
|
||||||
|
Homework = value;
|
||||||
|
OnPropertyChanged(nameof(HomeworkSymbol));
|
||||||
|
OnPropertyChanged(nameof(HomeworkTooltip));
|
||||||
|
HomeworkChangedCallback?.Invoke(StudentId, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -398,10 +422,13 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
{
|
{
|
||||||
Attendance = Attendance switch
|
Attendance = Attendance switch
|
||||||
{
|
{
|
||||||
null => AttendanceStatus.ExcusePending,
|
null => AttendanceStatus.Present,
|
||||||
|
AttendanceStatus.Present => AttendanceStatus.ExcusePending,
|
||||||
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
|
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
|
||||||
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
|
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
|
||||||
AttendanceStatus.Unexcused => null,
|
AttendanceStatus.Unexcused => AttendanceStatus.Truant,
|
||||||
|
AttendanceStatus.Truant => AttendanceStatus.OtherSchoolEvent,
|
||||||
|
AttendanceStatus.OtherSchoolEvent => null,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
OnPropertyChanged(nameof(AttendanceLabel));
|
OnPropertyChanged(nameof(AttendanceLabel));
|
||||||
@@ -419,25 +446,107 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hausaufgaben-Anzeige ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static class HomeworkDisplay
|
||||||
|
{
|
||||||
|
public static HomeworkStatus? Effective(ParticipationEntry entry) =>
|
||||||
|
entry.Homework ?? (entry.HomeworkMissing ? HomeworkStatus.MissingOpen : null);
|
||||||
|
|
||||||
|
public static HomeworkStatus? Next(HomeworkStatus? status) => status switch
|
||||||
|
{
|
||||||
|
null => HomeworkStatus.Completed,
|
||||||
|
HomeworkStatus.Completed => HomeworkStatus.PartiallyCompleted,
|
||||||
|
HomeworkStatus.PartiallyCompleted => HomeworkStatus.PartialSubmittedLate,
|
||||||
|
HomeworkStatus.PartialSubmittedLate => HomeworkStatus.PartialMissingOverdue,
|
||||||
|
HomeworkStatus.PartialMissingOverdue => HomeworkStatus.MissingOpen,
|
||||||
|
HomeworkStatus.MissingOpen => HomeworkStatus.MissingOverdue,
|
||||||
|
HomeworkStatus.MissingOverdue => HomeworkStatus.SubmittedLate,
|
||||||
|
HomeworkStatus.SubmittedLate => null,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Label(HomeworkStatus? status) => status switch
|
||||||
|
{
|
||||||
|
null => "Keine Hausaufgabe erfasst",
|
||||||
|
HomeworkStatus.Completed => "Hausaufgabe gemacht",
|
||||||
|
HomeworkStatus.PartiallyCompleted => "Teilweise angefertigt – Rest offen",
|
||||||
|
HomeworkStatus.PartialSubmittedLate => "Teilweise angefertigt – Rest nachgereicht",
|
||||||
|
HomeworkStatus.PartialMissingOverdue => "Teilweise angefertigt – Rest nicht nachgereicht",
|
||||||
|
HomeworkStatus.MissingOpen => "Nicht gemacht – Nachreichen offen",
|
||||||
|
HomeworkStatus.MissingOverdue => "Nicht gemacht – nicht mehr nachgereicht",
|
||||||
|
HomeworkStatus.SubmittedLate => "Hausaufgabe nachgereicht",
|
||||||
|
_ => "Keine Hausaufgabe erfasst",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Symbol(HomeworkStatus? status) => status switch
|
||||||
|
{
|
||||||
|
null => "·",
|
||||||
|
HomeworkStatus.Completed => "✓",
|
||||||
|
HomeworkStatus.PartiallyCompleted => "◐",
|
||||||
|
HomeworkStatus.PartialSubmittedLate => "◕",
|
||||||
|
HomeworkStatus.PartialMissingOverdue => "◒",
|
||||||
|
HomeworkStatus.MissingOpen => "!",
|
||||||
|
HomeworkStatus.MissingOverdue => "✕",
|
||||||
|
HomeworkStatus.SubmittedLate => "↺",
|
||||||
|
_ => "·",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Color(HomeworkStatus? status) => status switch
|
||||||
|
{
|
||||||
|
HomeworkStatus.Completed => "#2E9D57",
|
||||||
|
HomeworkStatus.PartiallyCompleted => "#D98200",
|
||||||
|
HomeworkStatus.PartialSubmittedLate => "#7F77DD",
|
||||||
|
HomeworkStatus.PartialMissingOverdue => "#D64545",
|
||||||
|
HomeworkStatus.MissingOpen => "#D98200",
|
||||||
|
HomeworkStatus.MissingOverdue => "#D64545",
|
||||||
|
HomeworkStatus.SubmittedLate => "#7F77DD",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool CountsAsMissing(HomeworkStatus? status) => status is
|
||||||
|
HomeworkStatus.MissingOpen or
|
||||||
|
HomeworkStatus.MissingOverdue or
|
||||||
|
HomeworkStatus.PartiallyCompleted or
|
||||||
|
HomeworkStatus.PartialMissingOverdue;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Anwesenheits-Anzeige ──────────────────────────────────────────────────────
|
// ── Anwesenheits-Anzeige ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
public static class AttendanceDisplay
|
public static class AttendanceDisplay
|
||||||
{
|
{
|
||||||
public static string Label(AttendanceStatus? s) => s switch
|
public static string Label(AttendanceStatus? s) => s switch
|
||||||
{
|
{
|
||||||
null => "Anwesend",
|
null => "Noch nicht kontrolliert",
|
||||||
|
AttendanceStatus.Present => "Anwesend",
|
||||||
AttendanceStatus.ExcusePending => "Krank (Entschuldigung offen)",
|
AttendanceStatus.ExcusePending => "Krank (Entschuldigung offen)",
|
||||||
AttendanceStatus.Excused => "Krank, entschuldigt",
|
AttendanceStatus.Excused => "Krank, entschuldigt",
|
||||||
AttendanceStatus.Unexcused => "Krank, unentschuldigt",
|
AttendanceStatus.Unexcused => "Krank, unentschuldigt",
|
||||||
|
AttendanceStatus.Truant => "Geschwänzt",
|
||||||
|
AttendanceStatus.OtherSchoolEvent => "Andere Schulveranstaltung",
|
||||||
_ => "Anwesend",
|
_ => "Anwesend",
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string ShortLabel(AttendanceStatus? s) => s switch
|
public static string ShortLabel(AttendanceStatus? s) => s switch
|
||||||
{
|
{
|
||||||
null => "",
|
null => "",
|
||||||
AttendanceStatus.ExcusePending => "K ?",
|
AttendanceStatus.Present => "✓",
|
||||||
AttendanceStatus.Excused => "K ✓",
|
AttendanceStatus.ExcusePending => "?",
|
||||||
AttendanceStatus.Unexcused => "K ✗",
|
AttendanceStatus.Excused => "⊘",
|
||||||
|
AttendanceStatus.Unexcused => "!",
|
||||||
|
AttendanceStatus.Truant => "✕",
|
||||||
|
AttendanceStatus.OtherSchoolEvent => "◇",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string Color(AttendanceStatus? s) => s switch
|
||||||
|
{
|
||||||
|
AttendanceStatus.Present => "#2E9D57",
|
||||||
|
AttendanceStatus.ExcusePending => "#D98200",
|
||||||
|
AttendanceStatus.Excused => "#4C86A8",
|
||||||
|
AttendanceStatus.Unexcused => "#D96C00",
|
||||||
|
AttendanceStatus.Truant => "#D64545",
|
||||||
|
AttendanceStatus.OtherSchoolEvent => "#5277C3",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -731,3 +840,48 @@ public partial class QuickAspectRow : ObservableObject
|
|||||||
2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "·",
|
2 => "++", 1 => "+", 0 => "~", -1 => "−", -2 => "−−", _ => "·",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Schnelleingabe Anwesenheit / Hausaufgaben ────────────────────────────────
|
||||||
|
|
||||||
|
public partial class AttendanceHomeworkQuickInputViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
public List<ParticipationStudentRow> Rows { get; }
|
||||||
|
public string SessionLabel { get; }
|
||||||
|
|
||||||
|
[ObservableProperty] private ParticipationStudentRow? _selectedRow;
|
||||||
|
[ObservableProperty] private bool _advanceAfterInput = true;
|
||||||
|
|
||||||
|
public AttendanceHomeworkQuickInputViewModel(
|
||||||
|
IEnumerable<ParticipationStudentRow> rows, string sessionLabel)
|
||||||
|
{
|
||||||
|
Rows = rows.ToList();
|
||||||
|
SessionLabel = sessionLabel;
|
||||||
|
SelectedRow = Rows.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyAttendance(ParticipationStudentRow row, AttendanceStatus? status)
|
||||||
|
{
|
||||||
|
SelectedRow = row;
|
||||||
|
row.SetAttendance(status);
|
||||||
|
AdvanceIfRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyHomework(ParticipationStudentRow row, HomeworkStatus? status)
|
||||||
|
{
|
||||||
|
SelectedRow = row;
|
||||||
|
row.SetHomework(status);
|
||||||
|
AdvanceIfRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveSelection(int delta)
|
||||||
|
{
|
||||||
|
if (Rows.Count == 0) return;
|
||||||
|
var current = SelectedRow is null ? 0 : Rows.IndexOf(SelectedRow);
|
||||||
|
SelectedRow = Rows[Math.Clamp(current + delta, 0, Rows.Count - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdvanceIfRequested()
|
||||||
|
{
|
||||||
|
if (AdvanceAfterInput) MoveSelection(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _sectionValidationMessage = "";
|
[ObservableProperty] private string _sectionValidationMessage = "";
|
||||||
[ObservableProperty] private ParticipationPeriodOption _rollupPeriod;
|
[ObservableProperty] private ParticipationPeriodOption _rollupPeriod;
|
||||||
[ObservableProperty] private string _rollupStatusMessage = "";
|
[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; } =
|
public List<ParticipationPeriodOption> RollupPeriodOptions { get; } =
|
||||||
[
|
[
|
||||||
@@ -68,8 +77,10 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
||||||
GroupLabel = groupLabel;
|
GroupLabel = groupLabel;
|
||||||
|
|
||||||
_aspectWeights = aspects.GetDefaults().Concat(aspects.GetByGroup(groupId))
|
_aspectWeights = aspects.GetDefaults()
|
||||||
.ToDictionary(a => a.Key, a => a.Weight);
|
.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();
|
_students = students.GetByGroup(groupId).OrderBy(s => s.LastName).ThenBy(s => s.FirstName).ToList();
|
||||||
_allSessions = sessions.GetByGroup(groupId).OrderBy(s => s.Date).ToList();
|
_allSessions = sessions.GetByGroup(groupId).OrderBy(s => s.Date).ToList();
|
||||||
_sectionList = sectionRepo.GetByGroup(groupId).OrderBy(s => s.StartDate).ToList();
|
_sectionList = sectionRepo.GetByGroup(groupId).OrderBy(s => s.StartDate).ToList();
|
||||||
@@ -120,6 +131,11 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
if (result is null) continue;
|
if (result is null) continue;
|
||||||
points.Add((exam.Date, BuildExamPoint(exam, result)));
|
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();
|
points = points.OrderBy(p => p.Date).ToList();
|
||||||
|
|
||||||
Timeline.Clear();
|
Timeline.Clear();
|
||||||
@@ -140,7 +156,8 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
private WizardTimelinePoint BuildSessionPoint(ParticipationSession session, ParticipationEntry? entry, Guid studentId)
|
private WizardTimelinePoint BuildSessionPoint(ParticipationSession session, ParticipationEntry? entry, Guid studentId)
|
||||||
{
|
{
|
||||||
var ratingLabel = entry is not null ? WeightedRatingLabel(entry) : "";
|
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 note = entry?.Note;
|
||||||
var tooltip = $"{session.Date:dd.MM.yyyy}" +
|
var tooltip = $"{session.Date:dd.MM.yyyy}" +
|
||||||
(ratingLabel.Length > 0 ? $" · {ratingLabel}" : "") +
|
(ratingLabel.Length > 0 ? $" · {ratingLabel}" : "") +
|
||||||
@@ -149,9 +166,14 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
var point = new WizardTimelinePoint(session.Date, isExam: false, ratingLabel, examLabel: "",
|
var point = new WizardTimelinePoint(session.Date, isExam: false, ratingLabel, examLabel: "",
|
||||||
hasNote: !string.IsNullOrWhiteSpace(note), tooltip: tooltip)
|
hasNote: !string.IsNullOrWhiteSpace(note), tooltip: tooltip)
|
||||||
{
|
{
|
||||||
HasHomework = entry?.HomeworkMissing ?? false,
|
Homework = entry is null ? null : HomeworkDisplay.Effective(entry),
|
||||||
AttendanceIcon = AttendanceDisplay.ShortLabel(entry?.Attendance),
|
AttendanceIcon = AttendanceDisplay.ShortLabel(entry?.Attendance),
|
||||||
AttendanceTooltip = AttendanceDisplay.Label(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.ToggleHomeworkCommand = new RelayCommand(() => ToggleHomeworkAt(session.Id, studentId, point));
|
||||||
point.CycleAttendanceCommand = new RelayCommand(() => CycleAttendanceAt(session.Id, studentId, point));
|
point.CycleAttendanceCommand = new RelayCommand(() => CycleAttendanceAt(session.Id, studentId, point));
|
||||||
@@ -165,9 +187,24 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
hasNote: false, tooltip: $"{exam.Date:dd.MM.yyyy} · Klausur {examLabel}");
|
hasNote: false, tooltip: $"{exam.Date:dd.MM.yyyy} · Klausur {examLabel}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string WeightedRatingLabel(ParticipationEntry entry)
|
private static WizardTimelinePoint BuildOtherGradePoint(Grade grade)
|
||||||
{
|
{
|
||||||
if (entry.Ratings.Count == 0) return "";
|
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;
|
var weightSum = 0.0; var valueSum = 0.0;
|
||||||
foreach (var r in entry.Ratings)
|
foreach (var r in entry.Ratings)
|
||||||
{
|
{
|
||||||
@@ -175,8 +212,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
if (w <= 0) continue;
|
if (w <= 0) continue;
|
||||||
valueSum += r.Value * w; weightSum += w;
|
valueSum += r.Value * w; weightSum += w;
|
||||||
}
|
}
|
||||||
if (weightSum <= 0) return "";
|
return weightSum <= 0 ? null : valueSum / weightSum;
|
||||||
return RatingLabel((int)Math.Round(valueSum / weightSum, MidpointRounding.AwayFromZero));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RatingLabel(int v) => v switch
|
private static string RatingLabel(int v) => v switch
|
||||||
@@ -188,9 +224,10 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
|
||||||
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||||
entry.HomeworkMissing = !entry.HomeworkMissing;
|
entry.Homework = HomeworkDisplay.Next(HomeworkDisplay.Effective(entry));
|
||||||
|
entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(entry.Homework);
|
||||||
_entries.Save(entry);
|
_entries.Save(entry);
|
||||||
point.HasHomework = entry.HomeworkMissing;
|
point.Homework = entry.Homework;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CycleAttendanceAt(Guid sessionId, Guid studentId, WizardTimelinePoint point)
|
private void CycleAttendanceAt(Guid sessionId, Guid studentId, WizardTimelinePoint point)
|
||||||
@@ -199,15 +236,21 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
|
||||||
entry.Attendance = entry.Attendance switch
|
entry.Attendance = entry.Attendance switch
|
||||||
{
|
{
|
||||||
null => AttendanceStatus.ExcusePending,
|
null => AttendanceStatus.Present,
|
||||||
|
AttendanceStatus.Present => AttendanceStatus.ExcusePending,
|
||||||
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
|
AttendanceStatus.ExcusePending => AttendanceStatus.Excused,
|
||||||
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
|
AttendanceStatus.Excused => AttendanceStatus.Unexcused,
|
||||||
AttendanceStatus.Unexcused => null,
|
AttendanceStatus.Unexcused => AttendanceStatus.Truant,
|
||||||
|
AttendanceStatus.Truant => AttendanceStatus.OtherSchoolEvent,
|
||||||
|
AttendanceStatus.OtherSchoolEvent => null,
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
_entries.Save(entry);
|
_entries.Save(entry);
|
||||||
point.AttendanceIcon = AttendanceDisplay.ShortLabel(entry.Attendance);
|
// 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.AttendanceTooltip = AttendanceDisplay.Label(entry.Attendance);
|
||||||
|
point.AttendanceIcon = AttendanceDisplay.ShortLabel(entry.Attendance);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Abschnitte ────────────────────────────────────────────────────────────
|
// ── Abschnitte ────────────────────────────────────────────────────────────
|
||||||
@@ -364,19 +407,36 @@ public class WizardSectionGroup(string bandLabel, bool isOpen)
|
|||||||
|
|
||||||
public partial class WizardTimelinePoint : ObservableObject
|
public partial class WizardTimelinePoint : ObservableObject
|
||||||
{
|
{
|
||||||
|
public DateOnly Date { get; }
|
||||||
public string DateDisplay { get; }
|
public string DateDisplay { get; }
|
||||||
public bool IsExam { get; }
|
public bool IsExam { get; }
|
||||||
public string RatingLabel { get; }
|
public string RatingLabel { get; }
|
||||||
public string ExamLabel { get; }
|
public string ExamLabel { get; }
|
||||||
public bool HasNote { get; }
|
public bool HasNote { get; }
|
||||||
public string TooltipText { 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 bool _hasHomework;
|
[ObservableProperty] private HomeworkStatus? _homework;
|
||||||
[ObservableProperty] private string _attendanceIcon = "";
|
[ObservableProperty] private string _attendanceIcon = "";
|
||||||
[ObservableProperty] private string _attendanceTooltip = "Anwesend";
|
[ObservableProperty] private string _attendanceTooltip = "Anwesend";
|
||||||
|
[ObservableProperty] private string _attendanceColor = "";
|
||||||
|
|
||||||
public bool IsAbsent => AttendanceIcon.Length > 0;
|
public bool IsAbsent => AttendanceIcon.Length > 0;
|
||||||
public string AttendanceButtonLabel => AttendanceIcon.Length > 0 ? AttendanceIcon : "Anw";
|
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)
|
partial void OnAttendanceIconChanged(string value)
|
||||||
{
|
{
|
||||||
@@ -390,6 +450,7 @@ public partial class WizardTimelinePoint : ObservableObject
|
|||||||
public WizardTimelinePoint(DateOnly date, bool isExam, string ratingLabel, string examLabel,
|
public WizardTimelinePoint(DateOnly date, bool isExam, string ratingLabel, string examLabel,
|
||||||
bool hasNote, string tooltip)
|
bool hasNote, string tooltip)
|
||||||
{
|
{
|
||||||
|
Date = date;
|
||||||
DateDisplay = date.ToString("dd.MM.", CultureInfo.InvariantCulture);
|
DateDisplay = date.ToString("dd.MM.", CultureInfo.InvariantCulture);
|
||||||
IsExam = isExam;
|
IsExam = isExam;
|
||||||
RatingLabel = ratingLabel;
|
RatingLabel = ratingLabel;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<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.AttendanceHomeworkQuickInputDialog"
|
||||||
|
x:DataType="vm:AttendanceHomeworkQuickInputViewModel"
|
||||||
|
Title="Schnelleingabe Anwesenheit und Hausaufgaben"
|
||||||
|
Width="820" Height="620" MinWidth="680" MinHeight="460"
|
||||||
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="20">
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,12">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Anwesenheit und Hausaufgaben" FontSize="20" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding SessionLabel}" FontSize="12" Opacity="0.55"/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox Grid.Column="1" Content="Nach Eingabe nächste Zeile"
|
||||||
|
IsChecked="{Binding AdvanceAfterInput}" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<DataGrid Grid.Row="1" x:Name="StatusGrid"
|
||||||
|
ItemsSource="{Binding Rows}"
|
||||||
|
SelectedItem="{Binding SelectedRow}"
|
||||||
|
AutoGenerateColumns="False" IsReadOnly="True"
|
||||||
|
GridLinesVisibility="Horizontal"
|
||||||
|
CanUserReorderColumns="False" CanUserResizeColumns="True"
|
||||||
|
SelectionMode="Single"/>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="3">
|
||||||
|
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||||
|
Text="Anwesenheit: A anwesend · K Entschuldigung offen · E entschuldigt · U unentschuldigt · G geschwänzt · V Schulveranstaltung · 0 nicht kontrolliert"/>
|
||||||
|
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||||
|
Text="Hausaufgaben mit Alt/⌥: M gemacht · T teilweise · R Rest nachgereicht · X Rest nicht nachgereicht · O Nachreichen offen · F endgültig nicht nachgereicht · S nachgereicht · 0 keine"/>
|
||||||
|
<TextBlock FontSize="10" Opacity="0.55"
|
||||||
|
Text="↑/↓ Zeile wählen · Enter nächste Zeile · Esc schließen"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.RowSpan="2" Grid.Column="1" Content="Schließen" Click="OnClose"
|
||||||
|
VerticalAlignment="Bottom" Margin="18,0,0,0" Padding="16,6"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Controls.Templates;
|
||||||
|
using Avalonia.Data;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class AttendanceHomeworkQuickInputDialog : Window
|
||||||
|
{
|
||||||
|
public AttendanceHomeworkQuickInputDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
BuildColumns();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
StatusGrid.Focus();
|
||||||
|
if (ViewModel?.SelectedRow is { } row)
|
||||||
|
StatusGrid.ScrollIntoView(row, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildColumns()
|
||||||
|
{
|
||||||
|
StatusGrid.Columns.Add(new DataGridTextColumn
|
||||||
|
{
|
||||||
|
Header = "Schüler",
|
||||||
|
Binding = new Binding(nameof(ParticipationStudentRow.Name)),
|
||||||
|
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
|
||||||
|
});
|
||||||
|
StatusGrid.Columns.Add(new DataGridTemplateColumn
|
||||||
|
{
|
||||||
|
Header = "Anwesenheit",
|
||||||
|
Width = new DataGridLength(210, DataGridLengthUnitType.Pixel),
|
||||||
|
CellTemplate = BuildAttendanceTemplate(),
|
||||||
|
});
|
||||||
|
StatusGrid.Columns.Add(new DataGridTemplateColumn
|
||||||
|
{
|
||||||
|
Header = "Hausaufgaben",
|
||||||
|
Width = new DataGridLength(270, DataGridLengthUnitType.Pixel),
|
||||||
|
CellTemplate = BuildHomeworkTemplate(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private IDataTemplate BuildAttendanceTemplate() =>
|
||||||
|
new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||||||
|
{
|
||||||
|
if (row is null) return new TextBlock();
|
||||||
|
var button = StatusButton(185);
|
||||||
|
var flyout = new MenuFlyout { Placement = PlacementMode.BottomEdgeAlignedLeft };
|
||||||
|
AddAttendanceItem(flyout, row, "· Nicht kontrolliert", null);
|
||||||
|
AddAttendanceItem(flyout, row, "✓ Anwesend", AttendanceStatus.Present);
|
||||||
|
AddAttendanceItem(flyout, row, "? Entschuldigung offen", AttendanceStatus.ExcusePending);
|
||||||
|
AddAttendanceItem(flyout, row, "⊘ Krank, entschuldigt", AttendanceStatus.Excused);
|
||||||
|
AddAttendanceItem(flyout, row, "! Unentschuldigt", AttendanceStatus.Unexcused);
|
||||||
|
AddAttendanceItem(flyout, row, "✕ Geschwänzt", AttendanceStatus.Truant);
|
||||||
|
AddAttendanceItem(flyout, row, "◇ Andere Schulveranstaltung", AttendanceStatus.OtherSchoolEvent);
|
||||||
|
button.Flyout = flyout;
|
||||||
|
|
||||||
|
void Refresh()
|
||||||
|
{
|
||||||
|
UpdateButton(button,
|
||||||
|
string.IsNullOrEmpty(row.AttendanceLabel) ? "·" : row.AttendanceLabel,
|
||||||
|
row.AttendanceTooltip, AttendanceDisplay.Color(row.Attendance));
|
||||||
|
}
|
||||||
|
Refresh();
|
||||||
|
row.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ParticipationStudentRow.AttendanceLabel)) Refresh();
|
||||||
|
};
|
||||||
|
return Center(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
private IDataTemplate BuildHomeworkTemplate() =>
|
||||||
|
new FuncDataTemplate<ParticipationStudentRow>((row, _) =>
|
||||||
|
{
|
||||||
|
if (row is null) return new TextBlock();
|
||||||
|
var button = StatusButton(245);
|
||||||
|
var flyout = new MenuFlyout { Placement = PlacementMode.BottomEdgeAlignedLeft };
|
||||||
|
AddHomeworkItem(flyout, row, "· Keine Hausaufgabe", null);
|
||||||
|
AddHomeworkItem(flyout, row, "✓ Gemacht", HomeworkStatus.Completed);
|
||||||
|
AddHomeworkItem(flyout, row, "◐ Teilweise, Rest offen", HomeworkStatus.PartiallyCompleted);
|
||||||
|
AddHomeworkItem(flyout, row, "◕ Rest nachgereicht", HomeworkStatus.PartialSubmittedLate);
|
||||||
|
AddHomeworkItem(flyout, row, "◒ Rest nicht nachgereicht", HomeworkStatus.PartialMissingOverdue);
|
||||||
|
AddHomeworkItem(flyout, row, "! Nicht gemacht, Nachreichen offen", HomeworkStatus.MissingOpen);
|
||||||
|
AddHomeworkItem(flyout, row, "✕ Nicht mehr nachgereicht", HomeworkStatus.MissingOverdue);
|
||||||
|
AddHomeworkItem(flyout, row, "↺ Nachgereicht", HomeworkStatus.SubmittedLate);
|
||||||
|
button.Flyout = flyout;
|
||||||
|
|
||||||
|
void Refresh() => UpdateButton(button, row.HomeworkSymbol, row.HomeworkTooltip,
|
||||||
|
HomeworkDisplay.Color(row.Homework));
|
||||||
|
Refresh();
|
||||||
|
row.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(ParticipationStudentRow.HomeworkSymbol)) Refresh();
|
||||||
|
};
|
||||||
|
return Center(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
private void AddAttendanceItem(MenuFlyout flyout, ParticipationStudentRow row,
|
||||||
|
string header, AttendanceStatus? status)
|
||||||
|
{
|
||||||
|
var item = new MenuItem { Header = header };
|
||||||
|
item.Click += (_, _) => ViewModel?.ApplyAttendance(row, status);
|
||||||
|
flyout.Items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddHomeworkItem(MenuFlyout flyout, ParticipationStudentRow row,
|
||||||
|
string header, HomeworkStatus? status)
|
||||||
|
{
|
||||||
|
var item = new MenuItem { Header = header };
|
||||||
|
item.Click += (_, _) => ViewModel?.ApplyHomework(row, status);
|
||||||
|
flyout.Items.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button StatusButton(double width) => new()
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
Height = 29,
|
||||||
|
Padding = new Thickness(8, 2),
|
||||||
|
FontSize = 12,
|
||||||
|
HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Control Center(Control child) => new Grid
|
||||||
|
{
|
||||||
|
Margin = new Thickness(0, 2),
|
||||||
|
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
Children = { child },
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void UpdateButton(Button button, string symbol, string tooltip, string color)
|
||||||
|
{
|
||||||
|
button.Content = $"{symbol} {tooltip}";
|
||||||
|
ToolTip.SetTip(button, tooltip + " – klicken, um Status auszuwählen");
|
||||||
|
if (Color.TryParse(color, out var parsed))
|
||||||
|
{
|
||||||
|
button.Background = new SolidColorBrush(parsed);
|
||||||
|
button.Foreground = Brushes.White;
|
||||||
|
button.Opacity = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.ClearValue(Button.BackgroundProperty);
|
||||||
|
button.ClearValue(Button.ForegroundProperty);
|
||||||
|
button.Opacity = 0.48;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
var vm = ViewModel;
|
||||||
|
var row = vm?.SelectedRow;
|
||||||
|
if (vm is null || row is null) { base.OnKeyDown(e); return; }
|
||||||
|
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Alt))
|
||||||
|
{
|
||||||
|
var homework = e.Key switch
|
||||||
|
{
|
||||||
|
Key.D0 or Key.NumPad0 => (HomeworkStatus?)null,
|
||||||
|
Key.M => HomeworkStatus.Completed,
|
||||||
|
Key.T => HomeworkStatus.PartiallyCompleted,
|
||||||
|
Key.R => HomeworkStatus.PartialSubmittedLate,
|
||||||
|
Key.X => HomeworkStatus.PartialMissingOverdue,
|
||||||
|
Key.O => HomeworkStatus.MissingOpen,
|
||||||
|
Key.F => HomeworkStatus.MissingOverdue,
|
||||||
|
Key.S => HomeworkStatus.SubmittedLate,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
var recognized = e.Key is Key.D0 or Key.NumPad0 or Key.M or Key.T or Key.R or Key.X or Key.O or Key.F or Key.S;
|
||||||
|
if (recognized)
|
||||||
|
{
|
||||||
|
vm.ApplyHomework(row, homework);
|
||||||
|
BringSelectionIntoView();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AttendanceStatus? attendance = e.Key switch
|
||||||
|
{
|
||||||
|
Key.A => AttendanceStatus.Present,
|
||||||
|
Key.K => AttendanceStatus.ExcusePending,
|
||||||
|
Key.E => AttendanceStatus.Excused,
|
||||||
|
Key.U => AttendanceStatus.Unexcused,
|
||||||
|
Key.G => AttendanceStatus.Truant,
|
||||||
|
Key.V => AttendanceStatus.OtherSchoolEvent,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
var attendanceRecognized = e.Key is Key.D0 or Key.NumPad0 or Key.A or Key.K or Key.E or Key.U or Key.G or Key.V;
|
||||||
|
if (attendanceRecognized)
|
||||||
|
{
|
||||||
|
vm.ApplyAttendance(row, attendance);
|
||||||
|
BringSelectionIntoView();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Up: vm.MoveSelection(-1); BringSelectionIntoView(); e.Handled = true; break;
|
||||||
|
case Key.Down or Key.Enter: vm.MoveSelection(1); BringSelectionIntoView(); e.Handled = true; break;
|
||||||
|
case Key.Escape: Close(); e.Handled = true; break;
|
||||||
|
default: base.OnKeyDown(e); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AttendanceHomeworkQuickInputViewModel? ViewModel =>
|
||||||
|
DataContext as AttendanceHomeworkQuickInputViewModel;
|
||||||
|
|
||||||
|
private void BringSelectionIntoView()
|
||||||
|
{
|
||||||
|
if (ViewModel?.SelectedRow is { } row)
|
||||||
|
StatusGrid.ScrollIntoView(row, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@@ -10,20 +10,24 @@
|
|||||||
<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,Auto,*">
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto,*">
|
||||||
|
|
||||||
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="6" Margin="10,10,10,6">
|
<Button Grid.Row="0" Content="+ Sitzung" Command="{Binding AddSessionCommand}"
|
||||||
<Button Content="+ Sitzung" Command="{Binding AddSessionCommand}" HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch" Margin="10,10,10,6"/>
|
||||||
<Button Content="Schnell" Command="{Binding QuickInputCommand}"/>
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="6" Margin="10,0,10,6">
|
||||||
|
<Button Content="Schnell Mitarbeit" Command="{Binding QuickInputCommand}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
<Button Content="Anw./HA" Command="{Binding StatusQuickInputCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Row="1" Content="Ø Mitarbeitsnote" Command="{Binding ComputeGradeCommand}"
|
<Button Grid.Row="2" Content="Ø Mitarbeitsnote" Command="{Binding ComputeGradeCommand}"
|
||||||
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
||||||
|
|
||||||
<Button Grid.Row="2" Content="Mitarbeits-Assistent" Command="{Binding OpenWizardCommand}"
|
<Button Grid.Row="3" Content="Mitarbeits-Assistent" Command="{Binding OpenWizardCommand}"
|
||||||
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
HorizontalAlignment="Stretch" Margin="10,0,10,6"/>
|
||||||
|
|
||||||
<ListBox Grid.Row="3"
|
<ListBox Grid.Row="4"
|
||||||
ItemsSource="{Binding Sessions}"
|
ItemsSource="{Binding Sessions}"
|
||||||
SelectedItem="{Binding SelectedSession}"
|
SelectedItem="{Binding SelectedSession}"
|
||||||
BorderThickness="0">
|
BorderThickness="0">
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
_vm = vm;
|
_vm = vm;
|
||||||
vm.OnAddSession = ShowAddSessionDialog;
|
vm.OnAddSession = ShowAddSessionDialog;
|
||||||
vm.OnQuickInput = ShowQuickInputDialog;
|
vm.OnQuickInput = ShowQuickInputDialog;
|
||||||
|
vm.OnStatusQuickInput = ShowStatusQuickInputDialog;
|
||||||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||||||
vm.OnOpenWizard = ShowWizardDialog;
|
vm.OnOpenWizard = ShowWizardDialog;
|
||||||
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
vm.Aspects.CollectionChanged += (_, _) => BuildColumns();
|
||||||
@@ -141,17 +142,34 @@ public partial class ParticipationTabView : UserControl
|
|||||||
|
|
||||||
var btn = new Button
|
var btn = new Button
|
||||||
{
|
{
|
||||||
Content = "HA vergessen",
|
FontSize = 12,
|
||||||
FontSize = 10,
|
Width = 30,
|
||||||
Padding = new Avalonia.Thickness(5, 1),
|
Height = 26,
|
||||||
Opacity = row.HomeworkMissing ? 1.0 : 0.25,
|
Padding = new Avalonia.Thickness(3, 1),
|
||||||
Command = row.ToggleHomeworkCommand,
|
Command = row.ToggleHomeworkCommand,
|
||||||
};
|
};
|
||||||
ToolTip.SetTip(btn, "Hausaufgaben vergessen (an/aus)");
|
void RefreshHomework()
|
||||||
|
{
|
||||||
|
btn.Content = row.HomeworkSymbol;
|
||||||
|
ToolTip.SetTip(btn, row.HomeworkTooltip + " – klicken für nächsten Status");
|
||||||
|
if (row.Homework is null)
|
||||||
|
{
|
||||||
|
btn.ClearValue(Button.BackgroundProperty);
|
||||||
|
btn.ClearValue(Button.ForegroundProperty);
|
||||||
|
btn.Opacity = 0.38;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
btn.Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(HomeworkDisplay.Color(row.Homework)));
|
||||||
|
btn.Foreground = Avalonia.Media.Brushes.White;
|
||||||
|
btn.Opacity = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefreshHomework();
|
||||||
row.PropertyChanged += (_, pe) =>
|
row.PropertyChanged += (_, pe) =>
|
||||||
{
|
{
|
||||||
if (pe.PropertyName == nameof(ParticipationStudentRow.HomeworkMissing))
|
if (pe.PropertyName == nameof(ParticipationStudentRow.HomeworkSymbol))
|
||||||
btn.Opacity = row.HomeworkMissing ? 1.0 : 0.25;
|
RefreshHomework();
|
||||||
};
|
};
|
||||||
return new StackPanel
|
return new StackPanel
|
||||||
{
|
{
|
||||||
@@ -170,19 +188,34 @@ public partial class ParticipationTabView : UserControl
|
|||||||
|
|
||||||
var btn = new Button
|
var btn = new Button
|
||||||
{
|
{
|
||||||
Content = string.IsNullOrEmpty(row.AttendanceLabel) ? "anwesend" : row.AttendanceLabel,
|
FontSize = 12,
|
||||||
FontSize = 10,
|
Width = 30,
|
||||||
Padding = new Avalonia.Thickness(5, 1),
|
Height = 26,
|
||||||
|
Padding = new Avalonia.Thickness(3, 1),
|
||||||
Command = row.CycleAttendanceCommand,
|
Command = row.CycleAttendanceCommand,
|
||||||
};
|
};
|
||||||
ToolTip.SetTip(btn, row.AttendanceTooltip);
|
void RefreshAttendance()
|
||||||
|
{
|
||||||
|
btn.Content = string.IsNullOrEmpty(row.AttendanceLabel) ? "·" : row.AttendanceLabel;
|
||||||
|
ToolTip.SetTip(btn, row.AttendanceTooltip + " – klicken für nächsten Status");
|
||||||
|
if (row.Attendance is null)
|
||||||
|
{
|
||||||
|
btn.ClearValue(Button.BackgroundProperty);
|
||||||
|
btn.ClearValue(Button.ForegroundProperty);
|
||||||
|
btn.Opacity = 0.38;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
btn.Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse(AttendanceDisplay.Color(row.Attendance)));
|
||||||
|
btn.Foreground = Avalonia.Media.Brushes.White;
|
||||||
|
btn.Opacity = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RefreshAttendance();
|
||||||
row.PropertyChanged += (_, pe) =>
|
row.PropertyChanged += (_, pe) =>
|
||||||
{
|
{
|
||||||
if (pe.PropertyName == nameof(ParticipationStudentRow.AttendanceLabel))
|
if (pe.PropertyName == nameof(ParticipationStudentRow.AttendanceLabel))
|
||||||
{
|
RefreshAttendance();
|
||||||
btn.Content = string.IsNullOrEmpty(row.AttendanceLabel) ? "anwesend" : row.AttendanceLabel;
|
|
||||||
ToolTip.SetTip(btn, row.AttendanceTooltip);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
return new StackPanel
|
return new StackPanel
|
||||||
{
|
{
|
||||||
@@ -220,6 +253,17 @@ public partial class ParticipationTabView : UserControl
|
|||||||
await dialog.ShowDialog(owner);
|
await dialog.ShowDialog(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ShowStatusQuickInputDialog(ParticipationTabViewModel tabVm)
|
||||||
|
{
|
||||||
|
if (tabVm.StudentRows.Count == 0 || tabVm.SelectedSession is null) return;
|
||||||
|
var quickVm = new AttendanceHomeworkQuickInputViewModel(
|
||||||
|
tabVm.StudentRows, tabVm.SelectedSessionDisplay);
|
||||||
|
var dialog = new AttendanceHomeworkQuickInputDialog { DataContext = quickVm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is not null)
|
||||||
|
await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ShowComputeGradeDialog(ParticipationTabViewModel tabVm)
|
private async Task ShowComputeGradeDialog(ParticipationTabViewModel tabVm)
|
||||||
{
|
{
|
||||||
var dialogVm = new ParticipationGradeDialogViewModel(
|
var dialogVm = new ParticipationGradeDialogViewModel(
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.ParticipationTimelineChart"
|
||||||
|
ClipToBounds="False">
|
||||||
|
<Canvas x:Name="ChartCanvas" Height="370" ClipToBounds="False"/>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,455 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Shapes;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zoomfähige Canvas-Zeitleiste für die drei Mitarbeitmerkmale. Die Shapes werden bewusst
|
||||||
|
/// als Controls erzeugt, damit Ereignisse anklickbar und Tooltips weiterhin verfügbar sind.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ParticipationTimelineChart : UserControl
|
||||||
|
{
|
||||||
|
private const double LeftMargin = 76;
|
||||||
|
private const double RightMargin = 34;
|
||||||
|
private const double PlotTop = 78;
|
||||||
|
private const double PlotStep = 42;
|
||||||
|
private const double ChartHeight = 370;
|
||||||
|
|
||||||
|
private static readonly Color QualityColor = Color.Parse("#2E86DE");
|
||||||
|
private static readonly Color QuantityColor = Color.Parse("#E67E22");
|
||||||
|
private static readonly Color WorkphaseColor = Color.Parse("#16A085");
|
||||||
|
private static readonly Color TrendColor = Color.Parse("#6C5CE7");
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IEnumerable<WizardSectionGroup>?> TimelineProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, IEnumerable<WizardSectionGroup>?>(nameof(Timeline));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<double> ZoomProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, double>(nameof(Zoom), 1.0);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<bool> ShowQualityProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowQuality), true);
|
||||||
|
public static readonly StyledProperty<bool> ShowQuantityProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowQuantity), true);
|
||||||
|
public static readonly StyledProperty<bool> ShowWorkphaseProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowWorkphase), true);
|
||||||
|
public static readonly StyledProperty<bool> ShowDataPointsProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowDataPoints), true);
|
||||||
|
public static readonly StyledProperty<bool> ShowWeightedTrendProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowWeightedTrend), true);
|
||||||
|
public static readonly StyledProperty<bool> ShowQualityTrendProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowQualityTrend));
|
||||||
|
public static readonly StyledProperty<bool> ShowQuantityTrendProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowQuantityTrend));
|
||||||
|
public static readonly StyledProperty<bool> ShowWorkphaseTrendProperty =
|
||||||
|
AvaloniaProperty.Register<ParticipationTimelineChart, bool>(nameof(ShowWorkphaseTrend));
|
||||||
|
|
||||||
|
private INotifyCollectionChanged? _subscribedTimeline;
|
||||||
|
private readonly List<WizardTimelinePoint> _subscribedPoints = [];
|
||||||
|
|
||||||
|
public IEnumerable<WizardSectionGroup>? Timeline
|
||||||
|
{
|
||||||
|
get => GetValue(TimelineProperty);
|
||||||
|
set => SetValue(TimelineProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Zoom
|
||||||
|
{
|
||||||
|
get => GetValue(ZoomProperty);
|
||||||
|
set => SetValue(ZoomProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShowQuality { get => GetValue(ShowQualityProperty); set => SetValue(ShowQualityProperty, value); }
|
||||||
|
public bool ShowQuantity { get => GetValue(ShowQuantityProperty); set => SetValue(ShowQuantityProperty, value); }
|
||||||
|
public bool ShowWorkphase { get => GetValue(ShowWorkphaseProperty); set => SetValue(ShowWorkphaseProperty, value); }
|
||||||
|
public bool ShowDataPoints { get => GetValue(ShowDataPointsProperty); set => SetValue(ShowDataPointsProperty, value); }
|
||||||
|
public bool ShowWeightedTrend { get => GetValue(ShowWeightedTrendProperty); set => SetValue(ShowWeightedTrendProperty, value); }
|
||||||
|
public bool ShowQualityTrend { get => GetValue(ShowQualityTrendProperty); set => SetValue(ShowQualityTrendProperty, value); }
|
||||||
|
public bool ShowQuantityTrend { get => GetValue(ShowQuantityTrendProperty); set => SetValue(ShowQuantityTrendProperty, value); }
|
||||||
|
public bool ShowWorkphaseTrend { get => GetValue(ShowWorkphaseTrendProperty); set => SetValue(ShowWorkphaseTrendProperty, value); }
|
||||||
|
|
||||||
|
public ParticipationTimelineChart() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
if (change.Property == TimelineProperty)
|
||||||
|
{
|
||||||
|
SubscribeToTimeline();
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
else if (change.Property == ZoomProperty ||
|
||||||
|
change.Property == ShowQualityProperty ||
|
||||||
|
change.Property == ShowQuantityProperty ||
|
||||||
|
change.Property == ShowWorkphaseProperty ||
|
||||||
|
change.Property == ShowDataPointsProperty ||
|
||||||
|
change.Property == ShowWeightedTrendProperty ||
|
||||||
|
change.Property == ShowQualityTrendProperty ||
|
||||||
|
change.Property == ShowQuantityTrendProperty ||
|
||||||
|
change.Property == ShowWorkphaseTrendProperty)
|
||||||
|
{
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeToTimeline()
|
||||||
|
{
|
||||||
|
if (_subscribedTimeline is not null)
|
||||||
|
_subscribedTimeline.CollectionChanged -= TimelineChanged;
|
||||||
|
foreach (var point in _subscribedPoints)
|
||||||
|
point.PropertyChanged -= PointChanged;
|
||||||
|
_subscribedPoints.Clear();
|
||||||
|
|
||||||
|
_subscribedTimeline = Timeline as INotifyCollectionChanged;
|
||||||
|
if (_subscribedTimeline is not null)
|
||||||
|
_subscribedTimeline.CollectionChanged += TimelineChanged;
|
||||||
|
SubscribeToPoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeToPoints()
|
||||||
|
{
|
||||||
|
foreach (var point in _subscribedPoints)
|
||||||
|
point.PropertyChanged -= PointChanged;
|
||||||
|
_subscribedPoints.Clear();
|
||||||
|
foreach (var point in Timeline?.SelectMany(g => g.Points) ?? [])
|
||||||
|
{
|
||||||
|
point.PropertyChanged += PointChanged;
|
||||||
|
_subscribedPoints.Add(point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TimelineChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
SubscribeToPoints();
|
||||||
|
Rebuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PointChanged(object? sender, PropertyChangedEventArgs e) => Rebuild();
|
||||||
|
|
||||||
|
private void Rebuild()
|
||||||
|
{
|
||||||
|
if (ChartCanvas is null) return;
|
||||||
|
ChartCanvas.Children.Clear();
|
||||||
|
|
||||||
|
var groups = Timeline?.ToList() ?? [];
|
||||||
|
var slots = new List<(WizardTimelinePoint Point, double X)>();
|
||||||
|
var spacing = 82 * Math.Clamp(Zoom, 0.65, 2.25);
|
||||||
|
var slotIndex = 0;
|
||||||
|
|
||||||
|
foreach (var (group, groupIndex) in groups.Select((g, i) => (g, i)))
|
||||||
|
{
|
||||||
|
// Die erste Bandkante gehört zur Zeichenfläche und darf nicht mit dem Zoom
|
||||||
|
// nach links über die Achsenbeschriftung wandern.
|
||||||
|
var groupStart = groupIndex == 0
|
||||||
|
? LeftMargin - 10
|
||||||
|
: LeftMargin + slotIndex * spacing - spacing / 2;
|
||||||
|
foreach (var point in group.Points)
|
||||||
|
{
|
||||||
|
slots.Add((point, LeftMargin + slotIndex * spacing));
|
||||||
|
slotIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.Points.Count == 0) slotIndex++;
|
||||||
|
var groupEnd = LeftMargin + (slotIndex - 1) * spacing + spacing / 2;
|
||||||
|
AddSectionBand(group, groupIndex, groupStart, Math.Max(1, groupEnd - groupStart));
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = Math.Max(720, LeftMargin + Math.Max(1, slotIndex - 1) * spacing + Math.Max(RightMargin, spacing / 2 + 10));
|
||||||
|
Width = width;
|
||||||
|
Height = ChartHeight;
|
||||||
|
ChartCanvas.Width = width;
|
||||||
|
ChartCanvas.Height = ChartHeight;
|
||||||
|
|
||||||
|
DrawGrid(width);
|
||||||
|
if (ShowWeightedTrend) DrawTrend(slots);
|
||||||
|
DrawAspect(slots, "quality", QualityColor,
|
||||||
|
showLine: ShowQuality,
|
||||||
|
showPoints: ShowDataPoints && (ShowQuality || ShowQualityTrend));
|
||||||
|
DrawAspect(slots, "quantity", QuantityColor,
|
||||||
|
showLine: ShowQuantity,
|
||||||
|
showPoints: ShowDataPoints && (ShowQuantity || ShowQuantityTrend));
|
||||||
|
DrawAspect(slots, "workphase", WorkphaseColor,
|
||||||
|
showLine: ShowWorkphase,
|
||||||
|
showPoints: ShowDataPoints && (ShowWorkphase || ShowWorkphaseTrend));
|
||||||
|
if (ShowQualityTrend) DrawAspectTrend(slots, "quality", QualityColor);
|
||||||
|
if (ShowQuantityTrend) DrawAspectTrend(slots, "quantity", QuantityColor);
|
||||||
|
if (ShowWorkphaseTrend) DrawAspectTrend(slots, "workphase", WorkphaseColor);
|
||||||
|
DrawPoints(slots);
|
||||||
|
|
||||||
|
if (slots.Count == 0)
|
||||||
|
AddText("Noch keine Mitarbeit oder Leistungsnachweise vorhanden.", LeftMargin, 165, 13, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSectionBand(WizardSectionGroup group, int index, double x, double width)
|
||||||
|
{
|
||||||
|
var fill = group.IsOpen
|
||||||
|
? Color.Parse("#1320A464")
|
||||||
|
: index % 2 == 0 ? Color.Parse("#102E86DE") : Color.Parse("#0816A085");
|
||||||
|
var band = new Rectangle
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
Height = 352,
|
||||||
|
Fill = new SolidColorBrush(fill),
|
||||||
|
RadiusX = 6,
|
||||||
|
RadiusY = 6,
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
};
|
||||||
|
Canvas.SetLeft(band, x);
|
||||||
|
Canvas.SetTop(band, 2);
|
||||||
|
ChartCanvas.Children.Add(band);
|
||||||
|
|
||||||
|
var label = AddText(group.BandLabel.Replace('\n', ' '), x + 8, 9, 10, 0.62);
|
||||||
|
label.FontWeight = FontWeight.SemiBold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawGrid(double width)
|
||||||
|
{
|
||||||
|
var labels = new[] { "++", "+", "~", "−", "−−" };
|
||||||
|
for (var i = 0; i < labels.Length; i++)
|
||||||
|
{
|
||||||
|
var y = PlotTop + i * PlotStep;
|
||||||
|
var line = new Line
|
||||||
|
{
|
||||||
|
StartPoint = new Point(LeftMargin - 10, y),
|
||||||
|
EndPoint = new Point(width - RightMargin, y),
|
||||||
|
Stroke = new SolidColorBrush(Color.Parse(i == 2 ? "#352F3E46" : "#202F3E46")),
|
||||||
|
StrokeThickness = i == 2 ? 1.4 : 1,
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
};
|
||||||
|
ChartCanvas.Children.Add(line);
|
||||||
|
AddText(labels[i], 22, y - 9, 11, i == 2 ? 0.75 : 0.48);
|
||||||
|
}
|
||||||
|
AddText("Stundennote", 8, 42, 10, 0.55);
|
||||||
|
AddText("Datum / Ereignisse", 8, 289, 10, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAspect(List<(WizardTimelinePoint Point, double X)> slots, string key, Color color,
|
||||||
|
bool showLine, bool showPoints)
|
||||||
|
{
|
||||||
|
var values = slots
|
||||||
|
.Where(s => !s.Point.IsExam && s.Point.AspectValues.ContainsKey(key))
|
||||||
|
.Select(s => new Point(s.X, ValueY(s.Point.AspectValues[key])))
|
||||||
|
.ToList();
|
||||||
|
if (showLine)
|
||||||
|
AddSmoothPath(values, color, 2.2, dashed: false, opacity: 0.92);
|
||||||
|
|
||||||
|
if (!showPoints) return;
|
||||||
|
foreach (var point in values)
|
||||||
|
{
|
||||||
|
var marker = new Ellipse
|
||||||
|
{
|
||||||
|
Width = 8,
|
||||||
|
Height = 8,
|
||||||
|
Fill = new SolidColorBrush(color),
|
||||||
|
Stroke = Brushes.White,
|
||||||
|
StrokeThickness = 1.2,
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
};
|
||||||
|
Canvas.SetLeft(marker, point.X - 4);
|
||||||
|
Canvas.SetTop(marker, point.Y - 4);
|
||||||
|
ChartCanvas.Children.Add(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTrend(List<(WizardTimelinePoint Point, double X)> slots)
|
||||||
|
{
|
||||||
|
var rated = slots.Where(s => !s.Point.IsExam && s.Point.WeightedValue is not null).ToList();
|
||||||
|
var smoothed = new List<Point>();
|
||||||
|
for (var i = 0; i < rated.Count; i++)
|
||||||
|
{
|
||||||
|
var weightedSum = rated[i].Point.WeightedValue!.Value * 2.0;
|
||||||
|
var weightSum = 2.0;
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
weightedSum += rated[i - 1].Point.WeightedValue!.Value;
|
||||||
|
weightSum++;
|
||||||
|
}
|
||||||
|
if (i + 1 < rated.Count)
|
||||||
|
{
|
||||||
|
weightedSum += rated[i + 1].Point.WeightedValue!.Value;
|
||||||
|
weightSum++;
|
||||||
|
}
|
||||||
|
smoothed.Add(new Point(rated[i].X, ValueY(weightedSum / weightSum)));
|
||||||
|
}
|
||||||
|
AddSmoothPath(smoothed, TrendColor, 4, dashed: true, opacity: 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawAspectTrend(List<(WizardTimelinePoint Point, double X)> slots, string key, Color color)
|
||||||
|
{
|
||||||
|
var rated = slots
|
||||||
|
.Where(s => !s.Point.IsExam && s.Point.AspectValues.ContainsKey(key))
|
||||||
|
.ToList();
|
||||||
|
var smoothed = new List<Point>();
|
||||||
|
for (var i = 0; i < rated.Count; i++)
|
||||||
|
{
|
||||||
|
// Gewichtete 3-Termine-Glättung: der aktuelle Termin zählt doppelt,
|
||||||
|
// direkter Vorgänger und Nachfolger jeweils einfach (1–2–1).
|
||||||
|
var weightedSum = rated[i].Point.AspectValues[key] * 2.0;
|
||||||
|
var weightSum = 2.0;
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
weightedSum += rated[i - 1].Point.AspectValues[key];
|
||||||
|
weightSum++;
|
||||||
|
}
|
||||||
|
if (i + 1 < rated.Count)
|
||||||
|
{
|
||||||
|
weightedSum += rated[i + 1].Point.AspectValues[key];
|
||||||
|
weightSum++;
|
||||||
|
}
|
||||||
|
smoothed.Add(new Point(rated[i].X, ValueY(weightedSum / weightSum)));
|
||||||
|
}
|
||||||
|
AddSmoothPath(smoothed, color, 4.2, dashed: true, opacity: 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSmoothPath(IReadOnlyList<Point> points, Color color, double thickness, bool dashed, double opacity)
|
||||||
|
{
|
||||||
|
if (points.Count < 2) return;
|
||||||
|
var geometry = new StreamGeometry();
|
||||||
|
using (var context = geometry.Open())
|
||||||
|
{
|
||||||
|
context.BeginFigure(points[0], false);
|
||||||
|
for (var i = 1; i < points.Count; i++)
|
||||||
|
{
|
||||||
|
var previous = points[i - 1];
|
||||||
|
var current = points[i];
|
||||||
|
var middleX = (previous.X + current.X) / 2;
|
||||||
|
context.CubicBezierTo(
|
||||||
|
new Point(middleX, previous.Y),
|
||||||
|
new Point(middleX, current.Y),
|
||||||
|
current,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
context.EndFigure(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = new Avalonia.Controls.Shapes.Path
|
||||||
|
{
|
||||||
|
Data = geometry,
|
||||||
|
Stroke = new SolidColorBrush(color),
|
||||||
|
StrokeThickness = thickness,
|
||||||
|
Opacity = opacity,
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
};
|
||||||
|
if (dashed)
|
||||||
|
path.StrokeDashArray = new AvaloniaList<double> { 7, 4 };
|
||||||
|
ChartCanvas.Children.Add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawPoints(List<(WizardTimelinePoint Point, double X)> slots)
|
||||||
|
{
|
||||||
|
foreach (var (point, x) in slots)
|
||||||
|
{
|
||||||
|
var guide = new Line
|
||||||
|
{
|
||||||
|
StartPoint = new Point(x, PlotTop),
|
||||||
|
EndPoint = new Point(x, 276),
|
||||||
|
Stroke = new SolidColorBrush(Color.Parse("#122F3E46")),
|
||||||
|
StrokeThickness = 1,
|
||||||
|
IsHitTestVisible = false,
|
||||||
|
};
|
||||||
|
ChartCanvas.Children.Add(guide);
|
||||||
|
|
||||||
|
if (!point.IsExam && point.OverallGrade.Length > 0)
|
||||||
|
AddBadge($"Note {point.OverallGrade}", x - 26, 40, "#E8EEF8", "#243B53", 52);
|
||||||
|
|
||||||
|
var date = AddText(point.DateDisplay, x - 28, 278, 10, 0.62);
|
||||||
|
date.Width = 56;
|
||||||
|
date.TextAlignment = TextAlignment.Center;
|
||||||
|
|
||||||
|
if (point.IsExam)
|
||||||
|
{
|
||||||
|
AddBadge(point.ExamLabel, x - 40, 307, "#7F77DD", "White", 80, point.TooltipText);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double attendanceWidth = 30;
|
||||||
|
const double homeworkWidth = 30;
|
||||||
|
const double eventGap = 4;
|
||||||
|
var eventX = x - (attendanceWidth + eventGap + homeworkWidth) / 2;
|
||||||
|
AddCommandBadge(point.AttendanceButtonLabel, eventX, 307, attendanceWidth, point.IsAbsent,
|
||||||
|
point.AttendanceColor,
|
||||||
|
point.CycleAttendanceCommand, point.AttendanceTooltip + " – klicken für nächsten Status");
|
||||||
|
eventX += attendanceWidth + eventGap;
|
||||||
|
AddCommandBadge(point.HomeworkSymbol, eventX, 307, homeworkWidth, point.HasHomeworkStatus,
|
||||||
|
point.HomeworkColor, point.ToggleHomeworkCommand,
|
||||||
|
point.HomeworkTooltip + " – klicken für nächsten Status");
|
||||||
|
eventX += homeworkWidth + eventGap;
|
||||||
|
if (point.HasNote)
|
||||||
|
AddBadge("●", eventX, 311, "#607D8B", "White", 22, point.TooltipText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Border AddBadge(string text, double x, double y, string background, string foreground,
|
||||||
|
double width, string? tooltip = null)
|
||||||
|
{
|
||||||
|
var badge = new Border
|
||||||
|
{
|
||||||
|
Width = width,
|
||||||
|
MinHeight = 22,
|
||||||
|
CornerRadius = new CornerRadius(4),
|
||||||
|
Background = new SolidColorBrush(Color.Parse(background)),
|
||||||
|
Padding = new Thickness(4, 2),
|
||||||
|
Child = new TextBlock
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
FontSize = 9,
|
||||||
|
Foreground = new SolidColorBrush(Color.Parse(foreground)),
|
||||||
|
TextAlignment = TextAlignment.Center,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (tooltip is not null) ToolTip.SetTip(badge, tooltip);
|
||||||
|
Canvas.SetLeft(badge, x);
|
||||||
|
Canvas.SetTop(badge, y);
|
||||||
|
ChartCanvas.Children.Add(badge);
|
||||||
|
return badge;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddCommandBadge(string text, double x, double y, double width, bool isActive,
|
||||||
|
string activeBackground, System.Windows.Input.ICommand? command, string tooltip)
|
||||||
|
{
|
||||||
|
var button = new Button
|
||||||
|
{
|
||||||
|
Content = text,
|
||||||
|
Command = command,
|
||||||
|
FontSize = 12,
|
||||||
|
Width = width,
|
||||||
|
Height = 25,
|
||||||
|
Padding = new Thickness(3, 1),
|
||||||
|
HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center,
|
||||||
|
VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center,
|
||||||
|
};
|
||||||
|
// Im inaktiven Zustand bleiben Farben vollständig beim aktuellen Fluent-Theme.
|
||||||
|
// So ist der Text sowohl im hellen als auch im dunklen Modus kontrastreich.
|
||||||
|
if (isActive && Color.TryParse(activeBackground, out var parsedBackground))
|
||||||
|
{
|
||||||
|
button.Background = new SolidColorBrush(parsedBackground);
|
||||||
|
button.Foreground = Brushes.White;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
button.Opacity = 0.34;
|
||||||
|
}
|
||||||
|
ToolTip.SetTip(button, tooltip);
|
||||||
|
Canvas.SetLeft(button, x);
|
||||||
|
Canvas.SetTop(button, y);
|
||||||
|
ChartCanvas.Children.Add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextBlock AddText(string text, double x, double y, double fontSize, double opacity)
|
||||||
|
{
|
||||||
|
var block = new TextBlock { Text = text, FontSize = fontSize, Opacity = opacity, IsHitTestVisible = false };
|
||||||
|
Canvas.SetLeft(block, x);
|
||||||
|
Canvas.SetTop(block, y);
|
||||||
|
ChartCanvas.Children.Add(block);
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double ValueY(double value) => PlotTop + (2 - Math.Clamp(value, -2, 2)) * PlotStep;
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:views="clr-namespace:LehrerApp.Desktop.Views.Groups"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.ParticipationWizardDialog"
|
x:Class="LehrerApp.Desktop.Views.Groups.ParticipationWizardDialog"
|
||||||
x:DataType="vm:ParticipationWizardDialogViewModel"
|
x:DataType="vm:ParticipationWizardDialogViewModel"
|
||||||
Title="Mitarbeits-Assistent"
|
Title="Mitarbeits-Assistent"
|
||||||
Width="920" Height="760" MinWidth="720" MinHeight="520"
|
Width="1080" Height="820" MinWidth="760" MinHeight="620"
|
||||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto,Auto" Margin="24">
|
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto,Auto" Margin="24">
|
||||||
@@ -21,89 +22,85 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Text="Zeitleiste" FontSize="13" FontWeight="SemiBold" Margin="0,14,0,6"/>
|
<Grid Grid.Row="1" RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="0,14,0,6">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Text="Mitarbeit im Zeitverlauf" FontSize="13"
|
||||||
<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||||
<ItemsControl ItemsSource="{Binding Timeline}">
|
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||||
<ItemsControl.ItemsPanel>
|
<TextBlock Text="Zoom" FontSize="10" Opacity="0.6" VerticalAlignment="Center"/>
|
||||||
<ItemsPanelTemplate>
|
<Slider Minimum="0.65" Maximum="2.25" Value="{Binding TimelineZoom}" Width="120"
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10"/>
|
TickFrequency="0.1"/>
|
||||||
</ItemsPanelTemplate>
|
<Button Content="Legende ▾" Padding="8,3" FontSize="10">
|
||||||
</ItemsControl.ItemsPanel>
|
<Button.Flyout>
|
||||||
<ItemsControl.Styles>
|
<Flyout Placement="BottomEdgeAlignedRight">
|
||||||
<Style Selector="Border.sectionband">
|
<StackPanel Width="310" Spacing="6" Margin="12">
|
||||||
<Setter Property="Background" Value="#E1F5EE"/>
|
<TextBlock Text="Hausaufgaben" FontWeight="SemiBold" FontSize="11"/>
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
<TextBlock Text="· keine Hausaufgabe erfasst" FontSize="10" Opacity="0.65"/>
|
||||||
</Style>
|
<TextBlock Text="✓ gemacht" FontSize="10" Foreground="#2E9D57"/>
|
||||||
<Style Selector="Border.sectionband.open">
|
<TextBlock Text="◐ teilweise angefertigt – Rest offen" FontSize="10" Foreground="#D98200"/>
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<TextBlock Text="◕ teilweise angefertigt – Rest nachgereicht" FontSize="10" Foreground="#7F77DD"/>
|
||||||
<Setter Property="BorderThickness" Value="1.5"/>
|
<TextBlock Text="◒ teilweise angefertigt – Rest nicht nachgereicht" FontSize="10" Foreground="#D64545"/>
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseMediumBrush}"/>
|
<TextBlock Text="! nicht gemacht – Nachreichen offen" FontSize="10" Foreground="#D98200"/>
|
||||||
</Style>
|
<TextBlock Text="✕ nicht gemacht – nicht mehr nachgereicht" FontSize="10" Foreground="#D64545"/>
|
||||||
<Style Selector="Button.haicon">
|
<TextBlock Text="↺ nachgereicht" FontSize="10" Foreground="#7F77DD"/>
|
||||||
<Setter Property="Opacity" Value="0.2"/>
|
<Separator Margin="0,4"/>
|
||||||
</Style>
|
<TextBlock Text="Anwesenheit" FontWeight="SemiBold" FontSize="11"/>
|
||||||
<Style Selector="Button.haicon.active">
|
<TextBlock Text="· noch nicht kontrolliert" FontSize="10" Opacity="0.65"/>
|
||||||
<Setter Property="Opacity" Value="1"/>
|
<TextBlock Text="✓ anwesend" FontSize="10" Foreground="#2E9D57"/>
|
||||||
<Setter Property="Background" Value="#E24B4A"/>
|
<TextBlock Text="? Entschuldigung offen" FontSize="10" Foreground="#D98200"/>
|
||||||
<Setter Property="Foreground" Value="White"/>
|
<TextBlock Text="⊘ krank, entschuldigt" FontSize="10" Foreground="#4C86A8"/>
|
||||||
</Style>
|
<TextBlock Text="! unentschuldigt" FontSize="10" Foreground="#D96C00"/>
|
||||||
<Style Selector="Button.attendanceicon">
|
<TextBlock Text="✕ geschwänzt" FontSize="10" Foreground="#D64545"/>
|
||||||
<Setter Property="Opacity" Value="0.2"/>
|
<TextBlock Text="◇ andere Schulveranstaltung" FontSize="10" Foreground="#5277C3"/>
|
||||||
</Style>
|
<TextBlock Text="Klick auf ein Symbol wechselt zum nächsten Status."
|
||||||
<Style Selector="Button.attendanceicon.active">
|
FontSize="9" Opacity="0.55" Margin="0,5,0,0" TextWrapping="Wrap"/>
|
||||||
<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>
|
||||||
|
</Flyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
<WrapPanel Grid.Row="1" Grid.ColumnSpan="2" Margin="0,7,0,0" VerticalAlignment="Center">
|
||||||
</ItemsControl>
|
<TextBlock Text="Kurven" FontSize="10" FontWeight="SemiBold" Opacity="0.6"
|
||||||
</StackPanel>
|
VerticalAlignment="Center" Margin="0,0,7,0"/>
|
||||||
</Border>
|
<ToggleButton IsChecked="{Binding ShowQuality}" Padding="7,2" FontSize="10" Margin="0,0,5,3">
|
||||||
</DataTemplate>
|
<StackPanel Orientation="Horizontal" Spacing="5"><Border Width="9" Height="9" CornerRadius="5" Background="#2E86DE"/><TextBlock Text="Qualität"/></StackPanel>
|
||||||
</ItemsControl.ItemTemplate>
|
</ToggleButton>
|
||||||
</ItemsControl>
|
<ToggleButton IsChecked="{Binding ShowQuantity}" Padding="7,2" FontSize="10" Margin="0,0,5,3">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5"><Border Width="9" Height="9" CornerRadius="5" Background="#E67E22"/><TextBlock Text="Quantität"/></StackPanel>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton IsChecked="{Binding ShowWorkphase}" Padding="7,2" FontSize="10" Margin="0,0,12,3">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5"><Border Width="9" Height="9" CornerRadius="5" Background="#16A085"/><TextBlock Text="Arbeitsphase"/></StackPanel>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton Content="Datenpunkte" IsChecked="{Binding ShowDataPoints}"
|
||||||
|
Padding="7,2" FontSize="10" Margin="0,0,12,3"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Trends (1–2–1)" FontSize="10" FontWeight="SemiBold" Opacity="0.6"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,7,0"/>
|
||||||
|
<ToggleButton Content="Gesamt gewichtet" IsChecked="{Binding ShowWeightedTrend}"
|
||||||
|
Padding="7,2" FontSize="10" Margin="0,0,5,3"/>
|
||||||
|
<ToggleButton Content="Qualität" IsChecked="{Binding ShowQualityTrend}"
|
||||||
|
Padding="7,2" FontSize="10" Margin="0,0,5,3"/>
|
||||||
|
<ToggleButton Content="Quantität" IsChecked="{Binding ShowQuantityTrend}"
|
||||||
|
Padding="7,2" FontSize="10" Margin="0,0,5,3"/>
|
||||||
|
<ToggleButton Content="Arbeitsphase" IsChecked="{Binding ShowWorkphaseTrend}"
|
||||||
|
Padding="7,2" FontSize="10" Margin="0,0,0,3"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Grid.Row="2" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="7" ClipToBounds="True">
|
||||||
|
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||||
|
<views:ParticipationTimelineChart Timeline="{Binding Timeline}" Zoom="{Binding TimelineZoom}"
|
||||||
|
ShowQuality="{Binding ShowQuality}"
|
||||||
|
ShowQuantity="{Binding ShowQuantity}"
|
||||||
|
ShowWorkphase="{Binding ShowWorkphase}"
|
||||||
|
ShowDataPoints="{Binding ShowDataPoints}"
|
||||||
|
ShowWeightedTrend="{Binding ShowWeightedTrend}"
|
||||||
|
ShowQualityTrend="{Binding ShowQualityTrend}"
|
||||||
|
ShowQuantityTrend="{Binding ShowQuantityTrend}"
|
||||||
|
ShowWorkphaseTrend="{Binding ShowWorkphaseTrend}"/>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Row="3" Spacing="4" Margin="0,14,0,0">
|
<StackPanel Grid.Row="3" Spacing="4" Margin="0,14,0,0">
|
||||||
<TextBlock Text="Abschnittsnoten dieses Schülers" FontSize="13" FontWeight="SemiBold"/>
|
<TextBlock Text="Abschnittsnoten dieses Schülers" FontSize="13" FontWeight="SemiBold"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user