Dashboard und Schnellnavigation fix
This commit is contained in:
@@ -56,6 +56,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
[ObservableProperty] private string _currentSchoolYear = "";
|
||||
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
|
||||
[ObservableProperty] private string _selectedDayLabel = "";
|
||||
[ObservableProperty] private DateOnly _selectedCalendarDate = DateOnly.FromDateTime(DateTime.Today);
|
||||
[ObservableProperty] private bool _isDashboardSettingsOpen;
|
||||
[ObservableProperty] private bool _isWeatherPanelVisible;
|
||||
[ObservableProperty] private string _weatherSummary = "";
|
||||
@@ -196,7 +197,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
IsHighPriority = t.Priority == TaskPriority.High });
|
||||
|
||||
CurrentGroups.Clear();
|
||||
foreach (var g in groups.Values.OrderBy(g => g.Name))
|
||||
foreach (var g in groups.Values)
|
||||
CurrentGroups.Add(new()
|
||||
{
|
||||
GroupId = g.Id,
|
||||
@@ -749,9 +750,35 @@ public partial class DashboardViewModel : ObservableObject
|
||||
{
|
||||
if (day is null) return;
|
||||
foreach (var cell in CalendarDays) cell.IsSelected = cell == day;
|
||||
SelectedCalendarDate = day.Date;
|
||||
SelectedDayLabel = day.Date.ToString("dddd, d. MMMM", De);
|
||||
SelectedDayEvents.Clear();
|
||||
foreach (var item in day.Events) SelectedDayEvents.Add(item);
|
||||
SortCurrentGroups(day.Date);
|
||||
}
|
||||
|
||||
private void SortCurrentGroups(DateOnly date)
|
||||
{
|
||||
var schoolHolidays = _schoolHolidays.GetAll();
|
||||
var publicHolidays = _publicHolidays.GetHolidays(date.Year, _calendarSettings.State)
|
||||
.Select(h => h.Date).ToHashSet();
|
||||
var isFreeDay = IsFreeDay(date, schoolHolidays, publicHolidays);
|
||||
var cancelledPeriods = _substitutions.GetByDate(date)
|
||||
.Where(s => s.Kind == SubstitutionKind.Cancelled)
|
||||
.Select(s => s.PeriodNumber).ToHashSet();
|
||||
|
||||
foreach (var chip in CurrentGroups)
|
||||
{
|
||||
var hasLesson = _lessons.GetByGroupAndDate(chip.GroupId, date).Count > 0;
|
||||
var hasActiveSlot = !isFreeDay && _timetableSlots.GetByGroup(chip.GroupId)
|
||||
.Any(s => s.Weekday == date.DayOfWeek && !cancelledPeriods.Contains(s.PeriodNumber));
|
||||
chip.IsOnSelectedDay = hasLesson || hasActiveSlot;
|
||||
}
|
||||
|
||||
var sorted = CurrentGroups.OrderByDescending(g => g.IsOnSelectedDay)
|
||||
.ThenBy(g => g.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
CurrentGroups.Clear();
|
||||
foreach (var chip in sorted) CurrentGroups.Add(chip);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -891,7 +918,13 @@ public class LessonItem
|
||||
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 bool IsReminder { get; set; } public bool IsHighPriority { get; set; } }
|
||||
public class GroupChip { public Guid GroupId { get; set; } public string Name { get; set; } = ""; public string Subject { get; set; } = ""; }
|
||||
public class GroupChip
|
||||
{
|
||||
public Guid GroupId { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string Subject { get; set; } = "";
|
||||
public bool IsOnSelectedDay { get; set; }
|
||||
}
|
||||
|
||||
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ public partial class GlobalSearchViewModel : ObservableObject
|
||||
|
||||
private readonly IStudentRepository _students;
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
|
||||
@@ -30,13 +31,15 @@ public partial class GlobalSearchViewModel : ObservableObject
|
||||
public Action<GlobalSearchResult>? OnNavigate { get; set; }
|
||||
public Func<bool, Task>? OnQuickAddTask { get; set; }
|
||||
public Func<Task>? OnQuickAddStudent { get; set; }
|
||||
public Func<Task>? OnQuickAddGroupDocumentation { get; set; }
|
||||
public Action? OnClose { get; set; }
|
||||
|
||||
public GlobalSearchViewModel(IStudentRepository students, IGroupRepository groups,
|
||||
IExamRepository exams, IWorkTaskRepository tasks)
|
||||
ISubjectRepository subjects, IExamRepository exams, IWorkTaskRepository tasks)
|
||||
{
|
||||
_students = students;
|
||||
_groups = groups;
|
||||
_subjects = subjects;
|
||||
_exams = exams;
|
||||
_tasks = tasks;
|
||||
RefreshResults();
|
||||
@@ -59,23 +62,34 @@ public partial class GlobalSearchViewModel : ObservableObject
|
||||
|
||||
if (query.Length > 0)
|
||||
{
|
||||
var groups = _groups.GetAll(includeInactive: true);
|
||||
var groups = _groups.GetAll().Where(g => g.IsActive).ToList();
|
||||
var groupNames = groups.ToDictionary(g => g.Id, g => g.Name);
|
||||
var subjects = _subjects.GetAll().ToDictionary(s => s.Id);
|
||||
Subject? GroupSubject(LearningGroup group) => group.SubjectId is { } subjectId
|
||||
? subjects.GetValueOrDefault(subjectId)
|
||||
: null;
|
||||
static string SubjectLabel(Subject? subject) => subject is null ? ""
|
||||
: string.IsNullOrWhiteSpace(subject.ShortName)
|
||||
|| subject.Name.Equals(subject.ShortName, StringComparison.CurrentCultureIgnoreCase)
|
||||
? subject.Name
|
||||
: $"{subject.ShortName} – {subject.Name}";
|
||||
var candidates = new List<GlobalSearchResult>();
|
||||
|
||||
candidates.AddRange(_students.GetAll(includeInactive: true)
|
||||
candidates.AddRange(_students.GetAll().Where(s => s.IsActive)
|
||||
.Where(s => Matches(s.FullName, query))
|
||||
.Select(s => GlobalSearchResult.ForStudent(s)));
|
||||
|
||||
candidates.AddRange(groups
|
||||
.Where(g => Matches($"{g.Name} {g.SchoolYear} {g.GradeLevel}", query))
|
||||
.Select(GlobalSearchResult.ForGroup));
|
||||
.Where(g => Matches($"{g.Name} {GroupSubject(g)?.Name} {GroupSubject(g)?.ShortName} {g.SchoolYear} {g.GradeLevel}", query))
|
||||
.Select(g => GlobalSearchResult.ForGroup(g, SubjectLabel(GroupSubject(g)))));
|
||||
|
||||
candidates.AddRange(_exams.GetAll()
|
||||
.Where(e => groupNames.ContainsKey(e.GroupId))
|
||||
.Where(e => Matches($"{e.Title} {groupNames.GetValueOrDefault(e.GroupId)}", query))
|
||||
.Select(e => GlobalSearchResult.ForExam(e, groupNames.GetValueOrDefault(e.GroupId) ?? "")));
|
||||
|
||||
candidates.AddRange(_tasks.GetAll()
|
||||
.Where(t => t.GroupId is null || groupNames.ContainsKey(t.GroupId.Value))
|
||||
.Where(t => Matches($"{t.Title} {t.Notes} {groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty)}", query))
|
||||
.Select(t => GlobalSearchResult.ForTask(t, groupNames.GetValueOrDefault(t.GroupId ?? Guid.Empty) ?? "")));
|
||||
|
||||
@@ -99,6 +113,7 @@ public partial class GlobalSearchViewModel : ObservableObject
|
||||
GlobalSearchResult.ForAction(GlobalSearchAction.NewTask, "Aufgabe anlegen", "Mit Fälligkeit, Gruppe und Priorität", "+"),
|
||||
GlobalSearchResult.ForAction(GlobalSearchAction.NewReminder, "Erinnerung anlegen", "Kurze Notiz ohne Zeiterfassung", "◷"),
|
||||
GlobalSearchResult.ForAction(GlobalSearchAction.NewStudent, "Schüler anlegen", "Neue Stammdaten erfassen", "+"),
|
||||
GlobalSearchResult.ForAction(GlobalSearchAction.NewGroupDocumentation, "Lerngruppen-Eintrag", "Planung, Klausur oder Erinnerung dokumentieren", "N"),
|
||||
};
|
||||
|
||||
foreach (var action in actions.Where(a => query.Length == 0 || Matches(a.Title, query)))
|
||||
@@ -124,6 +139,9 @@ public partial class GlobalSearchViewModel : ObservableObject
|
||||
case GlobalSearchAction.NewStudent:
|
||||
if (OnQuickAddStudent is not null) await OnQuickAddStudent();
|
||||
break;
|
||||
case GlobalSearchAction.NewGroupDocumentation:
|
||||
if (OnQuickAddGroupDocumentation is not null) await OnQuickAddGroupDocumentation();
|
||||
break;
|
||||
default:
|
||||
OnNavigate?.Invoke(result);
|
||||
break;
|
||||
@@ -134,7 +152,7 @@ public partial class GlobalSearchViewModel : ObservableObject
|
||||
}
|
||||
|
||||
public enum GlobalSearchResultKind { Action, Student, Group, Exam, Task }
|
||||
public enum GlobalSearchAction { None, NewTask, NewReminder, NewStudent }
|
||||
public enum GlobalSearchAction { None, NewTask, NewReminder, NewStudent, NewGroupDocumentation }
|
||||
|
||||
public sealed class GlobalSearchResult
|
||||
{
|
||||
@@ -171,10 +189,13 @@ public sealed class GlobalSearchResult
|
||||
Title = student.FullName, Subtitle = student.IsActive ? "Aktiv" : "Inaktiv", Icon = "P",
|
||||
};
|
||||
|
||||
public static GlobalSearchResult ForGroup(LearningGroup group) => new()
|
||||
public static GlobalSearchResult ForGroup(LearningGroup group, string subjectName) => new()
|
||||
{
|
||||
Kind = GlobalSearchResultKind.Group, EntityId = group.Id, GroupId = group.Id,
|
||||
Title = group.Name, Subtitle = $"{group.SchoolYear} · Stufe {group.GradeLevel}", Icon = "G",
|
||||
Title = group.Name,
|
||||
Subtitle = string.Join(" · ", new[] { subjectName, group.SchoolYear, $"Stufe {group.GradeLevel}" }
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))),
|
||||
Icon = "G",
|
||||
};
|
||||
|
||||
public static GlobalSearchResult ForExam(Exam exam, string groupName) => new()
|
||||
|
||||
@@ -25,6 +25,7 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
|
||||
private List<StudentOption> _groupStudents = [];
|
||||
|
||||
public static readonly StudentOption AllStudentsOption = new(Guid.Empty, "Alle Schüler");
|
||||
public static readonly StudentOption WholeGroupOption = new(Guid.Empty, "Gesamte Lerngruppe");
|
||||
|
||||
public ObservableCollection<DocumentationItem> Entries { get; } = [];
|
||||
public ObservableCollection<StudentOption> StudentFilterOptions { get; } = [AllStudentsOption];
|
||||
@@ -62,8 +63,6 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
|
||||
private void Load()
|
||||
{
|
||||
Entries.Clear();
|
||||
if (_groupStudents.Count == 0) return;
|
||||
|
||||
var studentNameById = _groupStudents.ToDictionary(s => s.Id, s => s.Name);
|
||||
var groupNameCache = new Dictionary<Guid, string>();
|
||||
string GroupLabel(Guid id)
|
||||
@@ -80,6 +79,10 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
|
||||
|
||||
var all = relevantStudentIds
|
||||
.SelectMany(id => _docs.GetByStudent(id))
|
||||
.Concat(SelectedStudentFilter.Id == Guid.Empty
|
||||
? _docs.GetAll().Where(d => d.StudentId == Guid.Empty && d.GroupId == _groupId)
|
||||
: [])
|
||||
.DistinctBy(d => d.Id)
|
||||
.Where(d => !OnlyThisGroup || d.GroupId == _groupId)
|
||||
.OrderByDescending(d => d.IsDraft)
|
||||
.ThenByDescending(d => d.Date)
|
||||
@@ -90,7 +93,10 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
|
||||
{
|
||||
var isOwnGroup = d.GroupId is null || d.GroupId == _groupId;
|
||||
var otherGroupLabel = isOwnGroup ? "" : GroupLabel(d.GroupId!.Value);
|
||||
Entries.Add(new DocumentationItem(d, studentNameById.GetValueOrDefault(d.StudentId, ""),
|
||||
var studentName = d.StudentId == Guid.Empty
|
||||
? WholeGroupOption.Name
|
||||
: studentNameById.GetValueOrDefault(d.StudentId, "");
|
||||
Entries.Add(new DocumentationItem(d, studentName,
|
||||
isOwnGroup, otherGroupLabel));
|
||||
}
|
||||
}
|
||||
@@ -100,7 +106,7 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
|
||||
[RelayCommand]
|
||||
private async Task AddDocumentation()
|
||||
{
|
||||
if (OnEditDocumentation is null || _groupStudents.Count == 0) return;
|
||||
if (OnEditDocumentation is null) return;
|
||||
var result = await OnEditDocumentation(_groupId, _groupStudents, null);
|
||||
if (result is null) return;
|
||||
_docs.Save(result);
|
||||
|
||||
@@ -17,7 +17,7 @@ public record StudentOption(Guid Id, string Name);
|
||||
public static class DocumentationTypeDisplay
|
||||
{
|
||||
public static string[] Options { get; } =
|
||||
["Gespräch", "Vorkommnis", "Förderplan", "Fehlzeit", "Elternanruf", "Elternbrief"];
|
||||
["Gespräch", "Vorkommnis", "Förderplan", "Fehlzeit", "Elternanruf", "Elternbrief", "Planung / Erinnerung"];
|
||||
|
||||
public static string Label(DocumentationType t) => t switch
|
||||
{
|
||||
@@ -27,6 +27,7 @@ public static class DocumentationTypeDisplay
|
||||
DocumentationType.Absence => "Fehlzeit",
|
||||
DocumentationType.ParentCall => "Elternanruf",
|
||||
DocumentationType.ParentLetter => "Elternbrief",
|
||||
DocumentationType.Planning => "Planung / Erinnerung",
|
||||
_ => "",
|
||||
};
|
||||
|
||||
@@ -37,6 +38,7 @@ public static class DocumentationTypeDisplay
|
||||
"Fehlzeit" => DocumentationType.Absence,
|
||||
"Elternanruf" => DocumentationType.ParentCall,
|
||||
"Elternbrief" => DocumentationType.ParentLetter,
|
||||
"Planung / Erinnerung" => DocumentationType.Planning,
|
||||
_ => DocumentationType.Conversation,
|
||||
};
|
||||
}
|
||||
@@ -301,7 +303,7 @@ public partial class DocumentationDialogViewModel : ObservableObject
|
||||
LetterSentDateError = ""; LetterResponseDateError = "";
|
||||
var valid = true;
|
||||
|
||||
if (CanPickStudent && SelectedStudent is null) { StudentError = "Schüler auswählen."; valid = false; }
|
||||
if (CanPickStudent && SelectedStudent is null) { StudentError = "Bezug auswählen."; valid = false; }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Models;
|
||||
using System.Globalization;
|
||||
|
||||
namespace LehrerApp.Desktop.ViewModels.Students;
|
||||
|
||||
public sealed record GroupDocumentationOption(Guid Id, string DisplayName);
|
||||
|
||||
/// Kompakte Erfassung einer gruppenweiten Planungsnotiz aus der globalen Befehlspalette.
|
||||
public partial class GroupDocumentationQuickViewModel(IEnumerable<GroupDocumentationOption> groups)
|
||||
: ObservableObject
|
||||
{
|
||||
public List<GroupDocumentationOption> Groups { get; } = groups.ToList();
|
||||
[ObservableProperty] private GroupDocumentationOption? _selectedGroup;
|
||||
[ObservableProperty] private string _title = "";
|
||||
[ObservableProperty] private string _content = "";
|
||||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||
[ObservableProperty] private string _groupError = "";
|
||||
[ObservableProperty] private string _titleError = "";
|
||||
[ObservableProperty] private string _dateError = "";
|
||||
|
||||
public Documentation? Result { get; private set; }
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
GroupError = TitleError = DateError = "";
|
||||
if (SelectedGroup is null) GroupError = "Lerngruppe auswählen.";
|
||||
if (string.IsNullOrWhiteSpace(Title)) TitleError = "Titel erforderlich.";
|
||||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
|
||||
DateError = "Format TT.MM.JJJJ.";
|
||||
if (GroupError.Length > 0 || TitleError.Length > 0 || DateError.Length > 0) return;
|
||||
|
||||
Result = new Documentation
|
||||
{
|
||||
StudentId = Guid.Empty,
|
||||
GroupId = SelectedGroup!.Id,
|
||||
Type = DocumentationType.Planning,
|
||||
Date = date,
|
||||
Title = Title.Trim(),
|
||||
Content = Content.Trim(),
|
||||
ExcludeFromWebUntisSync = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user