feat: Pädagogische Klassen-Aufgaben, Prioritäten/Checklisten, Dashboard-Schnellzugriffe
Neue Aufgabenart über bestehende Felder (WorkTask.Kind=Reminder + GroupId) statt Vererbung, mit Priorität (TaskPriority) und optionaler Abhak-Liste (ChecklistItems, wahlweise Kurs- Roster oder Freitext). Kurs-Dashboard zeigt jetzt eine "Anstehende Aufgaben"-Karte samt direktem Anlege-Button; derselbe Anlege-Einstieg wurde auch ins Hauptdashboard und (als Schnellüberblick samt fehlender Übersicht/Mitarbeit-Buttons) in die Gruppenliste gezogen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -143,6 +143,8 @@ public interface IWorkTaskRepository
|
|||||||
{
|
{
|
||||||
List<WorkTask> GetByStatus(WorkTaskStatus status);
|
List<WorkTask> GetByStatus(WorkTaskStatus status);
|
||||||
List<WorkTask> GetAll();
|
List<WorkTask> GetAll();
|
||||||
|
/// Für das "Anstehende Aufgaben"-Widget im Kurs-Dashboard (9.2/7.2).
|
||||||
|
List<WorkTask> GetByGroup(Guid groupId);
|
||||||
void Save(WorkTask task);
|
void Save(WorkTask task);
|
||||||
void Delete(Guid id);
|
void Delete(Guid id);
|
||||||
/// <summary>Siehe ISeatingPlanRepository.Restore (14.3).</summary>
|
/// <summary>Siehe ISeatingPlanRepository.Restore (14.3).</summary>
|
||||||
|
|||||||
@@ -110,9 +110,28 @@ public class WorkTask
|
|||||||
/// TimeEntry verknüpft und fließen deshalb schon rein datenmodell-bedingt (WorkloadEvaluationViewModel
|
/// TimeEntry verknüpft und fließen deshalb schon rein datenmodell-bedingt (WorkloadEvaluationViewModel
|
||||||
/// wertet nur TimeEntry aus, nicht WorkTask) nicht in die Zeitauswertung (6.3) ein.
|
/// wertet nur TimeEntry aus, nicht WorkTask) nicht in die Zeitauswertung (6.3) ein.
|
||||||
public TaskKind Kind { get; set; } = TaskKind.WorkItem;
|
public TaskKind Kind { get; set; } = TaskKind.WorkItem;
|
||||||
|
/// Nutzer-Feedback: pädagogische Erinnerungen ("Ansage an die Klasse", "Material einsammeln")
|
||||||
|
/// sollen sich je nach Dringlichkeit farblich absetzen können, unabhängig vom Fälligkeitsdatum.
|
||||||
|
public TaskPriority Priority { get; set; } = TaskPriority.Normal;
|
||||||
|
/// Optionale Abhak-Liste (Nutzer-Feedback: "Namensliste aller Kurs-Schüler" ODER freie
|
||||||
|
/// Teil-Punkte, wahlweise). Beides läuft über denselben Listentyp — bei aus der Kursliste
|
||||||
|
/// befüllten Einträgen ist StudentId gesetzt (Sprungziel zum Schüler), bei frei eingegebenen
|
||||||
|
/// Einträgen bleibt es leer. Als verschachtelte LiteDB-Liste statt eigener Collection/JSON-
|
||||||
|
/// String, siehe TODO.md-Nachtrag zu 6.1/9 — synct dadurch automatisch über den bestehenden
|
||||||
|
/// Sync-Unterbau mit (EventPublisher serialisiert das ganze WorkTask-Dokument je Save()).
|
||||||
|
public List<ChecklistItem> ChecklistItems { get; set; } = [];
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
public class ChecklistItem
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public bool IsDone { get; set; }
|
||||||
|
/// Gesetzt, wenn der Eintrag aus der Kursliste befüllt wurde (Sprungziel zum Schüler);
|
||||||
|
/// bei frei eingegebenen Einträgen null.
|
||||||
|
public Guid? StudentId { get; set; }
|
||||||
|
}
|
||||||
public class TimeEntry
|
public class TimeEntry
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
public Guid Id { get; set; } = Guid.NewGuid();
|
||||||
@@ -130,3 +149,4 @@ public enum TaskCategory { Correction, Preparation, Admin, Meeting, Other, Teach
|
|||||||
public enum WorkTaskStatus { Open, InProgress, Done }
|
public enum WorkTaskStatus { Open, InProgress, Done }
|
||||||
public enum TaskRecurrence { None, Weekly, Monthly }
|
public enum TaskRecurrence { None, Weekly, Monthly }
|
||||||
public enum TaskKind { WorkItem, Reminder }
|
public enum TaskKind { WorkItem, Reminder }
|
||||||
|
public enum TaskPriority { Low, Normal, High }
|
||||||
|
|||||||
@@ -954,4 +954,45 @@ public sealed class RepositoryTests
|
|||||||
Assert.Single(result);
|
Assert.Single(result);
|
||||||
Assert.Equal("Vertretung 8a", result[0].Description);
|
Assert.Equal("Vertretung 8a", result[0].Description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── WorkTaskRepository ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WorkTaskRepository_GetByGroup_FindetNurAufgabenDieserGruppe()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new WorkTaskRepository(db);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
repo.Save(new WorkTask { Title = "Klassenaufgabe", GroupId = groupId });
|
||||||
|
repo.Save(new WorkTask { Title = "Andere Gruppe", GroupId = Guid.NewGuid() });
|
||||||
|
repo.Save(new WorkTask { Title = "Ohne Gruppe", GroupId = null });
|
||||||
|
|
||||||
|
var result = repo.GetByGroup(groupId);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal("Klassenaufgabe", result[0].Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WorkTaskRepository_SaveUndLoad_RundetPrioritaetUndChecklistItemsRoundTrip()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var repo = new WorkTaskRepository(db);
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var task = new WorkTask
|
||||||
|
{
|
||||||
|
Title = "Material einsammeln",
|
||||||
|
Priority = TaskPriority.High,
|
||||||
|
ChecklistItems = [new ChecklistItem { Label = "Anna", StudentId = studentId, IsDone = true }],
|
||||||
|
};
|
||||||
|
|
||||||
|
repo.Save(task);
|
||||||
|
var loaded = repo.GetAll().Single(t => t.Id == task.Id);
|
||||||
|
|
||||||
|
Assert.Equal(TaskPriority.High, loaded.Priority);
|
||||||
|
var item = Assert.Single(loaded.ChecklistItems);
|
||||||
|
Assert.Equal("Anna", item.Label);
|
||||||
|
Assert.Equal(studentId, item.StudentId);
|
||||||
|
Assert.True(item.IsDone);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,6 +437,8 @@ public class WorkTaskRepository(LiteDbContext db) : IWorkTaskRepository
|
|||||||
db.Tasks.Find(t => t.Status == s).OrderBy(t => t.DueDate).ToList();
|
db.Tasks.Find(t => t.Status == s).OrderBy(t => t.DueDate).ToList();
|
||||||
public List<WorkTask> GetAll() =>
|
public List<WorkTask> GetAll() =>
|
||||||
db.Tasks.FindAll().OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList();
|
db.Tasks.FindAll().OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList();
|
||||||
|
public List<WorkTask> GetByGroup(Guid groupId) =>
|
||||||
|
db.Tasks.Find(t => t.GroupId == groupId).OrderBy(t => t.Status).ThenBy(t => t.DueDate).ToList();
|
||||||
public void Save(WorkTask t)
|
public void Save(WorkTask t)
|
||||||
{
|
{
|
||||||
t.UpdatedAt = DateTime.UtcNow;
|
t.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -462,4 +462,33 @@ public sealed class DashboardViewModelTests
|
|||||||
Assert.Equal(student.FullName, item.StudentName);
|
Assert.Equal(student.FullName, item.StudentName);
|
||||||
Assert.Equal(30.0, item.AbsenceRatePercent);
|
Assert.Equal(30.0, item.AbsenceRatePercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddTask_SpeichertErgebnisUndAktualisiertOffeneAufgaben()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, tasks: tasks);
|
||||||
|
vm.OnAddTask = startAsReminder => Task.FromResult<WorkTask?>(
|
||||||
|
new WorkTask { Title = "Ansage an die Klasse", Kind = startAsReminder ? TaskKind.Reminder : TaskKind.WorkItem });
|
||||||
|
|
||||||
|
await vm.AddReminderCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Single(tasks.GetAll());
|
||||||
|
Assert.Contains(vm.OpenTasks, t => t.Title == "Ansage an die Klasse" && t.IsReminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddTask_OhneDelegatMachtNichts()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, tasks: tasks);
|
||||||
|
|
||||||
|
await vm.AddTaskCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Empty(tasks.GetAll());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ public class FakeWorkTasks : IWorkTaskRepository
|
|||||||
public void Add(WorkTask t) => _all.Add(t);
|
public void Add(WorkTask t) => _all.Add(t);
|
||||||
public List<WorkTask> GetByStatus(WorkTaskStatus status) => _all.Where(t => t.Status == status).ToList();
|
public List<WorkTask> GetByStatus(WorkTaskStatus status) => _all.Where(t => t.Status == status).ToList();
|
||||||
public List<WorkTask> GetAll() => _all.ToList();
|
public List<WorkTask> GetAll() => _all.ToList();
|
||||||
|
public List<WorkTask> GetByGroup(Guid groupId) => _all.Where(t => t.GroupId == groupId).ToList();
|
||||||
public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); }
|
public void Save(WorkTask task) { _all.RemoveAll(t => t.Id == task.Id); _all.Add(task); }
|
||||||
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
public void Delete(Guid id) => _all.RemoveAll(t => t.Id == id);
|
||||||
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
public void Restore(Guid trashId) { } // siehe FakeSeatingPlans.Restore
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public sealed class GroupDetailViewModelTests
|
|||||||
|
|
||||||
var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks,
|
var vm = new GroupDetailViewModel(groups, students, memberships, subjects, exams, grades, tasks,
|
||||||
new GroupOverviewViewModel(new FakeLessons(), exams, new FakeSessions([]), new FakeEntries(), students,
|
new GroupOverviewViewModel(new FakeLessons(), exams, new FakeSessions([]), new FakeEntries(), students,
|
||||||
new FakeDocumentation(), new AttendanceBalanceService(), new SchoolYearService()),
|
new FakeDocumentation(), tasks, new AttendanceBalanceService(), new SchoolYearService()),
|
||||||
new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(),
|
new ParticipationTabViewModel(new FakeSessions([]), new FakeEntries(), new FakeAspects(),
|
||||||
students, memberships, groups, new FakeCompetencyDomains()),
|
students, memberships, groups, new FakeCompetencyDomains()),
|
||||||
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
new GradeOverviewTabViewModel(grades, exams, new FakeResults(), students, memberships, new GradingService()),
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
/// Tests für den Schnellüberblick im Auswahl-Panel der Gruppenliste (Nutzer-Feedback:
|
||||||
|
/// "oberhalb der Buttonliste ein paar Daten auswerfen. Nächste Stunde, nächste Arbeit,
|
||||||
|
/// wichtige Todos").
|
||||||
|
public sealed class GroupListViewModelTests
|
||||||
|
{
|
||||||
|
private static GroupListViewModel BuildVm(LearningGroup group, FakeLessons? lessons = null,
|
||||||
|
FakeExams? exams = null, FakeWorkTasks? tasks = null) =>
|
||||||
|
new(new FakeGroups([group]), new FakeSubjects([]), new SchoolYearService(),
|
||||||
|
lessons ?? new FakeLessons(), exams ?? new FakeExams([]), tasks ?? new FakeWorkTasks());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedGroup_OhneDatenZeigtKeinenSchnellueberblick()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var vm = BuildVm(group);
|
||||||
|
|
||||||
|
vm.SelectedGroup = vm.Groups.Count > 0 ? vm.Groups[0] : new GroupListItem(group, "");
|
||||||
|
|
||||||
|
Assert.False(vm.QuickHasAnything);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedGroup_ZeigtNaechsteGeplanteStundeUndKlausur()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(new Lesson { GroupId = group.Id, Date = today.AddDays(3), Topic = "Redox", Status = LessonStatus.Planned });
|
||||||
|
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur 1" }]);
|
||||||
|
var vm = BuildVm(group, lessons: lessons, exams: exams);
|
||||||
|
|
||||||
|
vm.SelectedGroup = new GroupListItem(group, "");
|
||||||
|
|
||||||
|
Assert.True(vm.QuickHasNextLesson);
|
||||||
|
Assert.Contains("Redox", vm.QuickNextLessonLabel);
|
||||||
|
Assert.True(vm.QuickHasNextExam);
|
||||||
|
Assert.Contains("Klausur 1", vm.QuickNextExamLabel);
|
||||||
|
Assert.True(vm.QuickHasAnything);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedGroup_ZeigtOffeneAufgabenDieserGruppeSortiertNachFaelligkeit()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { GroupId = group.Id, Title = "Bald fällig", DueDate = today.AddDays(1), Status = WorkTaskStatus.Open });
|
||||||
|
tasks.Add(new WorkTask { GroupId = group.Id, Title = "Erledigt", DueDate = today, Status = WorkTaskStatus.Done });
|
||||||
|
tasks.Add(new WorkTask { GroupId = Guid.NewGuid(), Title = "Andere Gruppe", DueDate = today, Status = WorkTaskStatus.Open });
|
||||||
|
var vm = BuildVm(group, tasks: tasks);
|
||||||
|
|
||||||
|
vm.SelectedGroup = new GroupListItem(group, "");
|
||||||
|
|
||||||
|
Assert.True(vm.QuickHasTasks);
|
||||||
|
Assert.Equal("Bald fällig", Assert.Single(vm.QuickTasks).Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SelectedGroup_Zuruecksetzen_LeertSchnellueberblick()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "9c" };
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var exams = new FakeExams([new Exam { GroupId = group.Id, Date = today.AddDays(10), Title = "Klausur 1" }]);
|
||||||
|
var vm = BuildVm(group, exams: exams);
|
||||||
|
vm.SelectedGroup = new GroupListItem(group, "");
|
||||||
|
Assert.True(vm.QuickHasAnything);
|
||||||
|
|
||||||
|
vm.SelectedGroup = null;
|
||||||
|
|
||||||
|
Assert.False(vm.QuickHasAnything);
|
||||||
|
Assert.Empty(vm.QuickTasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,10 +12,10 @@ public sealed class GroupOverviewViewModelTests
|
|||||||
{
|
{
|
||||||
private static GroupOverviewViewModel NewVm(FakeLessons? lessons = null, FakeExams? exams = null,
|
private static GroupOverviewViewModel NewVm(FakeLessons? lessons = null, FakeExams? exams = null,
|
||||||
FakeSessions? sessions = null, FakeEntries? entries = null, FakeStudents? students = null,
|
FakeSessions? sessions = null, FakeEntries? entries = null, FakeStudents? students = null,
|
||||||
FakeDocumentation? documentation = null) =>
|
FakeDocumentation? documentation = null, FakeWorkTasks? tasks = null) =>
|
||||||
new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), sessions ?? new FakeSessions([]),
|
new(lessons ?? new FakeLessons(), exams ?? new FakeExams([]), sessions ?? new FakeSessions([]),
|
||||||
entries ?? new FakeEntries(), students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
entries ?? new FakeEntries(), students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||||
new AttendanceBalanceService(), new SchoolYearService());
|
tasks ?? new FakeWorkTasks(), new AttendanceBalanceService(), new SchoolYearService());
|
||||||
|
|
||||||
private static (GroupOverviewViewModel Vm, FakeLessons Lessons, FakeExams Exams,
|
private static (GroupOverviewViewModel Vm, FakeLessons Lessons, FakeExams Exams,
|
||||||
FakeSessions Sessions, FakeEntries Entries, FakeStudents Students, Guid GroupId) BuildScenario(
|
FakeSessions Sessions, FakeEntries Entries, FakeStudents Students, Guid GroupId) BuildScenario(
|
||||||
@@ -301,4 +301,68 @@ public sealed class GroupOverviewViewModelTests
|
|||||||
var (vm, _, _, _, _, _, _) = BuildScenario();
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
Assert.False(vm.HasMissingHomework);
|
Assert.False(vm.HasMissingHomework);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnstehendeAufgaben_ZeigtNurOffeneAufgabenDieserGruppeSortiertNachFaelligkeit()
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var otherGroupId = Guid.NewGuid();
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
tasks.Add(new WorkTask { GroupId = groupId, Title = "Später fällig", DueDate = today.AddDays(10), Status = WorkTaskStatus.Open });
|
||||||
|
tasks.Add(new WorkTask { GroupId = groupId, Title = "Bald fällig", DueDate = today.AddDays(1), Status = WorkTaskStatus.Open });
|
||||||
|
tasks.Add(new WorkTask { GroupId = groupId, Title = "Erledigt", DueDate = today, Status = WorkTaskStatus.Done });
|
||||||
|
tasks.Add(new WorkTask { GroupId = otherGroupId, Title = "Anderer Kurs", DueDate = today, Status = WorkTaskStatus.Open });
|
||||||
|
|
||||||
|
var vm = NewVm(tasks: tasks);
|
||||||
|
vm.Initialize(groupId, "Testkurs");
|
||||||
|
|
||||||
|
Assert.True(vm.HasGroupTasks);
|
||||||
|
Assert.Equal(["Bald fällig", "Später fällig"], vm.GroupTasks.Select(t => t.Title));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AnstehendeAufgaben_OhneAufgabenIstLeer()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
|
Assert.False(vm.HasGroupTasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigateToWorkload_RuftDelegatAuf()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
|
var called = false;
|
||||||
|
vm.OnNavigateToWorkload = () => called = true;
|
||||||
|
|
||||||
|
vm.NavigateToWorkloadCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.True(called);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddGroupTask_SpeichertErgebnisMitGruppeUndAktualisiertListe()
|
||||||
|
{
|
||||||
|
var tasks = new FakeWorkTasks();
|
||||||
|
var (vm, _, _, _, _, _, groupId) = BuildScenario();
|
||||||
|
var vmWithTasks = NewVm(tasks: tasks);
|
||||||
|
vmWithTasks.Initialize(groupId, "Testkurs");
|
||||||
|
vmWithTasks.OnAddGroupTask = requestedGroupId =>
|
||||||
|
Task.FromResult<WorkTask?>(new WorkTask { Title = "Material einsammeln", GroupId = requestedGroupId });
|
||||||
|
|
||||||
|
await vmWithTasks.AddGroupTaskCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
var saved = Assert.Single(tasks.GetByGroup(groupId));
|
||||||
|
Assert.Equal("Material einsammeln", saved.Title);
|
||||||
|
Assert.True(vmWithTasks.HasGroupTasks);
|
||||||
|
Assert.Equal("Material einsammeln", Assert.Single(vmWithTasks.GroupTasks).Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AddGroupTask_OhneDelegatMachtNichts()
|
||||||
|
{
|
||||||
|
var (vm, _, _, _, _, _, _) = BuildScenario();
|
||||||
|
await vm.AddGroupTaskCommand.ExecuteAsync(null);
|
||||||
|
Assert.False(vm.HasGroupTasks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,6 +337,90 @@ public sealed class AddEditWorkTaskDialogViewModelTests
|
|||||||
var vm = new AddEditWorkTaskDialogViewModel(source, []);
|
var vm = new AddEditWorkTaskDialogViewModel(source, []);
|
||||||
Assert.True(vm.IsReminder);
|
Assert.True(vm.IsReminder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_MitPrioritaet_UebernimmtSieInsResult()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, [])
|
||||||
|
{
|
||||||
|
Title = "Wichtig",
|
||||||
|
SelectedPriority = TaskPriorityDisplay.Label(TaskPriority.High),
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(TaskPriority.High, vm.Result!.Priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Konstruktor_OhneQuelle_StartetMitNormalerPrioritaet()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, []);
|
||||||
|
Assert.Equal(TaskPriorityDisplay.Label(TaskPriority.Normal), vm.SelectedPriority);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ChecklistItems_HinzufuegenUndEntfernen_WirdInResultUebernommen()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, []) { Title = "Material einsammeln" };
|
||||||
|
vm.NewChecklistItemText = "Erste Position";
|
||||||
|
vm.AddChecklistItemCommand.Execute(null);
|
||||||
|
vm.NewChecklistItemText = "Zweite Position";
|
||||||
|
vm.AddChecklistItemCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal(["Erste Position", "Zweite Position"], vm.ChecklistItems.Select(c => c.Label));
|
||||||
|
Assert.Equal("", vm.NewChecklistItemText);
|
||||||
|
|
||||||
|
vm.RemoveChecklistItemCommand.Execute(vm.ChecklistItems[0]);
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
var item = Assert.Single(vm.Result!.ChecklistItems);
|
||||||
|
Assert.Equal("Zweite Position", item.Label);
|
||||||
|
Assert.Null(item.StudentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Konstruktor_BestehendeAufgabeMitChecklist_UebernimmtEintraege()
|
||||||
|
{
|
||||||
|
var source = new WorkTask
|
||||||
|
{
|
||||||
|
Title = "Alt",
|
||||||
|
ChecklistItems = [new ChecklistItem { Label = "Schon da", IsDone = true }],
|
||||||
|
};
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(source, []);
|
||||||
|
|
||||||
|
var row = Assert.Single(vm.ChecklistItems);
|
||||||
|
Assert.Equal("Schon da", row.Label);
|
||||||
|
Assert.True(row.IsDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FillFromRoster_OhneGruppeOderLader_MachtNichts()
|
||||||
|
{
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, []);
|
||||||
|
vm.FillFromRosterCommand.Execute(null);
|
||||||
|
Assert.Empty(vm.ChecklistItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FillFromRoster_BefuelltAusRosterUndUeberspringtDoppelteSchueler()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a" };
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var otherStudentId = Guid.NewGuid();
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(null, [group], loadRoster: _ =>
|
||||||
|
[(studentId, "Anna Beispiel"), (otherStudentId, "Ben Muster")])
|
||||||
|
{
|
||||||
|
SelectedGroup = group,
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.True(vm.CanFillFromRoster);
|
||||||
|
vm.FillFromRosterCommand.Execute(null);
|
||||||
|
Assert.Equal(2, vm.ChecklistItems.Count);
|
||||||
|
|
||||||
|
vm.FillFromRosterCommand.Execute(null);
|
||||||
|
Assert.Equal(2, vm.ChecklistItems.Count); // kein Duplikat beim zweiten Aufruf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TimeTrackingViewModelTests
|
public sealed class TimeTrackingViewModelTests
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
// OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten
|
// OnNavigateToGroup (Lerngruppen-Kacheln, Tab "Übersicht"), da der Sprung von einer konkreten
|
||||||
// Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt.
|
// Stunde aus sinnvollerweise direkt in die Mitarbeitserfassung führt.
|
||||||
public Action<Guid>? OnNavigateToLesson { get; set; }
|
public Action<Guid>? OnNavigateToLesson { get; set; }
|
||||||
|
// Direkter Anlege-Einstieg aus der "Offene Aufgaben"-Kachel (Nutzer-Feedback), statt erst über
|
||||||
|
// "Arbeitszeit" navigieren zu müssen — gleiches Dialog-Delegate-Muster wie im Kurs-Dashboard.
|
||||||
|
public Func<bool, Task<WorkTask?>>? OnAddTask { get; set; }
|
||||||
public Action<Guid>? OnNavigateToExam { get; set; }
|
public Action<Guid>? OnNavigateToExam { get; set; }
|
||||||
// Sprungziel für eine ungeplante Stunde — führt direkt in den Verlaufsplan-Tab der Gruppe
|
// Sprungziel für eine ungeplante Stunde — führt direkt in den Verlaufsplan-Tab der Gruppe
|
||||||
// (nicht den Standard-Tab von OnNavigateToGroup), damit das Thema gleich ergänzt werden kann.
|
// (nicht den Standard-Tab von OnNavigateToGroup), damit das Thema gleich ergänzt werden kann.
|
||||||
@@ -162,7 +165,8 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
OpenTasks.Add(new() { Title = t.Title,
|
OpenTasks.Add(new() { Title = t.Title,
|
||||||
DueDate = t.DueDate?.ToString("dd.MM.") ?? "",
|
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 });
|
IsReminder = t.Kind == TaskKind.Reminder,
|
||||||
|
IsHighPriority = t.Priority == TaskPriority.High });
|
||||||
|
|
||||||
CurrentGroups.Clear();
|
CurrentGroups.Clear();
|
||||||
foreach (var g in groups.Values.OrderBy(g => g.Name))
|
foreach (var g in groups.Values.OrderBy(g => g.Name))
|
||||||
@@ -678,6 +682,18 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
||||||
[RelayCommand] private void Refresh() => Load();
|
[RelayCommand] private void Refresh() => Load();
|
||||||
|
|
||||||
|
[RelayCommand] private Task AddTask() => AddTaskInternal(startAsReminder: false);
|
||||||
|
[RelayCommand] private Task AddReminder() => AddTaskInternal(startAsReminder: true);
|
||||||
|
|
||||||
|
private async Task AddTaskInternal(bool startAsReminder)
|
||||||
|
{
|
||||||
|
if (OnAddTask is null) return;
|
||||||
|
var result = await OnAddTask(startAsReminder);
|
||||||
|
if (result is null) return;
|
||||||
|
_tasks.Save(result);
|
||||||
|
Load();
|
||||||
|
}
|
||||||
|
|
||||||
private class DayAgg
|
private class DayAgg
|
||||||
{
|
{
|
||||||
public bool HasLesson;
|
public bool HasLesson;
|
||||||
@@ -698,7 +714,7 @@ public class LessonItem
|
|||||||
public string Room { get; set; } = "";
|
public string Room { get; set; } = "";
|
||||||
public bool HasRoom => !string.IsNullOrWhiteSpace(Room);
|
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 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; } = ""; }
|
||||||
|
|
||||||
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
|
// ── Offene Entschuldigungen (aus Mitarbeit-Fehltagen) ────────────────────────
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using LehrerApp.Core.Interfaces;
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.ViewModels;
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
@@ -36,6 +37,7 @@ public partial class GroupOverviewViewModel : ObservableObject
|
|||||||
private readonly IParticipationRepository _entries;
|
private readonly IParticipationRepository _entries;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IDocumentationRepository _documentation;
|
private readonly IDocumentationRepository _documentation;
|
||||||
|
private readonly IWorkTaskRepository _tasks;
|
||||||
private readonly AttendanceBalanceService _attendanceBalance;
|
private readonly AttendanceBalanceService _attendanceBalance;
|
||||||
private readonly SchoolYearService _schoolYear;
|
private readonly SchoolYearService _schoolYear;
|
||||||
|
|
||||||
@@ -69,18 +71,46 @@ public partial class GroupOverviewViewModel : ObservableObject
|
|||||||
/// 6 Planung, 7 Kompetenzen, 8 Dokumentation).
|
/// 6 Planung, 7 Kompetenzen, 8 Dokumentation).
|
||||||
public Action<int>? OnNavigateToTab { get; set; }
|
public Action<int>? OnNavigateToTab { get; set; }
|
||||||
|
|
||||||
|
/// Anders als OnNavigateToTab kein Tab innerhalb dieser Detailansicht, sondern ein Sprung in
|
||||||
|
/// den eigenständigen Top-Level-Aufgabenbereich (MainWindowViewModel.NavigateToWorkload) — von
|
||||||
|
/// hier aus nicht direkt aufrufbar (keine ViewModel-zu-ViewModel-Referenz, siehe
|
||||||
|
/// GroupDetailView.axaml.cs, gleiches Muster wie LessonViewerDialog/TeachingModeWindow).
|
||||||
|
public Action? OnNavigateToWorkload { get; set; }
|
||||||
|
|
||||||
[RelayCommand] private void NavigateToPlanning() => OnNavigateToTab?.Invoke(6);
|
[RelayCommand] private void NavigateToPlanning() => OnNavigateToTab?.Invoke(6);
|
||||||
[RelayCommand] private void NavigateToExams() => OnNavigateToTab?.Invoke(4);
|
[RelayCommand] private void NavigateToExams() => OnNavigateToTab?.Invoke(4);
|
||||||
[RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3);
|
[RelayCommand] private void NavigateToParticipation() => OnNavigateToTab?.Invoke(3);
|
||||||
[RelayCommand] private void NavigateToDocumentation() => OnNavigateToTab?.Invoke(8);
|
[RelayCommand] private void NavigateToDocumentation() => OnNavigateToTab?.Invoke(8);
|
||||||
|
[RelayCommand] private void NavigateToWorkload() => OnNavigateToWorkload?.Invoke();
|
||||||
|
|
||||||
|
// ── Anstehende Aufgaben für diese Klasse (Nutzer-Feedback: pädagogische Erinnerungen/Aufgaben
|
||||||
|
// sollen "gleichberechtigt" auch im Kurs-Dashboard stehen, nicht nur im Hauptdashboard) ───────
|
||||||
|
private const int GroupTasksMaxCount = 5;
|
||||||
|
public ObservableCollection<GroupTaskItem> GroupTasks { get; } = [];
|
||||||
|
public bool HasGroupTasks => GroupTasks.Count > 0;
|
||||||
|
|
||||||
|
/// Öffnet den Aufgaben-Dialog mit dieser Gruppe vorbelegt (Nutzer-Feedback: direkter
|
||||||
|
/// Anlege-Einstieg aus dem Kurs-Dashboard, statt erst über "Zu den Aufgaben" springen zu
|
||||||
|
/// müssen). Gleiches View-Code-Behind-Delegate-Muster wie OnNavigateToWorkload.
|
||||||
|
public Func<Guid, Task<WorkTask?>>? OnAddGroupTask { get; set; }
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task AddGroupTask()
|
||||||
|
{
|
||||||
|
if (OnAddGroupTask is null) return;
|
||||||
|
var result = await OnAddGroupTask(_groupId);
|
||||||
|
if (result is null) return;
|
||||||
|
_tasks.Save(result);
|
||||||
|
LoadGroupTasks(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
}
|
||||||
|
|
||||||
public GroupOverviewViewModel(ILessonRepository lessons, IExamRepository exams,
|
public GroupOverviewViewModel(ILessonRepository lessons, IExamRepository exams,
|
||||||
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
IParticipationSessionRepository sessions, IParticipationRepository entries,
|
||||||
IStudentRepository students, IDocumentationRepository documentation,
|
IStudentRepository students, IDocumentationRepository documentation, IWorkTaskRepository tasks,
|
||||||
AttendanceBalanceService attendanceBalance, SchoolYearService schoolYear)
|
AttendanceBalanceService attendanceBalance, SchoolYearService schoolYear)
|
||||||
{
|
{
|
||||||
_lessons = lessons; _exams = exams; _sessions = sessions;
|
_lessons = lessons; _exams = exams; _sessions = sessions;
|
||||||
_entries = entries; _students = students; _documentation = documentation;
|
_entries = entries; _students = students; _documentation = documentation; _tasks = tasks;
|
||||||
_attendanceBalance = attendanceBalance; _schoolYear = schoolYear;
|
_attendanceBalance = attendanceBalance; _schoolYear = schoolYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +132,7 @@ public partial class GroupOverviewViewModel : ObservableObject
|
|||||||
LoadDraftDocumentationCount();
|
LoadDraftDocumentationCount();
|
||||||
LoadAttendanceNotices(today);
|
LoadAttendanceNotices(today);
|
||||||
LoadMissingHomework();
|
LoadMissingHomework();
|
||||||
|
LoadGroupTasks(today);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadNextLesson(DateOnly today)
|
private void LoadNextLesson(DateOnly today)
|
||||||
@@ -260,6 +291,19 @@ public partial class GroupOverviewViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
OnPropertyChanged(nameof(HasMissingHomework));
|
OnPropertyChanged(nameof(HasMissingHomework));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadGroupTasks(DateOnly today)
|
||||||
|
{
|
||||||
|
GroupTasks.Clear();
|
||||||
|
foreach (var t in _tasks.GetByGroup(_groupId)
|
||||||
|
.Where(t => t.Status != WorkTaskStatus.Done)
|
||||||
|
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(GroupTasksMaxCount))
|
||||||
|
GroupTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder,
|
||||||
|
t.DueDate?.ToString("dd.MM.yyyy") ?? "",
|
||||||
|
t.DueDate is { } d && d < today, TaskPriorityDisplay.ColorHex(t.Priority),
|
||||||
|
t.Priority == TaskPriority.High));
|
||||||
|
OnPropertyChanged(nameof(HasGroupTasks));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class MissingHomeworkItem(string studentName, string statusLabel)
|
public sealed class MissingHomeworkItem(string studentName, string statusLabel)
|
||||||
@@ -267,3 +311,14 @@ public sealed class MissingHomeworkItem(string studentName, string statusLabel)
|
|||||||
public string StudentName { get; } = studentName;
|
public string StudentName { get; } = studentName;
|
||||||
public string StatusLabel { get; } = statusLabel;
|
public string StatusLabel { get; } = statusLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class GroupTaskItem(string title, bool isReminder, string dueDateDisplay, bool isOverdue,
|
||||||
|
string priorityColorHex, bool isHighPriority)
|
||||||
|
{
|
||||||
|
public string Title { get; } = title;
|
||||||
|
public bool IsReminder { get; } = isReminder;
|
||||||
|
public string DueDateDisplay { get; } = dueDateDisplay;
|
||||||
|
public bool IsOverdue { get; } = isOverdue;
|
||||||
|
public string PriorityColorHex { get; } = priorityColorHex;
|
||||||
|
public bool IsHighPriority { get; } = isHighPriority;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using LehrerApp.Core.Interfaces;
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||||||
@@ -12,8 +13,13 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
|
|||||||
|
|
||||||
public partial class GroupListViewModel : ObservableObject
|
public partial class GroupListViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private const int QuickTasksMaxCount = 3;
|
||||||
|
|
||||||
private readonly IGroupRepository _groups;
|
private readonly IGroupRepository _groups;
|
||||||
private readonly ISubjectRepository _subjects;
|
private readonly ISubjectRepository _subjects;
|
||||||
|
private readonly ILessonRepository _lessons;
|
||||||
|
private readonly IExamRepository _exams;
|
||||||
|
private readonly IWorkTaskRepository _tasks;
|
||||||
|
|
||||||
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
public Action<Guid, int>? OnNavigateToDetail { get; set; }
|
||||||
public Func<Task>? OnAddGroup { get; set; }
|
public Func<Task>? OnAddGroup { get; set; }
|
||||||
@@ -39,9 +45,22 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
public ObservableCollection<string> SchoolYears { get; } = [];
|
public ObservableCollection<string> SchoolYears { get; } = [];
|
||||||
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
public ObservableCollection<GroupListItem> Groups { get; } = [];
|
||||||
|
|
||||||
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy)
|
// ── Schnellüberblick im Auswahl-Panel (Nutzer-Feedback: "oberhalb der Buttonliste ein paar
|
||||||
|
// Daten auswerfen. Nächste Stunde, nächste Arbeit, wichtige Todos") — bewusst dieselben
|
||||||
|
// kompakten Kennzahlen wie die obersten Karten des Kurs-Dashboards (GroupOverviewViewModel),
|
||||||
|
// hier nur ohne eigenen Tab-Wechsel, da man ohnehin schon auf der Gruppenliste steht.
|
||||||
|
[ObservableProperty] private bool _quickHasNextLesson;
|
||||||
|
[ObservableProperty] private string _quickNextLessonLabel = "";
|
||||||
|
[ObservableProperty] private bool _quickHasNextExam;
|
||||||
|
[ObservableProperty] private string _quickNextExamLabel = "";
|
||||||
|
public ObservableCollection<GroupTaskItem> QuickTasks { get; } = [];
|
||||||
|
public bool QuickHasTasks => QuickTasks.Count > 0;
|
||||||
|
public bool QuickHasAnything => QuickHasNextLesson || QuickHasNextExam || QuickHasTasks;
|
||||||
|
|
||||||
|
public GroupListViewModel(IGroupRepository groups, ISubjectRepository subjects, SchoolYearService sy,
|
||||||
|
ILessonRepository lessons, IExamRepository exams, IWorkTaskRepository tasks)
|
||||||
{
|
{
|
||||||
_groups = groups; _subjects = subjects;
|
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||||
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
foreach (var y in sy.RecentSchoolYears()) SchoolYears.Add(y);
|
||||||
SelectedSchoolYear = sy.CurrentSchoolYear();
|
SelectedSchoolYear = sy.CurrentSchoolYear();
|
||||||
}
|
}
|
||||||
@@ -58,6 +77,44 @@ public partial class GroupListViewModel : ObservableObject
|
|||||||
RollOverGroupCommand.NotifyCanExecuteChanged();
|
RollOverGroupCommand.NotifyCanExecuteChanged();
|
||||||
ToggleArchiveCommand.NotifyCanExecuteChanged();
|
ToggleArchiveCommand.NotifyCanExecuteChanged();
|
||||||
DeleteGroupCommand.NotifyCanExecuteChanged();
|
DeleteGroupCommand.NotifyCanExecuteChanged();
|
||||||
|
LoadQuickInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadQuickInfo()
|
||||||
|
{
|
||||||
|
QuickTasks.Clear();
|
||||||
|
if (SelectedGroup is null)
|
||||||
|
{
|
||||||
|
QuickHasNextLesson = false; QuickNextLessonLabel = "";
|
||||||
|
QuickHasNextExam = false; QuickNextExamLabel = "";
|
||||||
|
OnPropertyChanged(nameof(QuickHasTasks));
|
||||||
|
OnPropertyChanged(nameof(QuickHasAnything));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupId = SelectedGroup.Id;
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
|
||||||
|
var nextLesson = _lessons.GetByGroupAndRange(groupId, today, today.AddDays(90))
|
||||||
|
.Where(l => l.Status == LessonStatus.Planned)
|
||||||
|
.OrderBy(l => l.Date).ThenBy(l => l.LessonNumber ?? 0).FirstOrDefault();
|
||||||
|
QuickHasNextLesson = nextLesson is not null;
|
||||||
|
QuickNextLessonLabel = nextLesson is null ? ""
|
||||||
|
: string.IsNullOrWhiteSpace(nextLesson.Topic)
|
||||||
|
? nextLesson.Date.ToString("dd.MM.yyyy")
|
||||||
|
: $"{nextLesson.Date:dd.MM.yyyy} — {nextLesson.Topic}";
|
||||||
|
|
||||||
|
var nextExam = _exams.GetByGroup(groupId).Where(e => e.Date >= today).MinBy(e => e.Date);
|
||||||
|
QuickHasNextExam = nextExam is not null;
|
||||||
|
QuickNextExamLabel = nextExam is null ? "" : $"{nextExam.Date:dd.MM.yyyy} — {nextExam.Title}";
|
||||||
|
|
||||||
|
foreach (var t in _tasks.GetByGroup(groupId).Where(t => t.Status != WorkTaskStatus.Done)
|
||||||
|
.OrderBy(t => t.DueDate ?? DateOnly.MaxValue).Take(QuickTasksMaxCount))
|
||||||
|
QuickTasks.Add(new GroupTaskItem(t.Title, t.Kind == TaskKind.Reminder,
|
||||||
|
t.DueDate?.ToString("dd.MM.yyyy") ?? "", t.DueDate is { } d && d < today,
|
||||||
|
TaskPriorityDisplay.ColorHex(t.Priority), t.Priority == TaskPriority.High));
|
||||||
|
OnPropertyChanged(nameof(QuickHasTasks));
|
||||||
|
OnPropertyChanged(nameof(QuickHasAnything));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadGroups()
|
public void LoadGroups()
|
||||||
|
|||||||
@@ -168,6 +168,11 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void NavigateToStudents() => NavigateTo(NavItem.Students);
|
public void NavigateToStudents() => NavigateTo(NavItem.Students);
|
||||||
|
|
||||||
|
/// Sprungziel aus dem Kurs-Dashboard ("Anstehende Aufgaben für diese Klasse") in den
|
||||||
|
/// Aufgaben-Bereich — anders als NavigateToGroupDetail/NavigateToSettings kein Tab innerhalb
|
||||||
|
/// einer Detailansicht, sondern ein eigener Top-Level-Bereich (NavItem.Workload).
|
||||||
|
public void NavigateToWorkload() => NavigateTo(NavItem.Workload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings }
|
public enum NavItem { Dashboard, Groups, Students, Exams, Planner, Workload, Settings }
|
||||||
|
|||||||
@@ -57,6 +57,30 @@ public static class TaskCategoryDisplay
|
|||||||
Enum.GetValues<TaskCategory>().FirstOrDefault(c => Label(c) == label, TaskCategory.Other);
|
Enum.GetValues<TaskCategory>().FirstOrDefault(c => Label(c) == label, TaskCategory.Other);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TaskPriorityDisplay
|
||||||
|
{
|
||||||
|
public static string Label(TaskPriority p) => p switch
|
||||||
|
{
|
||||||
|
TaskPriority.Low => "Niedrig",
|
||||||
|
TaskPriority.Normal => "Normal",
|
||||||
|
TaskPriority.High => "Hoch",
|
||||||
|
_ => p.ToString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string ColorHex(TaskPriority p) => p switch
|
||||||
|
{
|
||||||
|
TaskPriority.Low => "#9E9E9E",
|
||||||
|
TaskPriority.Normal => "#1976D2",
|
||||||
|
TaskPriority.High => "#D32F2F",
|
||||||
|
_ => "#9E9E9E",
|
||||||
|
};
|
||||||
|
|
||||||
|
public static string[] Options { get; } = Enum.GetValues<TaskPriority>().Select(Label).ToArray();
|
||||||
|
|
||||||
|
public static TaskPriority FromLabel(string? label) =>
|
||||||
|
Enum.GetValues<TaskPriority>().FirstOrDefault(p => Label(p) == label, TaskPriority.Normal);
|
||||||
|
}
|
||||||
|
|
||||||
public static class TaskRecurrenceDisplay
|
public static class TaskRecurrenceDisplay
|
||||||
{
|
{
|
||||||
public static string Label(TaskRecurrence r) => r switch
|
public static string Label(TaskRecurrence r) => r switch
|
||||||
@@ -218,6 +242,7 @@ public partial class WorkTaskListViewModel : ObservableObject
|
|||||||
EstimatedMinutes = item.Model.EstimatedMinutes,
|
EstimatedMinutes = item.Model.EstimatedMinutes,
|
||||||
Recurrence = item.Model.Recurrence,
|
Recurrence = item.Model.Recurrence,
|
||||||
Kind = item.Model.Kind,
|
Kind = item.Model.Kind,
|
||||||
|
Priority = item.Model.Priority,
|
||||||
Notes = item.Model.Notes,
|
Notes = item.Model.Notes,
|
||||||
Status = WorkTaskStatus.Open,
|
Status = WorkTaskStatus.Open,
|
||||||
});
|
});
|
||||||
@@ -250,6 +275,17 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
|
|||||||
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
|
public string StatusColorHex => WorkTaskStatusDisplay.ColorHex(Model.Status);
|
||||||
public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : "";
|
public string EstimatedMinutesDisplay => Model.EstimatedMinutes is { } m ? $"{m} min" : "";
|
||||||
|
|
||||||
|
// Priorität (Nutzer-Feedback: pädagogische Erinnerungen farblich nach Dringlichkeit absetzen) —
|
||||||
|
// nur bei High überhaupt anzeigen, Normal/Low sollen nicht zusätzlich "schreien".
|
||||||
|
public bool ShowPriority => Model.Priority == TaskPriority.High;
|
||||||
|
public string PriorityLabel => TaskPriorityDisplay.Label(Model.Priority);
|
||||||
|
public string PriorityColorHex => TaskPriorityDisplay.ColorHex(Model.Priority);
|
||||||
|
|
||||||
|
// Abhak-Liste (Nutzer-Feedback: Namensliste/Teil-Punkte optional zuschaltbar).
|
||||||
|
public bool HasChecklist => Model.ChecklistItems.Count > 0;
|
||||||
|
public string ChecklistProgressDisplay => Model.ChecklistItems.Count == 0 ? ""
|
||||||
|
: $"{Model.ChecklistItems.Count(c => c.IsDone)} / {Model.ChecklistItems.Count} erledigt";
|
||||||
|
|
||||||
// Wiederkehrende Aufgabe (6.1.4).
|
// Wiederkehrende Aufgabe (6.1.4).
|
||||||
public bool IsRecurring => Model.Recurrence != TaskRecurrence.None;
|
public bool IsRecurring => Model.Recurrence != TaskRecurrence.None;
|
||||||
public string RecurrenceLabel => $"🔁 {TaskRecurrenceDisplay.Label(Model.Recurrence)}";
|
public string RecurrenceLabel => $"🔁 {TaskRecurrenceDisplay.Label(Model.Recurrence)}";
|
||||||
@@ -266,6 +302,10 @@ public class WorkTaskListItem(WorkTask model, string? groupName, int actualMinut
|
|||||||
public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly WorkTask? _source;
|
private readonly WorkTask? _source;
|
||||||
|
/// Liefert die aktiven Mitglieder einer Gruppe (StudentId, Anzeigename) für "Aus Kursliste
|
||||||
|
/// befüllen" — optional/injizierbar statt eines festen Repository-Typs, damit dieses ViewModel
|
||||||
|
/// (wie bisher) auch ohne DI-Container in Tests per `new` gebaut werden kann.
|
||||||
|
private readonly Func<Guid, List<(Guid StudentId, string Name)>>? _loadRoster;
|
||||||
|
|
||||||
[ObservableProperty] private string _title = "";
|
[ObservableProperty] private string _title = "";
|
||||||
[ObservableProperty] private string _selectedCategory = TaskCategoryDisplay.Options[0];
|
[ObservableProperty] private string _selectedCategory = TaskCategoryDisplay.Options[0];
|
||||||
@@ -273,8 +313,10 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _dueDateText = "";
|
[ObservableProperty] private string _dueDateText = "";
|
||||||
[ObservableProperty] private string _estimatedMinutesText = "";
|
[ObservableProperty] private string _estimatedMinutesText = "";
|
||||||
[ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0];
|
[ObservableProperty] private string _selectedRecurrence = TaskRecurrenceDisplay.Options[0];
|
||||||
|
[ObservableProperty] private string _selectedPriority = TaskPriorityDisplay.Label(TaskPriority.Normal);
|
||||||
[ObservableProperty] private string _notes = "";
|
[ObservableProperty] private string _notes = "";
|
||||||
[ObservableProperty] private bool _isReminder;
|
[ObservableProperty] private bool _isReminder;
|
||||||
|
[ObservableProperty] private string _newChecklistItemText = "";
|
||||||
[ObservableProperty] private string _titleError = "";
|
[ObservableProperty] private string _titleError = "";
|
||||||
[ObservableProperty] private string _dueDateError = "";
|
[ObservableProperty] private string _dueDateError = "";
|
||||||
[ObservableProperty] private string _estimatedMinutesError = "";
|
[ObservableProperty] private string _estimatedMinutesError = "";
|
||||||
@@ -284,15 +326,24 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
: (IsReminder ? "Erinnerung bearbeiten" : "Aufgabe bearbeiten");
|
: (IsReminder ? "Erinnerung bearbeiten" : "Aufgabe bearbeiten");
|
||||||
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
|
public List<string> CategoryOptions { get; } = [.. TaskCategoryDisplay.Options];
|
||||||
public List<string> RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options];
|
public List<string> RecurrenceOptions { get; } = [.. TaskRecurrenceDisplay.Options];
|
||||||
|
public List<string> PriorityOptions { get; } = [.. TaskPriorityDisplay.Options];
|
||||||
public List<LearningGroup> Groups { get; }
|
public List<LearningGroup> Groups { get; }
|
||||||
public WorkTask? Result { get; private set; }
|
public WorkTask? Result { get; private set; }
|
||||||
|
|
||||||
partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle));
|
// Abhak-Liste (Nutzer-Feedback: wahlweise Namensliste der Kurs-Schüler oder freie Teil-Punkte,
|
||||||
|
// beides über denselben Zeilentyp).
|
||||||
|
public ObservableCollection<ChecklistItemRow> ChecklistItems { get; } = [];
|
||||||
|
public bool CanFillFromRoster => SelectedGroup is not null && _loadRoster is not null;
|
||||||
|
|
||||||
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups, bool startAsReminder = false)
|
partial void OnIsReminderChanged(bool value) => OnPropertyChanged(nameof(DialogTitle));
|
||||||
|
partial void OnSelectedGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(CanFillFromRoster));
|
||||||
|
|
||||||
|
public AddEditWorkTaskDialogViewModel(WorkTask? source, List<LearningGroup> groups, bool startAsReminder = false,
|
||||||
|
Func<Guid, List<(Guid StudentId, string Name)>>? loadRoster = null)
|
||||||
{
|
{
|
||||||
_source = source;
|
_source = source;
|
||||||
Groups = groups;
|
Groups = groups;
|
||||||
|
_loadRoster = loadRoster;
|
||||||
if (source is null) { IsReminder = startAsReminder; return; }
|
if (source is null) { IsReminder = startAsReminder; return; }
|
||||||
|
|
||||||
Title = source.Title;
|
Title = source.Title;
|
||||||
@@ -301,8 +352,37 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
|
DueDateText = source.DueDate?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "";
|
||||||
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
|
EstimatedMinutesText = source.EstimatedMinutes?.ToString(CultureInfo.InvariantCulture) ?? "";
|
||||||
SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence);
|
SelectedRecurrence = TaskRecurrenceDisplay.Label(source.Recurrence);
|
||||||
|
SelectedPriority = TaskPriorityDisplay.Label(source.Priority);
|
||||||
Notes = source.Notes ?? "";
|
Notes = source.Notes ?? "";
|
||||||
IsReminder = source.Kind == TaskKind.Reminder;
|
IsReminder = source.Kind == TaskKind.Reminder;
|
||||||
|
foreach (var item in source.ChecklistItems)
|
||||||
|
ChecklistItems.Add(new ChecklistItemRow(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void AddChecklistItem()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(NewChecklistItemText)) return;
|
||||||
|
ChecklistItems.Add(new ChecklistItemRow(NewChecklistItemText.Trim()));
|
||||||
|
NewChecklistItemText = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveChecklistItem(ChecklistItemRow? row)
|
||||||
|
{
|
||||||
|
if (row is not null) ChecklistItems.Remove(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void FillFromRoster()
|
||||||
|
{
|
||||||
|
if (SelectedGroup is null || _loadRoster is null) return;
|
||||||
|
var existingStudentIds = ChecklistItems.Where(c => c.StudentId is not null).Select(c => c.StudentId!.Value).ToHashSet();
|
||||||
|
foreach (var (studentId, name) in _loadRoster(SelectedGroup.Id))
|
||||||
|
{
|
||||||
|
if (!existingStudentIds.Contains(studentId))
|
||||||
|
ChecklistItems.Add(new ChecklistItemRow(name, studentId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -346,13 +426,39 @@ public partial class AddEditWorkTaskDialogViewModel : ObservableObject
|
|||||||
EstimatedMinutes = estimatedMinutes,
|
EstimatedMinutes = estimatedMinutes,
|
||||||
Recurrence = recurrence,
|
Recurrence = recurrence,
|
||||||
Kind = IsReminder ? TaskKind.Reminder : TaskKind.WorkItem,
|
Kind = IsReminder ? TaskKind.Reminder : TaskKind.WorkItem,
|
||||||
|
Priority = TaskPriorityDisplay.FromLabel(SelectedPriority),
|
||||||
Status = _source?.Status ?? WorkTaskStatus.Open,
|
Status = _source?.Status ?? WorkTaskStatus.Open,
|
||||||
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
|
Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(),
|
||||||
|
ChecklistItems = ChecklistItems.Select(c => new ChecklistItem
|
||||||
|
{ Id = c.Id, Label = c.Label, IsDone = c.IsDone, StudentId = c.StudentId }).ToList(),
|
||||||
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
|
CreatedAt = _source?.CreatedAt ?? DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class ChecklistItemRow : ObservableObject
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
public Guid? StudentId { get; }
|
||||||
|
[ObservableProperty] private string _label;
|
||||||
|
[ObservableProperty] private bool _isDone;
|
||||||
|
|
||||||
|
public ChecklistItemRow(ChecklistItem source)
|
||||||
|
{
|
||||||
|
Id = source.Id;
|
||||||
|
StudentId = source.StudentId;
|
||||||
|
_label = source.Label;
|
||||||
|
_isDone = source.IsDone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChecklistItemRow(string label, Guid? studentId = null)
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
StudentId = studentId;
|
||||||
|
_label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Zeiterfassung (6.2) ──────────────────────────────────────────────────────
|
// ── Zeiterfassung (6.2) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
public partial class TimeTrackingViewModel : ObservableObject
|
public partial class TimeTrackingViewModel : ObservableObject
|
||||||
|
|||||||
@@ -103,18 +103,26 @@
|
|||||||
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="OFFENE AUFGABEN" FontSize="11" FontWeight="Bold"
|
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,10">
|
||||||
Opacity="0.5" Margin="0,0,0,10"/>
|
<TextBlock Grid.Column="0" Text="OFFENE AUFGABEN" FontSize="11" FontWeight="Bold"
|
||||||
|
Opacity="0.5" VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Content="🔔+" FontSize="12" Padding="7,2" Margin="0,0,4,0"
|
||||||
|
ToolTip.Tip="Erinnerung anlegen" Command="{Binding AddReminderCommand}"/>
|
||||||
|
<Button Grid.Column="2" Content="+" FontSize="12" Padding="8,2"
|
||||||
|
ToolTip.Tip="Aufgabe anlegen" Command="{Binding AddTaskCommand}"/>
|
||||||
|
</Grid>
|
||||||
<ItemsControl ItemsSource="{Binding OpenTasks}">
|
<ItemsControl ItemsSource="{Binding OpenTasks}">
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="vm:TaskItem">
|
<DataTemplate DataType="vm:TaskItem">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,3">
|
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,3">
|
||||||
<TextBlock Grid.Column="0" Text="🔔" FontSize="12" Margin="0,0,4,0"
|
<Border Grid.Column="0" Background="#D32F2F" CornerRadius="2" Margin="0,0,6,0"
|
||||||
|
IsVisible="{Binding IsHighPriority}" ToolTip.Tip="Hohe Priorität"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="🔔" FontSize="12" Margin="0,0,4,0"
|
||||||
IsVisible="{Binding IsReminder}"
|
IsVisible="{Binding IsReminder}"
|
||||||
ToolTip.Tip="Erinnerung — kein Zeitbezug"/>
|
ToolTip.Tip="Erinnerung — kein Zeitbezug"/>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Title}"
|
<TextBlock Grid.Column="2" Text="{Binding Title}"
|
||||||
FontSize="13" TextTrimming="CharacterEllipsis"/>
|
FontSize="13" TextTrimming="CharacterEllipsis"/>
|
||||||
<TextBlock Grid.Column="2" Text="{Binding DueDate}"
|
<TextBlock Grid.Column="3" Text="{Binding DueDate}"
|
||||||
FontSize="12" Opacity="0.6" Margin="8,0,0,0"/>
|
FontSize="12" Opacity="0.6" Margin="8,0,0,0"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels;
|
||||||
|
using LehrerApp.Desktop.Views.Workload;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Dashboard;
|
namespace LehrerApp.Desktop.Views.Dashboard;
|
||||||
public partial class DashboardView : UserControl { public DashboardView() => InitializeComponent(); }
|
|
||||||
|
public partial class DashboardView : UserControl
|
||||||
|
{
|
||||||
|
public DashboardView() => InitializeComponent();
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is DashboardViewModel vm)
|
||||||
|
vm.OnAddTask = ShowAddTaskDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<WorkTask?> ShowAddTaskDialog(bool startAsReminder)
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
return await WorkTaskDialogHelper.ShowDialog(owner, startAsReminder: startAsReminder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ using LehrerApp.Core.Importing;
|
|||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using LehrerApp.Desktop.ViewModels.Students;
|
using LehrerApp.Desktop.ViewModels.Students;
|
||||||
using LehrerApp.Desktop.Services;
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.Views.Shared;
|
using LehrerApp.Desktop.Views.Shared;
|
||||||
using LehrerApp.Desktop.Views.Students;
|
using LehrerApp.Desktop.Views.Students;
|
||||||
|
using LehrerApp.Desktop.Views.Workload;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Groups;
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
@@ -34,9 +36,19 @@ public partial class GroupDetailView : UserControl
|
|||||||
vm.OnGradeExam = ShowGradeExamDialog;
|
vm.OnGradeExam = ShowGradeExamDialog;
|
||||||
vm.OnEvaluateExam = ShowEvaluateExamDialog;
|
vm.OnEvaluateExam = ShowEvaluateExamDialog;
|
||||||
vm.OnConfirmReactivate = ShowReactivateConfirmDialog;
|
vm.OnConfirmReactivate = ShowReactivateConfirmDialog;
|
||||||
|
vm.OverviewTab.OnNavigateToWorkload = () =>
|
||||||
|
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToWorkload();
|
||||||
|
vm.OverviewTab.OnAddGroupTask = ShowAddGroupTaskDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<WorkTask?> ShowAddGroupTaskDialog(Guid groupId)
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
return await WorkTaskDialogHelper.ShowDialog(owner, preselectedGroupId: groupId);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> ShowReactivateConfirmDialog()
|
private async Task<bool> ShowReactivateConfirmDialog()
|
||||||
{
|
{
|
||||||
var dialog = new ConfirmDialog
|
var dialog = new ConfirmDialog
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
x:Class="LehrerApp.Desktop.Views.Groups.GroupListView"
|
x:Class="LehrerApp.Desktop.Views.Groups.GroupListView"
|
||||||
x:DataType="vm:GroupListViewModel">
|
x:DataType="vm:GroupListViewModel">
|
||||||
|
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="TextBlock.overdue">
|
||||||
|
<Setter Property="Foreground" Value="Red"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
|
||||||
<!-- Kopfzeile mit Schuljahr-Wähler und Neu-Button -->
|
<!-- Kopfzeile mit Schuljahr-Wähler und Neu-Button -->
|
||||||
@@ -108,10 +115,48 @@
|
|||||||
|
|
||||||
<Separator/>
|
<Separator/>
|
||||||
|
|
||||||
|
<!-- Schnellüberblick (Nutzer-Feedback) -->
|
||||||
|
<StackPanel Spacing="8" IsVisible="{Binding QuickHasAnything}">
|
||||||
|
<TextBlock Text="SCHNELLÜBERBLICK" FontSize="10" FontWeight="Bold" Opacity="0.4"/>
|
||||||
|
<StackPanel Spacing="1" IsVisible="{Binding QuickHasNextLesson}">
|
||||||
|
<TextBlock Text="Nächste Stunde" FontSize="11" Opacity="0.55"/>
|
||||||
|
<TextBlock Text="{Binding QuickNextLessonLabel}" FontSize="13" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="1" IsVisible="{Binding QuickHasNextExam}">
|
||||||
|
<TextBlock Text="Nächste Klausur" FontSize="11" Opacity="0.55"/>
|
||||||
|
<TextBlock Text="{Binding QuickNextExamLabel}" FontSize="13" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4" IsVisible="{Binding QuickHasTasks}">
|
||||||
|
<TextBlock Text="Wichtige Aufgaben" FontSize="11" Opacity="0.55"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding QuickTasks}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:GroupTaskItem">
|
||||||
|
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,2">
|
||||||
|
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
|
||||||
|
Margin="0,0,6,0" IsVisible="{Binding IsHighPriority}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="🔔" FontSize="11" Margin="0,0,4,0"
|
||||||
|
IsVisible="{Binding IsReminder}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Title}" FontSize="12"
|
||||||
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
<Separator IsVisible="{Binding QuickHasAnything}"/>
|
||||||
|
|
||||||
<!-- Bereichs-Navigation -->
|
<!-- Bereichs-Navigation -->
|
||||||
<TextBlock Text="BEREICHE" FontSize="10" FontWeight="Bold"
|
<TextBlock Text="BEREICHE" FontSize="10" FontWeight="Bold"
|
||||||
Opacity="0.4" Margin="0,0,0,2"/>
|
Opacity="0.4" Margin="0,0,0,2"/>
|
||||||
<StackPanel Spacing="6">
|
<StackPanel Spacing="6">
|
||||||
|
<Button Content="📊 Übersicht"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
|
Padding="12,9"
|
||||||
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
|
CommandParameter="0"/>
|
||||||
<Button Content="👤 Schülerliste"
|
<Button Content="👤 Schülerliste"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
@@ -122,6 +167,11 @@
|
|||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="2"/>
|
CommandParameter="2"/>
|
||||||
|
<Button Content="✋ Mitarbeit"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
|
Padding="12,9"
|
||||||
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
|
CommandParameter="3"/>
|
||||||
<Button Content="📝 Klausuren"
|
<Button Content="📝 Klausuren"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
<Setter Property="Foreground" Value="#D97706"/>
|
<Setter Property="Foreground" Value="#D97706"/>
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="TextBlock.overdue">
|
||||||
|
<Setter Property="Foreground" Value="Red"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
<Style Selector="Button.cardLink">
|
<Style Selector="Button.cardLink">
|
||||||
<Setter Property="FontSize" Value="11"/>
|
<Setter Property="FontSize" Value="11"/>
|
||||||
<Setter Property="Padding" Value="0"/>
|
<Setter Property="Padding" Value="0"/>
|
||||||
@@ -165,6 +169,40 @@
|
|||||||
<Button Content="Zur Mitarbeit ›" Classes="cardLink" Command="{Binding NavigateToParticipationCommand}"/>
|
<Button Content="Zur Mitarbeit ›" Classes="cardLink" Command="{Binding NavigateToParticipationCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Anstehende Aufgaben für diese Klasse (pädagogische Erinnerungen/Aufgaben, Nutzer-
|
||||||
|
Feedback: "dürfen gerne auch im passenden Kurs-Dashboard stehen") — anders als die
|
||||||
|
übrigen Karten dieser Reihe bewusst immer sichtbar (statt Has…-gated), da sie zugleich
|
||||||
|
der Anlege-Einstieg für die erste Aufgabe dieser Gruppe ist. -->
|
||||||
|
<Border Classes="card" MinWidth="340" MaxWidth="420">
|
||||||
|
<StackPanel>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="ANSTEHENDE AUFGABEN" Classes="cardTitle"/>
|
||||||
|
<Button Grid.Column="1" Content="+" FontSize="12" Padding="8,2" Margin="0,-4,0,0"
|
||||||
|
ToolTip.Tip="Aufgabe/Erinnerung für diese Gruppe anlegen"
|
||||||
|
Command="{Binding AddGroupTaskCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding GroupTasks}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:GroupTaskItem">
|
||||||
|
<Grid ColumnDefinitions="4,Auto,*,Auto" Margin="0,4">
|
||||||
|
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
|
||||||
|
Margin="0,0,6,0" IsVisible="{Binding IsHighPriority}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="🔔" FontSize="12" Margin="0,0,4,0"
|
||||||
|
IsVisible="{Binding IsReminder}" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding Title}" FontSize="13"
|
||||||
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" FontSize="12"
|
||||||
|
VerticalAlignment="Center" Classes.overdue="{Binding IsOverdue}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Keine anstehenden Aufgaben für diese Gruppe." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !HasGroupTasks}"/>
|
||||||
|
<Button Content="Zu den Aufgaben ›" Classes="cardLink" Command="{Binding NavigateToWorkloadCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</WrapPanel>
|
</WrapPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@@ -57,17 +57,51 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
<TextBlock Text="Wiederholung" FontSize="12" Opacity="0.7"
|
<TextBlock Text="Wiederholung" FontSize="12" Opacity="0.7"
|
||||||
ToolTip.Tip="Beim Abschließen wird automatisch die nächste Instanz mit neuem Fälligkeitsdatum angelegt. Braucht ein Fälligkeitsdatum als Ausgangspunkt."/>
|
ToolTip.Tip="Beim Abschließen wird automatisch die nächste Instanz mit neuem Fälligkeitsdatum angelegt. Braucht ein Fälligkeitsdatum als Ausgangspunkt."/>
|
||||||
<ComboBox ItemsSource="{Binding RecurrenceOptions}" SelectedItem="{Binding SelectedRecurrence}"
|
<ComboBox ItemsSource="{Binding RecurrenceOptions}" SelectedItem="{Binding SelectedRecurrence}"
|
||||||
HorizontalAlignment="Stretch"/>
|
HorizontalAlignment="Stretch"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Priorität" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding PriorityOptions}" SelectedItem="{Binding SelectedPriority}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Notizen (optional)" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Notizen (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="80"/>
|
<TextBox Text="{Binding Notes}" AcceptsReturn="True" TextWrapping="Wrap" Height="80"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="Abhak-Liste (optional)" FontSize="12" Opacity="0.7"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1" Content="Aus Kursliste befüllen" FontSize="11" Padding="7,3"
|
||||||
|
Command="{Binding FillFromRosterCommand}" IsVisible="{Binding CanFillFromRoster}"/>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding ChecklistItems}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ChecklistItemRow">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto" Margin="0,2">
|
||||||
|
<CheckBox Grid.Column="0" IsChecked="{Binding IsDone}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Label}" VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<Button Grid.Column="2" Content="✕" FontSize="11" Padding="6,2"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:AddEditWorkTaskDialogViewModel)DataContext).RemoveChecklistItemCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<Grid ColumnDefinitions="*,8,Auto">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding NewChecklistItemText}" PlaceholderText="Eigener Punkt..."/>
|
||||||
|
<Button Grid.Column="2" Content="+" Command="{Binding AddChecklistItemCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using LehrerApp.Core.Interfaces;
|
||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Workload;
|
||||||
|
|
||||||
|
/// Gemeinsamer Aufruf des Aufgaben-Dialogs für alle drei Entry-Points (Aufgabenliste,
|
||||||
|
/// Kurs-Dashboard, Hauptdashboard) — vermeidet drei fast identische Kopien der Gruppen-/
|
||||||
|
/// Kursliste-Ladelogik.
|
||||||
|
public static class WorkTaskDialogHelper
|
||||||
|
{
|
||||||
|
public static async Task<WorkTask?> ShowDialog(Window owner, WorkTask? source = null,
|
||||||
|
bool startAsReminder = false, Guid? preselectedGroupId = null)
|
||||||
|
{
|
||||||
|
var groups = App.Services.GetRequiredService<IGroupRepository>().GetAll(includeInactive: true);
|
||||||
|
var vm = new AddEditWorkTaskDialogViewModel(source, groups, startAsReminder, LoadActiveRoster);
|
||||||
|
if (source is null && preselectedGroupId is { } groupId)
|
||||||
|
vm.SelectedGroup = groups.FirstOrDefault(g => g.Id == groupId);
|
||||||
|
var dialog = new AddEditWorkTaskDialog { DataContext = vm };
|
||||||
|
await dialog.ShowDialog<bool>(owner);
|
||||||
|
return vm.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Aus Kursliste befüllen" (Nutzer-Feedback): nur aktuell aktive Mitglieder, wie überall sonst
|
||||||
|
// im Dashboard/Übersicht-Tab (GroupMembershipService.IsActiveOn).
|
||||||
|
private static List<(Guid StudentId, string Name)> LoadActiveRoster(Guid groupId)
|
||||||
|
{
|
||||||
|
var memberships = App.Services.GetRequiredService<IGroupMembershipRepository>();
|
||||||
|
var students = App.Services.GetRequiredService<IStudentRepository>();
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
return memberships.GetByGroup(groupId)
|
||||||
|
.Where(m => GroupMembershipService.IsActiveOn(m, today))
|
||||||
|
.Select(m => students.GetById(m.StudentId))
|
||||||
|
.Where(s => s is not null)
|
||||||
|
.Select(s => (s!.Id, s.FullName))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,15 +25,18 @@
|
|||||||
<DataTemplate x:DataType="vm:WorkTaskListItem">
|
<DataTemplate x:DataType="vm:WorkTaskListItem">
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="6" Padding="12,10" Margin="0,0,0,8">
|
CornerRadius="6" Padding="12,10" Margin="0,0,0,8">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
|
<Grid ColumnDefinitions="4,Auto,*,Auto,Auto,Auto,Auto">
|
||||||
<Button Grid.Column="0" Background="{Binding StatusColorHex}" Padding="8,4"
|
<Border Grid.Column="0" Background="{Binding PriorityColorHex}" CornerRadius="2"
|
||||||
|
Margin="0,0,8,0" IsVisible="{Binding ShowPriority}"
|
||||||
|
ToolTip.Tip="{Binding PriorityLabel}"/>
|
||||||
|
<Button Grid.Column="1" Background="{Binding StatusColorHex}" Padding="8,4"
|
||||||
CornerRadius="4" VerticalAlignment="Center"
|
CornerRadius="4" VerticalAlignment="Center"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).CycleStatusCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).CycleStatusCommand}"
|
||||||
CommandParameter="{Binding}"
|
CommandParameter="{Binding}"
|
||||||
ToolTip.Tip="Klicken, um den Status zu wechseln">
|
ToolTip.Tip="Klicken, um den Status zu wechseln">
|
||||||
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
|
<TextBlock Text="{Binding StatusLabel}" FontSize="12" Foreground="White"/>
|
||||||
</Button>
|
</Button>
|
||||||
<StackPanel Grid.Column="1" Margin="10,0" VerticalAlignment="Center">
|
<StackPanel Grid.Column="2" Margin="10,0" VerticalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
<TextBlock Text="🔔" FontSize="12" IsVisible="{Binding IsReminder}"
|
<TextBlock Text="🔔" FontSize="12" IsVisible="{Binding IsReminder}"
|
||||||
ToolTip.Tip="Erinnerung — kein Zeitbezug, nicht Teil der Auswertung"/>
|
ToolTip.Tip="Erinnerung — kein Zeitbezug, nicht Teil der Auswertung"/>
|
||||||
@@ -50,16 +53,19 @@
|
|||||||
<!-- Ist-Zeit vs. Schätzung (6.2.4) -->
|
<!-- Ist-Zeit vs. Schätzung (6.2.4) -->
|
||||||
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
|
<TextBlock Text="{Binding ActualVsEstimateDisplay}" FontSize="11" Opacity="0.6"
|
||||||
IsVisible="{Binding HasActualTime}"/>
|
IsVisible="{Binding HasActualTime}"/>
|
||||||
|
<!-- Abhak-Liste -->
|
||||||
|
<TextBlock Text="{Binding ChecklistProgressDisplay}" FontSize="11" Opacity="0.6"
|
||||||
|
IsVisible="{Binding HasChecklist}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="2" Text="{Binding DueDateDisplay}" VerticalAlignment="Center"
|
<TextBlock Grid.Column="3" Text="{Binding DueDateDisplay}" VerticalAlignment="Center"
|
||||||
Margin="0,0,10,0" FontSize="12" Foreground="{Binding DueDateColorHex}"/>
|
Margin="0,0,10,0" FontSize="12" Foreground="{Binding DueDateColorHex}"/>
|
||||||
<TextBlock Grid.Column="3" Text="{Binding EstimatedMinutesDisplay}"
|
<TextBlock Grid.Column="4" Text="{Binding EstimatedMinutesDisplay}"
|
||||||
VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,10,0"
|
VerticalAlignment="Center" Opacity="0.6" FontSize="12" Margin="0,0,10,0"
|
||||||
IsVisible="{Binding !HasActualTime}"/>
|
IsVisible="{Binding !HasActualTime}"/>
|
||||||
<Button Grid.Column="4" Content="Bearbeiten" FontSize="11" Padding="8,3" Margin="0,0,4,0"
|
<Button Grid.Column="5" Content="Bearbeiten" FontSize="11" Padding="8,3" Margin="0,0,4,0"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).EditTaskCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).EditTaskCommand}"
|
||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}"/>
|
||||||
<Button Grid.Column="5" Content="Löschen" FontSize="11" Padding="8,3"
|
<Button Grid.Column="6" Content="Löschen" FontSize="11" Padding="8,3"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).DeleteTaskCommand}"
|
Command="{Binding $parent[ItemsControl].((vm:WorkTaskListViewModel)DataContext).DeleteTaskCommand}"
|
||||||
CommandParameter="{Binding}"/>
|
CommandParameter="{Binding}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using LehrerApp.Core.Interfaces;
|
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Desktop.ViewModels.Workload;
|
using LehrerApp.Desktop.ViewModels.Workload;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Views.Workload;
|
namespace LehrerApp.Desktop.Views.Workload;
|
||||||
|
|
||||||
@@ -21,11 +19,6 @@ public partial class WorkTaskListView : UserControl
|
|||||||
{
|
{
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner is null) return null;
|
if (owner is null) return null;
|
||||||
|
return await WorkTaskDialogHelper.ShowDialog(owner, source, startAsReminder);
|
||||||
var groups = App.Services.GetRequiredService<IGroupRepository>().GetAll(includeInactive: true);
|
|
||||||
var vm = new AddEditWorkTaskDialogViewModel(source, groups, startAsReminder);
|
|
||||||
var dialog = new AddEditWorkTaskDialog { DataContext = vm };
|
|
||||||
await dialog.ShowDialog<bool>(owner);
|
|
||||||
return vm.Result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1436,6 +1436,79 @@ CSV-Exports der Auswertung (6.3.3) über die gemeinsame Export-Infrastruktur aus
|
|||||||
nur tatsächlich erfasste `TimeEntry`-Zeilen auswertet, keine `WorkTask`-Metadaten. In der
|
nur tatsächlich erfasste `TimeEntry`-Zeilen auswertet, keine `WorkTask`-Metadaten. In der
|
||||||
Aufgabenliste und im Dashboard-Widget "Offene Aufgaben" durch ein 🔔-Symbol gekennzeichnet.
|
Aufgabenliste und im Dashboard-Widget "Offene Aufgaben" durch ein 🔔-Symbol gekennzeichnet.
|
||||||
|
|
||||||
|
**Nachtrag — Pädagogische Klassen-Aufgaben (Nutzer-Feedback):** Wunsch nach einer zweiten,
|
||||||
|
"weniger arbeitszeitrelevant als pädagogisch" gedachten Art von Todo-Item (Beispiele:
|
||||||
|
"Ansage an die Klasse", "Etwas zum Stichtag einsammeln/austeilen"), gleichberechtigt im
|
||||||
|
Hauptdashboard **und** im jeweiligen Kurs-Dashboard, farblich nach Frist/Priorität absetzbar,
|
||||||
|
mit optionaler Abhak-Liste.
|
||||||
|
|
||||||
|
Nutzervorschlag war eine eigene `TodoItem`-Basisklasse mit `JobTodo`/`KlassenTodo`-Ableitungen
|
||||||
|
(Vererbung). Dagegen entschieden: In dieser Codebasis mappt jedes Modell 1:1 auf eine eigene
|
||||||
|
benannte LiteDB-Collection (`LiteDbContext.cs`, keine `BsonMapper.Entity<T>()`-Discriminator-
|
||||||
|
Registrierung irgendwo im Projekt) — eine polymorphe Collection wäre der erste Fall dieser Art.
|
||||||
|
Zusätzlich verwendet die Papierkorb-Wiederherstellung (14.3, `LiteDbContext.MoveToTrash`/
|
||||||
|
`RestoreFromTrash<T>`) einfaches generisches `JsonSerializer.Serialize<T>`/`Deserialize<T>` ohne
|
||||||
|
Typ-Diskriminierung — polymorphes Round-Tripping bräuchte dort zusätzliche Handhabung. Stattdessen
|
||||||
|
wird die "Klassen-Aufgabe" rein über bereits vorhandene Felder abgebildet: `WorkTask.Kind ==
|
||||||
|
TaskKind.Reminder` (6.1.6, dessen Doku-Kommentar bereits "Ansage an die Klasse" als Beispiel
|
||||||
|
nennt) kombiniert mit dem längst nullable `WorkTask.GroupId`. Kein neues Modell, keine neue
|
||||||
|
Collection, kein neuer Dialog nötig — `AddEditWorkTaskDialog` konnte eine solche Aufgabe schon
|
||||||
|
vorher anlegen.
|
||||||
|
|
||||||
|
Neu auf `WorkTask`: `Priority` (`TaskPriority`: Low/Normal/High, Default Normal) und
|
||||||
|
`ChecklistItems` (`List<ChecklistItem>`, je Eintrag `Label`/`IsDone`/optionale `StudentId`).
|
||||||
|
Nutzervorschlag war "JSON als DB-String", um keine neue Tabelle zu brauchen und den bestehenden
|
||||||
|
Sync-Unterbau mitzunutzen — LiteDB verschachtelt C#-Listen/Objekte aber bereits nativ in einem
|
||||||
|
BSON-Dokument, ein JSON-String-Encoding wäre in einer Dokumenten-DB ein Umweg. Da
|
||||||
|
`WorkTaskRepository.Save` bei jedem Speichern das komplette `WorkTask`-Dokument über
|
||||||
|
`db.OnChange` an den Sync-Event-Publisher weiterreicht, syncen neue verschachtelte Felder
|
||||||
|
automatisch mit, ganz ohne Sync-Layer-Änderung — erreicht damit dasselbe Ziel wie der
|
||||||
|
JSON-String-Vorschlag, nur ohne Encoding-Zwischenschritt.
|
||||||
|
|
||||||
|
`ChecklistItems` vereinheitlicht die zwei vom Nutzer genannten Alternativen (Namensliste aller
|
||||||
|
Kurs-Schüler *oder* freie Teil-Punkte zum Abhaken) in einem Listentyp: `StudentId` ist gesetzt,
|
||||||
|
wenn ein Eintrag über "Aus Kursliste befüllen" aus der aktiven Kursmitgliedschaft erzeugt wurde
|
||||||
|
(`WorkTaskListView.LoadActiveRoster`, dieselbe `GroupMembershipService.IsActiveOn`-Prüfung wie
|
||||||
|
überall sonst), sonst bleibt es leer für frei eingetippte Punkte — beides im selben
|
||||||
|
`AddEditWorkTaskDialog`-Editor (Checkbox je Zeile, Entfernen-Button, Freitext-Eingabe).
|
||||||
|
|
||||||
|
Farbcodierung: `TaskPriorityDisplay` (Label/ColorHex/Options, gleiches Muster wie
|
||||||
|
`TaskCategoryDisplay`/`TaskRecurrenceDisplay`) — in der Aufgabenliste als linker Farbbalken nur
|
||||||
|
bei `High` sichtbar (Normal/Low sollen nicht zusätzlich "schreien"), im Hauptdashboard-Widget
|
||||||
|
"Offene Aufgaben" ebenso; Fälligkeitsfarbe (überfällig = rot) läuft weiterhin über den
|
||||||
|
bestehenden `Classes.overdue`-Stil.
|
||||||
|
|
||||||
|
Neue Karte "Anstehende Aufgaben" im Kurs-Dashboard (`GroupOverviewViewModel.GroupTasks`, über
|
||||||
|
neues `IWorkTaskRepository.GetByGroup`) — gleiches Karten-Muster wie die übrigen
|
||||||
|
Übersicht-Karten (`Has…`-Flag, `ObservableCollection`). Der "Zu den Aufgaben"-Link springt anders
|
||||||
|
als die übrigen Karten-Links nicht auf einen Tab *innerhalb* der Kurs-Detailansicht (die gibt es
|
||||||
|
für "Aufgaben" nicht), sondern in den eigenständigen Top-Level-Bereich "Arbeitszeit"
|
||||||
|
(`NavItem.Workload`) — neue `MainWindowViewModel.NavigateToWorkload()`, verdrahtet in
|
||||||
|
`GroupDetailView.axaml.cs` über `App.Services`, gleiches Delegate-Muster wie
|
||||||
|
`LessonViewerDialog`/`TeachingModeWindow` (View-Code-Behind statt ViewModel-zu-ViewModel-Referenz).
|
||||||
|
|
||||||
|
**Nachtrag — Direkter Anlege-Einstieg + Schnellüberblick in der Gruppenliste (Folge-Feedback):**
|
||||||
|
Die "Anstehende Aufgaben"-Karte im Kurs-Dashboard und die "Offene Aufgaben"-Kachel im
|
||||||
|
Hauptdashboard waren zunächst rein lesend — Anlegen ging nur über den Umweg
|
||||||
|
"Arbeitszeit". Beide Stellen haben jetzt einen "+"-Button, der denselben
|
||||||
|
`AddEditWorkTaskDialog` öffnet wie die Aufgabenliste selbst (im Kurs-Dashboard mit der
|
||||||
|
aktuellen Gruppe vorbelegt). Damit das nicht zu drei fast identischen Kopien der
|
||||||
|
Gruppenlisten-/Kursliste-Ladelogik führt, wurde die Dialog-Öffnung in
|
||||||
|
`WorkTaskDialogHelper.ShowDialog` (`LehrerApp.Desktop/Views/Workload/`) extrahiert und wird von
|
||||||
|
`WorkTaskListView`, `GroupDetailView` (`GroupOverviewViewModel.OnAddGroupTask`) und
|
||||||
|
`DashboardView` (`DashboardViewModel.OnAddTask`) gemeinsam genutzt — gleiches
|
||||||
|
View-Code-Behind-Delegate-Muster wie überall sonst in dieser Schicht.
|
||||||
|
|
||||||
|
Bei der Gelegenheit auch das Auswahl-Panel der Gruppenliste (`GroupListView`) nachgezogen: Beim
|
||||||
|
Anwählen eines Kurses erschien bisher nur eine reine Sprung-Buttonliste ("BEREICHE") ohne jede
|
||||||
|
Kennzahl, und der Button für Tab 0 (Übersicht/Kurs-Dashboard, die oben gebaute Karten-Ansicht)
|
||||||
|
sowie Tab 3 (Mitarbeit) fehlten dort komplett — beide Tabs waren aus diesem Panel gar nicht
|
||||||
|
erreichbar. Ergänzt: ein "SCHNELLÜBERBLICK"-Block oberhalb der Buttonliste
|
||||||
|
(`GroupListViewModel.LoadQuickInfo`) mit nächster geplanter Stunde, nächster Klausur und den
|
||||||
|
(max. 3) nächsten offenen Aufgaben dieser Gruppe — bewusst dieselbe kompakte Auswertung wie die
|
||||||
|
obersten Karten des Kurs-Dashboards, hier nur ohne eigenen Tab-Wechsel. Sowie die beiden
|
||||||
|
fehlenden Buttons "📊 Übersicht" (Tab 0) und "✋ Mitarbeit" (Tab 3).
|
||||||
|
|
||||||
### 6.2 Zeiterfassung
|
### 6.2 Zeiterfassung
|
||||||
- [x] **6.2.1** Timer starten/stoppen mit Zuordnung zu Aufgabe oder Kategorie —
|
- [x] **6.2.1** Timer starten/stoppen mit Zuordnung zu Aufgabe oder Kategorie —
|
||||||
`TimeTrackingViewModel`. Bewusst ohne live mitlaufende Sekundenanzeige (keine
|
`TimeTrackingViewModel`. Bewusst ohne live mitlaufende Sekundenanzeige (keine
|
||||||
|
|||||||
Reference in New Issue
Block a user