feat: complete dashboard chapter
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LehrerApp.Core.Services;
|
||||
|
||||
public sealed class DashboardCardSetting
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public bool IsVisible { get; set; } = true;
|
||||
public int Order { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Speichert Sichtbarkeit und Reihenfolge der Dashboard-Kacheln lokal.</summary>
|
||||
public sealed class DashboardSettingsService
|
||||
{
|
||||
public static readonly string[] DefaultCardOrder =
|
||||
[
|
||||
"today", "tasks", "calendar", "excuses", "upcoming",
|
||||
"corrections", "alerts", "attendance", "support", "groups",
|
||||
];
|
||||
|
||||
private readonly string _configPath;
|
||||
|
||||
public DashboardSettingsService(string appDataPath) =>
|
||||
_configPath = Path.Combine(appDataPath, "dashboardsettings.json");
|
||||
|
||||
public List<DashboardCardSetting> Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
var saved = JsonSerializer.Deserialize<List<DashboardCardSetting>>(
|
||||
File.ReadAllText(_configPath)) ?? [];
|
||||
var byKey = saved
|
||||
.Where(s => DefaultCardOrder.Contains(s.Key))
|
||||
.GroupBy(s => s.Key).ToDictionary(g => g.Key, g => g.First());
|
||||
return DefaultCardOrder.Select((key, defaultOrder) => byKey.TryGetValue(key, out var item)
|
||||
? new DashboardCardSetting { Key = key, IsVisible = item.IsVisible, Order = item.Order }
|
||||
: new DashboardCardSetting { Key = key, IsVisible = true, Order = defaultOrder })
|
||||
.OrderBy(s => s.Order).ThenBy(s => Array.IndexOf(DefaultCardOrder, s.Key))
|
||||
.Select((s, index) => new DashboardCardSetting
|
||||
{ Key = s.Key, IsVisible = s.IsVisible, Order = index })
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch { /* beschädigte Konfiguration -> Standardreihenfolge */ }
|
||||
|
||||
return DefaultCardOrder.Select((key, index) => new DashboardCardSetting
|
||||
{ Key = key, IsVisible = true, Order = index }).ToList();
|
||||
}
|
||||
|
||||
public void Save(IEnumerable<DashboardCardSetting> settings)
|
||||
{
|
||||
var normalized = settings.Select((s, index) => new DashboardCardSetting
|
||||
{
|
||||
Key = s.Key,
|
||||
IsVisible = s.IsVisible,
|
||||
Order = index,
|
||||
}).ToList();
|
||||
File.WriteAllText(_configPath, JsonSerializer.Serialize(normalized));
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,31 @@ public sealed class DashboardViewModelTests
|
||||
return new PeriodScheduleService(tempPath);
|
||||
}
|
||||
|
||||
private static DashboardSettingsService NewDashboardSettings()
|
||||
{
|
||||
var tempPath = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(), $"lehrerapp-dashboardsettings-tests-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
return new DashboardSettingsService(tempPath);
|
||||
}
|
||||
|
||||
private static DashboardViewModel BuildVm(LearningGroup group, Lesson lesson,
|
||||
FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null)
|
||||
FakeTimetableSlots? slots = null, PeriodScheduleService? periodSchedule = null,
|
||||
FakeExams? exams = null, FakeResults? results = null, FakeGrades? grades = null,
|
||||
FakeReportGrades? reportGrades = null, FakeMemberships? memberships = null,
|
||||
FakeWorkTasks? tasks = null, FakeStudents? students = null, FakeDocumentation? documentation = null,
|
||||
DashboardSettingsService? dashboardSettings = null)
|
||||
{
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
return new DashboardViewModel(
|
||||
new FakeGroups([group]), new FakeSubjects([]), lessons,
|
||||
new FakeExams([]), new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
|
||||
new FakeStudents([]), new FakeDocumentation(),
|
||||
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
|
||||
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
|
||||
tasks ?? new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
|
||||
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||
new AttendanceBalanceService(), new SchoolYearService());
|
||||
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -108,4 +122,109 @@ public sealed class DashboardViewModelTests
|
||||
|
||||
Assert.Equal(group.Id, navigatedTo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpcomingDates_BuendeltKlausurenFristenUndFoerderplanPruefungen()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var exams = new FakeExams([
|
||||
new Exam { GroupId = group.Id, Title = "Chemie-Test", Date = today.AddDays(5) },
|
||||
]);
|
||||
var tasks = new FakeWorkTasks();
|
||||
tasks.Add(new WorkTask { Title = "Notenschluss", DueDate = today.AddDays(10) });
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var documentation = new FakeDocumentation();
|
||||
documentation.Add(new Documentation
|
||||
{
|
||||
StudentId = student.Id, Type = DocumentationType.SupportPlan, Title = "Leseförderung",
|
||||
SupportData = new SupportData { Status = SupportStatus.Active, ReviewDate = today.AddDays(3) },
|
||||
});
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams,
|
||||
tasks: tasks, students: new FakeStudents([student]), documentation: documentation);
|
||||
|
||||
Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.Exam && i.Title == "Chemie-Test");
|
||||
Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.Deadline && i.Title == "Notenschluss");
|
||||
Assert.Contains(vm.UpcomingDates, i => i.Kind == UpcomingDateKind.SupportPlan && i.StudentId == student.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenCorrections_ZeigtBewertungsfortschrittJeKlausur()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var exam = new Exam
|
||||
{ GroupId = group.Id, Title = "Klausur 1", Date = today.AddDays(-2), Status = ExamStatus.Conducted };
|
||||
var anna = new Student { FirstName = "Anna", LastName = "A" };
|
||||
var ben = new Student { FirstName = "Ben", LastName = "B" };
|
||||
var memberships = new FakeMemberships([
|
||||
new GroupMembership { GroupId = group.Id, StudentId = anna.Id },
|
||||
new GroupMembership { GroupId = group.Id, StudentId = ben.Id },
|
||||
]);
|
||||
var results = new FakeResults();
|
||||
results.Add(new ExamResult { ExamId = exam.Id, StudentId = anna.Id, Grade = "2" });
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
exams: new FakeExams([exam]), results: results, memberships: memberships,
|
||||
students: new FakeStudents([anna, ben]));
|
||||
|
||||
var correction = Assert.Single(vm.OpenCorrections);
|
||||
Assert.Equal(1, correction.Completed);
|
||||
Assert.Equal(2, correction.Total);
|
||||
Assert.Equal(50, correction.Percent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alerts_ErkenntDeutlichenNotenabfall()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c", GradingSystem = GradingSystem.Grades1To6 };
|
||||
var student = new Student { FirstName = "Anna", LastName = "Beispiel" };
|
||||
var memberships = new FakeMemberships([
|
||||
new GroupMembership { GroupId = group.Id, StudentId = student.Id },
|
||||
]);
|
||||
var grades = new FakeGrades();
|
||||
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-20), Value = "2" });
|
||||
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-15), Value = "2" });
|
||||
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-10), Value = "4" });
|
||||
grades.Add(new Grade { GroupId = group.Id, StudentId = student.Id, Date = today.AddDays(-5), Value = "4" });
|
||||
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, grades: grades,
|
||||
memberships: memberships, students: new FakeStudents([student]));
|
||||
|
||||
Assert.Contains(vm.Alerts, a => a.StudentId == student.Id && a.KindLabel == "Notenabfall");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kalenderauswahl_ZeigtTermineDesGeklicktenTages()
|
||||
{
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var examDate = today.AddDays(2);
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today },
|
||||
exams: new FakeExams([new Exam { GroupId = group.Id, Title = "Test", Date = examDate }]));
|
||||
var day = vm.CalendarDays.Single(d => d.Date == examDate);
|
||||
|
||||
vm.SelectCalendarDayCommand.Execute(day);
|
||||
|
||||
Assert.True(day.IsSelected);
|
||||
Assert.Contains(vm.SelectedDayEvents, e => e.Kind == CalendarEventKind.Exam && e.Title == "Test");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DashboardKacheln_SichtbarkeitUndReihenfolgeWerdenGespeichert()
|
||||
{
|
||||
var settings = NewDashboardSettings();
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var group = new LearningGroup { Name = "9c" };
|
||||
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, dashboardSettings: settings);
|
||||
vm.TodayCard.IsVisible = false;
|
||||
vm.MoveCardDownCommand.Execute(vm.TasksCard);
|
||||
|
||||
var saved = settings.Load();
|
||||
|
||||
Assert.False(saved.Single(c => c.Key == "today").IsVisible);
|
||||
Assert.True(saved.FindIndex(c => c.Key == "tasks") > 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ public class App : Application
|
||||
var dash = Services.GetRequiredService<DashboardViewModel>();
|
||||
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
||||
dash.OnNavigateToStudent = id => main.NavigateToStudent(id);
|
||||
dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 2); // Tab "Mitarbeit"
|
||||
dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 3); // Tab "Mitarbeit"
|
||||
dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren"
|
||||
|
||||
// StudentList → StudentDetail + Anlegen
|
||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||
@@ -109,7 +110,7 @@ public class App : Application
|
||||
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
|
||||
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
||||
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
|
||||
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
|
||||
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6);
|
||||
}
|
||||
|
||||
private static async Task ShowAddStudentDialog()
|
||||
|
||||
@@ -154,6 +154,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
||||
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
||||
services.AddSingleton(_ => new DashboardSettingsService(appData));
|
||||
services.AddSingleton(_ => new LetterTemplateService(appData));
|
||||
|
||||
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
|
||||
|
||||
@@ -16,6 +16,10 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly IExamResultRepository _examResults;
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly IReportGradeRepository _reportGrades;
|
||||
private readonly IGroupMembershipRepository _memberships;
|
||||
private readonly IWorkTaskRepository _tasks;
|
||||
private readonly IParticipationSessionRepository _participationSessions;
|
||||
private readonly IParticipationRepository _participationEntries;
|
||||
@@ -25,14 +29,18 @@ public partial class DashboardViewModel : ObservableObject
|
||||
private readonly PeriodScheduleService _periodSchedule;
|
||||
private readonly AttendanceBalanceService _attendanceBalance;
|
||||
private readonly SchoolYearService _sy;
|
||||
private readonly DashboardSettingsService _dashboardSettings;
|
||||
|
||||
private const int OpenExcuseMaxAgeDays = 21;
|
||||
private const int SupportPlanDueWithinDays = 14;
|
||||
private const int UpcomingWithinDays = 30;
|
||||
|
||||
[ObservableProperty] private string _greeting = "";
|
||||
[ObservableProperty] private string _currentDate = "";
|
||||
[ObservableProperty] private string _currentSchoolYear = "";
|
||||
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
|
||||
[ObservableProperty] private string _selectedDayLabel = "";
|
||||
[ObservableProperty] private bool _isDashboardSettingsOpen;
|
||||
|
||||
public string CalendarMonthLabel => CalendarMonth.ToString("MMMM yyyy", De);
|
||||
|
||||
@@ -43,6 +51,11 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
||||
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
|
||||
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
|
||||
public ObservableCollection<CorrectionProgressItem> OpenCorrections { get; } = [];
|
||||
public ObservableCollection<DashboardAlertItem> Alerts { get; } = [];
|
||||
public ObservableCollection<CalendarEventItem> SelectedDayEvents { get; } = [];
|
||||
public ObservableCollection<DashboardCardOption> DashboardCards { get; } = [];
|
||||
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
|
||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
||||
@@ -52,21 +65,40 @@ public partial class DashboardViewModel : ObservableObject
|
||||
// OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten
|
||||
// Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt.
|
||||
public Action<Guid>? OnNavigateToLesson { get; set; }
|
||||
public Action<Guid>? OnNavigateToExam { get; set; }
|
||||
|
||||
public DashboardCardOption TodayCard => Card("today");
|
||||
public DashboardCardOption TasksCard => Card("tasks");
|
||||
public DashboardCardOption CalendarCard => Card("calendar");
|
||||
public DashboardCardOption ExcusesCard => Card("excuses");
|
||||
public DashboardCardOption UpcomingCard => Card("upcoming");
|
||||
public DashboardCardOption CorrectionsCard => Card("corrections");
|
||||
public DashboardCardOption AlertsCard => Card("alerts");
|
||||
public DashboardCardOption AttendanceCard => Card("attendance");
|
||||
public DashboardCardOption SupportCard => Card("support");
|
||||
public DashboardCardOption GroupsCard => Card("groups");
|
||||
|
||||
public DashboardViewModel(IGroupRepository groups, ISubjectRepository subjects, ILessonRepository lessons,
|
||||
IExamRepository exams, IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
|
||||
IExamRepository exams, IExamResultRepository examResults, IGradeRepository grades,
|
||||
IReportGradeRepository reportGrades, IGroupMembershipRepository memberships,
|
||||
IWorkTaskRepository tasks, IParticipationSessionRepository participationSessions,
|
||||
IParticipationRepository participationEntries, IStudentRepository students,
|
||||
IDocumentationRepository documentation, ITimetableSlotRepository timetableSlots,
|
||||
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy)
|
||||
PeriodScheduleService periodSchedule, AttendanceBalanceService attendanceBalance, SchoolYearService sy,
|
||||
DashboardSettingsService dashboardSettings)
|
||||
{
|
||||
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
|
||||
_participationSessions = participationSessions; _participationEntries = participationEntries;
|
||||
_students = students; _documentation = documentation;
|
||||
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
|
||||
_attendanceBalance = attendanceBalance; _sy = sy;
|
||||
_attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
|
||||
LoadDashboardCards();
|
||||
Load();
|
||||
}
|
||||
|
||||
private DashboardCardOption Card(string key) => DashboardCards.First(c => c.Key == key);
|
||||
|
||||
private static DateOnly FirstOfMonth(DateTime d) => new(d.Year, d.Month, 1);
|
||||
|
||||
private void Load()
|
||||
@@ -123,6 +155,9 @@ public partial class DashboardViewModel : ObservableObject
|
||||
LoadOpenExcuses(groups.Values.ToList(), today);
|
||||
LoadAttendanceWarnings(today);
|
||||
LoadSupportPlanReviews(today);
|
||||
LoadUpcomingDates(groups, today);
|
||||
LoadOpenCorrections(groups, today);
|
||||
LoadAlerts(groups, today);
|
||||
}
|
||||
|
||||
// ── Fehlzeiten-Warnung (5.2.3) ────────────────────────────────────────────
|
||||
@@ -173,6 +208,174 @@ public partial class DashboardViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// ── Anstehende Termine (9.3) ─────────────────────────────────────────────
|
||||
|
||||
private void LoadUpcomingDates(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||
{
|
||||
UpcomingDates.Clear();
|
||||
var dueBy = today.AddDays(UpcomingWithinDays);
|
||||
var items = new List<UpcomingDateItem>();
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
foreach (var exam in _exams.GetByGroup(group.Id)
|
||||
.Where(e => e.Date >= today && e.Date <= dueBy && e.Status == ExamStatus.Planned))
|
||||
items.Add(new UpcomingDateItem(UpcomingDateKind.Exam, exam.Date, exam.Title,
|
||||
group.Name, group.Id, null, today));
|
||||
|
||||
foreach (var task in _tasks.GetByStatus(WorkTaskStatus.Open)
|
||||
.Concat(_tasks.GetByStatus(WorkTaskStatus.InProgress))
|
||||
.Where(t => t.DueDate.HasValue && t.DueDate.Value <= dueBy))
|
||||
items.Add(new UpcomingDateItem(UpcomingDateKind.Deadline, task.DueDate!.Value,
|
||||
task.Title, task.GroupId is Guid groupId && groups.TryGetValue(groupId, out var group)
|
||||
? group.Name : "Aufgabe", task.GroupId, null, today));
|
||||
|
||||
foreach (var doc in _documentation.GetAll()
|
||||
.Where(d => d.Type == DocumentationType.SupportPlan
|
||||
&& d.SupportData is { Status: SupportStatus.Active, ReviewDate: not null }
|
||||
&& d.SupportData.ReviewDate.Value <= dueBy))
|
||||
{
|
||||
var student = _students.GetById(doc.StudentId);
|
||||
if (student is not null)
|
||||
items.Add(new UpcomingDateItem(UpcomingDateKind.SupportPlan,
|
||||
doc.SupportData!.ReviewDate!.Value, doc.Title, student.FullName,
|
||||
doc.GroupId, doc.StudentId, today));
|
||||
}
|
||||
|
||||
foreach (var item in items.OrderBy(i => i.Date).ThenBy(i => i.Title).Take(8))
|
||||
UpcomingDates.Add(item);
|
||||
}
|
||||
|
||||
// ── Offene Korrekturen (9.4) ─────────────────────────────────────────────
|
||||
|
||||
private void LoadOpenCorrections(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||
{
|
||||
OpenCorrections.Clear();
|
||||
foreach (var group in groups.Values)
|
||||
foreach (var exam in _exams.GetByGroup(group.Id)
|
||||
.Where(e => e.Status is ExamStatus.Conducted or ExamStatus.Graded)
|
||||
.OrderBy(e => e.Date))
|
||||
{
|
||||
var expected = _memberships.GetByGroup(group.Id)
|
||||
.Count(m => GroupMembershipService.IsActiveOn(m, exam.Date));
|
||||
var evaluated = _examResults.GetByExam(exam.Id)
|
||||
.Count(r => r.Absent || !string.IsNullOrWhiteSpace(r.Grade) || r.Points.Count > 0);
|
||||
OpenCorrections.Add(new CorrectionProgressItem(exam.Id, group.Id, exam.Title,
|
||||
group.Name, exam.Date, Math.Min(evaluated, expected), expected, today));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Auffälligkeiten (9.5) ────────────────────────────────────────────────
|
||||
|
||||
private void LoadAlerts(IReadOnlyDictionary<Guid, LearningGroup> groups, DateOnly today)
|
||||
{
|
||||
Alerts.Clear();
|
||||
foreach (var warning in AttendanceWarnings)
|
||||
Alerts.Add(new DashboardAlertItem(warning.StudentId, null, warning.StudentName,
|
||||
"Fehlzeiten", $"Fehlzeitenquote {warning.AbsenceRatePercent:0.#} %", AlertSeverity.High));
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
var groupReportGrades = _reportGrades.GetByGroup(group.Id);
|
||||
foreach (var membership in _memberships.GetByGroup(group.Id)
|
||||
.Where(m => GroupMembershipService.IsActiveOn(m, today)))
|
||||
{
|
||||
var student = _students.GetById(membership.StudentId);
|
||||
if (student is null) continue;
|
||||
var values = _grades.GetByStudentAndGroup(student.Id, group.Id)
|
||||
.OrderBy(g => g.Date)
|
||||
.Select(g => int.TryParse(g.Value, out var value) ? (int?)value : null)
|
||||
.Where(v => v.HasValue).Select(v => v!.Value).ToList();
|
||||
|
||||
if (values.Count >= 4)
|
||||
{
|
||||
var previous = values.TakeLast(4).Take(2).Average();
|
||||
var recent = values.TakeLast(2).Average();
|
||||
var declined = group.GradingSystem == GradingSystem.Grades1To6
|
||||
? recent - previous >= 1.0
|
||||
: previous - recent >= 3.0;
|
||||
if (declined)
|
||||
Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
|
||||
"Notenabfall", $"{group.Name}: zuletzt {recent:0.0}, zuvor {previous:0.0}",
|
||||
AlertSeverity.Medium));
|
||||
}
|
||||
|
||||
var latestReport = groupReportGrades
|
||||
.Where(r => r.StudentId == student.Id)
|
||||
.OrderByDescending(r => r.UpdatedAt).FirstOrDefault();
|
||||
var effective = latestReport?.OverrideValue ?? latestReport?.CalculatedValue;
|
||||
if (int.TryParse(effective, out var reportValue)
|
||||
&& (group.GradingSystem == GradingSystem.Grades1To6 ? reportValue >= 5 : reportValue <= 4))
|
||||
Alerts.Add(new DashboardAlertItem(student.Id, group.Id, student.FullName,
|
||||
"Versetzungsgefährdung", $"{group.Name}: aktueller Stand {reportValue}",
|
||||
AlertSeverity.High));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Konfigurierbare Kacheln (9.6) ────────────────────────────────────────
|
||||
|
||||
private void LoadDashboardCards()
|
||||
{
|
||||
DashboardCards.Clear();
|
||||
foreach (var setting in _dashboardSettings.Load())
|
||||
{
|
||||
var option = new DashboardCardOption(setting.Key, CardTitle(setting.Key), setting.IsVisible);
|
||||
option.OnVisibilityChanged = SaveAndApplyCardLayout;
|
||||
DashboardCards.Add(option);
|
||||
}
|
||||
ApplyCardLayout();
|
||||
}
|
||||
|
||||
private static string CardTitle(string key) => key switch
|
||||
{
|
||||
"today" => "Heute", "tasks" => "Offene Aufgaben", "calendar" => "Kalender",
|
||||
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
|
||||
"corrections" => "Offene Korrekturen", "alerts" => "Auffälligkeiten",
|
||||
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
|
||||
"groups" => "Meine Lerngruppen", _ => key,
|
||||
};
|
||||
|
||||
private void ApplyCardLayout()
|
||||
{
|
||||
var visibleIndex = 0;
|
||||
foreach (var card in DashboardCards)
|
||||
{
|
||||
var index = card.IsVisible ? visibleIndex++ : 0;
|
||||
card.Row = index / 2;
|
||||
card.Column = index % 2;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveAndApplyCardLayout()
|
||||
{
|
||||
ApplyCardLayout();
|
||||
_dashboardSettings.Save(DashboardCards.Select((c, i) => new DashboardCardSetting
|
||||
{ Key = c.Key, IsVisible = c.IsVisible, Order = i }));
|
||||
}
|
||||
|
||||
[RelayCommand] private void ToggleDashboardSettings() =>
|
||||
IsDashboardSettingsOpen = !IsDashboardSettingsOpen;
|
||||
|
||||
[RelayCommand]
|
||||
private void MoveCardUp(DashboardCardOption? card)
|
||||
{
|
||||
if (card is null) return;
|
||||
var index = DashboardCards.IndexOf(card);
|
||||
if (index <= 0) return;
|
||||
DashboardCards.Move(index, index - 1);
|
||||
SaveAndApplyCardLayout();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void MoveCardDown(DashboardCardOption? card)
|
||||
{
|
||||
if (card is null) return;
|
||||
var index = DashboardCards.IndexOf(card);
|
||||
if (index < 0 || index >= DashboardCards.Count - 1) return;
|
||||
DashboardCards.Move(index, index + 1);
|
||||
SaveAndApplyCardLayout();
|
||||
}
|
||||
|
||||
[RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
|
||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
||||
|
||||
@@ -240,8 +443,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
var agg = Agg(lesson.Date);
|
||||
agg.HasLesson = true;
|
||||
if (g.IsOwnClass) agg.IsOwnClassDay = true;
|
||||
agg.Details.Add($"Unterricht: {g.Name}" +
|
||||
(string.IsNullOrWhiteSpace(lesson.Topic) ? "" : $" – {lesson.Topic}"));
|
||||
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Lesson, lesson.Date,
|
||||
g.Name, lesson.Topic, g.Id));
|
||||
}
|
||||
|
||||
foreach (var exam in _exams.GetByGroup(g.Id).Where(e => e.Date >= gridStart && e.Date <= gridEnd))
|
||||
@@ -249,7 +452,8 @@ public partial class DashboardViewModel : ObservableObject
|
||||
var agg = Agg(exam.Date);
|
||||
agg.HasExam = true;
|
||||
if (g.IsOwnClass) agg.IsOwnClassDay = true;
|
||||
agg.Details.Add($"Klausur: {exam.Title} ({g.Name})");
|
||||
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Exam, exam.Date,
|
||||
exam.Title, g.Name, g.Id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +465,26 @@ public partial class DashboardViewModel : ObservableObject
|
||||
agg?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
|
||||
agg?.Details ?? []));
|
||||
}
|
||||
SelectCalendarDay(CalendarDays.FirstOrDefault(d => d.Date == today && d.IsCurrentMonth)
|
||||
?? CalendarDays.First(d => d.IsCurrentMonth));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectCalendarDay(CalendarDayCell? day)
|
||||
{
|
||||
if (day is null) return;
|
||||
foreach (var cell in CalendarDays) cell.IsSelected = cell == day;
|
||||
SelectedDayLabel = day.Date.ToString("dddd, d. MMMM", De);
|
||||
SelectedDayEvents.Clear();
|
||||
foreach (var item in day.Events) SelectedDayEvents.Add(item);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenCalendarEvent(CalendarEventItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
if (item.Kind == CalendarEventKind.Exam) OnNavigateToExam?.Invoke(item.GroupId);
|
||||
else OnNavigateToLesson?.Invoke(item.GroupId);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -289,6 +513,19 @@ public partial class DashboardViewModel : ObservableObject
|
||||
|
||||
[RelayCommand] private void OpenGroup(GroupChip? c) { if (c is not null) OnNavigateToGroup?.Invoke(c.GroupId); }
|
||||
[RelayCommand] private void OpenLesson(LessonItem? l) { if (l is not null) OnNavigateToLesson?.Invoke(l.GroupId); }
|
||||
[RelayCommand] private void OpenUpcomingDate(UpcomingDateItem? item)
|
||||
{
|
||||
if (item?.StudentId is Guid studentId) OnNavigateToStudent?.Invoke(studentId);
|
||||
else if (item?.GroupId is Guid groupId)
|
||||
{
|
||||
if (item.Kind == UpcomingDateKind.Exam) OnNavigateToExam?.Invoke(groupId);
|
||||
else OnNavigateToGroup?.Invoke(groupId);
|
||||
}
|
||||
}
|
||||
[RelayCommand] private void OpenCorrection(CorrectionProgressItem? item)
|
||||
{ if (item is not null) OnNavigateToExam?.Invoke(item.GroupId); }
|
||||
[RelayCommand] private void OpenAlert(DashboardAlertItem? item)
|
||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
||||
[RelayCommand] private void Refresh() => Load();
|
||||
|
||||
private class DayAgg
|
||||
@@ -296,7 +533,7 @@ public partial class DashboardViewModel : ObservableObject
|
||||
public bool HasLesson;
|
||||
public bool HasExam;
|
||||
public bool IsOwnClassDay;
|
||||
public List<string> Details { get; } = [];
|
||||
public List<CalendarEventItem> Details { get; } = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,8 +606,10 @@ public class SupportPlanDueItem
|
||||
}
|
||||
}
|
||||
|
||||
public class CalendarDayCell
|
||||
public partial class CalendarDayCell : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private bool _isSelected;
|
||||
public DateOnly Date { get; }
|
||||
public int DayNumber { get; }
|
||||
public bool IsCurrentMonth { get; }
|
||||
public bool IsToday { get; }
|
||||
@@ -378,16 +617,103 @@ public class CalendarDayCell
|
||||
public bool HasExam { get; }
|
||||
public bool IsOwnClassDay { get; }
|
||||
public string Tooltip { get; }
|
||||
public IReadOnlyList<CalendarEventItem> Events { get; }
|
||||
|
||||
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
|
||||
bool hasLesson, bool hasExam, bool isOwnClassDay, List<string> details)
|
||||
bool hasLesson, bool hasExam, bool isOwnClassDay, List<CalendarEventItem> details)
|
||||
{
|
||||
Date = date;
|
||||
DayNumber = date.Day;
|
||||
IsCurrentMonth = isCurrentMonth;
|
||||
IsToday = isToday;
|
||||
HasLesson = hasLesson;
|
||||
HasExam = hasExam;
|
||||
IsOwnClassDay = isOwnClassDay;
|
||||
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy") : string.Join("\n", details);
|
||||
Events = details;
|
||||
Tooltip = details.Count == 0 ? date.ToString("dd.MM.yyyy")
|
||||
: string.Join("\n", details.Select(d => $"{d.KindLabel}: {d.Title}"));
|
||||
}
|
||||
}
|
||||
|
||||
public enum CalendarEventKind { Lesson, Exam }
|
||||
|
||||
public sealed class CalendarEventItem(CalendarEventKind kind, DateOnly date, string title,
|
||||
string subtitle, Guid groupId)
|
||||
{
|
||||
public CalendarEventKind Kind { get; } = kind;
|
||||
public DateOnly Date { get; } = date;
|
||||
public string Title { get; } = title;
|
||||
public string Subtitle { get; } = subtitle;
|
||||
public Guid GroupId { get; } = groupId;
|
||||
public string KindLabel => Kind == CalendarEventKind.Exam ? "Klausur" : "Unterricht";
|
||||
}
|
||||
|
||||
public enum UpcomingDateKind { Exam, SupportPlan, Deadline }
|
||||
|
||||
public sealed class UpcomingDateItem(UpcomingDateKind kind, DateOnly date, string title,
|
||||
string subtitle, Guid? groupId, Guid? studentId, DateOnly today)
|
||||
{
|
||||
public UpcomingDateKind Kind { get; } = kind;
|
||||
public DateOnly Date { get; } = date;
|
||||
public string Title { get; } = title;
|
||||
public string Subtitle { get; } = subtitle;
|
||||
public Guid? GroupId { get; } = groupId;
|
||||
public Guid? StudentId { get; } = studentId;
|
||||
public bool IsOverdue { get; } = date < today;
|
||||
public string DateDisplay => Date.ToString("dd.MM.");
|
||||
public string KindLabel => Kind switch
|
||||
{
|
||||
UpcomingDateKind.Exam => "Klausur",
|
||||
UpcomingDateKind.SupportPlan => "Förderplan",
|
||||
_ => "Frist",
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class CorrectionProgressItem(Guid examId, Guid groupId, string title, string groupName,
|
||||
DateOnly date, int completed, int total, DateOnly today)
|
||||
{
|
||||
public Guid ExamId { get; } = examId;
|
||||
public Guid GroupId { get; } = groupId;
|
||||
public string Title { get; } = title;
|
||||
public string GroupName { get; } = groupName;
|
||||
public DateOnly Date { get; } = date;
|
||||
public int Completed { get; } = completed;
|
||||
public int Total { get; } = total;
|
||||
public int Percent => Total == 0 ? 0 : (int)Math.Round(Completed * 100.0 / Total);
|
||||
public string ProgressDisplay => $"{Completed} von {Total} Arbeiten bewertet";
|
||||
public string DateDisplay => Date.ToString("dd.MM.yyyy");
|
||||
public bool IsOverdue => Date < today.AddDays(-7) && Completed < Total;
|
||||
}
|
||||
|
||||
public enum AlertSeverity { Medium, High }
|
||||
|
||||
public sealed class DashboardAlertItem(Guid studentId, Guid? groupId, string studentName,
|
||||
string kindLabel, string detail, AlertSeverity severity)
|
||||
{
|
||||
public Guid StudentId { get; } = studentId;
|
||||
public Guid? GroupId { get; } = groupId;
|
||||
public string StudentName { get; } = studentName;
|
||||
public string KindLabel { get; } = kindLabel;
|
||||
public string Detail { get; } = detail;
|
||||
public AlertSeverity Severity { get; } = severity;
|
||||
public string SeverityColor => Severity == AlertSeverity.High ? "#D32F2F" : "#F59E0B";
|
||||
}
|
||||
|
||||
public partial class DashboardCardOption : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private bool _isVisible;
|
||||
[ObservableProperty] private int _row;
|
||||
[ObservableProperty] private int _column;
|
||||
public string Key { get; }
|
||||
public string Title { get; }
|
||||
public Action? OnVisibilityChanged { get; set; }
|
||||
|
||||
public DashboardCardOption(string key, string title, bool isVisible)
|
||||
{
|
||||
Key = key;
|
||||
Title = title;
|
||||
_isVisible = isVisible;
|
||||
}
|
||||
|
||||
partial void OnIsVisibleChanged(bool value) => OnVisibilityChanged?.Invoke();
|
||||
}
|
||||
|
||||
@@ -9,21 +9,56 @@
|
||||
<Setter Property="Foreground" Value="Red"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
<Style Selector="Border.daycell.selected">
|
||||
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAccentBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="2"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<ScrollViewer Padding="24">
|
||||
<StackPanel Spacing="20">
|
||||
|
||||
<!-- Begrüßung -->
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel>
|
||||
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Dashboard anpassen" Command="{Binding ToggleDashboardSettingsCommand}"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto">
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}" CornerRadius="8"
|
||||
Padding="12" IsVisible="{Binding IsDashboardSettingsOpen}">
|
||||
<ItemsControl ItemsSource="{Binding DashboardCards}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel Orientation="Horizontal"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:DashboardCardOption">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="6" Padding="8" Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<CheckBox Content="{Binding Title}" IsChecked="{Binding IsVisible}" VerticalAlignment="Center"/>
|
||||
<Button Content="↑" Padding="6,2"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardUpCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Button Content="↓" Padding="6,2"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).MoveCardDownCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Border>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||
|
||||
<!-- Heutige Stunden -->
|
||||
<Border Grid.Column="0" Grid.Row="0" Margin="0,0,8,8"
|
||||
<Border Grid.Column="{Binding TodayCard.Column}" Grid.Row="{Binding TodayCard.Row}"
|
||||
IsVisible="{Binding TodayCard.IsVisible}" Margin="0,0,8,8"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
@@ -63,7 +98,8 @@
|
||||
</Border>
|
||||
|
||||
<!-- Offene Aufgaben -->
|
||||
<Border Grid.Column="1" Grid.Row="0" Margin="8,0,0,8"
|
||||
<Border Grid.Column="{Binding TasksCard.Column}" Grid.Row="{Binding TasksCard.Row}"
|
||||
IsVisible="{Binding TasksCard.IsVisible}" Margin="8,0,0,8"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
@@ -88,7 +124,8 @@
|
||||
|
||||
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
||||
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
|
||||
<Border Grid.Column="0" Grid.Row="1" Margin="0,0,8,8" HorizontalAlignment="Left"
|
||||
<Border Grid.Column="{Binding CalendarCard.Column}" Grid.Row="{Binding CalendarCard.Row}"
|
||||
IsVisible="{Binding CalendarCard.IsVisible}" Margin="0,0,8,8"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel Spacing="8">
|
||||
@@ -150,20 +187,24 @@
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CalendarDayCell">
|
||||
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
|
||||
Classes.haslesson="{Binding HasLesson}"
|
||||
Classes.outside="{Binding !IsCurrentMonth}"
|
||||
Width="32" Height="32" Margin="1" CornerRadius="6"
|
||||
ToolTip.Tip="{Binding Tooltip}">
|
||||
<Grid>
|
||||
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
||||
Text="{Binding DayNumber}" FontSize="12"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Ellipse Width="5" Height="5" Fill="#E53935" Margin="0,0,0,3"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||
IsVisible="{Binding HasExam}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="1"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).SelectCalendarDayCommand}"
|
||||
CommandParameter="{Binding}" ToolTip.Tip="{Binding Tooltip}">
|
||||
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
|
||||
Classes.haslesson="{Binding HasLesson}"
|
||||
Classes.outside="{Binding !IsCurrentMonth}"
|
||||
Classes.selected="{Binding IsSelected}"
|
||||
Width="32" Height="32" CornerRadius="6">
|
||||
<Grid>
|
||||
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
||||
Text="{Binding DayNumber}" FontSize="12"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Ellipse Width="5" Height="5" Fill="#E53935" Margin="0,0,0,3"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Bottom"
|
||||
IsVisible="{Binding HasExam}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@@ -183,11 +224,35 @@
|
||||
<TextBlock Text="Meine Klasse" FontSize="10" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
<TextBlock Text="{Binding SelectedDayLabel}" FontWeight="SemiBold" FontSize="12"/>
|
||||
<ItemsControl ItemsSource="{Binding SelectedDayEvents}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CalendarEventItem">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,3"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenCalendarEventCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Grid ColumnDefinitions="Auto,*">
|
||||
<TextBlock Text="{Binding KindLabel}" FontSize="10" Opacity="0.55" Width="58"/>
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Text="{Binding Title}" FontSize="12" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Subtitle}" FontSize="10" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine Termine an diesem Tag." Classes="emptyhint"
|
||||
IsVisible="{Binding !SelectedDayEvents.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
|
||||
<Border Grid.Column="1" Grid.Row="1" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
<Border Grid.Column="{Binding ExcusesCard.Column}" Grid.Row="{Binding ExcusesCard.Row}"
|
||||
IsVisible="{Binding ExcusesCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
@@ -220,7 +285,8 @@
|
||||
</Border>
|
||||
|
||||
<!-- Fehlzeiten-Warnung (5.2.3) -->
|
||||
<Border Grid.Column="0" Grid.Row="2" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||
<Border Grid.Column="{Binding AttendanceCard.Column}" Grid.Row="{Binding AttendanceCard.Row}"
|
||||
IsVisible="{Binding AttendanceCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
@@ -249,7 +315,8 @@
|
||||
</Border>
|
||||
|
||||
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
||||
<Border Grid.Column="1" Grid.Row="2" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
|
||||
IsVisible="{Binding SupportCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
@@ -278,8 +345,113 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Meine Lerngruppen: wächst mit der Zeit, deshalb ganz unten und volle Breite -->
|
||||
<Border Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2"
|
||||
<!-- Anstehende Termine (9.3) -->
|
||||
<Border Grid.Column="{Binding UpcomingCard.Column}" Grid.Row="{Binding UpcomingCard.Row}"
|
||||
IsVisible="{Binding UpcomingCard.IsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="ANSTEHENDE TERMINE" FontSize="11" FontWeight="Bold"
|
||||
Opacity="0.5" Margin="0,0,0,10"/>
|
||||
<ItemsControl ItemsSource="{Binding UpcomingDates}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:UpcomingDateItem">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenUpcomingDateCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Background="{DynamicResource SystemAccentColorLight2}" CornerRadius="4"
|
||||
Padding="5,2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding KindLabel}" FontSize="10"/>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="8,0">
|
||||
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding Subtitle}" FontSize="11" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding DateDisplay}" FontSize="12"
|
||||
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine Termine in den nächsten 30 Tagen." Classes="emptyhint"
|
||||
IsVisible="{Binding !UpcomingDates.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Offene Korrekturen (9.4) -->
|
||||
<Border Grid.Column="{Binding CorrectionsCard.Column}" Grid.Row="{Binding CorrectionsCard.Row}"
|
||||
IsVisible="{Binding CorrectionsCard.IsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="OFFENE KORREKTUREN" FontSize="11" FontWeight="Bold"
|
||||
Opacity="0.5" Margin="0,0,0,10"/>
|
||||
<ItemsControl ItemsSource="{Binding OpenCorrections}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CorrectionProgressItem">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,5"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenCorrectionCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<StackPanel Spacing="3">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="{Binding Title}" FontSize="13" FontWeight="SemiBold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding DateDisplay}" FontSize="11" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding GroupName}" FontSize="11" Opacity="0.6"/>
|
||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Percent}" Height="6"/>
|
||||
<TextBlock Text="{Binding ProgressDisplay}" FontSize="10" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine offenen Korrekturen." Classes="emptyhint"
|
||||
IsVisible="{Binding !OpenCorrections.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"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
<TextBlock Text="AUFFÄLLIGKEITEN" FontSize="11" FontWeight="Bold"
|
||||
Opacity="0.5" Margin="0,0,0,10"/>
|
||||
<ItemsControl ItemsSource="{Binding Alerts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:DashboardAlertItem">
|
||||
<Button Background="Transparent" BorderThickness="0" Padding="0" Margin="0,4"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
|
||||
Command="{Binding $parent[ItemsControl].((vm:DashboardViewModel)DataContext).OpenAlertCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<Grid ColumnDefinitions="4,*">
|
||||
<Border Background="{Binding SeverityColor}" CornerRadius="2" Margin="0,0,9,0"/>
|
||||
<StackPanel Grid.Column="1">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="{Binding StudentName}" FontSize="13" FontWeight="SemiBold"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding KindLabel}" FontSize="10" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Opacity="0.65"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Keine Auffälligkeiten erkannt." Classes="emptyhint"
|
||||
IsVisible="{Binding !Alerts.Count}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Meine Lerngruppen -->
|
||||
<Border Grid.Column="{Binding GroupsCard.Column}" Grid.Row="{Binding GroupsCard.Row}"
|
||||
IsVisible="{Binding GroupsCard.IsVisible}"
|
||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16">
|
||||
<StackPanel>
|
||||
|
||||
@@ -117,31 +117,36 @@
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="1"/>
|
||||
<Button Content="🪑 Sitzpläne"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="2"/>
|
||||
<Button Content="📝 Klausuren"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="3"/>
|
||||
CommandParameter="4"/>
|
||||
<Button Content="🔢 Notenübersicht"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="4"/>
|
||||
CommandParameter="5"/>
|
||||
<Button Content="📅 Unterrichtsplanung"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="5"/>
|
||||
CommandParameter="6"/>
|
||||
<Button Content="🎯 Kompetenzübersicht"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="6"/>
|
||||
CommandParameter="7"/>
|
||||
<Button Content="📋 Dokumentation"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||
Padding="12,9"
|
||||
Command="{Binding NavigateToSectionCommand}"
|
||||
CommandParameter="7"/>
|
||||
CommandParameter="8"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -1260,20 +1260,36 @@ Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreit
|
||||
Stunde, für den vollständigen Tagesüberblick bleibt der Stundenplan zuständig.
|
||||
- [x] **9.2** Direkter Absprung von einer Stunde in Mitarbeitserfassung bzw. Stundenplanung.
|
||||
**Umsetzung:** Klick auf eine Stunde in "Heute" springt in die Lerngruppe, Tab "Mitarbeit"
|
||||
(`DashboardViewModel.OnNavigateToLesson`, `NavigateToGroupDetail(id, 2)`) — bewusst anderes
|
||||
(`DashboardViewModel.OnNavigateToLesson`, `NavigateToGroupDetail(id, 3)`) — bewusst anderes
|
||||
Sprungziel als der bereits bestehende Klick in der Stundenplan-eigenen "Heute"-Ansicht
|
||||
(springt dort auf Tab "Planung", siehe 4.4.2): vom Dashboard aus ist der naheliegende nächste
|
||||
Schritt morgens eher die Mitarbeitserfassung als die Planung.
|
||||
- [ ] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
|
||||
- [ ] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
|
||||
- [ ] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
|
||||
- [ ] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
|
||||
- [ ] **9.7** Automatische Aktualisierung beim Zurücknavigieren (aktuell nur manueller Refresh).
|
||||
- [ ] **9.8** Kalender-Detailansicht: Tag im Monatskalender anklickbar/auswählbar, zeigt in
|
||||
- [x] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
|
||||
Bündelt geplante Klausuren, aktive Förderplan-Wiedervorlagen und offene Aufgaben mit
|
||||
Fälligkeitsdatum für die nächsten 30 Tage; überfällige Einträge bleiben sichtbar und alle
|
||||
Einträge springen zur passenden Lerngruppe bzw. zum Schüler.
|
||||
- [x] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
|
||||
Durchgeführte und noch nicht zurückgegebene Klausuren zeigen den Fortschritt als Zahl und
|
||||
Balken (`bewertete Arbeiten / am Klausurtag aktive Gruppenmitglieder`) und führen direkt in
|
||||
den Klausuren-Tab der Lerngruppe.
|
||||
- [x] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
|
||||
Fehlzeiten nutzen den bestehenden konfigurierten Schwellenwert. Ein Notenabfall wird beim
|
||||
Vergleich der letzten zwei mit den beiden vorherigen Einzelnoten erkannt (mindestens eine
|
||||
Notenstufe bzw. drei Punkte); Versetzungsgefährdung basiert auf dem jüngsten gespeicherten
|
||||
Zeugnisnotenstand (Note 5/6 bzw. höchstens 4 Punkte). Klick öffnet den betroffenen Schüler.
|
||||
- [x] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
|
||||
„Dashboard anpassen“ bietet für jede Kachel Sichtbarkeit sowie Hoch-/Runter-Sortierung;
|
||||
die Konfiguration wird lokal in `dashboardsettings.json` gespeichert und das Raster ohne
|
||||
Lücken neu angeordnet.
|
||||
- [x] **9.7** Automatische Aktualisierung beim Zurücknavigieren.
|
||||
`MainWindowViewModel.GetDashboard()` führt beim Wechsel zurück zum Dashboard bereits den
|
||||
`RefreshCommand` aus; damit werden Termine, Korrekturstände und Auffälligkeiten neu geladen.
|
||||
- [x] **9.8** Kalender-Detailansicht: Tag im Monatskalender anklickbar/auswählbar, zeigt in
|
||||
einem angrenzenden Feld die Termine dieses Tages (Unterricht, Klausuren, perspektivisch
|
||||
Konferenzen/Sondertermine) mit Details und Sprungmöglichkeit in die jeweilige Ansicht.
|
||||
Sinnvoll erst, wenn weitere Terminarten existieren — Abhängigkeit zu Kapitel 4
|
||||
(Unterrichtsplanung/Termine) sowie ggf. einem neuen Termine-Modell.
|
||||
Unterricht und Klausuren werden jetzt im Detailbereich der Kalenderkachel angezeigt und
|
||||
verlinkt. Weitere Terminarten können später über das vorhandene `CalendarEventItem` ergänzt
|
||||
werden, sobald dafür ein eigenes Termine-Modell existiert.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user