Kurs-Übersicht: fehlende Hausaufgaben; Erinnerungen als eigene Aufgabenart; Kalender-/Sitzplan-Bugfixes

Kurs-Übersicht: neue Karte "Fehlende Hausaufgaben" listet Schüler mit nicht
gemachter Hausaufgabe aus der letzten Mitarbeitssitzung.

Aufgabenverwaltung: WorkTask.Kind unterscheidet jetzt Arbeitsaufträge von
reinen Erinnerungen ohne Zeitbezug (z.B. "morgen Ansage an die Klasse
machen") - eigener Button "🔔 Erinnerung", eigenes Icon in Liste/Dashboard,
läuft nicht in die Zeitauswertung ein.

Drei Bugfixes aus Nutzer-Feedback:
- Dashboard-Kalender zeigte eine Stunde doppelt, wenn sie über "Sitzung
  erzeugen" mit einer Mitarbeitssitzung verknüpft war.
- Der Sitzplan-Tab legte beim bloßen Öffnen einer Gruppe (nicht erst bei
  echter Nutzung) eine leere "Sitzplan"-Sitzung für heute an.
- Der "Meine Klasse"-Ring im Kalender erschien erst, sobald für den Tag
  eine Lesson existierte, statt schon laut Stundenplan.

Außerdem: Drawer-Icons pinnen jetzt explizit auf die farbige Emoji-Schrift
(Windows kann sonst je nach Font-Fallback auf eine einfarbige Variante
ausweichen).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 23:17:18 +02:00
co-authored by Claude Sonnet 5
parent 4d2a89b14b
commit 52a1d45539
16 changed files with 505 additions and 57 deletions
@@ -42,6 +42,11 @@ public partial class DashboardViewModel : ObservableObject
/// genug, dass die Erinnerung nicht zu einer ignorierbaren Dauerliste wird, aber früh genug,
/// um sich abends noch vorzubereiten.
private const int UnplannedLessonsLookaheadDays = 1;
/// Nutzer-Feedback: unterhalb dieser Anzahl erfasster Anwesenheits-Einträge im laufenden
/// Schuljahr wird eine hohe Fehlquote NICHT gemeldet — wer zu Schuljahresbeginn zweimal fehlt,
/// 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;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
@@ -156,7 +161,8 @@ public partial class DashboardViewModel : ObservableObject
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(5))
OpenTasks.Add(new() { Title = t.Title,
DueDate = t.DueDate?.ToString("dd.MM.") ?? "",
IsOverdue = t.DueDate.HasValue && t.DueDate < today });
IsOverdue = t.DueDate.HasValue && t.DueDate < today,
IsReminder = t.Kind == TaskKind.Reminder });
CurrentGroups.Clear();
foreach (var g in groups.Values.OrderBy(g => g.Name))
@@ -197,7 +203,7 @@ public partial class DashboardViewModel : ObservableObject
.Select(t => (t.Item1!.Value, t.Attendance));
var balance = _attendanceBalance.Calculate(entries, from, to);
if (balance.ExceedsThreshold)
if (balance.TotalChecked >= AttendanceMinSampleSize && balance.ExceedsThreshold)
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
}
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
@@ -555,8 +561,12 @@ public partial class DashboardViewModel : ObservableObject
exam.Title, g.Name, g.Id));
}
// Sitzungen, die über "Sitzung erzeugen" (3.3.1) aus einer Stunde entstanden sind
// (LessonId gesetzt), bekommen bewusst keinen eigenen Kalendereintrag — die Stunde
// selbst ist an diesem Tag/dieser Gruppe schon als Lesson-Termin gelistet, ein
// zweiter Eintrag für dieselbe Unterrichtsstunde wäre eine Dopplung.
foreach (var session in _participationSessions.GetByGroup(g.Id)
.Where(s => s.Date >= gridStart && s.Date <= gridEnd))
.Where(s => s.LessonId is null && s.Date >= gridStart && s.Date <= gridEnd))
{
var agg = Agg(session.Date);
agg.HasSession = true;
@@ -566,6 +576,35 @@ public partial class DashboardViewModel : ObservableObject
}
}
// "Meine Klasse"-Ring: bisher nur gesetzt, wenn für den Tag schon eine Lesson/Exam/Sitzung
// existiert — ein Tag, an dem laut Stundenplan (4.3) eine eigene Klasse ansteht, für den
// aber noch keine Lesson angelegt wurde (z.B. "morgen"), zeigte den Ring fälschlich nicht.
// Dieselbe Projektion wie bei den ungeplanten Stunden (LoadUnplannedLessons), nur über den
// gesamten Kalenderraster statt nur die nächsten Tage.
var publicHolidayDatesForGrid = Enumerable.Range(gridStart.Year, gridEnd.Year - gridStart.Year + 1)
.SelectMany(y => _publicHolidays.GetHolidays(y, _calendarSettings.State))
.Select(h => h.Date).ToHashSet();
var schoolHolidaysForGrid = _schoolHolidays.GetAll();
foreach (var g in groups.Where(g => g.IsOwnClass))
{
var slots = _timetableSlots.GetByGroup(g.Id);
if (slots.Count == 0) continue;
for (var date = gridStart; date <= gridEnd; date = date.AddDays(1))
{
if (IsFreeDay(date, schoolHolidaysForGrid, publicHolidayDatesForGrid)) continue;
var daySlots = slots.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();
if (daySlots.All(s => cancelledPeriods.Contains(s.PeriodNumber))) continue;
Agg(date).IsOwnClassDay = true;
}
}
for (var i = 0; i < 42; i++)
{
var date = gridStart.AddDays(i);
@@ -659,7 +698,7 @@ public class LessonItem
public string Room { get; set; } = "";
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
}
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } }
public class TaskItem { public string Title { get; set; } = ""; public string DueDate { get; set; } = ""; public bool IsOverdue { get; set; } public bool IsReminder { get; set; } }
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
@@ -25,9 +25,9 @@ public partial class GroupOverviewViewModel : ObservableObject
/// Unterhalb dieser Anzahl erfasster Anwesenheits-Einträge im laufenden Schuljahr wird eine
/// hohe Fehlquote bewusst NICHT gemeldet — wer zu Schuljahresbeginn zweimal fehlt, hat rein
/// rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
/// Stichprobe. Dasselbe Problem hat aktuell noch die Fehlzeiten-Warnung im Hauptdashboard
/// (AttendanceBalanceService/AttendanceWarningItem) — bewusst unangetastet, wird dort separat
/// nachgezogen.
/// Stichprobe. Dieselbe Konstante wie DashboardViewModel.AttendanceMinSampleSize
/// (Hauptdashboard-Fehlzeiten-Warnung, 5.2.3) — dort dasselbe Problem, mittlerweile ebenfalls
/// gefixt.
private const int AttendanceMinSampleSize = 8;
private readonly ILessonRepository _lessons;
@@ -55,6 +55,10 @@ public partial class GroupOverviewViewModel : ObservableObject
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
public bool HasOpenExcuses => OpenExcuses.Count > 0;
[ObservableProperty] private string _missingHomeworkSessionLabel = "";
public ObservableCollection<MissingHomeworkItem> MissingHomeworkStudents { get; } = [];
public bool HasMissingHomework => MissingHomeworkStudents.Count > 0;
/// Absichtlich zurückhaltender formuliert/gestylt als das Hauptdashboard (kein "Warnfall") —
/// siehe <see cref="AttendanceMinSampleSize"/>.
public ObservableCollection<AttendanceWarningItem> AttendanceNotices { get; } = [];
@@ -97,6 +101,7 @@ public partial class GroupOverviewViewModel : ObservableObject
LoadOpenExcuses(today);
LoadDraftDocumentationCount();
LoadAttendanceNotices(today);
LoadMissingHomework();
}
private void LoadNextLesson(DateOnly today)
@@ -200,10 +205,9 @@ public partial class GroupOverviewViewModel : ObservableObject
.Count(d => d.IsDraft && (d.GroupId is null || d.GroupId == _groupId));
}
/// Bewusst kein 1:1-Abbild der Dashboard-Fehlzeiten-Warnung: dieselbe Quote
/// (AttendanceBalanceService.WarningThresholdPercent), aber erst ab
/// <see cref="AttendanceMinSampleSize"/> erfassten Terminen, damit ein Fehltag in der ersten
/// Schulwoche nicht sofort als auffällig gilt.
/// Dieselbe Mindeststichprobe wie DashboardViewModel.LoadAttendanceWarnings
/// (AttendanceMinSampleSize) — erst ab genug erfassten Terminen wird eine hohe Quote gemeldet,
/// damit ein Fehltag in der ersten Schulwoche nicht sofort als auffällig gilt.
private void LoadAttendanceNotices(DateOnly today)
{
AttendanceNotices.Clear();
@@ -225,15 +229,41 @@ public partial class GroupOverviewViewModel : ObservableObject
var items = new List<AttendanceWarningItem>();
foreach (var student in _students.GetByGroup(_groupId))
{
if (!entriesByStudent.TryGetValue(student.Id, out var entries) || entries.Count < AttendanceMinSampleSize)
continue;
if (!entriesByStudent.TryGetValue(student.Id, out var entries)) continue;
var balance = _attendanceBalance.Calculate(entries, from, to);
if (balance.ExceedsThreshold)
if (balance.TotalChecked >= AttendanceMinSampleSize && balance.ExceedsThreshold)
items.Add(new AttendanceWarningItem(student.Id, student.FullName, balance.AbsenceRatePercent));
}
foreach (var item in items.OrderByDescending(i => i.AbsenceRatePercent))
AttendanceNotices.Add(item);
OnPropertyChanged(nameof(HasAttendanceNotices));
}
/// Schüler mit nicht gemachter Hausaufgabe aus der letzten Mitarbeitssitzung
/// (ParticipationEntry.HomeworkMissing, gepflegt über HomeworkDisplay.CountsAsMissing) —
/// bewusst nur die letzte Sitzung, nicht die gesamte Historie, analog zur
/// Hausaufgaben-Kontrolle in <see cref="LoadOpenHomeworkCheck"/>.
private void LoadMissingHomework()
{
MissingHomeworkStudents.Clear();
var last = _sessions.GetByGroup(_groupId).OrderByDescending(s => s.Date).FirstOrDefault();
if (last is not null)
{
MissingHomeworkSessionLabel = $"Aus der Sitzung vom {last.Date:dd.MM.yyyy}:";
foreach (var entry in _entries.GetBySession(last.Id).Where(e => e.HomeworkMissing))
{
var student = _students.GetById(entry.StudentId);
if (student is not null)
MissingHomeworkStudents.Add(new MissingHomeworkItem(student.FullName, HomeworkDisplay.Label(entry.Homework)));
}
}
OnPropertyChanged(nameof(HasMissingHomework));
}
}
public sealed class MissingHomeworkItem(string studentName, string statusLabel)
{
public string StudentName { get; } = studentName;
public string StatusLabel { get; } = statusLabel;
}
@@ -76,22 +76,39 @@ public partial class SeatingPlanTabViewModel : ObservableObject
NotifyCommands();
}
/// Legt bewusst KEINE Sitzung an, nur weil der Tab geöffnet wird — Initialize() läuft für
/// jede Gruppe schon beim bloßen Navigieren zur Kursübersicht (GroupDetailViewModel.LoadGroup),
/// unabhängig davon, ob der Sitzplan-Tab überhaupt angesehen wird. Eine Sitzung entsteht erst
/// bei der ersten tatsächlichen Bewertung/Markierung, siehe EnsureTodaySession().
private void LoadTodaySessions()
{
TodaySessions.Clear();
var today = DateOnly.FromDateTime(DateTime.Today);
var sessions = _sessions.GetByGroup(_groupId).Where(s => s.Date == today)
.OrderBy(s => s.CreatedAt).ToList();
if (sessions.Count == 0 && !_isReadOnly)
{
var created = new ParticipationSession { GroupId = _groupId, Date = today, Comment = "Sitzplan" };
_sessions.Save(created);
sessions.Add(created);
}
foreach (var session in sessions) TodaySessions.Add(new ParticipationSessionOption(session));
SelectedSession = TodaySessions.LastOrDefault();
}
/// Legt bei Bedarf die "Sitzung für heute" an, mit der Sitzplan-Bewertungen/-Markierungen
/// verknüpft werden — aber erst, wenn tatsächlich etwas bewertet/markiert wird (AssessStudent/
/// ToggleSituationTag), nicht schon beim Öffnen des Tabs.
private ParticipationSessionOption? EnsureTodaySession()
{
if (SelectedSession is not null) return SelectedSession;
if (!IsEditable) return null;
var created = new ParticipationSession
{
GroupId = _groupId, Date = DateOnly.FromDateTime(DateTime.Today), Comment = "Sitzplan",
};
_sessions.Save(created);
var option = new ParticipationSessionOption(created);
TodaySessions.Add(option);
SelectedSession = option;
return option;
}
partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData();
private void LoadStudentOptions()
@@ -193,19 +210,21 @@ public partial class SeatingPlanTabViewModel : ObservableObject
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
{
if (!IsEditable || _documentation is null || SelectedSession is null ||
if (!IsEditable || _documentation is null ||
seat.SelectedOption.StudentId is not Guid studentId) return;
var session = EnsureTodaySession();
if (session is null) return;
var draft = _documentation.GetByStudent(studentId)
.FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id);
.FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == session.Id);
if (draft is null)
{
draft = new Documentation
{
StudentId = studentId, GroupId = _groupId,
ParticipationSessionId = SelectedSession.Id,
LessonId = SelectedSession.LessonId,
ParticipationSessionId = session.Id,
LessonId = session.LessonId,
Type = DocumentationType.Incident,
Date = SelectedSession.Date,
Date = session.Date,
Title = tag,
IsDraft = true,
Tags = [tag],
@@ -297,9 +316,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public async Task AssessStudent(SeatCellViewModel seat)
{
if (!seat.SelectedOption.StudentId.HasValue || OnAssessStudent is null) return;
var session = EnsureTodaySession();
var assessment = new SeatAssessmentViewModel(_sessions, _participation, _aspects,
_groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable,
SelectedSession?.Id);
session?.Id);
await OnAssessStudent(assessment);
RefreshSeatLessonData();
OnAssessmentChanged?.Invoke();
@@ -97,7 +97,8 @@ public partial class WorkTaskListViewModel : ObservableObject
public ObservableCollection<WorkTaskListItem> Tasks { get; } = [];
public string CountSummary => $"{Tasks.Count} Aufgabe(n)";
public Func<WorkTask?, Task<WorkTask?>>? OnEditTask { get; set; }
/// Zweiter Parameter = Dialog startet vorbelegt als Erinnerung (nur bei AddReminder true).
public Func<WorkTask?, bool, Task<WorkTask?>>? OnEditTask { get; set; }
private Dictionary<Guid, string> _groupNames = [];
@@ -166,7 +167,20 @@ public partial class WorkTaskListViewModel : ObservableObject
private async Task AddTask()
{
if (OnEditTask is null) return;
var result = await OnEditTask(null);
var result = await OnEditTask(null, false);
if (result is null) return;
_tasks.Save(result);
Refresh();
}
/// Eigener Einstieg statt eines Umschalters im selben Dialog-Aufruf wie AddTask (6.1.1
/// Nutzer-Feedback): "Erinnerung" soll sich nicht wie eine Variante der Zeiterfassung anfühlen,
/// sondern wie eine eigene, schnelle Aktion.
[RelayCommand]
private async Task AddReminder()
{
if (OnEditTask is null) return;
var result = await OnEditTask(null, true);
if (result is null) return;
_tasks.Save(result);
Refresh();
@@ -176,7 +190,7 @@ public partial class WorkTaskListViewModel : ObservableObject
private async Task EditTask(WorkTaskListItem? item)
{
if (item is null || OnEditTask is null) return;
var result = await OnEditTask(item.Model);
var result = await OnEditTask(item.Model, false);
if (result is null) return;
_tasks.Save(result);
Refresh();
@@ -203,6 +217,7 @@ public partial class WorkTaskListViewModel : ObservableObject
? dueDate.AddDays(7) : dueDate.AddMonths(1),
EstimatedMinutes = item.Model.EstimatedMinutes,
Recurrence = item.Model.Recurrence,
Kind = item.Model.Kind,
Notes = item.Model.Notes,
Status = WorkTaskStatus.Open,
});
@@ -224,7 +239,8 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
{
public WorkTask Model { get; } = model;
public string Title => Model.Title;
public string CategoryLabel => TaskCategoryDisplay.Label(Model.Category);
public bool IsReminder => Model.Kind == TaskKind.Reminder;
public string CategoryLabel => IsReminder ? "Erinnerung" : TaskCategoryDisplay.Label(Model.Category);
public string GroupName => groupName ?? "";
public string DueDateDisplay => Model.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
public bool IsOverdue => Model.DueDate is { } d && d < DateOnly.FromDateTime(DateTime.Today)
@@ -258,21 +274,26 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
[ObservableProperty] private string _estimatedMinutesText = "";
[ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0];
[ObservableProperty] private string _notes = "";
[ObservableProperty] private bool _isReminder;
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dueDateError = "";
[ObservableProperty] private string _estimatedMinutesError = "";
public string DialogTitle => _source is null ? "Aufgabe anlegen" : "Aufgabe bearbeiten";
public string DialogTitle => _source is null
? (IsReminder ? "Erinnerung anlegen" : "Aufgabe anlegen")
: (IsReminder ? "Erinnerung bearbeiten" : "Aufgabe bearbeiten");
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
public List<string> RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options];
public List<LearningGroup> Groups { get; }
public WorkTask? Result { get; private set; }
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups)
partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle));
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups, bool startAsReminder = false)
{
_source = source;
Groups = groups;
if (source is null) return;
if (source is null) { IsReminder = startAsReminder; return; }
Title = source.Title;
SelectedCategory = TaskCategoryDisplay.Label(source.Category);
@@ -281,6 +302,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence);
Notes = source.Notes ?? "";
IsReminder = source.Kind == TaskKind.Reminder;
}
[RelayCommand]
@@ -303,8 +325,9 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
if (recurrence != TaskRecurrence.None && dueDate is null)
{ DueDateError = "Fälligkeitsdatum erforderlich, damit die nächste Instanz geplant werden kann."; valid = false; }
// Erinnerungen haben bewusst keinen Zeitbezug — die Minutenschätzung entfällt.
int? estimatedMinutes = null;
if (!string.IsNullOrWhiteSpace(EstimatedMinutesText))
if (!IsReminder && !string.IsNullOrWhiteSpace(EstimatedMinutesText))
{
if (!int.TryParse(EstimatedMinutesText, out var m) || m <= 0)
{ EstimatedMinutesError = "Ganze Zahl > 0 erwartet."; valid = false; }
@@ -322,6 +345,7 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
DueDate = dueDate,
EstimatedMinutes = estimatedMinutes,
Recurrence = recurrence,
Kind = IsReminder ? TaskKind.Reminder : TaskKind.WorkItem,
Status = _source?.Status ?? WorkTaskStatus.Open,
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
@@ -108,10 +108,13 @@
<ItemsControl ItemsSource="{Binding OpenTasks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TaskItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,3">
<TextBlock Grid.Column="0" Text="{Binding Title}"
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,3">
<TextBlock Grid.Column="0" Text="🔔" FontSize="12" Margin="0,0,4,0"
IsVisible="{Binding IsReminder}"
ToolTip.Tip="Erinnerung — kein Zeitbezug"/>
<TextBlock Grid.Column="1" Text="{Binding Title}"
FontSize="13" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="1" Text="{Binding DueDate}"
<TextBlock Grid.Column="2" Text="{Binding DueDate}"
FontSize="12" Opacity="0.6" Margin="8,0,0,0"/>
</Grid>
</DataTemplate>
@@ -143,6 +143,28 @@
</ItemsControl>
</StackPanel>
</Border>
<!-- Fehlende Hausaufgaben aus der letzten Sitzung -->
<Border Classes="card" MinWidth="340" MaxWidth="420"
IsVisible="{Binding HasMissingHomework}">
<StackPanel>
<TextBlock Text="FEHLENDE HAUSAUFGABEN" Classes="cardTitle"/>
<TextBlock Text="{Binding MissingHomeworkSessionLabel}" FontSize="11" Opacity="0.6" Margin="0,-6,0,6"/>
<ItemsControl ItemsSource="{Binding MissingHomeworkStudents}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:MissingHomeworkItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
<TextBlock Grid.Column="0" Text="{Binding StudentName}" FontSize="13"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding StatusLabel}" FontSize="11"
Opacity="0.7" VerticalAlignment="Center" TextAlignment="Right"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="Zur Mitarbeit " Classes="cardLink" Command="{Binding NavigateToParticipationCommand}"/>
</StackPanel>
</Border>
</WrapPanel>
</StackPanel>
</ScrollViewer>
+5 -1
View File
@@ -67,10 +67,14 @@
<DockPanel Classes.compact="{Binding !#RootDrawer.IsOpen}">
<DockPanel.Styles>
<!-- Basisgröße der Icons als Style (nicht lokal), damit der Compact-Selector sie
überschreiben kann lokale Werte hätten immer Vorrang vor Style-Settern. -->
überschreiben kann lokale Werte hätten immer Vorrang vor Style-Settern.
FontFamily explizit auf die farbige Emoji-Schrift gepinnt: Windows kann für
Emoji-Codepoints je nach Fallback-Auflösung sonst auf "Segoe UI Symbol"
(einfarbig/schwarz) statt "Segoe UI Emoji" (farbig) ausweichen. -->
<Style Selector="TextBlock.navicon">
<Setter Property="FontSize" Value="15"/>
<Setter Property="Width" Value="24"/>
<Setter Property="FontFamily" Value="Segoe UI Emoji,Segoe UI Symbol,Segoe UI"/>
</Style>
<Style Selector="Button.navitem">
<Setter Property="Background" Value="Transparent"/>
@@ -20,6 +20,9 @@
IsVisible="{Binding TitleError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<CheckBox Content="🔔 Nur Erinnerung (kein Zeitbezug, läuft nicht in die Zeiterfassung ein)"
IsChecked="{Binding IsReminder}"/>
<Grid ColumnDefinitions="*,8,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Kategorie *" FontSize="12" Opacity="0.7"/>
@@ -46,7 +49,7 @@
<TextBlock Text="{Binding DueDateError}" Foreground="Red" FontSize="11"
IsVisible="{Binding DueDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<StackPanel Grid.Column="2" Spacing="4" IsVisible="{Binding !IsReminder}">
<TextBlock Text="Geschätzte Dauer in min (optional)" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding EstimatedMinutesText}" PlaceholderText="z.B. 90"/>
<TextBlock Text="{Binding EstimatedMinutesError}" Foreground="Red" FontSize="11"
@@ -11,7 +11,11 @@
SelectedItem="{Binding CategoryFilter}" Margin="0,0,8,0"/>
<ComboBox Grid.Column="2" ItemsSource="{Binding GroupFilterOptions}"
SelectedItem="{Binding GroupFilter}"/>
<Button Grid.Column="4" Content=" Neue Aufgabe" Command="{Binding AddTaskCommand}"/>
<StackPanel Grid.Column="4" Orientation="Horizontal" Spacing="8">
<Button Content="🔔 Erinnerung" Command="{Binding AddReminderCommand}"
ToolTip.Tip="Kurze Notiz ohne Zeitbezug, z.B. 'morgen Ansage an die Klasse machen' — läuft nicht in die Zeiterfassung/Auswertung ein."/>
<Button Content=" Neue Aufgabe" Command="{Binding AddTaskCommand}"/>
</StackPanel>
</Grid>
<ScrollViewer Grid.Row="1" Margin="16,10,16,16">
@@ -30,7 +34,11 @@
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
</Button>
<StackPanel Grid.Column="1" Margin="10,0" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" FontSize="13"/>
<StackPanel Orientation="Horizontal" Spacing="4">
<TextBlock Text="🔔" FontSize="12" IsVisible="{Binding IsReminder}"
ToolTip.Tip="Erinnerung — kein Zeitbezug, nicht Teil der Auswertung"/>
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" FontSize="13"/>
</StackPanel>
<TextBlock FontSize="11" Opacity="0.6">
<Run Text="{Binding CategoryLabel}"/>
<Run Text=" · "/>
@@ -17,13 +17,13 @@ public partial class WorkTaskListView : UserControl
vm.OnEditTask = ShowEditDialog;
}
private async Task<WorkTask?> ShowEditDialog(WorkTask? source)
private async Task<WorkTask?> ShowEditDialog(WorkTask? source, bool startAsReminder)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
var groups = App.Services.GetRequiredService<IGroupRepository>().GetAll(includeInactive: true);
var vm = new AddEditWorkTaskDialogViewModel(source, groups);
var vm = new AddEditWorkTaskDialogViewModel(source, groups, startAsReminder);
var dialog = new AddEditWorkTaskDialog { DataContext = vm };
await dialog.ShowDialog<bool>(owner);
return vm.Result;