129 lines
6.4 KiB
C#
129 lines
6.4 KiB
C#
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 linked = courseStudents.Select(student => (Student: student, Key: StudentKey(student)))
|
||
.Where(x => x.Key is not null).ToDictionary(x => x.Key!.Value, x => x.Student);
|
||
var absences = await _untis.GetAbsencesAsync(start, end);
|
||
var loaded = absences.Where(absence => linked.ContainsKey(absence.StudentKey))
|
||
.Select(absence => (Student: linked[absence.StudentKey], Absence: absence))
|
||
.OrderBy(x => x.Absence.Date).ThenBy(x => x.Student.FullName);
|
||
|
||
foreach (var item in loaded)
|
||
{
|
||
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}";
|
||
}
|