Jahresplanimport und Ablgeich von Untis

This commit is contained in:
2026-08-23 19:23:05 +02:00
parent 4a59bff9de
commit dd5ecb0334
20 changed files with 1039 additions and 20 deletions
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
using System.Collections.ObjectModel;
using System.Globalization;
@@ -34,6 +35,7 @@ public partial class DashboardViewModel : ObservableObject
private readonly PublicHolidayService _publicHolidays;
private readonly SchoolCalendarSettingsService _calendarSettings;
private readonly ISubstitutionEntryRepository _substitutions;
private readonly IAnnualPlanEventRepository? _annualPlanEvents;
private const int OpenExcuseMaxAgeDays = 21;
private const int SupportPlanDueWithinDays = 14;
@@ -108,7 +110,8 @@ public partial class DashboardViewModel : ObservableObject
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy,
DashboardSettingsService dashboardSettings, ISchoolHolidayRepository schoolHolidays,
PublicHolidayService publicHolidays, SchoolCalendarSettingsService calendarSettings,
ISubstitutionEntryRepository substitutions)
ISubstitutionEntryRepository substitutions, IAnnualPlanEventRepository? annualPlanEvents = null,
AnnualPlanSyncService? annualPlanSync = null)
{
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
@@ -118,6 +121,12 @@ public partial class DashboardViewModel : ObservableObject
_attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
_schoolHolidays = schoolHolidays; _publicHolidays = publicHolidays; _calendarSettings = calendarSettings;
_substitutions = substitutions;
_annualPlanEvents = annualPlanEvents;
if (annualPlanSync is not null)
{
annualPlanSync.DataChanged += () => Avalonia.Threading.Dispatcher.UIThread.Post(LoadCalendar);
_ = annualPlanSync.PollAsync();
}
LoadDashboardCards();
Load();
}
@@ -580,6 +589,27 @@ public partial class DashboardViewModel : ObservableObject
}
}
// Der Jahresplan wird lediglich in dieselbe Anzeigeprojektion eingemischt. Er nimmt an
// keiner Stundenplan-/Vertretungslogik teil; mehrtägige Termine erscheinen an jedem
// betroffenen Kalendertag.
if (_annualPlanEvents is not null)
{
foreach (var annualEvent in _annualPlanEvents.GetByRange(gridStart, gridEnd)
.Where(e => !string.Equals(e.Status, "CANCELLED", StringComparison.OrdinalIgnoreCase)))
{
var visibleStart = annualEvent.StartDate < gridStart ? gridStart : annualEvent.StartDate;
var visibleEnd = annualEvent.EndDate > gridEnd ? gridEnd : annualEvent.EndDate;
for (var date = visibleStart; date <= visibleEnd; date = date.AddDays(1))
{
var agg = Agg(date);
agg.HasAnnualPlanEvent = true;
agg.Details.Add(new CalendarEventItem(CalendarEventKind.AnnualPlan, date,
annualEvent.Title, FormatAnnualPlanSubtitle(annualEvent), null,
annualEvent.Description));
}
}
}
// "Meine Klasse"-Ring: bisher nur gesetzt, wenn für den Tag schon eine Lesson/Exam/Sitzung
// existiert — ein Tag, an dem laut Stundenplan (4.3) eine eigene Klasse ansteht, für den
// aber noch keine Lesson angelegt wurde (z.B. "morgen"), zeigte den Ring fälschlich nicht.
@@ -615,7 +645,7 @@ public partial class DashboardViewModel : ObservableObject
byDay.TryGetValue(date, out var agg);
CalendarDays.Add(new CalendarDayCell(date, date.Month == firstOfMonth.Month, date == today,
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.HasSession ?? false,
agg?.IsOwnClassDay ?? false, agg?.Details ?? []));
agg?.HasAnnualPlanEvent ?? false, agg?.IsOwnClassDay ?? false, agg?.Details ?? []));
}
SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
?? CalendarDays.First(d => d.IsCurrentMonth));
@@ -635,8 +665,9 @@ public partial class DashboardViewModel : ObservableObject
private void OpenCalendarEvent(CalendarEventItem? item)
{
if (item is null) return;
if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(item.GroupId);
else OnNavigateToLesson?.Invoke(item.GroupId); // auch für ParticipationSession: Tab "Mitarbeit"
if (item.GroupId is not { } groupId) return; // Jahresplantermine sind reine Information.
if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(groupId);
else OnNavigateToLesson?.Invoke(groupId); // auch für ParticipationSession: Tab "Mitarbeit"
}
[RelayCommand]
@@ -699,9 +730,35 @@ public partial class DashboardViewModel : ObservableObject
public bool HasLesson;
public bool HasExam;
public bool HasSession;
public bool HasAnnualPlanEvent;
public bool IsOwnClassDay;
public List<CalendarEventItem> Details { get; } = [];
}
private static string FormatAnnualPlanSubtitle(AnnualPlanEvent entry)
{
string time;
if (entry.IsAllDay)
{
time = entry.StartDate == entry.EndDate
? "Ganztägig"
: $"{entry.StartDate:dd.MM.}{entry.EndDate:dd.MM.yyyy} · ganztägig";
}
else if (entry.StartDate == entry.EndDate)
{
time = entry.EndTime is { } end
? $"{entry.StartTime:HH\\:mm}{end:HH\\:mm}"
: $"{entry.StartTime:HH\\:mm}";
}
else
{
time = $"{entry.StartDate:dd.MM.} {entry.StartTime:HH\\:mm}" +
$"{entry.EndDate:dd.MM.} {entry.EndTime:HH\\:mm}";
}
return string.Join(" · ", new[] { time, entry.CalendarGroup, entry.Location }
.Where(part => !string.IsNullOrWhiteSpace(part)));
}
}
public class LessonItem
@@ -783,12 +840,14 @@ public partial class CalendarDayCell : ObservableObject
public bool HasLesson { get; }
public bool HasExam { get; }
public bool HasSession { get; }
public bool HasAnnualPlanEvent { get; }
public bool IsOwnClassDay { get; }
public string Tooltip { get; }
public IReadOnlyList<CalendarEventItem> Events { get; }
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
bool hasLesson, bool hasExam, bool hasSession, bool isOwnClassDay, List<CalendarEventItem> details)
bool hasLesson, bool hasExam, bool hasSession, bool hasAnnualPlanEvent,
bool isOwnClassDay, List<CalendarEventItem> details)
{
Date = date;
DayNumber = date.Day;
@@ -797,6 +856,7 @@ public partial class CalendarDayCell : ObservableObject
HasLesson = hasLesson;
HasExam = hasExam;
HasSession = hasSession;
HasAnnualPlanEvent = hasAnnualPlanEvent;
IsOwnClassDay = isOwnClassDay;
Events = details;
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy")
@@ -804,20 +864,22 @@ public partial class CalendarDayCell : ObservableObject
}
}
public enum CalendarEventKind { Lesson, Exam, ParticipationSession }
public enum CalendarEventKind { Lesson, Exam, ParticipationSession, AnnualPlan }
public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, string title,
string subtitle, Guid groupId)
string subtitle, Guid? groupId, string description = "")
{
public CalendarEventKind Kind { get; } = kind;
public DateOnly Date { get; } = date;
public string Title { get; } = title;
public string Subtitle { get; } = subtitle;
public Guid GroupId { get; } = groupId;
public Guid? GroupId { get; } = groupId;
public string Description { get; } = description;
public string KindLabel => Kind switch
{
CalendarEventKind.Exam => "Klausur",
CalendarEventKind.ParticipationSession => "Sitzung",
CalendarEventKind.AnnualPlan => "Jahresplan",
_ => "Unterricht"
};
}