Untis API Integration
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Importing;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
public partial class WebUntisAbsenceRow : ObservableObject
|
||||
{
|
||||
public required string StudentName { get; init; }
|
||||
public required DateOnly Date { get; init; }
|
||||
public required string TimeLabel { get; init; }
|
||||
public required string UntisStatus { get; init; }
|
||||
public required string LocalStatus { get; init; }
|
||||
public required AttendanceStatus TargetStatus { get; init; }
|
||||
public required Guid StudentId { get; init; }
|
||||
public required Guid? SessionId { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
public bool CanApply => SessionId is not null;
|
||||
[ObservableProperty] private bool _selected;
|
||||
}
|
||||
|
||||
public partial class WebUntisAbsenceComparisonViewModel : ObservableObject
|
||||
{
|
||||
private readonly LearningGroup _group;
|
||||
private readonly WebUntisIntegrationService _untis;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IParticipationSessionRepository _sessions;
|
||||
private readonly IParticipationRepository _participation;
|
||||
|
||||
public ObservableCollection<WebUntisAbsenceRow> Rows { get; } = [];
|
||||
[ObservableProperty] private DateTimeOffset _startDate = DateTimeOffset.Now.AddMonths(-2);
|
||||
[ObservableProperty] private DateTimeOffset _endDate = DateTimeOffset.Now;
|
||||
[ObservableProperty] private string _status = "Zeitraum wählen und Fehlzeiten laden.";
|
||||
[ObservableProperty] private bool _busy;
|
||||
|
||||
public WebUntisAbsenceComparisonViewModel(LearningGroup group, WebUntisIntegrationService untis,
|
||||
IStudentRepository students, IParticipationSessionRepository sessions,
|
||||
IParticipationRepository participation)
|
||||
{
|
||||
_group = group; _untis = untis; _students = students; _sessions = sessions;
|
||||
_participation = participation;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Load()
|
||||
{
|
||||
var start = DateOnly.FromDateTime(StartDate.LocalDateTime);
|
||||
var end = DateOnly.FromDateTime(EndDate.LocalDateTime);
|
||||
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
|
||||
Busy = true; Rows.Clear();
|
||||
try
|
||||
{
|
||||
var courseStudents = _students.GetByGroup(_group.Id);
|
||||
var localSessions = _sessions.GetByGroup(_group.Id)
|
||||
.Where(x => x.Date >= start && x.Date <= end).GroupBy(x => x.Date)
|
||||
.ToDictionary(x => x.Key, x => x.First());
|
||||
var loaded = new ConcurrentBag<(Student Student, UntisStudentAbsenceDto Absence)>();
|
||||
var linked = courseStudents.Select(student => (Student: student, Key: StudentKey(student)))
|
||||
.Where(x => x.Key is not null).ToList();
|
||||
await Parallel.ForEachAsync(linked, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (item, token) =>
|
||||
{
|
||||
var report = await _untis.GetAbsencesAsync(item.Key!.Value, start, end, token);
|
||||
foreach (var absence in report.Absences) loaded.Add((item.Student, absence));
|
||||
});
|
||||
|
||||
foreach (var item in loaded.OrderBy(x => x.Absence.Date).ThenBy(x => x.Student.FullName))
|
||||
{
|
||||
if (!TryDate(item.Absence.Date, out var date)) continue;
|
||||
localSessions.TryGetValue(date, out var session);
|
||||
var entry = session is null ? null : _participation.GetBySessionAndStudent(session.Id, item.Student.Id);
|
||||
var target = MapStatus(item.Absence.ExcuseStatus);
|
||||
Rows.Add(new WebUntisAbsenceRow
|
||||
{
|
||||
StudentName = item.Student.FullName, StudentId = item.Student.Id, Date = date,
|
||||
TimeLabel = $"{Time(item.Absence.StartTime)}–{Time(item.Absence.EndTime)}",
|
||||
UntisStatus = DisplayUntisStatus(item.Absence),
|
||||
LocalStatus = entry?.Attendance?.ToString() ?? (session is null ? "keine lokale Stunde" : "nicht erfasst"),
|
||||
TargetStatus = target, SessionId = session?.Id, Reason = item.Absence.AbsenceReason,
|
||||
Selected = session is not null && entry?.Attendance != target,
|
||||
});
|
||||
}
|
||||
var withoutKey = courseStudents.Count - linked.Count;
|
||||
Status = $"{Rows.Count} Untis-Fehlzeiten gefunden; {Rows.Count(x => x.CanApply)} sind einer lokalen Kursstunde zuordenbar."
|
||||
+ (withoutKey > 0 ? $" {withoutKey} Schüler haben noch keine WebUntis-Kennung." : "");
|
||||
}
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||
finally { Busy = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Apply()
|
||||
{
|
||||
var selected = Rows.Where(x => x.Selected && x.SessionId is not null).ToList();
|
||||
foreach (var row in selected)
|
||||
{
|
||||
var entry = _participation.GetBySessionAndStudent(row.SessionId!.Value, row.StudentId)
|
||||
?? new ParticipationEntry { SessionId = row.SessionId.Value, StudentId = row.StudentId };
|
||||
entry.Attendance = row.TargetStatus;
|
||||
entry.UpdatedAt = DateTime.UtcNow;
|
||||
_participation.Save(entry);
|
||||
}
|
||||
Status = $"{selected.Count} Anwesenheitsstatus übernommen.";
|
||||
foreach (var row in selected) row.Selected = false;
|
||||
}
|
||||
|
||||
private static int? StudentKey(Student student)
|
||||
{
|
||||
student.ExternalIds ??= [];
|
||||
return student.ExternalIds.TryGetValue(StudentImportFormats.MasterDataCsv.Value, out var value)
|
||||
&& int.TryParse(value, out var key) ? key : null;
|
||||
}
|
||||
private static AttendanceStatus MapStatus(string? value)
|
||||
{
|
||||
var text = value?.Trim().ToLowerInvariant() ?? "";
|
||||
if (text.Contains("unexcused") || text.Contains("unentschuldigt") || text.Contains("nicht entschuldigt"))
|
||||
return AttendanceStatus.Unexcused;
|
||||
if (text.Contains("excused") || text.Contains("entschuldigt")) return AttendanceStatus.Excused;
|
||||
return AttendanceStatus.ExcusePending;
|
||||
}
|
||||
private static string DisplayUntisStatus(UntisStudentAbsenceDto absence) =>
|
||||
string.Join(" · ", new[] { absence.ExcuseStatus, absence.AbsenceReason }.Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||
is { Length: > 0 } text ? text : "offen";
|
||||
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}";
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Desktop.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
public partial class WebUntisClassSelectionViewModel(WebUntisIntegrationService untis) : ObservableObject
|
||||
{
|
||||
public ObservableCollection<UntisSchoolYearDto> SchoolYears { get; } = [];
|
||||
public ObservableCollection<UntisClassDto> Classes { get; } = [];
|
||||
[ObservableProperty] private UntisSchoolYearDto? _selectedSchoolYear;
|
||||
[ObservableProperty] private UntisClassDto? _selectedClass;
|
||||
[ObservableProperty] private string _status = "";
|
||||
[ObservableProperty] private bool _busy;
|
||||
public bool CanConfirm => SelectedClass is not null && !Busy;
|
||||
|
||||
partial void OnSelectedClassChanged(UntisClassDto? value) => OnPropertyChanged(nameof(CanConfirm));
|
||||
partial void OnBusyChanged(bool value) => OnPropertyChanged(nameof(CanConfirm));
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
Busy = true;
|
||||
try
|
||||
{
|
||||
foreach (var year in (await untis.GetSchoolYearsAsync()).OrderByDescending(x => x.StartDate))
|
||||
SchoolYears.Add(year);
|
||||
SelectedSchoolYear = SchoolYears.FirstOrDefault(x => x.StartDate <= Today() && x.EndDate >= Today())
|
||||
?? SchoolYears.FirstOrDefault();
|
||||
await LoadClasses();
|
||||
}
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||
finally { Busy = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoadClasses()
|
||||
{
|
||||
if (SelectedSchoolYear is null) return;
|
||||
Busy = true; Classes.Clear(); SelectedClass = null;
|
||||
try
|
||||
{
|
||||
foreach (var entry in (await untis.GetClassesAsync(SelectedSchoolYear.UntisId)).OrderBy(x => x.Name))
|
||||
Classes.Add(entry);
|
||||
Status = Classes.Count == 0 ? "Keine Klassen gefunden." : $"{Classes.Count} Klassen gefunden.";
|
||||
}
|
||||
catch (WebUntisIntegrationException ex) { Status = ex.Message; }
|
||||
finally { Busy = false; }
|
||||
}
|
||||
|
||||
private static int Today()
|
||||
{
|
||||
var date = DateOnly.FromDateTime(DateTime.Today);
|
||||
return date.Year * 10000 + date.Month * 100 + date.Day;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user