feat: Komfort-Features für Unterrichtsplanung, Zeiterfassung und Dashboard
- Neue-Stunde-Dialog: Datum wird beim Anlegen anhand des Stundenplans
und der letzten Stunde der Einheit vorbelegt statt auf "heute"
(behebt eine falsch erkannte Doppelstunde, wenn "heute" nicht auf
den passenden Wochentag fiel).
- Zeiterfassung: Button "Unterrichtszeit heute übernehmen" schlägt
Start/Ende aus dem heutigen Stundenplan inkl. Puffer davor/danach vor.
- Dashboard: neue Kachel "Ungeplante Stunden" erinnert an Stunden ohne
Thema für heute/morgen, mit Opt-out je Gruppe ("Benötigt
Unterrichtsplanung"), Doppelstunden-Erkennung (keine doppelte Meldung
für die zweite Periode) und Berücksichtigung von Stundenausfall.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,7 @@ public class App : Application
|
||||
dash.OnNavigateToStudent = id => main.NavigateToStudent(id);
|
||||
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"
|
||||
|
||||
// StudentList → StudentDetail + Anlegen
|
||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||
|
||||
@@ -30,10 +30,18 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly AttendanceBalanceService _attendanceBalance;
|
||||
private readonly SchoolYearService _sy;
|
||||
private readonly DashboardSettingsService _dashboardSettings;
|
||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||
private readonly PublicHolidayService _publicHolidays;
|
||||
private readonly SchoolCalendarSettingsService _calendarSettings;
|
||||
private readonly ISubstitutionEntryRepository _substitutions;
|
||||
|
||||
private const int OpenExcuseMaxAgeDays = 21;
|
||||
private const int SupportPlanDueWithinDays = 14;
|
||||
private const int UpcomingWithinDays = 30;
|
||||
/// Nutzer-Feedback: "der zeitliche Vorgriff sollte sinnvoll sein" - heute + morgen ist knapp
|
||||
/// genug, dass die Erinnerung nicht zu einer ignorierbaren Dauerliste wird, aber früh genug,
|
||||
/// um sich abends noch vorzubereiten.
|
||||
private const int UnplannedLessonsLookaheadDays = 1;
|
||||
|
||||
[ObservableProperty] private string _greeting = "";
|
||||
[ObservableProperty] private string _currentDate = "";
|
||||
@@ -53,6 +61,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
|
||||
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
||||
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
|
||||
public ObservableCollection<UnplannedLessonItem> UnplannedLessons { get; } = [];
|
||||
public ObservableCollection<DashboardAlertItem> Alerts { get; } = [];
|
||||
public ObservableCollection<CalendarEventItem> SelectedDayEvents { get; } = [];
|
||||
public ObservableCollection<DashboardCardOption> DashboardCards { get; } = [];
|
||||
@@ -66,6 +75,9 @@ public partial class DashboardViewModel : ObservableObject
|
||||
// Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt.
|
||||
public Action<Guid>? OnNavigateToLesson { get; set; }
|
||||
public Action<Guid>? OnNavigateToExam { get; set; }
|
||||
// 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; }
|
||||
|
||||
public DashboardCardOption TodayCard => Card("today");
|
||||
public DashboardCardOption TasksCard => Card("tasks");
|
||||
@@ -73,6 +85,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public DashboardCardOption ExcusesCard => Card("excuses");
|
||||
public DashboardCardOption UpcomingCard => Card("upcoming");
|
||||
public DashboardCardOption CorrectionsCard => Card("corrections");
|
||||
public DashboardCardOption UnplannedCard => Card("unplanned");
|
||||
public DashboardCardOption AlertsCard => Card("alerts");
|
||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
||||
public DashboardCardOption SupportCard => Card("support");
|
||||
@@ -85,7 +98,9 @@ public partial class DashboardViewModel : ObservableObject
|
||||
IParticipationRepository participationEntries, IStudentRepository students,
|
||||
IDocumentationRepository documentation, ITimetableSlotRepository timetableSlots,
|
||||
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy,
|
||||
DashboardSettingsService dashboardSettings)
|
||||
DashboardSettingsService dashboardSettings, ISchoolHolidayRepository schoolHolidays,
|
||||
PublicHolidayService publicHolidays, SchoolCalendarSettingsService calendarSettings,
|
||||
ISubstitutionEntryRepository substitutions)
|
||||
{
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
|
||||
@@ -93,6 +108,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
_students = students; _documentation = documentation;
|
||||
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
|
||||
_attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
|
||||
_schoolHolidays = schoolHolidays; _publicHolidays = publicHolidays; _calendarSettings = calendarSettings;
|
||||
_substitutions = substitutions;
|
||||
LoadDashboardCards();
|
||||
Load();
|
||||
}
|
||||
@@ -157,6 +174,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
LoadSupportPlanReviews(today);
|
||||
LoadUpcomingDates(groups, today);
|
||||
LoadOpenCorrections(groups, today);
|
||||
LoadUnplannedLessons(groups, today);
|
||||
LoadAlerts(groups, today);
|
||||
}
|
||||
|
||||
@@ -264,6 +282,87 @@ public partial class DashboardViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ungeplante Stunden (Nutzer-Feedback: Erinnerung an Stunden ohne Thema) ────────────────
|
||||
//
|
||||
// Nur heute + UnplannedLessonsLookaheadDays (bewusst knapp, siehe Konstante oben) — für jede
|
||||
// Gruppe mit RequiresLessonPlanning und einem Stundenplan-Slot an diesem Wochentag wird
|
||||
// geprüft, ob dafür bereits eine Lesson mit Thema existiert. Gruppen ohne eigenen Verlaufsplan
|
||||
// (Klassenrat, Willkommenskreis) lassen sich in den Stammdaten der Gruppe ausnehmen. Ein Slot,
|
||||
// für den an diesem Datum ein SubstitutionKind.Cancelled-Eintrag (4.3, "Stundenausfall")
|
||||
// vorliegt, fällt ganz weg — gleiche Prüfung (Datum + Stundennummer, ohne Gruppenbezug) wie in
|
||||
// TimetableViewModel.BuildToday.
|
||||
|
||||
private void LoadUnplannedLessons(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||
{
|
||||
UnplannedLessons.Clear();
|
||||
var lastDay = today.AddDays(UnplannedLessonsLookaheadDays);
|
||||
var publicHolidayDates = Enumerable.Range(today.Year, lastDay.Year - today.Year + 1)
|
||||
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
|
||||
.Select(h => h.Date).ToHashSet();
|
||||
var schoolHolidays = _schoolHolidays.GetAll();
|
||||
|
||||
var items = new List<UnplannedLessonItem>();
|
||||
foreach (var group in groups.Values.Where(g => g.RequiresLessonPlanning))
|
||||
{
|
||||
var slots = _timetableSlots.GetByGroup(group.Id);
|
||||
if (slots.Count == 0) continue;
|
||||
|
||||
for (var date = today; date <= lastDay; date = date.AddDays(1))
|
||||
{
|
||||
if (IsFreeDay(date, schoolHolidays, publicHolidayDates)) continue;
|
||||
var daySlots = slots.Where(s => s.Weekday == date.DayOfWeek).ToList();
|
||||
if (daySlots.Count == 0) continue;
|
||||
var periodsWithSlot = daySlots.Select(s => s.PeriodNumber).ToHashSet();
|
||||
var dayLessons = _lessons.GetByGroupAndDate(group.Id, date).ToList();
|
||||
var cancelledPeriods = _substitutions.GetByDate(date)
|
||||
.Where(s => s.Kind == SubstitutionKind.Cancelled)
|
||||
.Select(s => s.PeriodNumber).ToHashSet();
|
||||
|
||||
foreach (var slot in daySlots)
|
||||
{
|
||||
if (cancelledPeriods.Contains(slot.PeriodNumber)) continue;
|
||||
var lesson = dayLessons.FirstOrDefault(l => l.LessonNumber == slot.PeriodNumber);
|
||||
if (lesson is not null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(lesson.Topic)) continue;
|
||||
}
|
||||
else if (IsCoveredByEarlierDoppelstunde(dayLessons, periodsWithSlot, slot.PeriodNumber))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
items.Add(new UnplannedLessonItem(group.Id, group.Name, date, slot.PeriodNumber, today));
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var item in items.OrderBy(i => i.Date).ThenBy(i => i.PeriodNumber).ThenBy(i => i.GroupName))
|
||||
UnplannedLessons.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>Erkennt, ob eine Stunde ohne eigene Lesson bereits Teil einer Doppelstunde ist, die
|
||||
/// bei einer früheren Stundennummer beginnt — gleiches Prinzip wie
|
||||
/// PlanningViewModels.LessonDialogViewModel.RecomputeTimeBudget: ausgehend von der Vorperiode
|
||||
/// wird rückwärts so lange die jeweils vorherige Periode geprüft, wie der Stundenplan dafür
|
||||
/// noch einen Slot hat. Trifft man dabei auf eine Lesson, entscheidet deren Thema (vorhanden =
|
||||
/// Doppelstunde bereits geplant); trifft man auf eine Periode ohne Lesson, wird weiter
|
||||
/// zurückgegangen; bricht die Slot-Kette ab, ohne eine Lesson gefunden zu haben, ist die Periode
|
||||
/// nicht abgedeckt.</summary>
|
||||
private static bool IsCoveredByEarlierDoppelstunde(
|
||||
List<Lesson> dayLessons, HashSet<int> periodsWithSlot, int periodNumber)
|
||||
{
|
||||
for (var period = periodNumber - 1; periodsWithSlot.Contains(period); period--)
|
||||
{
|
||||
var lesson = dayLessons.FirstOrDefault(l => l.LessonNumber == period);
|
||||
if (lesson is not null) return !string.IsNullOrWhiteSpace(lesson.Topic);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Dupliziert absichtlich TimetableViewModel.IsFreeDay/die gleichnamige Prüfung in
|
||||
/// PlanningViewModels.GenerateLessonSeriesDialogViewModel.Save() — zwei Zeilen, kein
|
||||
/// Service-Aufwand für eine dritte Fundstelle.</summary>
|
||||
private static bool IsFreeDay(DateOnly date, List<SchoolHoliday> schoolHolidays, HashSet<DateOnly> publicHolidayDates) =>
|
||||
publicHolidayDates.Contains(date) || schoolHolidays.Any(h => date >= h.StartDate && date <= h.EndDate);
|
||||
|
||||
// ── Auffälligkeiten (9.5) ────────────────────────────────────────────────
|
||||
|
||||
private void LoadAlerts(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||
@@ -330,7 +429,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
{
|
||||
"today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
|
||||
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
|
||||
"corrections" => "Offene Korrekturen", "alerts" => "Auffälligkeiten",
|
||||
"corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
|
||||
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
|
||||
"groups" => "Meine Lerngruppen", _ => key,
|
||||
};
|
||||
@@ -524,6 +623,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
}
|
||||
[RelayCommand] private void OpenCorrection(CorrectionProgressItem? item)
|
||||
{ if (item is not null) OnNavigateToExam?.Invoke(item.GroupId); }
|
||||
[RelayCommand] private void OpenUnplannedLesson(UnplannedLessonItem? item)
|
||||
{ if (item is not null) OnNavigateToUnplannedLesson?.Invoke(item.GroupId); }
|
||||
[RelayCommand] private void OpenAlert(DashboardAlertItem? item)
|
||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
||||
[RelayCommand] private void Refresh() => Load();
|
||||
@@ -685,6 +786,16 @@ public sealed class CorrectionProgressItem(Guid examId, Guid groupId, string tit
|
||||
public bool IsOverdue => Date < today.AddDays(-7) && Completed < Total;
|
||||
}
|
||||
|
||||
public sealed class UnplannedLessonItem(Guid groupId, string groupName, DateOnly date, int periodNumber, DateOnly today)
|
||||
{
|
||||
public Guid GroupId { get; } = groupId;
|
||||
public string GroupName { get; } = groupName;
|
||||
public DateOnly Date { get; } = date;
|
||||
public int PeriodNumber { get; } = periodNumber;
|
||||
public string DateDisplay => Date == today ? "Heute" : Date == today.AddDays(1) ? "Morgen" : Date.ToString("dd.MM.");
|
||||
public string Display => $"{GroupName} · {PeriodNumber}. Stunde";
|
||||
}
|
||||
|
||||
public enum AlertSeverity { Medium, High }
|
||||
|
||||
public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string studentName,
|
||||
|
||||
@@ -810,6 +810,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private int? _hoursPerWeek;
|
||||
[ObservableProperty] private bool _isOwnClass;
|
||||
[ObservableProperty] private bool _isDifferentiated;
|
||||
[ObservableProperty] private bool _requiresLessonPlanning = true;
|
||||
[ObservableProperty] private string _nameError = "";
|
||||
[ObservableProperty] private string _gradeLevelError = "";
|
||||
|
||||
@@ -848,6 +849,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
HoursPerWeek = group.HoursPerWeek;
|
||||
IsOwnClass = group.IsOwnClass;
|
||||
IsDifferentiated = group.IsDifferentiated;
|
||||
RequiresLessonPlanning = group.RequiresLessonPlanning;
|
||||
OnPropertyChanged(nameof(DialogTitle));
|
||||
OnPropertyChanged(nameof(SaveButtonText));
|
||||
}
|
||||
@@ -893,6 +895,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
|
||||
Result.HoursPerWeek = HoursPerWeek;
|
||||
Result.IsOwnClass = IsOwnClass;
|
||||
Result.IsDifferentiated = IsDifferentiated;
|
||||
Result.RequiresLessonPlanning = RequiresLessonPlanning;
|
||||
_groups.Save(Result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,9 +711,35 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
StatusName = LessonStatusDisplay.ToName(editingLesson.Status);
|
||||
foreach (var p in editingLesson.Phases) AddPhaseInternal(p);
|
||||
}
|
||||
else
|
||||
{
|
||||
DateText = SuggestNextLessonDate().ToString("dd.MM.yyyy");
|
||||
}
|
||||
RecomputeTimes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vorbelegung für eine neue Stunde: statt des sonst über den Feld-Default eingesetzten
|
||||
/// heutigen Datums (das an einem beliebigen Wochentag steht und die Doppelstunden-Erkennung
|
||||
/// in <see cref="RecomputeTimeBudget"/> stillschweigend auf eine Einzelperiode zurückfallen
|
||||
/// lässt, wenn der Wochentag nicht zufällig passt) der nächste laut Stundenplan (4.3) für
|
||||
/// diese Gruppe passende Wochentag — ab der letzten Stunde dieser Einheit, oder ab heute, wenn
|
||||
/// noch keine Stunde in dieser Einheit existiert oder die letzte in der Vergangenheit liegt.
|
||||
/// Ohne Stundenplan-Einträge für die Gruppe (z.B. Klassenrat) bleibt es beim heutigen Datum.
|
||||
/// </summary>
|
||||
private DateOnly SuggestNextLessonDate()
|
||||
{
|
||||
var weekdays = _timetableSlots.GetByGroup(_groupId).Select(s => s.Weekday).ToHashSet();
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
if (weekdays.Count == 0) return today;
|
||||
|
||||
var lastLessonDate = _lessons.GetByUnit(_unitId).Select(l => l.Date)
|
||||
.DefaultIfEmpty(today.AddDays(-1)).Max();
|
||||
var candidate = lastLessonDate >= today ? lastLessonDate.AddDays(1) : today;
|
||||
while (!weekdays.Contains(candidate.DayOfWeek)) candidate = candidate.AddDays(1);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddPhase()
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ public static class TaskCategoryDisplay
|
||||
TaskCategory.Admin => "Verwaltung",
|
||||
TaskCategory.Meeting => "Besprechung",
|
||||
TaskCategory.Other => "Sonstiges",
|
||||
TaskCategory.Teaching => "Unterricht",
|
||||
_ => c.ToString(),
|
||||
};
|
||||
|
||||
@@ -333,6 +334,15 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
{
|
||||
private readonly ITimeEntryRepository _entries;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
private readonly ITimetableSlotRepository _timetableSlots;
|
||||
private readonly PeriodScheduleService _periodSchedule;
|
||||
|
||||
// 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
|
||||
// Puffer für den Unterrichtszeit-Vorschlag. Der Vorschlag füllt nur den Nacherfassen-Dialog
|
||||
// vor, gespeichert wird erst nach ausdrücklicher Bestätigung dort (siehe SuggestTeachingTime).
|
||||
private const int BufferBeforeFirstPeriodMinutes = 15;
|
||||
private const int BufferAfterLastPeriodMinutes = 10;
|
||||
|
||||
public const string NoTaskOption = "Keine Aufgabe";
|
||||
|
||||
@@ -351,12 +361,24 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
public ObservableCollection<CategoryTimeSummary> CategorySummaries { get; } = [];
|
||||
public string TotalWeekMinutesDisplay => $"{WeekEntries.Sum(e => e.Model.DurationMinutes)} min diese Woche";
|
||||
|
||||
public Func<Task<TimeEntry?>>? OnAddEntry { get; set; }
|
||||
/// Ob heute laut Stundenplan überhaupt Unterricht ansteht - steuert, ob der
|
||||
/// "Unterrichtszeit übernehmen"-Button überhaupt sichtbar ist.
|
||||
public bool HasTeachingTimeSuggestionToday => ComputeTodaysTeachingWindow() is not null;
|
||||
|
||||
public TimeTrackingViewModel(ITimeEntryRepository entries, IWorkTaskRepository tasks)
|
||||
public Func<Task<TimeEntry?>>? OnAddEntry { get; set; }
|
||||
/// Wie OnAddEntry, aber öffnet den Dialog mit vorbefüllter Kategorie "Unterricht" und den
|
||||
/// laut Stundenplan/Stundenraster vorgeschlagenen Beginn-/Ende-Zeiten - eine echte Bestätigung
|
||||
/// 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 TimeTrackingViewModel(ITimeEntryRepository entries, IWorkTaskRepository tasks,
|
||||
ITimetableSlotRepository timetableSlots, PeriodScheduleService periodSchedule)
|
||||
{
|
||||
_entries = entries;
|
||||
_tasks = tasks;
|
||||
_timetableSlots = timetableSlots;
|
||||
_periodSchedule = periodSchedule;
|
||||
Load();
|
||||
}
|
||||
|
||||
@@ -442,6 +464,38 @@ public partial class TimeTrackingViewModel : ObservableObject
|
||||
Refresh();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SuggestTeachingTime()
|
||||
{
|
||||
if (OnSuggestTeachingTime is null || ComputeTodaysTeachingWindow() is not var (start, end)) return;
|
||||
var result = await OnSuggestTeachingTime(start, end);
|
||||
if (result is null) return;
|
||||
_entries.Save(result);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// Puffer erweitert. Ohne Stundenplan-Eintrag heute oder ohne im Stundenraster hinterlegte
|
||||
/// Zeiten gibt es keinen Vorschlag (null) statt einer erfundenen Zeit.
|
||||
/// </summary>
|
||||
private (TimeOnly Start, TimeOnly End)? ComputeTodaysTeachingWindow()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today).DayOfWeek;
|
||||
var periodTimes = _timetableSlots.GetAll()
|
||||
.Where(s => s.Weekday == today)
|
||||
.Select(s => _periodSchedule.GetTimes(s.PeriodNumber))
|
||||
.Where(t => t is not null)
|
||||
.Select(t => t!.Value)
|
||||
.ToList();
|
||||
if (periodTimes.Count == 0) return null;
|
||||
|
||||
var start = periodTimes.Min(t => t.Start).AddMinutes(-BufferBeforeFirstPeriodMinutes);
|
||||
var end = periodTimes.Max(t => t.End).AddMinutes(BufferAfterLastPeriodMinutes);
|
||||
return (start, end);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteEntry(TimeEntryListItem? item)
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
|
||||
|
||||
<!-- Heutige Stunden -->
|
||||
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
|
||||
@@ -415,6 +415,36 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Ungeplante Stunden -->
|
||||
<Border Grid.Column="{Binding UnplannedCard.Column}" Grid.Row="{Binding UnplannedCard.Row}"
|
||||
IsVisible="{Binding UnplannedCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="UNGEPLANTE STUNDEN" FontSize="11" FontWeight="Bold"
|
||||
Opacity="0.5" Margin="0,0,0,10"/>
|
||||
<ItemsControl ItemsSource="{Binding UnplannedLessons}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:UnplannedLessonItem">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,5"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenUnplannedLessonCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="{Binding Display}" FontSize="13"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="12"
|
||||
VerticalAlignment="Center" Opacity="0.6"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine ungeplanten Stunden." Classes="emptyhint"
|
||||
IsVisible="{Binding !UnplannedLessons.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Auffälligkeiten (9.5) -->
|
||||
<Border Grid.Column="{Binding AlertsCard.Column}" Grid.Row="{Binding AlertsCard.Row}"
|
||||
IsVisible="{Binding AlertsCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
<CheckBox Content="Niveaudifferenzierung (E/G/Förder)" IsChecked="{Binding IsDifferentiated}"
|
||||
ToolTip.Tip="Blendet im Schüler-Tab eine Niveau-Zuordnung je Schüler ein und erlaubt in Klausuren die Zuordnung zu einem Niveau."/>
|
||||
|
||||
<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>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||
|
||||
@@ -66,9 +66,13 @@
|
||||
</Border>
|
||||
|
||||
<!-- Einträge / Nacherfassung (6.2.2) -->
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Text="Einträge" FontWeight="SemiBold" FontSize="14" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="+ Nacherfassen" Command="{Binding AddEntryCommand}"/>
|
||||
<Button Grid.Column="1" Content="🕓 Unterrichtszeit heute übernehmen" Margin="0,0,8,0"
|
||||
Command="{Binding SuggestTeachingTimeCommand}"
|
||||
IsVisible="{Binding HasTeachingTimeSuggestionToday}"
|
||||
ToolTip.Tip="Schlägt Beginn/Ende anhand des heutigen Stundenplans vor (mit etwas Puffer davor/danach) - im Dialog vor dem Speichern noch anpassbar."/>
|
||||
<Button Grid.Column="2" Content="+ Nacherfassen" Command="{Binding AddEntryCommand}"/>
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding WeekEntries}">
|
||||
|
||||
@@ -14,10 +14,15 @@ public partial class TimeTrackingView : UserControl
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is TimeTrackingViewModel vm)
|
||||
vm.OnAddEntry = ShowAddEntryDialog;
|
||||
{
|
||||
vm.OnAddEntry = () => ShowAddEntryDialog();
|
||||
vm.OnSuggestTeachingTime = (start, end) => ShowAddEntryDialog(prefillCategory: "Unterricht",
|
||||
prefillStart: start, prefillEnd: end);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<TimeEntry?> ShowAddEntryDialog()
|
||||
private async Task<TimeEntry?> ShowAddEntryDialog(string? prefillCategory = null,
|
||||
TimeOnly? prefillStart = null, TimeOnly? prefillEnd = null)
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is null) return null;
|
||||
@@ -25,6 +30,9 @@ public partial class TimeTrackingView : UserControl
|
||||
var tasks = App.Services.GetRequiredService<IWorkTaskRepository>()
|
||||
.GetAll().Where(t => t.Status != WorkTaskStatus.Done).ToList();
|
||||
var vm = new AddTimeEntryDialogViewModel(tasks);
|
||||
if (prefillCategory is not null) vm.SelectedCategory = prefillCategory;
|
||||
if (prefillStart is { } s) vm.StartTimeText = s.ToString("HH:mm");
|
||||
if (prefillEnd is { } e) vm.EndTimeText = e.ToString("HH:mm");
|
||||
var dialog = new AddTimeEntryDialog { DataContext = vm };
|
||||
await dialog.ShowDialog<bool>(owner);
|
||||
return vm.Result;
|
||||
|
||||
Reference in New Issue
Block a user