Dashboard-Erinnerung für fehlende Unterrichtszeit-Erfassung
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
Neue Karte "Unterrichtszeit nacherfassen" schaut 14 Tage zurück und listet Schultage mit Unterricht laut Stundenplan ohne zugehörigen "Unterricht"-Zeiteintrag (Ferien/Feiertage/komplett ausgefallene Tage ausgenommen). Für heute erscheint der Eintrag erst 30 Minuten nach dem laut Stundenplan letzten Unterrichtsende. Klick auf "Erfassen" öffnet den bestehenden Nacherfassen-Dialog vorbelegt mit Datum, Kategorie "Unterricht" und berechnetem Zeitfenster. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ public sealed class DashboardSettingsService
|
||||
[
|
||||
"today", "tasks", "calendar", "excuses", "upcoming",
|
||||
"corrections", "unplanned", "alerts", "attendance", "support", "groups", "examload",
|
||||
"missingteachingtime",
|
||||
];
|
||||
|
||||
private readonly string _configPath;
|
||||
|
||||
@@ -60,7 +60,8 @@ public sealed class DashboardViewModelTests
|
||||
DashboardSettingsService? dashboardSettings = null, FakeSchoolHolidays? schoolHolidays = null,
|
||||
FakeLessons? lessons = null, FakeSubstitutionEntries? substitutions = null,
|
||||
FakeSessions? sessions = null, FakeEntries? entries = null,
|
||||
FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null)
|
||||
FakeAnnualPlanEvents? annualPlanEvents = null, List<LearningGroup>? allGroups = null,
|
||||
FakeTimeEntries? timeEntries = null)
|
||||
{
|
||||
lessons ??= new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
@@ -73,7 +74,7 @@ public sealed class DashboardViewModelTests
|
||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings(),
|
||||
schoolHolidays ?? new FakeSchoolHolidays(), new PublicHolidayService(), NewCalendarSettings(),
|
||||
substitutions ?? new FakeSubstitutionEntries(), annualPlanEvents);
|
||||
substitutions ?? new FakeSubstitutionEntries(), timeEntries ?? new FakeTimeEntries(), annualPlanEvents);
|
||||
}
|
||||
|
||||
/// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
|
||||
@@ -137,6 +138,120 @@ public sealed class DashboardViewModelTests
|
||||
Assert.Empty(vm.ExamWeekLoads);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_VergangenerUnterrichtstagOhneErfassung_WirdGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var pastDay = today.AddDays(-1);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
||||
|
||||
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
||||
Assert.True(vm.MissingTeachingTimeCard.EffectiveIsVisible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_BereitsErfassterTag_WirdNichtGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var pastDay = today.AddDays(-1);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
||||
var timeEntries = new FakeTimeEntries();
|
||||
timeEntries.Add(new TimeEntry { Date = pastDay, Category = "Unterricht", DurationMinutes = 45 });
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
slots: slots, periodSchedule: periodSchedule, timeEntries: timeEntries);
|
||||
|
||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_KomplettAusgefallenerTag_WirdNichtGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var pastDay = today.AddDays(-1);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
substitutions.Add(new SubstitutionEntry { Date = pastDay, Kind = SubstitutionKind.Cancelled, PeriodNumber = 1 });
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
slots: slots, periodSchedule: periodSchedule, substitutions: substitutions);
|
||||
|
||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_SchulferienTagWirdNichtGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var pastDay = today.AddDays(-1);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = pastDay.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = new TimeOnly(8, 0), End = new TimeOnly(8, 45) }]);
|
||||
var schoolHolidays = new FakeSchoolHolidays();
|
||||
schoolHolidays.Add(new SchoolHoliday { Name = "Ferien", StartDate = pastDay, EndDate = pastDay });
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
slots: slots, periodSchedule: periodSchedule, schoolHolidays: schoolHolidays);
|
||||
|
||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == pastDay);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_HeuteVorAblaufDerWartezeit_WirdNichtGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
// Unterrichtsende liegt garantiert noch keine 30 Minuten zurück.
|
||||
var futureEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(2);
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = futureEnd.AddHours(-1), End = futureEnd }]);
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
||||
|
||||
Assert.DoesNotContain(vm.MissingTeachingTimeEntries, i => i.Date == today);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MissingTeachingTime_HeuteNachAblaufDerWartezeit_WirdGemeldet()
|
||||
{
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var slots = new FakeTimetableSlots();
|
||||
slots.Add(new TimetableSlot { GroupId = group.Id, Weekday = today.DayOfWeek, PeriodNumber = 1 });
|
||||
var periodSchedule = NewPeriodSchedule();
|
||||
// Unterrichtsende liegt garantiert mehr als 30 Minuten zurück.
|
||||
var pastEnd = TimeOnly.FromDateTime(DateTime.Now).AddHours(-2);
|
||||
periodSchedule.SetPeriods([new PeriodTimeEntry
|
||||
{ PeriodNumber = 1, Start = pastEnd.AddHours(-1), End = pastEnd }]);
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, slots: slots, periodSchedule: periodSchedule);
|
||||
|
||||
Assert.Contains(vm.MissingTeachingTimeEntries, i => i.Date == today);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TodaysLessons_LoestRaumUeberPassendenStundenplanSlotAuf()
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Styling;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Desktop.Services;
|
||||
@@ -153,6 +154,7 @@ public class App : Application
|
||||
dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 3); // Tab "Mitarbeit"
|
||||
dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren"
|
||||
dash.OnNavigateToUnplannedLesson = id => main.NavigateToGroupDetail(id, 6); // Tab "Planung"
|
||||
dash.OnAddMissingTeachingTime = item => ShowMissingTeachingTimeDialog(item, dash);
|
||||
|
||||
var examsOverview = Services.GetRequiredService<ViewModels.Exams.ExamsOverviewViewModel>();
|
||||
examsOverview.OnNavigateToGroups = () => main.NavigateToCommand.Execute(NavItem.Groups);
|
||||
@@ -208,6 +210,29 @@ public class App : Application
|
||||
Services.GetRequiredService<WorkTaskListViewModel>().Load();
|
||||
}
|
||||
|
||||
private static async Task ShowMissingTeachingTimeDialog(MissingTeachingTimeItem item, DashboardViewModel dashboard)
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime
|
||||
{ MainWindow: { } owner }) return;
|
||||
|
||||
var tasks = Services.GetRequiredService<IWorkTaskRepository>()
|
||||
.GetAll().Where(t => t.Status != WorkTaskStatus.Done).ToList();
|
||||
var vm = new AddTimeEntryDialogViewModel(tasks)
|
||||
{
|
||||
DateText = item.Date.ToString("dd.MM.yyyy"),
|
||||
SelectedCategory = "Unterricht",
|
||||
StartTimeText = item.WindowStart.ToString("HH:mm"),
|
||||
EndTimeText = item.WindowEnd.ToString("HH:mm"),
|
||||
};
|
||||
var dialog = new AddTimeEntryDialog { DataContext = vm };
|
||||
await dialog.ShowDialog<bool>(owner);
|
||||
if (vm.Result is null) return;
|
||||
|
||||
Services.GetRequiredService<ITimeEntryRepository>().Save(vm.Result);
|
||||
dashboard.RefreshCommand.Execute(null);
|
||||
Services.GetRequiredService<TimeTrackingViewModel>().Load();
|
||||
}
|
||||
|
||||
private static async Task ShowQuickGroupDocumentationDialog(DashboardViewModel dashboard)
|
||||
{
|
||||
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime
|
||||
|
||||
@@ -35,6 +35,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly PublicHolidayService _publicHolidays;
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
private readonly ISubstitutionEntryRepository _substitutions;
|
||||
private readonly ITimeEntryRepository _timeEntries;
|
||||
private readonly IAnnualPlanEventRepository? _annualPlanEvents;
|
||||
private readonly SchoolWeatherService? _schoolWeather;
|
||||
|
||||
@@ -50,6 +51,18 @@ public partial class DashboardViewModel : ObservableObject
|
||||
/// hat rein rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
|
||||
/// Stichprobe. Dieselbe Konstante wie GroupOverviewViewModel.AttendanceMinSampleSize.
|
||||
private const int AttendanceMinSampleSize = 8;
|
||||
/// Nutzer-Feedback: "ich vergesse es, und fasse die App auch außerhalb der Schule manchmal
|
||||
/// nicht mehr an" — die Erinnerung an fehlende Unterrichtszeit-Erfassung schaut deshalb nicht
|
||||
/// nur auf heute zurück, sondern auf ein paar Tage, damit ein vergessener Tag nicht verloren
|
||||
/// geht, sobald der nächste Schultag anbricht.
|
||||
private const int MissingTeachingTimeLookbackDays = 14;
|
||||
/// Für den heutigen Tag soll die Erinnerung nicht schon mitten im Unterricht auftauchen —
|
||||
/// erst diese Zeitspanne nach dem laut Stundenplan letzten Unterrichtsende.
|
||||
private const int MissingTeachingTimeTodayDelayMinutes = 30;
|
||||
/// Gleiche Puffer-Idee wie TimeTrackingViewModel.ComputeTodaysTeachingWindow (bewusst hier
|
||||
/// noch einmal definiert statt geteilt — der Vorschlag ist nur zwei Zeilen Rechnung).
|
||||
private const int TeachingWindowBufferBeforeMinutes = 15;
|
||||
private const int TeachingWindowBufferAfterMinutes = 10;
|
||||
/// Vorausschau für die Klausurwochen-Karte (Nutzer-Feedback): weit genug, um eine sich
|
||||
/// anbahnende Häufung noch rechtzeitig vor dem Anlegen weiterer Klausuren zu zeigen, aber
|
||||
/// keine Vorschau auf das ganze Schuljahr.
|
||||
@@ -77,6 +90,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
||||
public ObservableCollection<ExamWeekLoadItem> ExamWeekLoads { get; } = [];
|
||||
public ObservableCollection<MissingTeachingTimeItem> MissingTeachingTimeEntries { get; } = [];
|
||||
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
|
||||
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
||||
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
|
||||
@@ -101,6 +115,10 @@ public partial class DashboardViewModel : ObservableObject
|
||||
// Sprungziel für eine ungeplante Stunde — führt direkt in den Verlaufsplan-Tab der Gruppe
|
||||
// (nicht den Standard-Tab von OnNavigateToGroup), damit das Thema gleich ergänzt werden kann.
|
||||
public Action<Guid>? OnNavigateToUnplannedLesson { get; set; }
|
||||
// Öffnet den Nacherfassen-Dialog für einen Tag mit fehlender Unterrichtszeit-Erfassung
|
||||
// (Nutzer-Feedback), vorbelegt mit Datum, Kategorie "Unterricht" und dem laut Stundenplan
|
||||
// berechneten Zeitfenster — echtes Speichern bleibt eine bewusste Bestätigung im Dialog.
|
||||
public Func<MissingTeachingTimeItem, Task>? OnAddMissingTeachingTime { get; set; }
|
||||
|
||||
public DashboardCardOption TodayCard => Card("today");
|
||||
public DashboardCardOption TasksCard => Card("tasks");
|
||||
@@ -112,6 +130,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public DashboardCardOption AlertsCard => Card("alerts");
|
||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
||||
public DashboardCardOption ExamLoadCard => Card("examload");
|
||||
public DashboardCardOption MissingTeachingTimeCard => Card("missingteachingtime");
|
||||
public DashboardCardOption SupportCard => Card("support");
|
||||
public DashboardCardOption GroupsCard => Card("groups");
|
||||
public int TodayLessonCount => TodaysLessons.Count;
|
||||
@@ -133,7 +152,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy,
|
||||
DashboardSettingsService dashboardSettings, ISchoolHolidayRepository schoolHolidays,
|
||||
PublicHolidayService publicHolidays, SchoolCalendarSettingsService calendarSettings,
|
||||
ISubstitutionEntryRepository substitutions, IAnnualPlanEventRepository? annualPlanEvents = null,
|
||||
ISubstitutionEntryRepository substitutions, ITimeEntryRepository timeEntries,
|
||||
IAnnualPlanEventRepository? annualPlanEvents = null,
|
||||
AnnualPlanSyncService? annualPlanSync = null, SchoolWeatherService? schoolWeather = null)
|
||||
{
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||
@@ -143,6 +163,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
|
||||
_attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
|
||||
_schoolHolidays = schoolHolidays; _publicHolidays = publicHolidays; _calendarSettings = calendarSettings;
|
||||
_timeEntries = timeEntries;
|
||||
_substitutions = substitutions;
|
||||
_annualPlanEvents = annualPlanEvents;
|
||||
_schoolWeather = schoolWeather;
|
||||
@@ -216,6 +237,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
LoadOpenExcuses(groups.Values.ToList(), today);
|
||||
LoadAttendanceWarnings(today);
|
||||
LoadExamWeekLoads(groups, today);
|
||||
LoadMissingTeachingTime(today);
|
||||
LoadSupportPlanReviews(today);
|
||||
LoadUpcomingDates(groups, today);
|
||||
LoadOpenCorrections(groups, today);
|
||||
@@ -236,6 +258,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
AlertsCard.IsEmpty = Alerts.Count == 0;
|
||||
AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
|
||||
ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
|
||||
MissingTeachingTimeCard.IsEmpty = MissingTeachingTimeEntries.Count == 0;
|
||||
SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
|
||||
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
|
||||
|
||||
@@ -351,6 +374,54 @@ public partial class DashboardViewModel : ObservableObject
|
||||
ExamWeekLoads.Add(new ExamWeekLoadItem(week.IsoWeek, week.WeekStart, week.WeekEnd, week.ExamCount));
|
||||
}
|
||||
|
||||
// ── Unterrichtszeit-Erfassung nachholen (Nutzer-Feedback) ─────────────────
|
||||
//
|
||||
// "Ich vergesse es, und fasse die App außerhalb der Schule manchmal nicht mehr an" — schaut
|
||||
// deshalb bewusst ein paar Tage zurück statt nur auf heute. Ein Tag zählt als erfasst, sobald
|
||||
// irgendein TimeEntry der Kategorie "Unterricht" an diesem Datum existiert; die genaue
|
||||
// Zeitüberschneidung mit dem Stundenplan wird nicht geprüft (dieselbe grobe Betrachtung wie
|
||||
// beim bestehenden "Unterrichtszeit übernehmen"-Vorschlag für heute).
|
||||
|
||||
private void LoadMissingTeachingTime(DateOnly today)
|
||||
{
|
||||
MissingTeachingTimeEntries.Clear();
|
||||
var firstDay = today.AddDays(-MissingTeachingTimeLookbackDays);
|
||||
var publicHolidayDates = Enumerable.Range(firstDay.Year, today.Year - firstDay.Year + 1)
|
||||
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
||||
.Select(h => h.Date).ToHashSet();
|
||||
var schoolHolidays = _schoolHolidays.GetAll();
|
||||
var nowTime = TimeOnly.FromDateTime(DateTime.Now);
|
||||
|
||||
var items = new List<MissingTeachingTimeItem>();
|
||||
for (var date = firstDay; date <= today; date = date.AddDays(1))
|
||||
{
|
||||
if (IsFreeDay(date, schoolHolidays, publicHolidayDates)) continue;
|
||||
|
||||
var daySlots = _timetableSlots.GetAll().Where(s => s.Weekday == date.DayOfWeek).ToList();
|
||||
if (daySlots.Count == 0) continue;
|
||||
|
||||
var cancelledPeriods = _substitutions.GetByDate(date)
|
||||
.Where(s => s.Kind == SubstitutionKind.Cancelled)
|
||||
.Select(s => s.PeriodNumber).ToHashSet();
|
||||
var periodTimes = daySlots.Where(s => !cancelledPeriods.Contains(s.PeriodNumber))
|
||||
.Select(s => _periodSchedule.GetTimes(s.PeriodNumber))
|
||||
.Where(t => t is not null).Select(t => t!.Value).ToList();
|
||||
if (periodTimes.Count == 0) continue; // alle Stunden entfallen oder kein Zeitraster hinterlegt
|
||||
|
||||
var lastPeriodEnd = periodTimes.Max(t => t.End);
|
||||
if (date == today && nowTime < lastPeriodEnd.AddMinutes(MissingTeachingTimeTodayDelayMinutes))
|
||||
continue; // heute: erst 30 Minuten nach Unterrichtsschluss erinnern
|
||||
|
||||
if (_timeEntries.GetByDate(date).Any(e => e.Category == "Unterricht")) continue;
|
||||
|
||||
var windowStart = periodTimes.Min(t => t.Start).AddMinutes(-TeachingWindowBufferBeforeMinutes);
|
||||
var windowEnd = lastPeriodEnd.AddMinutes(TeachingWindowBufferAfterMinutes);
|
||||
items.Add(new MissingTeachingTimeItem(date, windowStart, windowEnd));
|
||||
}
|
||||
foreach (var item in items.OrderBy(i => i.Date))
|
||||
MissingTeachingTimeEntries.Add(item);
|
||||
}
|
||||
|
||||
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
|
||||
|
||||
private void LoadSupportPlanReviews(DateOnly today)
|
||||
@@ -576,7 +647,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
|
||||
"corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
|
||||
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
|
||||
"groups" => "Meine Lerngruppen", "examload" => "Klausurwochen", _ => key,
|
||||
"groups" => "Meine Lerngruppen", "examload" => "Klausurwochen",
|
||||
"missingteachingtime" => "Unterrichtszeit nacherfassen", _ => key,
|
||||
};
|
||||
|
||||
private void ApplyCardLayout()
|
||||
@@ -626,6 +698,15 @@ public partial class DashboardViewModel : ObservableObject
|
||||
[RelayCommand] private void OpenStudentSupportPlan(SupportPlanDueItem? item)
|
||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddMissingTeachingTime(MissingTeachingTimeItem? item)
|
||||
{
|
||||
if (item is null || OnAddMissingTeachingTime is null) return;
|
||||
await OnAddMissingTeachingTime(item);
|
||||
LoadMissingTeachingTime(DateOnly.FromDateTime(DateTime.Today));
|
||||
UpdateDashboardSummary();
|
||||
}
|
||||
|
||||
private void LoadOpenExcuses(List<LearningGroup> groups, DateOnly today)
|
||||
{
|
||||
OpenExcuses.Clear();
|
||||
@@ -1002,6 +1083,16 @@ public class ExamWeekLoadItem(int isoWeek, DateOnly weekStart, DateOnly weekEnd,
|
||||
public string CountDisplay => ExamCount == 1 ? "1 Klausur" : $"{ExamCount} Klausuren";
|
||||
}
|
||||
|
||||
public class MissingTeachingTimeItem(DateOnly date, TimeOnly windowStart, TimeOnly windowEnd)
|
||||
{
|
||||
private static readonly CultureInfo De = new("de-DE");
|
||||
|
||||
public DateOnly Date { get; } = date;
|
||||
public TimeOnly WindowStart { get; } = windowStart;
|
||||
public TimeOnly WindowEnd { get; } = windowEnd;
|
||||
public string DateDisplay { get; } = date.ToString("dddd, dd.MM.", De);
|
||||
}
|
||||
|
||||
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
|
||||
|
||||
public class SupportPlanDueItem
|
||||
|
||||
@@ -444,6 +444,32 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Unterrichtszeit nacherfassen (Nutzer-Feedback) -->
|
||||
<Border Grid.Column="{Binding MissingTeachingTimeCard.Column}" Grid.Row="{Binding MissingTeachingTimeCard.Row}"
|
||||
IsVisible="{Binding MissingTeachingTimeCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="UNTERRICHTSZEIT NACHERFASSEN" FontSize="11" FontWeight="Bold"
|
||||
Opacity="0.5" Margin="0,0,0,10"/>
|
||||
<ItemsControl ItemsSource="{Binding MissingTeachingTimeEntries}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:MissingTeachingTimeItem">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
|
||||
<TextBlock Grid.Column="0" Text="{Binding DateDisplay}" FontSize="13"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Erfassen" FontSize="12" Padding="10,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).AddMissingTeachingTimeCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine fehlende Unterrichtszeit-Erfassung." Classes="emptyhint"
|
||||
IsVisible="{Binding !MissingTeachingTimeEntries.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
||||
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
||||
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
|
||||
@@ -2444,6 +2444,20 @@ Buchung ohne Blick darauf. Neuer `TaskCategory.Teaching`-Wert (`TaskCategoryDisp
|
||||
"Unterricht") ans Ende des Enums angehängt, um bestehende serialisierte Werte nicht zu
|
||||
verschieben.
|
||||
|
||||
**Nachtrag — Dashboard-Erinnerung "Unterrichtszeit nacherfassen" (Nutzer-Feedback):** "Ich vergesse
|
||||
es, und fasse die App außerhalb der Schule manchmal nicht mehr an" — der Vorschlag oben deckte nur
|
||||
den heutigen Tag ab und nur, solange man die Arbeitszeit-Ansicht überhaupt öffnet. Neue
|
||||
Dashboard-Karte (`DashboardViewModel.LoadMissingTeachingTime`) schaut die letzten 14 Tage zurück
|
||||
und listet jeden Schultag, an dem laut Stundenplan (alle Gruppen, wie beim bestehenden Vorschlag)
|
||||
Unterricht stattfand, aber noch kein `TimeEntry` der Kategorie "Unterricht" existiert — Ferien,
|
||||
Feiertage und per `SubstitutionKind.Cancelled` komplett ausgefallene Tage zählen nicht mit
|
||||
(gleiche Prüfung wie bei "Ungeplante Stunden", 9.x). Für den heutigen Tag erscheint der Eintrag
|
||||
bewusst erst 30 Minuten nach dem laut Stundenplan letzten Unterrichtsende
|
||||
(`MissingTeachingTimeTodayDelayMinutes`), damit die Erinnerung nicht schon mitten im Unterricht
|
||||
auftaucht. Klick auf "Erfassen" öffnet denselben `AddTimeEntryDialog` wie der bestehende
|
||||
Tages-Vorschlag, vorbelegt mit Datum, Kategorie "Unterricht" und dem für diesen Tag berechneten
|
||||
Zeitfenster — auch hier bleibt die Bestätigung im Dialog Pflicht, nichts wird automatisch gebucht.
|
||||
|
||||
### 6.3 Auswertung
|
||||
- [x] **6.3.1** Monats-/Jahresauswertung nach Kategorie und Gruppe (Diagramm + Tabelle) —
|
||||
dritter Tab "Auswertung" (`WorkloadEvaluationViewModel`). Zeitraum wahlweise Monat
|
||||
|
||||
Reference in New Issue
Block a user