@@ -467,6 +467,10 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
private readonly ITimetableSlotRepository _timetableSlots;
|
||||
private readonly PeriodScheduleService _periodSchedule;
|
||||
private readonly ISchoolHolidayRepository? _schoolHolidays;
|
||||
private readonly PublicHolidayService? _publicHolidays;
|
||||
private readonly SchoolCalendarSettingsService? _calendarSettings;
|
||||
private readonly ISubstitutionEntryRepository? _substitutions;
|
||||
|
||||
// Nutzer-Feedback: "man beginnt ja auch vermutlich vor 7:50" (erste Stunde) und "wird auch
|
||||
// nicht aus dem Unterricht nach Hause rennen" (nach der letzten) - grobe, aber plausible
|
||||
@@ -474,6 +478,8 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
// vor, gespeichert wird erst nach ausdrücklicher Bestätigung dort (siehe SuggestTeachingTime).
|
||||
private const int BufferBeforeFirstPeriodMinutes = 15;
|
||||
private const int BufferAfterLastPeriodMinutes = 10;
|
||||
private const int MissingTeachingTimeLookbackDays = 14;
|
||||
private const int MissingTeachingTimeTodayDelayMinutes = 30;
|
||||
|
||||
public const string NoTaskOption = "Keine Aufgabe";
|
||||
|
||||
@@ -490,6 +496,7 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
|
||||
public ObservableCollection<TimeEntryListItem> WeekEntries { get; } = [];
|
||||
public ObservableCollection<CategoryTimeSummary> CategorySummaries { get; } = [];
|
||||
public ObservableCollection<TeachingTimeGapItem> MissingTeachingTimeEntries { get; } = [];
|
||||
public string TotalWeekMinutesDisplay => $"{WeekEntries.Sum(e => e.Model.DurationMinutes)} min diese Woche";
|
||||
|
||||
/// Ob heute laut Stundenplan überhaupt Unterricht ansteht - steuert, ob der
|
||||
@@ -502,14 +509,22 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
/// im Dialog bleibt aber immer nötig, nichts wird automatisch gespeichert (siehe Puffer-
|
||||
/// Konstanten oben).
|
||||
public Func<TimeOnly, TimeOnly, Task<TimeEntry?>>? OnSuggestTeachingTime { get; set; }
|
||||
public Func<TeachingTimeGapItem, Task<TimeEntry?>>? OnAddMissingTeachingTime { get; set; }
|
||||
|
||||
public TimeTrackingViewModel(ITimeEntryRepository entries, IWorkTaskRepository tasks,
|
||||
ITimetableSlotRepository timetableSlots, PeriodScheduleService periodSchedule)
|
||||
ITimetableSlotRepository timetableSlots, PeriodScheduleService periodSchedule,
|
||||
ISchoolHolidayRepository? schoolHolidays = null, PublicHolidayService? publicHolidays = null,
|
||||
SchoolCalendarSettingsService? calendarSettings = null,
|
||||
ISubstitutionEntryRepository? substitutions = null)
|
||||
{
|
||||
_entries = entries;
|
||||
_tasks = tasks;
|
||||
_timetableSlots = timetableSlots;
|
||||
_periodSchedule = periodSchedule;
|
||||
_schoolHolidays = schoolHolidays;
|
||||
_publicHolidays = publicHolidays;
|
||||
_calendarSettings = calendarSettings;
|
||||
_substitutions = substitutions;
|
||||
Load();
|
||||
}
|
||||
|
||||
@@ -550,6 +565,7 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
minutes, maxMinutes > 0 ? minutes / (double)maxMinutes : 0));
|
||||
}
|
||||
|
||||
RefreshMissingTeachingTime(today);
|
||||
OnPropertyChanged(nameof(TotalWeekMinutesDisplay));
|
||||
}
|
||||
|
||||
@@ -605,6 +621,59 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
Refresh();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddMissingTeachingTime(TeachingTimeGapItem? item)
|
||||
{
|
||||
if (item is null || OnAddMissingTeachingTime is null) return;
|
||||
var result = await OnAddMissingTeachingTime(item);
|
||||
if (result is null) return;
|
||||
_entries.Save(result);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void RefreshMissingTeachingTime(DateOnly today)
|
||||
{
|
||||
MissingTeachingTimeEntries.Clear();
|
||||
var firstDay = today.AddDays(-MissingTeachingTimeLookbackDays);
|
||||
var timetableSlots = _timetableSlots.GetAll();
|
||||
var schoolHolidays = _schoolHolidays?.GetAll() ?? [];
|
||||
var publicHolidayDates = _publicHolidays is not null && _calendarSettings is not null
|
||||
? Enumerable.Range(firstDay.Year, today.Year - firstDay.Year + 1)
|
||||
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
||||
.Select(h => h.Date).ToHashSet()
|
||||
: [];
|
||||
var nowTime = TimeOnly.FromDateTime(DateTime.Now);
|
||||
var teachingCategory = TaskCategoryDisplay.Label(TaskCategory.Teaching);
|
||||
|
||||
for (var date = firstDay; date <= today; date = date.AddDays(1))
|
||||
{
|
||||
if (publicHolidayDates.Contains(date)
|
||||
|| schoolHolidays.Any(h => date >= h.StartDate && date <= h.EndDate))
|
||||
continue;
|
||||
|
||||
var daySlots = timetableSlots.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;
|
||||
|
||||
var lastPeriodEnd = periodTimes.Max(t => t.End);
|
||||
if (date == today && nowTime < lastPeriodEnd.AddMinutes(MissingTeachingTimeTodayDelayMinutes))
|
||||
continue;
|
||||
if (_entries.GetByDate(date).Any(e => e.Category == teachingCategory)) continue;
|
||||
|
||||
MissingTeachingTimeEntries.Add(new TeachingTimeGapItem(
|
||||
date,
|
||||
periodTimes.Min(t => t.Start).AddMinutes(-BufferBeforeFirstPeriodMinutes),
|
||||
lastPeriodEnd.AddMinutes(BufferAfterLastPeriodMinutes)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frühester Beginn / spätestes Ende aller heutigen Stundenplan-Perioden (alle Gruppen, nicht
|
||||
/// auf eine einzelne beschränkt - der Unterrichtstag als Ganzes), je um die oben definierten
|
||||
@@ -647,6 +716,17 @@ public class TimeEntryListItem(TimeEntry model, string? taskTitle)
|
||||
public string Description => Model.Description ?? "";
|
||||
}
|
||||
|
||||
public sealed class TeachingTimeGapItem(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);
|
||||
public string TimeDisplay { get; } = $"{windowStart:HH:mm}–{windowEnd:HH:mm} Uhr";
|
||||
}
|
||||
|
||||
public class CategoryTimeSummary(string category, int minutes, double barFraction)
|
||||
{
|
||||
public string Category { get; } = category;
|
||||
|
||||
Reference in New Issue
Block a user