Files
LehrerApp/LehrerApp.Desktop/ViewModels/Planning/WebUntisTimetableImportViewModel.cs
T
2026-10-06 13:29:05 +02:00

216 lines
12 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 System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
namespace LehrerApp.Desktop.ViewModels.Planning;
public sealed record WebUntisGroupOption(Guid Id, string DisplayName, int? LessonId = null);
public partial class WebUntisTimetableRow : ObservableObject
{
public required DayOfWeek Weekday { get; init; }
public required int PeriodNumber { get; init; }
public required string TimeLabel { get; init; }
public required string UntisLabel { get; init; }
public required string SuggestedGroupName { get; init; }
public string? SubjectName { get; init; }
public string? Room { get; init; }
public int? LessonId { get; init; }
public string LessonIdLabel => LessonId is { } id ? $"Unterrichtsnummer: {id}" : "Unterrichtsnummer nicht gefunden";
[ObservableProperty] private bool _applyLessonId = true;
[ObservableProperty] private bool _include = true;
public ObservableCollection<WebUntisGroupOption> GroupOptions { get; } = [];
[ObservableProperty] private WebUntisGroupOption? _selectedGroup;
public string ExistingLessonIdLabel => SelectedGroup?.LessonId is { } id ? $"Bisher: {id}" : "Bisher: keine Unterrichtsnummer";
partial void OnSelectedGroupChanged(WebUntisGroupOption? value) => OnPropertyChanged(nameof(ExistingLessonIdLabel));
public string WeekdayLabel => Weekday switch
{
DayOfWeek.Monday => "Mo", DayOfWeek.Tuesday => "Di", DayOfWeek.Wednesday => "Mi",
DayOfWeek.Thursday => "Do", DayOfWeek.Friday => "Fr", _ => Weekday.ToString()[..2],
};
}
public partial class WebUntisTimetableImportViewModel : ObservableObject
{
private readonly WebUntisIntegrationService _untis;
private readonly WebUntisSettingsService _settings;
private readonly ITimetableSlotRepository _slots;
private readonly IGroupRepository _groups;
public ObservableCollection<UntisTeacherDto> Teachers { get; } = [];
public ObservableCollection<WebUntisTimetableRow> Rows { get; } = [];
[ObservableProperty] private UntisTeacherDto? _selectedTeacher;
[ObservableProperty] private DateTimeOffset? _weekDate = DateTimeOffset.Now;
[ObservableProperty] private string _status = "Lehrkraft auswählen und Untis-Woche laden.";
[ObservableProperty] private bool _busy;
[ObservableProperty] private bool _updateLessonIds = true;
[ObservableProperty] private bool _updateTimetable;
[ObservableProperty] private bool _replaceTimetable;
private bool _loaded;
private int? _loadedTeacherId;
public bool Saved { get; private set; }
public Func<WebUntisTimetableRow, Task<LearningGroup?>>? OnCreateGroup { get; set; }
public WebUntisTimetableImportViewModel(WebUntisIntegrationService untis, WebUntisSettingsService settings,
ITimetableSlotRepository slots, IGroupRepository groups)
{
_untis = untis; _settings = settings; _slots = slots; _groups = groups;
}
public async Task InitializeAsync()
{
Busy = true;
try
{
foreach (var teacher in (await _untis.GetTeachersAsync()).Where(x => x.Active).OrderBy(x => x.DisplayName))
Teachers.Add(teacher);
SelectedTeacher = Teachers.FirstOrDefault(x => x.UntisId == _settings.TeacherUntisId)
?? Teachers.FirstOrDefault();
Status = Teachers.Count == 0 ? "WebUntis hat keine Lehrkräfte geliefert." : "Bereit zum Laden.";
}
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
finally { Busy = false; }
}
[RelayCommand]
private async Task Load()
{
if (SelectedTeacher is null) { Status = "Bitte eine Lehrkraft auswählen."; return; }
var teacherId = SelectedTeacher.UntisId;
Busy = true; Rows.Clear(); _loaded = false; Saved = false;
try
{
var selected = DateOnly.FromDateTime((WeekDate ?? DateTimeOffset.Now).LocalDateTime);
var monday = selected.AddDays(-(((int)selected.DayOfWeek + 6) % 7));
var periods = await _untis.GetTimetableAsync(teacherId, monday, monday.AddDays(6));
var lessonIds = await _untis.GetWeeklyLessonIdsAsync(teacherId, monday);
var grid = await _untis.GetTimeGridAsync();
var groupOptions = BuildGroupOptions();
var localSlots = _slots.GetAll().ToDictionary(x => (x.Weekday, x.PeriodNumber));
foreach (var period in periods.Where(x => string.IsNullOrWhiteSpace(x.Code) || x.Code != "cancelled")
.GroupBy(x => (x.Date, x.StartTime, x.EndTime, x.StudentGroup,
LessonId: lessonIds.TryGetValue(x.Id, out var id) ? (int?)id : null,
Class: string.Join("/", x.Classes.Select(c => c.Name)),
Subject: string.Join("/", x.Subjects.Select(s => s.Name)),
Room: string.Join("/", x.Rooms.Select(r => r.Name))))
.Select(x => x.First()).OrderBy(x => x.Date).ThenBy(x => x.StartTime))
{
if (!TryDate(period.Date, out var date) || date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
continue;
var dayGrid = grid.FirstOrDefault(x => x.Day == UntisDay(date.DayOfWeek));
var number = dayGrid?.TimeUnits.ToList().FindIndex(x => x.StartTime == period.StartTime) + 1 ?? 0;
if (number <= 0) continue;
var className = period.Classes.FirstOrDefault()?.Name ?? "";
var subject = period.Subjects.FirstOrDefault()?.Name;
var suggested = !string.IsNullOrWhiteSpace(period.StudentGroup) ? period.StudentGroup! : className;
var row = new WebUntisTimetableRow
{
Weekday = date.DayOfWeek, PeriodNumber = number,
TimeLabel = $"{Time(period.StartTime)}–{Time(period.EndTime)}",
UntisLabel = string.Join(" · ", new[] { subject, suggested, period.Rooms.FirstOrDefault()?.Name }
.Where(x => !string.IsNullOrWhiteSpace(x))),
SuggestedGroupName = suggested, SubjectName = subject,
Room = period.Rooms.FirstOrDefault()?.Name,
LessonId = lessonIds.TryGetValue(period.Id, out var lessonId) ? lessonId : null,
};
foreach (var option in groupOptions) row.GroupOptions.Add(option);
if (localSlots.TryGetValue((row.Weekday, row.PeriodNumber), out var existing))
row.SelectedGroup = groupOptions.FirstOrDefault(x => x.Id == existing.GroupId);
row.SelectedGroup ??= BestMatch(groupOptions, suggested, className, subject);
Rows.Add(row);
}
_loaded = true;
_loadedTeacherId = teacherId;
Status = Rows.Count == 0 ? "In dieser Woche wurde kein Unterricht gefunden."
: $"{Rows.Count} regelmäßige Termine gefunden. Zuordnung prüfen und übernehmen.";
if (Rows.Any(row => row.LessonId is null))
Status += " Für einzelne Termine wurde keine Unterrichtsnummer gefunden; deren bisherige Nummer bleibt erhalten.";
}
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
finally { Busy = false; }
}
[RelayCommand]
private async Task CreateGroup(WebUntisTimetableRow row)
{
if (OnCreateGroup is null) return;
var group = await OnCreateGroup(row);
if (group is null) return;
var option = new WebUntisGroupOption(group.Id, group.Name);
foreach (var item in Rows.Where(x => x.SuggestedGroupName == row.SuggestedGroupName))
{
item.GroupOptions.Add(option);
item.SelectedGroup = option;
}
}
[RelayCommand]
private void Save()
{
Saved = false;
if (Busy || !_loaded || Rows.Count == 0) { Status = "Bitte zuerst eine Unterrichtswoche laden."; return; }
if (!UpdateLessonIds && !UpdateTimetable) { Status = "Bitte auswählen, was übernommen werden soll."; return; }
var selected = Rows.Where(x => x.Include && x.SelectedGroup is not null).ToList();
if (selected.Count == 0) { Status = "Bitte mindestens eine Lerngruppe zuordnen."; return; }
var idRows = selected.Where(x => x.ApplyLessonId && x.LessonId is > 0).ToList();
if (UpdateLessonIds)
{
var conflict = idRows.GroupBy(x => x.SelectedGroup!.Id)
.FirstOrDefault(g => g.Select(x => x.LessonId).Distinct().Count() > 1);
if (conflict is not null)
{
Status = $"Mehrere Unterrichtsnummern für {conflict.First().SelectedGroup!.DisplayName}. " +
"Zuordnung korrigieren oder bei den betreffenden Zeilen die ID-Übernahme abwählen.";
return;
}
if (!UpdateTimetable && idRows.Count == 0) { Status = "Keine Unterrichtsnummern zur Übernahme ausgewählt."; return; }
}
if (UpdateTimetable && selected.GroupBy(x => (x.Weekday, x.PeriodNumber)).Any(g => g.Count() > 1))
{
Status = "Mehrere Einträge für dieselbe Stunde. Bitte jeweils nur einen zuordnen.";
return;
}
var groupChanges = 0;
if (UpdateLessonIds)
foreach (var assignment in idRows.GroupBy(x => x.SelectedGroup!.Id))
{
var group = _groups.GetById(assignment.Key);
if (group is null || group.WebUntisLessonId == assignment.First().LessonId) continue;
group.WebUntisLessonId = assignment.First().LessonId;
_groups.Save(group);
groupChanges++;
}
if (UpdateTimetable && ReplaceTimetable)
foreach (var slot in _slots.GetAll().Where(slot =>
!selected.Any(row => row.Weekday == slot.Weekday && row.PeriodNumber == slot.PeriodNumber)))
_slots.Delete(slot.Id);
foreach (var row in selected.Where(_ => UpdateTimetable))
{
var existing = _slots.GetAll().FirstOrDefault(x => x.Weekday == row.Weekday && x.PeriodNumber == row.PeriodNumber);
var slot = existing ?? new TimetableSlot { Weekday = row.Weekday, PeriodNumber = row.PeriodNumber };
slot.GroupId = row.SelectedGroup!.Id;
slot.Room = string.IsNullOrWhiteSpace(row.Room) ? null : row.Room;
_slots.Save(slot);
}
Saved = true;
_settings.SetTeacherUntisId(_loadedTeacherId);
Status = $"{groupChanges} Unterrichtsnummern aktualisiert, " +
$"{(UpdateTimetable ? selected.Count : 0)} Stundenplan-Einträge übernommen.";
}
private List<WebUntisGroupOption> BuildGroupOptions() => _groups.GetAll().OrderBy(x => x.Name)
.Where(x => x.IsActive).Select(x => new WebUntisGroupOption(x.Id, x.Name, x.WebUntisLessonId)).ToList();
private static WebUntisGroupOption? BestMatch(IEnumerable<WebUntisGroupOption> options, params string?[] terms) =>
options.FirstOrDefault(x => terms.Any(term => !string.IsNullOrWhiteSpace(term) &&
(x.DisplayName.Equals(term, StringComparison.OrdinalIgnoreCase) ||
x.DisplayName.Contains(term, StringComparison.OrdinalIgnoreCase))));
private static int UntisDay(DayOfWeek day) => day == DayOfWeek.Sunday ? 7 : (int)day;
private static bool TryDate(int value, out DateOnly date) => DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
private static string Time(int value) => $"{value / 100:00}:{value % 100:00}";
}