This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.RegularExpressions;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
/// <summary>Eine Zeile bleibt auch ohne automatische Zuordnung sichtbar - <see cref="AssignedStudent"/>
|
||||
/// kann manuell per Auswahlliste gesetzt werden, gleiches Muster wie
|
||||
/// <see cref="WebUntisLessonAbsenceComparisonViewModel"/> und
|
||||
/// <see cref="LehrerApp.Desktop.ViewModels.Students.WebUntisDocumentationComparisonViewModel"/>.</summary>
|
||||
public partial class WebUntisHomeworkRow : ObservableObject
|
||||
{
|
||||
public required string ClassName { get; init; }
|
||||
public required DateOnly Date { get; init; }
|
||||
public required string UntisStudentName { get; init; }
|
||||
public string? SubjectLabel { get; init; }
|
||||
public string? Text { get; init; }
|
||||
public required IReadOnlyList<Student> Candidates { get; init; }
|
||||
internal Guid? MatchedSubjectId { get; init; }
|
||||
internal Action<WebUntisHomeworkRow>? OnAssignmentChanged { 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>Abgleich negativer Klassenbucheinträge zum Stichwort "Hausaufgabe" (eigene, siehe
|
||||
/// <see cref="WebUntisIntegrationService.GetOwnClassRegisterEventsAsync"/>) gegen
|
||||
/// <see cref="ParticipationEntry.Homework"/> - Dashboard-weit statt pro Lerngruppe, aus demselben
|
||||
/// Grund wie beim Dokumentations-Abgleich (der WebUntis-"-alle-"-Bericht ist klassenübergreifend,
|
||||
/// siehe TODO.md). Nutzer-Feedback: WebUntis liefert für Klassenbucheinträge weder eine externe
|
||||
/// Schülerkennung noch eine feste Fach-/Lerngruppenzuordnung, aber der Lehrkraft-eigene Text enthält
|
||||
/// praktisch immer das Untis-Fachkürzel - darüber wird die passende Lerngruppe (SubjectId + aktive
|
||||
/// Mitgliedschaft am Eintragsdatum) aufgelöst, mehrdeutige Treffer bleiben unaufgelöst statt zu raten.
|
||||
/// Ein Eintrag setzt lokal ausschließlich <see cref="HomeworkStatus.MissingOpen"/> vor, und auch nur,
|
||||
/// wenn dort noch gar kein Status hinterlegt ist - bereits vorhandene, feinere Erfassungen ("Teilweise
|
||||
/// angefertigt", "nachgereicht" usw.) werden nie automatisch überschrieben, sondern nur zum Vergleich
|
||||
/// danebengestellt (Nutzerwunsch).</summary>
|
||||
public partial class WebUntisHomeworkComparisonViewModel : ObservableObject
|
||||
{
|
||||
private readonly WebUntisIntegrationService _untis;
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly IGroupMembershipRepository _memberships;
|
||||
private readonly IParticipationSessionRepository _sessions;
|
||||
private readonly IParticipationRepository _participation;
|
||||
private readonly SchoolYearService _schoolYears;
|
||||
|
||||
public ObservableCollection<WebUntisHomeworkRow> Rows { get; } = [];
|
||||
[ObservableProperty] private DateTimeOffset? _startDate = DateTimeOffset.Now.AddDays(-7);
|
||||
[ObservableProperty] private DateTimeOffset? _endDate = DateTimeOffset.Now;
|
||||
[ObservableProperty] private string _status = "Zeitraum wählen und Klassenbucheinträge laden.";
|
||||
[ObservableProperty] private bool _busy;
|
||||
|
||||
public WebUntisHomeworkComparisonViewModel(WebUntisIntegrationService untis, IStudentRepository students,
|
||||
IGroupRepository groups, ISubjectRepository subjects, IGroupMembershipRepository memberships,
|
||||
IParticipationSessionRepository sessions, IParticipationRepository participation,
|
||||
SchoolYearService schoolYears)
|
||||
{
|
||||
_untis = untis; _students = students; _groups = groups; _subjects = subjects;
|
||||
_memberships = memberships; _sessions = sessions; _participation = participation;
|
||||
_schoolYears = schoolYears;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Load()
|
||||
{
|
||||
var start = DateOnly.FromDateTime((StartDate ?? DateTimeOffset.Now).LocalDateTime);
|
||||
var end = DateOnly.FromDateTime((EndDate ?? DateTimeOffset.Now).LocalDateTime);
|
||||
if (end < start) { Status = "Das Enddatum darf nicht vor dem Startdatum liegen."; return; }
|
||||
Busy = true; Rows.Clear();
|
||||
try
|
||||
{
|
||||
var ownStudents = _students.GetAll();
|
||||
var globalIndex = BuildNameIndex(ownStudents);
|
||||
// Wie beim Dokumentations-Abgleich zusätzlich pro Klasse indiziert, um gleiche Namen in
|
||||
// verschiedenen Klassen unterscheiden zu können.
|
||||
var classIndexes = _groups.GetAll()
|
||||
.Where(g => g.Type == GroupType.Class)
|
||||
.GroupBy(g => g.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => BuildNameIndex(g.SelectMany(x => _students.GetByGroup(x.Id)).Distinct().ToList()),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var shortNameIndex = _subjects.GetAll()
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s.ShortName))
|
||||
.GroupBy(s => s.ShortName.Trim().ToUpperInvariant())
|
||||
.Where(g => g.Count() == 1) // mehrdeutiges Kürzel lieber nicht zuordnen als raten
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
var entries = await _untis.GetOwnClassRegisterEventsAsync(start, end);
|
||||
var ordered = entries
|
||||
.Where(IsMissingHomeworkEntry)
|
||||
.Select(e => (Entry: e, Date: TryDate(e.Date, out var d) ? d : (DateOnly?)null))
|
||||
.Where(x => x.Date is not null)
|
||||
.OrderBy(x => x.Date).ThenBy(x => x.Entry.StudentName);
|
||||
|
||||
void ResolveLocalMatch(WebUntisHomeworkRow row)
|
||||
{
|
||||
if (row.AssignedStudent is not { } student)
|
||||
{
|
||||
row.SessionId = null; row.LocalStatus = "ohne Zuordnung"; row.Selected = false;
|
||||
return;
|
||||
}
|
||||
var group = ResolveGroup(student, row.MatchedSubjectId, row.Date);
|
||||
var session = group is null ? null
|
||||
: _sessions.GetByGroup(group.Id).FirstOrDefault(s => s.Date == row.Date);
|
||||
var entry = session is null ? null : _participation.GetBySessionAndStudent(session.Id, student.Id);
|
||||
var current = entry is null ? null : HomeworkDisplay.Effective(entry);
|
||||
row.SessionId = session?.Id;
|
||||
row.LocalStatus = session is null
|
||||
? row.MatchedSubjectId is null ? "Fachkürzel nicht erkannt"
|
||||
: group is null ? "kein passender Kurs gefunden" : "keine lokale Stunde"
|
||||
: HomeworkDisplay.Label(current);
|
||||
// Nur vorbelegen, wenn lokal noch überhaupt nichts erfasst ist - jeder vorhandene
|
||||
// Status (auch ein bereits gesetztes "fehlt") bleibt unangetastet, siehe Klassenkommentar.
|
||||
row.Selected = session is not null && current is null;
|
||||
}
|
||||
|
||||
foreach (var (entry, date) in ordered)
|
||||
{
|
||||
var nameKey = NameKey(entry.StudentName);
|
||||
var match = (classIndexes.TryGetValue(entry.ClassName, out var classIndex)
|
||||
? classIndex.GetValueOrDefault(nameKey)
|
||||
: null)
|
||||
?? globalIndex.GetValueOrDefault(nameKey);
|
||||
var subject = MatchSubject(entry.Text, shortNameIndex) ?? MatchSubject(entry.CategoryName, shortNameIndex);
|
||||
|
||||
var row = new WebUntisHomeworkRow
|
||||
{
|
||||
ClassName = entry.ClassName, Date = date!.Value, UntisStudentName = entry.StudentName,
|
||||
SubjectLabel = subject?.ShortName, Text = entry.Text, Candidates = ownStudents,
|
||||
MatchedSubjectId = subject?.Id, OnAssignmentChanged = ResolveLocalMatch,
|
||||
};
|
||||
Rows.Add(row);
|
||||
row.AssignedStudent = match; // löst OnAssignedStudentChanged aus und setzt SessionId/LocalStatus/Selected
|
||||
}
|
||||
|
||||
var unresolvedStudent = Rows.Count(x => x.AssignedStudent is null);
|
||||
var unresolvedSubject = Rows.Count(x => x.MatchedSubjectId is null);
|
||||
Status = $"{Rows.Count} Einträge \"fehlende Hausaufgabe\" erhalten" +
|
||||
(unresolvedStudent > 0 ? $", {unresolvedStudent} bitte manuell zuordnen" : "") +
|
||||
(unresolvedSubject > 0 ? $", bei {unresolvedSubject} kein Fachkürzel im Text erkannt" : "") +
|
||||
$". {Rows.Count(x => x.CanApply)} einer lokalen Stunde 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.Homework = HomeworkStatus.MissingOpen;
|
||||
entry.HomeworkMissing = true;
|
||||
entry.UpdatedAt = DateTime.UtcNow;
|
||||
_participation.Save(entry);
|
||||
row.LocalStatus = HomeworkDisplay.Label(HomeworkStatus.MissingOpen);
|
||||
row.Selected = false;
|
||||
}
|
||||
Status = $"{selected.Count} Hausaufgaben-Status übernommen.";
|
||||
}
|
||||
|
||||
/// Nur Lerngruppen (nicht die Klasse selbst), bei denen der/die Schüler*in am Eintragsdatum aktiv
|
||||
/// Mitglied ist und deren Fach zum erkannten Kürzel passt - mehr als ein Treffer bleibt bewusst
|
||||
/// unaufgelöst statt irgendeinen davon zu wählen.
|
||||
private LearningGroup? ResolveGroup(Student student, Guid? subjectId, DateOnly date)
|
||||
{
|
||||
if (subjectId is null) return null;
|
||||
var schoolYear = _schoolYears.CurrentSchoolYear(date);
|
||||
var candidates = _memberships.GetByStudent(student.Id)
|
||||
.Where(m => GroupMembershipService.IsActiveOn(m, date))
|
||||
.Select(m => _groups.GetById(m.GroupId))
|
||||
.Where(g => g is not null && g.SubjectId == subjectId && g.SchoolYear == schoolYear)
|
||||
.Cast<LearningGroup>()
|
||||
.ToList();
|
||||
return candidates.Count == 1 ? candidates[0] : null;
|
||||
}
|
||||
|
||||
// Gleiche Heuristik wie ClassTeacherDetailsViewModel.ContainsHomework, zusätzlich auf negative
|
||||
// Einträge eingeschränkt (eine positive "Hausaufgabe"-Kategorie wäre kein Fehlen-Signal).
|
||||
private static bool IsMissingHomeworkEntry(UntisClassRegisterEventDto entry) =>
|
||||
string.Equals(entry.CategoryGroup, "Negativ", StringComparison.OrdinalIgnoreCase) &&
|
||||
(entry.CategoryName?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true ||
|
||||
entry.Text?.Contains("Hausauf", StringComparison.OrdinalIgnoreCase) == true);
|
||||
|
||||
private static Subject? MatchSubject(string? text, IReadOnlyDictionary<string, Subject> shortNameIndex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
foreach (var token in Regex.Split(text, @"[^\p{L}\p{Nd}]+"))
|
||||
if (token.Length > 0 && shortNameIndex.TryGetValue(token.ToUpperInvariant(), out var subject))
|
||||
return subject;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NameKey(string value) => value.Trim().ToLowerInvariant();
|
||||
|
||||
// Wie bei den übrigen WebUntis-Abgleichen: beide Namensreihenfolgen registriert, aber nur falls
|
||||
// innerhalb der Kandidaten eindeutig.
|
||||
private static Dictionary<string, Student> BuildNameIndex(IReadOnlyList<Student> candidates) =>
|
||||
candidates
|
||||
.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);
|
||||
|
||||
private static bool TryDate(int value, out DateOnly date) =>
|
||||
DateOnly.TryParseExact(value.ToString(), "yyyyMMdd", out date);
|
||||
}
|
||||
@@ -150,8 +150,11 @@ public partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
private TimetableViewModel GetTimetable()
|
||||
{
|
||||
// WeekOffset bewusst nicht zurücksetzen: TimetableViewModel ist ein DI-Singleton, die
|
||||
// zuletzt angezeigte Woche bleibt also automatisch über Tab-Wechsel hinweg erhalten
|
||||
// (Nutzer-Feedback: beim Planen der nächsten Woche nervt es, nach jedem kurzen Abstecher
|
||||
// in einen anderen Bereich wieder manuell dorthin zurückblättern zu müssen).
|
||||
var timetable = _services.GetRequiredService<TimetableViewModel>();
|
||||
timetable.WeekOffset = 0;
|
||||
timetable.Load();
|
||||
timetable.ActiveTabIndex = 0;
|
||||
return timetable;
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<WrapPanel Grid.Row="1" Orientation="Horizontal" ItemSpacing="8" LineSpacing="8" Margin="0,10,0,0">
|
||||
<Button Content="Klassenbuch abgleichen…" Click="OnCompareWebUntisDocumentationClick"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Content="Fehlende Hausaufgaben abgleichen…" Click="OnCompareWebUntisHomeworkClick"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Content="Bereiche anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
|
||||
VerticalAlignment="Center"/>
|
||||
</WrapPanel>
|
||||
|
||||
@@ -5,7 +5,9 @@ using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using LehrerApp.Desktop.ViewModels.Students;
|
||||
using LehrerApp.Desktop.Views.Groups;
|
||||
using LehrerApp.Desktop.Views.Students;
|
||||
using LehrerApp.Desktop.Views.Workload;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -42,4 +44,20 @@ public partial class DashboardView : UserControl
|
||||
App.Services.GetRequiredService<SchoolYearService>());
|
||||
await new WebUntisDocumentationComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
|
||||
}
|
||||
|
||||
private async void OnCompareWebUntisHomeworkClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return;
|
||||
var dialogVm = new WebUntisHomeworkComparisonViewModel(
|
||||
App.Services.GetRequiredService<WebUntisIntegrationService>(),
|
||||
App.Services.GetRequiredService<IStudentRepository>(),
|
||||
App.Services.GetRequiredService<IGroupRepository>(),
|
||||
App.Services.GetRequiredService<ISubjectRepository>(),
|
||||
App.Services.GetRequiredService<IGroupMembershipRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationSessionRepository>(),
|
||||
App.Services.GetRequiredService<IParticipationRepository>(),
|
||||
App.Services.GetRequiredService<SchoolYearService>());
|
||||
await new WebUntisHomeworkComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
xmlns:conv="clr-namespace:LehrerApp.Desktop.Converters"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.WebUntisHomeworkComparisonDialog"
|
||||
x:DataType="vm:WebUntisHomeworkComparisonViewModel"
|
||||
Title="Fehlende Hausaufgaben mit WebUntis abgleichen" Width="1100" Height="680"
|
||||
MinWidth="900" MinHeight="480" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto" Margin="24" RowSpacing="8">
|
||||
<StackPanel Grid.Row="0" Spacing="4">
|
||||
<TextBlock Text="Fehlende Hausaufgaben mit WebUntis abgleichen" Classes="dialogtitle"/>
|
||||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.65"
|
||||
Text="Nur eigene, negative Klassenbucheinträge zum Stichwort „Hausaufgabe“. Fach wird über das Untis-Fachkürzel im Text erkannt. Zeilen mit bereits vorhandenem lokalem Hausaufgaben-Status (z.B. „Teilweise“, „nachgereicht“) werden nie überschrieben, nur zum Vergleich angezeigt."/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
|
||||
<CalendarDatePicker SelectedDate="{Binding StartDate, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}"/>
|
||||
<TextBlock Text="bis" VerticalAlignment="Center"/>
|
||||
<CalendarDatePicker SelectedDate="{Binding EndDate, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}"/>
|
||||
<Button Content="Klassenbucheinträge laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="Auto,70,60,55,1.1*,1.1*,1.4*,1.2*" ColumnSpacing="8" Margin="4,0">
|
||||
<TextBlock Grid.Column="1" Text="Datum" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="2" Text="Klasse" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Fach" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Name (WebUntis)" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="5" Text="Zuordnung" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="6" Text="Text" FontSize="11" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="7" Text="Lokaler Hausaufgaben-Status" FontSize="11" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<ScrollViewer Grid.Row="3">
|
||||
<ItemsControl ItemsSource="{Binding Rows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WebUntisHomeworkRow">
|
||||
<Grid ColumnDefinitions="Auto,70,60,55,1.1*,1.1*,1.4*,1.2*" ColumnSpacing="8" Margin="0,3">
|
||||
<CheckBox Grid.Column="0" IsChecked="{Binding Selected}" IsEnabled="{Binding CanApply}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding ClassName}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding SubjectLabel}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding UntisStudentName}" VerticalAlignment="Center" TextWrapping="Wrap"/>
|
||||
<ComboBox Grid.Column="5" ItemsSource="{Binding Candidates}" SelectedItem="{Binding AssignedStudent}"
|
||||
DisplayMemberBinding="{Binding FullName}" PlaceholderText="Schüler*in wählen…"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
<TextBlock Grid.Column="6" Text="{Binding Text}" TextWrapping="Wrap" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="7" 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>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class WebUntisHomeworkComparisonDialog : Window
|
||||
{
|
||||
public WebUntisHomeworkComparisonDialog() => InitializeComponent();
|
||||
|
||||
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -982,7 +982,9 @@ angezeigten Woche geklebt, nicht nur in der einen Woche, zu der es gehört). Fü
|
||||
berechnen `HolidayBadgeFor`/`IsLastBeforeExamFor` die Badges deshalb jetzt je Zelle am dort
|
||||
gezeigten Datum statt an "heute" — das Bearbeiten-Raster (zeigt ohnehin nur das wiederkehrende
|
||||
Muster ohne Datum) behält die alte, "heute"-verankerte Berechnung. Beim erneuten Navigieren in den
|
||||
Stundenplan (Sidebar-Klick) springt die Ansicht wieder auf die laufende Woche zurück.
|
||||
Stundenplan (Sidebar-Klick) sprang die Ansicht ursprünglich wieder auf die laufende Woche zurück —
|
||||
siehe "Nachtrag zu 4.3, Bugfix Wochennavigation (September 2026)" am Ende dieses Abschnitts, wo
|
||||
dieses Verhalten auf Nutzer-Feedback hin wieder entfernt wurde.
|
||||
|
||||
**Nachtrag zu 4.3, fünfte Iteration (Aufsichten + Vertretung):** Nutzer-Feedback: zwischen manchen
|
||||
Stunden ist auch Pausenaufsicht zu erledigen, und gelegentlich kommen Sonderfälle vor — eine
|
||||
@@ -1930,6 +1932,69 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
|
||||
Vorgänge je Klassen-Roster-Namen über denselben Namensabgleich wie die übrigen `Build*`-Methoden
|
||||
dieser Klasse auf (`IVorgangRepository.GetByStudent`).
|
||||
|
||||
**Nachtrag zu 4.3, Bugfix Wochennavigation (September 2026):** Nutzer-Feedback: beim Planen der
|
||||
nächsten Woche musste nach jedem kurzen Abstecher in einen anderen Bereich der Sidebar wieder
|
||||
manuell zur zuvor betrachteten Woche zurückgeblättert werden ("man muss nicht nur einmal
|
||||
hinspringen, sondern jedes Mal wieder neu"). Ursache war das in der vierten Iteration dokumentierte
|
||||
Verhalten, dass `MainWindowViewModel.GetTimetable()` bei jedem erneuten Aufruf der Seitenleisten-
|
||||
Navigation `TimetableViewModel.WeekOffset` explizit auf 0 zurücksetzte — obwohl `TimetableViewModel`
|
||||
als DI-Singleton ohnehin über die gesamte Sitzung hinweg bestehen bleibt und `WeekOffset` daher ohne
|
||||
diese Zeile ganz von selbst erhalten bliebe. Der Reset wurde entfernt; ein separater Zustands-
|
||||
Singleton/eine statische Variable dafür war nicht nötig. Das erklärt vermutlich auch den zweiten
|
||||
gemeldeten Effekt ("der Zurück-Button springt manchmal zwei Wochen zurück"): wer z.B. in Woche +1
|
||||
plante, kurz wegnavigierte und danach einmal "‹" klickte, landete wegen des Resets in Woche -1 statt
|
||||
der erwarteten Woche 0 — gefühlt ein Sprung um zwei Wochen, obwohl `PreviousWeekCommand`/
|
||||
`NextWeekCommand` selbst korrekt je Klick nur um eine Woche verschieben (siehe
|
||||
`NextWeek_BadgeGehoertZurAngezeigtenWoche_NichtZuHeute`, das zwei aufeinanderfolgende Klicks bewusst
|
||||
als zwei Wochen erwartet). Der "Diese Woche"-Button existierte bereits seit der vierten Iteration
|
||||
(`CurrentWeekCommand`, nur sichtbar wenn `!IsCurrentWeek`).
|
||||
|
||||
**Nachtrag zu 4.3, Abgleich fehlender Hausaufgaben (September 2026):** Nutzer-Feedback: negative
|
||||
Klassenbucheinträge zur Kategorie "fehlende HA" sind ein brauchbares Signal, ob ein/e Schüler*in die
|
||||
Hausaufgabe nicht gemacht hat, und sollen als Vorschlag mit der eigenen Erfassung
|
||||
(`ParticipationEntry.Homework`) abgeglichen werden können — ausdrücklich nicht die erteilte
|
||||
Hausaufgabe selbst (`Lesson.Homework`), das bleibt ein offener, separater Punkt.
|
||||
- Neuer Dashboard-weiter Dialog `WebUntisHomeworkComparisonViewModel`/`WebUntisHomeworkComparisonDialog`
|
||||
([LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs](LehrerApp.Desktop/ViewModels/Groups/WebUntisHomeworkComparisonViewModel.cs)),
|
||||
Button "Fehlende Hausaufgaben abgleichen…" neben dem bereits vorhandenen "Klassenbuch
|
||||
abgleichen…" auf dem Dashboard. Baut auf demselben Muster wie
|
||||
`WebUntisDocumentationComparisonViewModel` (Dashboard-weit, da der WebUntis-"-alle-"-Bericht
|
||||
klassenübergreifend ist) und `WebUntisLessonAbsenceComparisonViewModel` (schreibt wie dort über
|
||||
`IParticipationRepository.GetBySessionAndStudent`/`Save`).
|
||||
- **Filter:** `CategoryGroup == "Negativ"` und `CategoryName`/`Text` enthält "Hausauf" — dieselbe
|
||||
Substring-Heuristik wie `ClassTeacherDetailsViewModel.ContainsHomework`, nur zusätzlich auf
|
||||
negative Einträge eingeschränkt.
|
||||
- **Fach-Erkennung:** Der Klassenbuch-Bericht liefert kein zuverlässiges Fach-Feld, aber laut Nutzer
|
||||
enthält der mitgeschickte Text praktisch immer das Untis-Fachkürzel. Deshalb wird `Text` (Fallback
|
||||
`CategoryName`) gegen `Subject.ShortName` alle Lerngruppen-Fächer abgeglichen (Wortgrenzen-Split,
|
||||
nicht `Contains`, um kurze Kürzel nicht versehentlich in anderen Wörtern zu treffen); mehrdeutige
|
||||
Kürzel (zwei Fächer mit demselben `ShortName`) werden bewusst nicht zugeordnet statt zu raten.
|
||||
- **Lerngruppe/Stunde auflösen:** aus dem per Namensabgleich gefundenen `Student` werden über
|
||||
`IGroupMembershipRepository.GetByStudent` alle am Eintragsdatum aktiven Mitgliedschaften
|
||||
(`GroupMembershipService.IsActiveOn`) genommen und auf das erkannte Fach + Schuljahr des
|
||||
Eintragsdatums eingeschränkt; bleibt mehr als eine Lerngruppe übrig, bleibt die Zeile unaufgelöst
|
||||
statt irgendeine zu wählen (gleiches Vorsichtsprinzip wie beim Namensabgleich). Die
|
||||
`ParticipationSession` der Lerngruppe am Eintragsdatum liefert dann die Stunde für
|
||||
`IParticipationRepository`.
|
||||
- **Kein Überschreiben (Kernanforderung):** eine Zeile wird nur vorausgewählt, wenn
|
||||
`HomeworkDisplay.Effective(entry)` noch `null` ist, also lokal überhaupt noch nichts erfasst wurde
|
||||
— jeder bereits vorhandene Status (auch ein bereits gesetztes "fehlt", nicht nur "Teilweise"/
|
||||
"nachgereicht") bleibt unangetastet und wird nur zum Vergleich daneben angezeigt; die Checkbox
|
||||
bleibt trotzdem bedienbar, falls doch bewusst überschrieben werden soll. Übernommen wird immer nur
|
||||
`HomeworkStatus.MissingOpen` ("Nachreichen offen") — nie automatisch `MissingOverdue`, die weitere
|
||||
Nachverfolgung bleibt laut Nutzer App-intern.
|
||||
- **Bewusst kein Caching:** `GetOwnClassRegisterEventsAsync` wird wie beim bereits bestehenden
|
||||
`WebUntisDocumentationComparisonViewModel` direkt (ungecacht) aufgerufen statt über
|
||||
`UntisReportCacheService`, dessen Cache-Tabellen aktuell klassen-/`GetForeignClassRegisterEventsAsync`-
|
||||
spezifisch aufgebaut sind (ein Schema-Umbau für die eigenen, klassenübergreifenden Einträge wäre ein
|
||||
eigenständiger, deutlich größerer Umbau gewesen, siehe Nutzer-Rückfrage). Beide "eigene Einträge"-
|
||||
Dialoge lösen pro Öffnen einen eigenen Live-Abruf aus; falls das in der Praxis stört, wäre eine
|
||||
gemeinsame Cache-Erweiterung ein sinnvoller separater Nachtrag.
|
||||
- Aus Konsistenz mit den beiden bestehenden, strukturell identischen Abgleichs-ViewModels bewusst
|
||||
ungetestet gelassen: `WebUntisIntegrationService` ist `sealed` und nicht auf ein Interface
|
||||
gemockt, `WebUntisLessonAbsenceComparisonViewModel`/`WebUntisDocumentationComparisonViewModel`
|
||||
haben aus demselben Grund ebenfalls keine Tests.
|
||||
|
||||
### 4.4 Wochen-/Tagesansicht
|
||||
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
|
||||
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.
|
||||
|
||||
Reference in New Issue
Block a user