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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user