Untis-API: Fehlzeiten abgleich
This commit is contained in:
@@ -27,9 +27,9 @@ public sealed record UntisStudentDto(int UntisId, int ExternKey, string ClassNam
|
||||
string? EntryDateRaw, int? ExitDate, string? ExitDateRaw, string? Text, string? MedicalReportDuty,
|
||||
string? Schulpflicht, string? Majority, UntisStudentAddressDto Address, string? AttributeIL);
|
||||
public sealed record UntisStudentReportDto(int Count, string? ClassNameFilter, IReadOnlyList<UntisStudentDto> Students);
|
||||
public sealed record UntisStudentAbsenceDto(int StudentKey, int Date, int StartTime, int EndTime, int AbsentMinutes,
|
||||
bool Checked, string? AbsenceReason, string? ExcuseStatus, int? SubjectId, IReadOnlyList<int> TeacherIds,
|
||||
string? StudentGroup);
|
||||
public sealed record UntisLessonAbsenceDto(string StudentName, int Date, int AbsentPeriods,
|
||||
int UnexcusedAbsentPeriods, int AbsentMinutes, int UnexcusedAbsentMinutes, int? StartTime, int? EndTime,
|
||||
string? Reason, int? ExternKey, bool ExternKeyInParentheses, string? HandledOn, bool Counts);
|
||||
|
||||
/// <summary>Direkter WebUntis-Client des Desktops. Personenbezogene Antworten und der
|
||||
/// unverschlüsselte CSV-Report passieren zu keinem Zeitpunkt den LehrerApp-Server.</summary>
|
||||
@@ -107,13 +107,14 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
||||
x.Address.PostCode, x.Address.Street), x.AttributeIL)).ToList());
|
||||
}, token);
|
||||
|
||||
public Task<IReadOnlyList<UntisStudentAbsenceDto>> GetAbsencesAsync(DateOnly start, DateOnly end,
|
||||
CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
public Task<IReadOnlyList<UntisLessonAbsenceDto>> GetLessonAbsencesAsync(int lessonId,
|
||||
DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
{
|
||||
var absences = await client.GetAbsencesAsync(Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisStudentAbsenceDto>)absences.Select(x => new UntisStudentAbsenceDto(
|
||||
x.StudentKey, x.Date, x.StartTime, x.EndTime, x.AbsentMinutes, x.Checked, x.AbsenceReason,
|
||||
x.ExcuseStatus, x.SubjectId, x.TeacherIds, x.StudentGroup)).ToList();
|
||||
var absences = await client.GetLessonAbsencesAsync(lessonId, Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisLessonAbsenceDto>)absences.Select(x => new UntisLessonAbsenceDto(
|
||||
x.StudentName, x.Date, x.AbsentPeriods, x.UnexcusedAbsentPeriods, x.AbsentMinutes,
|
||||
x.UnexcusedAbsentMinutes, x.StartTime, x.EndTime, x.Reason, x.ExternKey, x.ExternKeyInParentheses,
|
||||
x.HandledOn, x.Counts)).ToList();
|
||||
}, token);
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
||||
|
||||
@@ -878,6 +878,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private bool _isOwnClass;
|
||||
[ObservableProperty] private bool _isDifferentiated;
|
||||
[ObservableProperty] private bool _requiresLessonPlanning = true;
|
||||
[ObservableProperty] private int? _webUntisLessonId;
|
||||
[ObservableProperty] private string _nameError = "";
|
||||
[ObservableProperty] private string _gradeLevelError = "";
|
||||
|
||||
@@ -917,6 +918,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
IsOwnClass = group.IsOwnClass;
|
||||
IsDifferentiated = group.IsDifferentiated;
|
||||
RequiresLessonPlanning = group.RequiresLessonPlanning;
|
||||
WebUntisLessonId = group.WebUntisLessonId;
|
||||
OnPropertyChanged(nameof(DialogTitle));
|
||||
OnPropertyChanged(nameof(SaveButtonText));
|
||||
}
|
||||
@@ -963,6 +965,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
Result.IsOwnClass = IsOwnClass;
|
||||
Result.IsDifferentiated = IsDifferentiated;
|
||||
Result.RequiresLessonPlanning = RequiresLessonPlanning;
|
||||
Result.WebUntisLessonId = WebUntisLessonId;
|
||||
_groups.Save(Result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
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}";
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
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;
|
||||
|
||||
/// <summary>Eine Zeile bleibt auch ohne automatische Zuordnung sichtbar (statt stillschweigend
|
||||
/// weggefiltert zu werden) - <see cref="AssignedStudent"/> kann manuell per Auswahlliste gesetzt
|
||||
/// werden, wenn weder `ENr` noch der Name eindeutig auf ein Kursmitglied passen.</summary>
|
||||
public partial class WebUntisLessonAbsenceRow : ObservableObject
|
||||
{
|
||||
public required string UntisStudentName { get; init; }
|
||||
public required DateOnly Date { get; init; }
|
||||
public required string TimeLabel { get; init; }
|
||||
public required string UntisStatus { get; init; }
|
||||
public required AttendanceStatus TargetStatus { get; init; }
|
||||
public required IReadOnlyList<Student> Candidates { get; init; }
|
||||
internal Action<WebUntisLessonAbsenceRow>? OnAssignmentChanged { get; init; }
|
||||
public string? Reason { get; init; }
|
||||
public string DateLabel => Date.ToString("dd.MM.yyyy");
|
||||
public bool CanApply => SessionId is not null;
|
||||
|
||||
[ObservableProperty] private Student? _assignedStudent;
|
||||
[ObservableProperty] private string _localStatus = "ohne Zuordnung";
|
||||
[ObservableProperty] private Guid? _sessionId;
|
||||
[ObservableProperty] private bool _selected;
|
||||
|
||||
partial void OnAssignedStudentChanged(Student? value) => OnAssignmentChanged?.Invoke(this);
|
||||
}
|
||||
|
||||
/// <summary>Fehlzeitenabgleich über den "Fehlzeiten pro Unterricht"-Bericht (<see cref="LearningGroup.WebUntisLessonId"/>).
|
||||
/// Der früher genutzte, pro Schüler abgerufene Fehlzeiten-Report (getTimetableWithAbsences) brauchte
|
||||
/// weitergehende WebUntis-Rechte als dieser Lerngruppen-Bericht und wurde deshalb entfernt.</summary>
|
||||
public partial class WebUntisLessonAbsenceComparisonViewModel : ObservableObject
|
||||
{
|
||||
private readonly LearningGroup _group;
|
||||
private readonly WebUntisIntegrationService _untis;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IParticipationSessionRepository _sessions;
|
||||
private readonly IParticipationRepository _participation;
|
||||
|
||||
public ObservableCollection<WebUntisLessonAbsenceRow> 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 WebUntisLessonAbsenceComparisonViewModel(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 lessonId = _group.WebUntisLessonId;
|
||||
if (lessonId is null)
|
||||
{
|
||||
Status = "Für diese Lerngruppe ist noch keine WebUntis-Unterrichtsnummer hinterlegt " +
|
||||
"(Gruppe bearbeiten).";
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
// Erste Wahl: WebUntis-Kennung (ENr). Nicht jeder Schüler hat eine (z.B. manuell statt
|
||||
// per WebUntis-Import angelegt) - Fallback über den Namen, aber nur wenn er innerhalb
|
||||
// der Kursmitglieder eindeutig ist, sonst lieber unzugeordnet lassen als raten.
|
||||
var byKey = courseStudents.Select(student => (Student: student, Key: StudentKey(student)))
|
||||
.Where(x => x.Key is not null).ToDictionary(x => x.Key!.Value, x => x.Student);
|
||||
var byName = courseStudents
|
||||
.SelectMany(student => new[]
|
||||
{
|
||||
NameKey($"{student.LastName} {student.FirstName}"),
|
||||
NameKey($"{student.FirstName} {student.LastName}"),
|
||||
}.Select(key => (Key: key, Student: student)))
|
||||
.GroupBy(x => x.Key)
|
||||
.Where(group => group.Select(x => x.Student).Distinct().Count() == 1)
|
||||
.ToDictionary(group => group.Key, group => group.First().Student);
|
||||
|
||||
var absences = await _untis.GetLessonAbsencesAsync(lessonId.Value, start, end);
|
||||
var ordered = absences
|
||||
.Select(absence => (Absence: absence, Date: TryDate(absence.Date, out var date) ? date : (DateOnly?)null))
|
||||
.Where(x => x.Date is not null)
|
||||
.OrderBy(x => x.Date).ThenBy(x => x.Absence.StudentName);
|
||||
|
||||
void ResolveLocalMatch(WebUntisLessonAbsenceRow row)
|
||||
{
|
||||
if (row.AssignedStudent is not { } student)
|
||||
{
|
||||
row.SessionId = null; row.LocalStatus = "ohne Zuordnung"; row.Selected = false;
|
||||
return;
|
||||
}
|
||||
localSessions.TryGetValue(row.Date, out var session);
|
||||
var entry = session is null ? null : _participation.GetBySessionAndStudent(session.Id, student.Id);
|
||||
row.SessionId = session?.Id;
|
||||
row.LocalStatus = session is null ? "keine lokale Stunde" : AttendanceDisplay.Label(entry?.Attendance);
|
||||
row.Selected = session is not null && entry?.Attendance != row.TargetStatus;
|
||||
}
|
||||
|
||||
foreach (var (absence, date) in ordered)
|
||||
{
|
||||
var match = absence.ExternKey is { } key && byKey.TryGetValue(key, out var byKeyStudent)
|
||||
? byKeyStudent
|
||||
: byName.GetValueOrDefault(NameKey(absence.StudentName));
|
||||
var row = new WebUntisLessonAbsenceRow
|
||||
{
|
||||
UntisStudentName = absence.StudentName, Date = date!.Value,
|
||||
TimeLabel = TimeLabel(absence.StartTime, absence.EndTime),
|
||||
UntisStatus = DisplayUntisStatus(absence),
|
||||
TargetStatus = MapStatus(absence), Reason = absence.Reason,
|
||||
Candidates = courseStudents, OnAssignmentChanged = ResolveLocalMatch,
|
||||
};
|
||||
Rows.Add(row);
|
||||
row.AssignedStudent = match; // löst OnAssignedStudentChanged aus und setzt SessionId/LocalStatus/Selected
|
||||
}
|
||||
|
||||
var unresolved = Rows.Count(x => x.AssignedStudent is null);
|
||||
Status = $"{absences.Count} Fehlzeiten von WebUntis erhalten, {Rows.Count - unresolved} automatisch zugeordnet" +
|
||||
(unresolved > 0 ? $", {unresolved} bitte manuell zuordnen" : "") +
|
||||
$". {Rows.Count(x => x.CanApply)} sind einer lokalen Kursstunde zuordenbar.";
|
||||
}
|
||||
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 && x.AssignedStudent is not null).ToList();
|
||||
foreach (var row in selected)
|
||||
{
|
||||
var studentId = row.AssignedStudent!.Id;
|
||||
var entry = _participation.GetBySessionAndStudent(row.SessionId!.Value, studentId)
|
||||
?? new ParticipationEntry { SessionId = row.SessionId.Value, StudentId = 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;
|
||||
}
|
||||
|
||||
// Groß-/Kleinschreibung, Leerraum und - da die tatsächliche WebUntis-Reihenfolge nicht
|
||||
// dokumentiert und schulabhängig unterschiedlich beobachtet wurde - beide Namensreihenfolgen
|
||||
// werden beim Aufbau von `byName` registriert; hier wird nur normalisiert.
|
||||
private static string NameKey(string value) => value.Trim().ToLowerInvariant();
|
||||
|
||||
private const int FullLessonMinutes = 45;
|
||||
|
||||
// Der Bericht liefert keinen Entschuldigungstext, nur Minutenwerte, ein Bearbeitet-Datum und die
|
||||
// (laut Schule) über Klammerung der ENr codierte Entscheidung des Klassenlehrers - ENr in
|
||||
// Klammern bedeutet unentschuldigt, ohne Klammern abgeschlossen/entschuldigt. Reihenfolge ist
|
||||
// wichtig: "nach Hause entlassen" zählt immer als vorzeitige Entlassung, unabhängig von der
|
||||
// Dauer; darunter zählt jede Fehlzeit unter einer vollen Stunde (45 Min.) immer als Verspätung
|
||||
// oder sonstiger Teilverlust, nie als komplette Abwesenheit - der Text "Verspätung" allein ist
|
||||
// laut Schule nicht zuverlässig genug, deshalb primär über die Minutenschwelle erkannt.
|
||||
private static AttendanceStatus MapStatus(UntisLessonAbsenceDto absence)
|
||||
{
|
||||
if (IsEarlyRelease(absence)) return AttendanceStatus.LeftDuringClass;
|
||||
if (absence.AbsentMinutes < FullLessonMinutes) return AttendanceStatus.Late;
|
||||
if (string.IsNullOrWhiteSpace(absence.HandledOn)) return AttendanceStatus.ExcusePending;
|
||||
if (absence.ExternKey is null) return AttendanceStatus.ExcusePending;
|
||||
return absence.ExternKeyInParentheses ? AttendanceStatus.Unexcused : AttendanceStatus.Excused;
|
||||
}
|
||||
|
||||
private static bool IsEarlyRelease(UntisLessonAbsenceDto absence) =>
|
||||
absence.Reason?.Contains("entlassen", StringComparison.OrdinalIgnoreCase) == true;
|
||||
|
||||
// Nutzt dieselben deutschen Bezeichnungen wie die reguläre Mitarbeitserfassung
|
||||
// (AttendanceDisplay.Label), statt eigene Statustexte zu erfinden.
|
||||
private static string DisplayUntisStatus(UntisLessonAbsenceDto absence) =>
|
||||
string.Join(" · ", new[] { AttendanceDisplay.Label(MapStatus(absence)), absence.Reason }
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) => DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
private static string TimeLabel(int? start, int? end) =>
|
||||
start is null || end is null ? "" : $"{Time(start.Value)}–{Time(end.Value)}";
|
||||
private static string Time(int value) => $"{value / 100:00}:{value % 100:00}";
|
||||
}
|
||||
@@ -77,6 +77,13 @@
|
||||
<CheckBox Content="Benötigt Unterrichtsplanung" IsChecked="{Binding RequiresLessonPlanning}"
|
||||
ToolTip.Tip="Deaktivieren für Gruppen ohne inhaltlichen Verlaufsplan (z.B. Klassenrat, Willkommenskreis) - blendet für diese Gruppe die Dashboard-Erinnerung "Ungeplante Stunden" aus."/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="WebUntis-Unterrichtsnummer (optional)" FontSize="12" Opacity="0.7"/>
|
||||
<NumericUpDown Value="{Binding WebUntisLessonId}" Minimum="1" FormatString="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ToolTip.Tip="lsid aus WebUntis (Unterricht -> Mein Unterricht -> Berichte-Symbol der Zeile). Wird von WebUntis pro Schuljahr neu vergeben und muss deshalb jedes Schuljahr aktualisiert werden."/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
IsEnabled="{Binding IsEditable}"/>
|
||||
<Button Content="↻ Aus WebUntis…" Click="OnImportParticipantsFromWebUntisClick"
|
||||
IsEnabled="{Binding IsEditable}"/>
|
||||
<Button Content="Fehlzeiten abgleichen…" Click="OnCompareWebUntisAbsencesClick"/>
|
||||
<Button Content="Fehlzeiten je Unterricht…" Click="OnCompareWebUntisLessonAbsencesClick"/>
|
||||
<Button Content="{Binding SelectedStudent.WithdrawActionLabel}"
|
||||
Command="{Binding WithdrawStudentCommand}"
|
||||
IsVisible="{Binding SelectedStudent, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
|
||||
@@ -153,16 +153,16 @@ public partial class GroupDetailView : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnCompareWebUntisAbsencesClick(object? sender, RoutedEventArgs e)
|
||||
private async void OnCompareWebUntisLessonAbsencesClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null || DataContext is not GroupDetailViewModel vm || vm.Group is null) return;
|
||||
var dialogVm = new WebUntisAbsenceComparisonViewModel(vm.Group,
|
||||
var dialogVm = new WebUntisLessonAbsenceComparisonViewModel(vm.Group,
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>());
|
||||
await new WebUntisAbsenceComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
|
||||
await new WebUntisLessonAbsenceComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
|
||||
vm.ParticipationTab.RefreshCurrentGrid();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.WebUntisAbsenceComparisonDialog"
|
||||
x:DataType="vm:WebUntisAbsenceComparisonViewModel"
|
||||
Title="Fehlzeiten mit WebUntis abgleichen" Width="850" Height="620"
|
||||
MinWidth="700" MinHeight="450" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="24" RowSpacing="12">
|
||||
<StackPanel Grid.Row="0" Spacing="4">
|
||||
<TextBlock Text="Fehlzeiten mit WebUntis abgleichen" Classes="dialogtitle"/>
|
||||
<TextBlock Text="Nur markierte Zeilen mit einer vorhandenen lokalen Kursstunde werden übernommen."
|
||||
FontSize="12" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
|
||||
<DatePicker SelectedDate="{Binding StartDate}"/>
|
||||
<TextBlock Text="bis" VerticalAlignment="Center"/>
|
||||
<DatePicker SelectedDate="{Binding EndDate}"/>
|
||||
<Button Content="Fehlzeiten laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WebUntisAbsenceRow">
|
||||
<Grid ColumnDefinitions="Auto,1.5*,90,90,1.5*,1.5*" ColumnSpacing="8" Margin="0,3">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding Selected}" IsEnabled="{Binding CanApply}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding StudentName}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding TimeLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding UntisStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="5" Text="{Binding LocalStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
<Grid Grid.Row="3" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Schließen" Click="OnClose"/>
|
||||
<Button Grid.Column="2" Content="Markierte übernehmen" Command="{Binding ApplyCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,53 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.WebUntisLessonAbsenceComparisonDialog"
|
||||
x:DataType="vm:WebUntisLessonAbsenceComparisonViewModel"
|
||||
Title="Fehlzeiten je Unterricht mit WebUntis abgleichen" Width="1000" Height="640"
|
||||
MinWidth="800" MinHeight="450" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto" Margin="24" RowSpacing="8">
|
||||
<StackPanel Grid.Row="0" Spacing="4">
|
||||
<TextBlock Text="Fehlzeiten je Unterricht mit WebUntis abgleichen" Classes="dialogtitle"/>
|
||||
<TextBlock Text="Nur markierte Zeilen mit einer vorhandenen lokalen Kursstunde werden übernommen. Zeilen ohne automatische Zuordnung bitte manuell einem Kursmitglied zuweisen."
|
||||
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
|
||||
<DatePicker SelectedDate="{Binding StartDate}"/>
|
||||
<TextBlock Text="bis" VerticalAlignment="Center"/>
|
||||
<DatePicker SelectedDate="{Binding EndDate}"/>
|
||||
<Button Content="Fehlzeiten laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</StackPanel>
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,1.2*,1.2*,80,80,1.1*,1.1*" ColumnSpacing="8" Margin="4,0">
|
||||
<TextBlock Grid.Column="1" Text="Name (WebUntis)" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="2" Text="Zuordnung" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Datum" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Zeit" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="5" Text="WebUntis-Status" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="6" Text="Lokaler Status" FontSize="11" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="3">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WebUntisLessonAbsenceRow">
|
||||
<Grid ColumnDefinitions="Auto,1.2*,1.2*,80,80,1.1*,1.1*" ColumnSpacing="8" Margin="0,3">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding Selected}" IsEnabled="{Binding CanApply}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding UntisStudentName}" VerticalAlignment="Center" TextWrapping="Wrap"/>
|
||||
<ComboBox Grid.Column="2" ItemsSource="{Binding Candidates}" SelectedItem="{Binding AssignedStudent}"
|
||||
DisplayMemberBinding="{Binding FullName}" PlaceholderText="Schüler wählen…"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding TimeLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="5" Text="{Binding UntisStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="6" Text="{Binding LocalStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
<Grid Grid.Row="4" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Status}" FontSize="12" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Schließen" Click="OnClose"/>
|
||||
<Button Grid.Column="2" Content="Markierte übernehmen" Command="{Binding ApplyCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
+2
-2
@@ -3,8 +3,8 @@ using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class WebUntisAbsenceComparisonDialog : Window
|
||||
public partial class WebUntisLessonAbsenceComparisonDialog : Window
|
||||
{
|
||||
public WebUntisAbsenceComparisonDialog() => InitializeComponent();
|
||||
public WebUntisLessonAbsenceComparisonDialog() => InitializeComponent();
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
Reference in New Issue
Block a user