216 lines
12 KiB
C#
216 lines
12 KiB
C#
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}";
|
||
}
|