Files
LehrerApp/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
T
admin febcb0855a Notenverwaltung (Kapitel 2) und Mitarbeits-Assistent
Notenübersicht der Gruppe (Matrix, Gesamt-Spalte, Sortierung, Halbjahresfilter),
Einzelnoten-Pflege samt Sammelerfassung, Gewichtungsschema mit Voreinstellung je
Gruppentyp, Zeugnisnoten-Berechnung mit Übersteuern/Festschreiben/Export und
Notenentwicklung im Schülerdetail.

Dazu Anwesenheits-/Hausaufgaben-Tracking je Mitarbeit-Sitzung, ein neuer
Mitarbeits-Assistent (Zeitleiste mit Abschnitten, automatischer Notenvorschlag,
Zusammenzug zur Halbjahresnote) und eine Dashboard-Kachel für offene
Entschuldigungen. Außerdem: verbliebene englische Begriffe in Auswahlfeldern
und Buttons auf Deutsch umgestellt.
2026-08-12 19:19:22 +02:00

268 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
public partial class DashboardViewModel : ObservableObject
{
private static readonly CultureInfo De = new("de-DE");
private readonly IGroupRepository _groups;
private readonly ISubjectRepository _subjects;
private readonly ILessonRepository _lessons;
private readonly IExamRepository _exams;
private readonly IWorkTaskRepository _tasks;
private readonly IParticipationSessionRepository _participationSessions;
private readonly IParticipationRepository _participationEntries;
private readonly IStudentRepository _students;
private readonly SchoolYearService _sy;
private const int OpenExcuseMaxAgeDays = 21;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
[ObservableProperty] private string _currentSchoolYear = "";
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
public string CalendarMonthLabel => CalendarMonth.ToString("MMMM yyyy", De);
public ObservableCollection<LessonItem> TodaysLessons { get; } = [];
public ObservableCollection<TaskItem> OpenTasks { get; } = [];
public ObservableCollection<GroupChip> CurrentGroups { get; } = [];
public ObservableCollection<CalendarDayCell> CalendarDays { get; } = [];
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
// Navigation-Callback wird von App.axaml.cs verdrahtet
public Action<Guid>? OnNavigateToGroup { get; set; }
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
IParticipationRepository participationEntries, IStudentRepository students, SchoolYearService sy)
{
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
_participationSessions = participationSessions; _participationEntries = participationEntries;
_students = students; _sy = sy;
Load();
}
private static DateOnly FirstOfMonth(DateTime d) => new(d.Year, d.Month, 1);
private void Load()
{
var now = DateTime.Now;
var today = DateOnly.FromDateTime(now);
CurrentDate = now.ToString("dddd, d. MMMM yyyy", De);
CurrentSchoolYear = _sy.CurrentSchoolYear();
Greeting = now.Hour < 12 ? "Guten Morgen" : now.Hour < 18 ? "Guten Tag" : "Guten Abend";
TodaysLessons.Clear();
var groups = _groups.GetBySchoolYear(_sy.CurrentSchoolYear()).ToDictionary(g => g.Id);
foreach (var l in groups.Keys.SelectMany(gid => _lessons.GetByGroupAndDate(gid, today))
.OrderBy(l => l.LessonNumber))
{
if (groups.TryGetValue(l.GroupId, out var g))
TodaysLessons.Add(new() { GroupName = g.Name, Topic = l.Topic });
}
OpenTasks.Clear();
foreach (var t in _tasks.GetByStatus(WorkTaskStatus.Open)
.Concat(_tasks.GetByStatus(WorkTaskStatus.InProgress))
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(5))
OpenTasks.Add(new() { Title = t.Title,
DueDate = t.DueDate?.ToString("dd.MM.") ?? "",
IsOverdue = t.DueDate.HasValue && t.DueDate < today });
CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name))
CurrentGroups.Add(new()
{
GroupId = g.Id,
Name = g.Name,
Subject = g.SubjectId is Guid subjectId ? _subjects.GetById(subjectId)?.Name ?? "" : "",
});
CalendarMonth = FirstOfMonth(now);
LoadCalendar();
LoadOpenExcuses(groups.Values.ToList(), today);
}
private void LoadOpenExcuses(List<LearningGroup> groups, DateOnly today)
{
OpenExcuses.Clear();
var cutoff = today.AddDays(-OpenExcuseMaxAgeDays);
var items = new List<OpenExcuseItem>();
foreach (var g in groups)
{
foreach (var session in _participationSessions.GetByGroup(g.Id).Where(s => s.Date >= cutoff && s.Date <= today))
{
foreach (var entry in _participationEntries.GetBySession(session.Id)
.Where(e => e.Attendance == AttendanceStatus.ExcusePending))
{
var student = _students.GetById(entry.StudentId);
if (student is null) continue;
var item = new OpenExcuseItem(session.Id, entry.StudentId, student.FullName, g.Name, session.Date);
item.OnResolve = ResolveExcuse;
items.Add(item);
}
}
}
foreach (var item in items.OrderBy(i => i.Date))
OpenExcuses.Add(item);
}
private void ResolveExcuse(OpenExcuseItem item, AttendanceStatus status)
{
var entry = _participationEntries.GetBySessionAndStudent(item.SessionId, item.StudentId);
if (entry is null) return;
entry.Attendance = status;
_participationEntries.Save(entry);
OpenExcuses.Remove(item);
}
private void LoadCalendar()
{
CalendarDays.Clear();
var today = DateOnly.FromDateTime(DateTime.Today);
// Rasterstart: Montag der Woche, die den 1. des Monats enthält 6 Wochen (42 Tage) Raster.
var firstOfMonth = CalendarMonth;
var mondayOffset = ((int)firstOfMonth.DayOfWeek + 6) % 7; // Montag=0 ... Sonntag=6
var gridStart = firstOfMonth.AddDays(-mondayOffset);
var gridEnd = gridStart.AddDays(41);
var groups = _groups.GetBySchoolYear(_sy.CurrentSchoolYear());
var byDay = new Dictionary<DateOnly, DayAgg>();
DayAgg Agg(DateOnly d)
{
if (!byDay.TryGetValue(d, out var agg)) byDay[d] = agg = new DayAgg();
return agg;
}
foreach (var g in groups)
{
foreach (var lesson in _lessons.GetByGroupAndRange(g.Id, gridStart, gridEnd))
{
var agg = Agg(lesson.Date);
agg.HasLesson = true;
if (g.IsOwnClass) agg.IsOwnClassDay = true;
agg.Details.Add($"Unterricht: {g.Name}" +
(string.IsNullOrWhiteSpace(lesson.Topic) ? "" : $" {lesson.Topic}"));
}
foreach (var exam in _exams.GetByGroup(g.Id).Where(e => e.Date >= gridStart && e.Date <= gridEnd))
{
var agg = Agg(exam.Date);
agg.HasExam = true;
if (g.IsOwnClass) agg.IsOwnClassDay = true;
agg.Details.Add($"Klausur: {exam.Title} ({g.Name})");
}
}
for (var i = 0; i < 42; i++)
{
var date = gridStart.AddDays(i);
byDay.TryGetValue(date, out var agg);
CalendarDays.Add(new CalendarDayCell(date, date.Month == firstOfMonth.Month, date == today,
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
agg?.Details ?? []));
}
}
[RelayCommand]
private void PrevMonth()
{
CalendarMonth = CalendarMonth.AddMonths(-1);
OnPropertyChanged(nameof(CalendarMonthLabel));
LoadCalendar();
}
[RelayCommand]
private void NextMonth()
{
CalendarMonth = CalendarMonth.AddMonths(1);
OnPropertyChanged(nameof(CalendarMonthLabel));
LoadCalendar();
}
[RelayCommand]
private void CalendarToday()
{
CalendarMonth = FirstOfMonth(DateTime.Today);
OnPropertyChanged(nameof(CalendarMonthLabel));
LoadCalendar();
}
[RelayCommand] private void OpenGroup(GroupChip? c) { if (c is not null) OnNavigateToGroup?.Invoke(c.GroupId); }
[RelayCommand] private void Refresh() => Load();
private class DayAgg
{
public bool HasLesson;
public bool HasExam;
public bool IsOwnClassDay;
public List<string> Details { get; } = [];
}
}
public class LessonItem { public string GroupName { get; set; } = ""; public string Topic { get; set; } = ""; }
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } }
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
public partial class OpenExcuseItem : ObservableObject
{
public Guid SessionId { get; }
public Guid StudentId { get; }
public string StudentName { get; }
public string GroupName { get; }
public DateOnly Date { get; }
public string DateDisplay { get; }
public Action<OpenExcuseItem, AttendanceStatus>? OnResolve { get; set; }
public OpenExcuseItem(Guid sessionId, Guid studentId, string studentName, string groupName, DateOnly date)
{
SessionId = sessionId;
StudentId = studentId;
StudentName = studentName;
GroupName = groupName;
Date = date;
DateDisplay = date.ToString("dd.MM.yyyy");
}
[RelayCommand] private void MarkExcused() => OnResolve?.Invoke(this, AttendanceStatus.Excused);
[RelayCommand] private void MarkUnexcused() => OnResolve?.Invoke(this, AttendanceStatus.Unexcused);
}
public class CalendarDayCell
{
public int DayNumber { get; }
public bool IsCurrentMonth { get; }
public bool IsToday { get; }
public bool HasLesson { get; }
public bool HasExam { get; }
public bool IsOwnClassDay { get; }
public string Tooltip { get; }
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
bool hasLesson, bool hasExam, bool isOwnClassDay, List<string> details)
{
DayNumber = date.Day;
IsCurrentMonth = isCurrentMonth;
IsToday = isToday;
HasLesson = hasLesson;
HasExam = hasExam;
IsOwnClassDay = isOwnClassDay;
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy") : string.Join("\n", details);
}
}