Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50f3b2d6a9 | ||
|
|
ad40d11d6e |
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -299,6 +299,38 @@ public sealed class RepositoryTests
|
|||||||
Assert.Throws<InvalidOperationException>(() => new SeatingPlanRepository(db).Save(plan));
|
Assert.Throws<InvalidOperationException>(() => new SeatingPlanRepository(db).Save(plan));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeatingPlanRepository_AktualisiertPlanMitFehlendemOptionalenRaum()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
|
||||||
|
new GroupRepository(db).Save(group);
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||||
|
db.Students.Insert(student);
|
||||||
|
db.Memberships.Insert(new GroupMembership { GroupId = group.Id, StudentId = student.Id });
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = group.Id,
|
||||||
|
Name = "Standard",
|
||||||
|
Room = null!,
|
||||||
|
Rows = 2,
|
||||||
|
Columns = 2,
|
||||||
|
};
|
||||||
|
db.SeatingPlans.Insert(plan);
|
||||||
|
plan.Assignments.Add(new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id });
|
||||||
|
|
||||||
|
new SeatingPlanRepository(db).Save(plan);
|
||||||
|
|
||||||
|
var saved = db.SeatingPlans.FindById(plan.Id);
|
||||||
|
Assert.NotNull(saved);
|
||||||
|
Assert.True(string.IsNullOrEmpty(saved.Room));
|
||||||
|
Assert.Equal(student.Id, saved.Assignments.Single().StudentId);
|
||||||
|
|
||||||
|
// LiteDB may materialize the empty optional value as null again. A later
|
||||||
|
// drag/drop save must therefore remain safe as well.
|
||||||
|
new SeatingPlanRepository(db).Save(saved);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GroupMembershipRepository_Save_LehntZweiteZuordnungFuerGleichesPaarAb()
|
public void GroupMembershipRepository_Save_LehntZweiteZuordnungFuerGleichesPaarAb()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -162,8 +162,11 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
|
|||||||
public void Save(SeatingPlan plan)
|
public void Save(SeatingPlan plan)
|
||||||
{
|
{
|
||||||
ArchivedGroupWriteGuard.EnsureActive(db, plan.GroupId);
|
ArchivedGroupWriteGuard.EnsureActive(db, plan.GroupId);
|
||||||
plan.Name = plan.Name.Trim();
|
// LiteDB can deserialize missing/legacy optional string fields as null even
|
||||||
plan.Room = plan.Room.Trim();
|
// though the current model initializes them with an empty string.
|
||||||
|
plan.Name = plan.Name?.Trim() ?? "";
|
||||||
|
plan.Room = plan.Room?.Trim() ?? "";
|
||||||
|
plan.Assignments ??= [];
|
||||||
if (plan.Name.Length == 0)
|
if (plan.Name.Length == 0)
|
||||||
throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein.");
|
throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein.");
|
||||||
if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10)
|
if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10)
|
||||||
|
|||||||
@@ -18,17 +18,31 @@ public sealed class DashboardViewModelTests
|
|||||||
return new PeriodScheduleService(tempPath);
|
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,
|
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();
|
var lessons = new FakeLessons();
|
||||||
lessons.Add(lesson);
|
lessons.Add(lesson);
|
||||||
return new DashboardViewModel(
|
return new DashboardViewModel(
|
||||||
new FakeGroups([group]), new FakeSubjects([]), lessons,
|
new FakeGroups([group]), new FakeSubjects([]), lessons,
|
||||||
new FakeExams([]), new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
|
exams ?? new FakeExams([]), results ?? new FakeResults(), grades ?? new FakeGrades(),
|
||||||
new FakeStudents([]), new FakeDocumentation(),
|
reportGrades ?? new FakeReportGrades(), memberships ?? new FakeMemberships([]),
|
||||||
|
tasks ?? new FakeWorkTasks(), new FakeSessions([]), new FakeEntries(),
|
||||||
|
students ?? new FakeStudents([]), documentation ?? new FakeDocumentation(),
|
||||||
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
slots ?? new FakeTimetableSlots(), periodSchedule ?? NewPeriodSchedule(),
|
||||||
new AttendanceBalanceService(), new SchoolYearService());
|
new AttendanceBalanceService(), new SchoolYearService(), dashboardSettings ?? NewDashboardSettings());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -108,4 +122,109 @@ public sealed class DashboardViewModelTests
|
|||||||
|
|
||||||
Assert.Equal(group.Id, navigatedTo);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,12 @@ public class FakeSessions(List<ParticipationSession> all) : IParticipationSessio
|
|||||||
{
|
{
|
||||||
public List<ParticipationSession> GetByGroup(Guid groupId) => all.Where(s => s.GroupId == groupId).ToList();
|
public List<ParticipationSession> GetByGroup(Guid groupId) => all.Where(s => s.GroupId == groupId).ToList();
|
||||||
public ParticipationSession? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
public ParticipationSession? GetById(Guid id) => all.FirstOrDefault(s => s.Id == id);
|
||||||
public void Save(ParticipationSession session) { }
|
public void Save(ParticipationSession session)
|
||||||
public void Delete(Guid id) { }
|
{
|
||||||
|
all.RemoveAll(s => s.Id == session.Id);
|
||||||
|
all.Add(session);
|
||||||
|
}
|
||||||
|
public void Delete(Guid id) => all.RemoveAll(s => s.Id == id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FakeEntries : IParticipationRepository
|
public class FakeEntries : IParticipationRepository
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ public sealed class GroupDetailViewModelTests
|
|||||||
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService()),
|
new FakeCompetencyDomains(), TestSupport.BuildAiSettingsService()),
|
||||||
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
new CompetencyOverviewTabViewModel(new FakeUnits(), exams, new FakeResults(),
|
||||||
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
new FakeCompetencyDomains(), students, new CompetencyAnalysisService()),
|
||||||
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships));
|
new SeatingPlanTabViewModel(new FakeSeatingPlans(), students, memberships,
|
||||||
|
new FakeSessions([]), new FakeEntries(), new FakeAspects()));
|
||||||
|
|
||||||
vm.LoadGroup(group.Id);
|
vm.LoadGroup(group.Id);
|
||||||
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
vm.SelectedExam = vm.Exams.First(e => e.Id == exam.Id);
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
|
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
|
||||||
};
|
};
|
||||||
var plans = new FakeSeatingPlans([plan]);
|
var plans = new FakeSeatingPlans([plan]);
|
||||||
var vm = new SeatingPlanTabViewModel(plans, students, memberships);
|
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
|
||||||
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
vm.Initialize(groupId, isReadOnly: false);
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
|
||||||
vm.Seats[1].SelectedOption = vm.StudentOptions.Single(o => o.StudentId == student.Id);
|
vm.Seats[1].SelectedOption = vm.StudentOptions.Single(o => o.StudentId == student.Id);
|
||||||
@@ -40,7 +41,8 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
var groupId = Guid.NewGuid();
|
var groupId = Guid.NewGuid();
|
||||||
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
|
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1 };
|
||||||
var vm = new SeatingPlanTabViewModel(
|
var vm = new SeatingPlanTabViewModel(
|
||||||
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]));
|
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
|
||||||
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
|
|
||||||
vm.Initialize(groupId, isReadOnly: true);
|
vm.Initialize(groupId, isReadOnly: true);
|
||||||
|
|
||||||
@@ -48,4 +50,74 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assert.False(vm.Seats.Single().CanEdit);
|
Assert.False(vm.Seats.Single().CanEdit);
|
||||||
Assert.False(vm.AddPlanCommand.CanExecute(null));
|
Assert.False(vm.AddPlanCommand.CanExecute(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DragDrop_ZwischenBelegtenPlaetzen_TauschtDieSchueler()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var anna = new Student { FirstName = "Anna", LastName = "A" };
|
||||||
|
var ben = new Student { FirstName = "Ben", LastName = "B" };
|
||||||
|
var students = new FakeStudents([anna, ben]);
|
||||||
|
var memberships = new FakeMemberships([
|
||||||
|
new GroupMembership { GroupId = groupId, StudentId = anna.Id },
|
||||||
|
new GroupMembership { GroupId = groupId, StudentId = ben.Id },
|
||||||
|
]);
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
|
||||||
|
Assignments =
|
||||||
|
[
|
||||||
|
new SeatAssignment { Row = 0, Column = 0, StudentId = anna.Id },
|
||||||
|
new SeatAssignment { Row = 0, Column = 1, StudentId = ben.Id },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
var plans = new FakeSeatingPlans([plan]);
|
||||||
|
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
|
||||||
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
|
||||||
|
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
|
||||||
|
|
||||||
|
Assert.Equal(ben.Id, vm.Seats[0].SelectedOption.StudentId);
|
||||||
|
Assert.Equal(anna.Id, vm.Seats[1].SelectedOption.StudentId);
|
||||||
|
Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SitzplatzBewertung_ErstelltHeutigeSitzungUndSpeichertAlleDreiBereiche()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var studentId = Guid.NewGuid();
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
var entries = new FakeEntries();
|
||||||
|
var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
|
||||||
|
groupId, studentId, "Beispiel, Anna", canEdit: true);
|
||||||
|
|
||||||
|
vm.SetRatingByNumber(5);
|
||||||
|
vm.ApplyAttendanceShortcut(1, clear: false);
|
||||||
|
vm.ApplyHomeworkShortcut(7, clear: false);
|
||||||
|
|
||||||
|
var session = Assert.Single(sessions.GetByGroup(groupId));
|
||||||
|
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
|
||||||
|
Assert.Equal("Sitzplan", session.Comment);
|
||||||
|
var entry = entries.GetBySessionAndStudent(session.Id, studentId)!;
|
||||||
|
Assert.Equal(2, entry.Ratings.Single(r => r.Key == "quality").Value);
|
||||||
|
Assert.Equal(AttendanceStatus.Present, entry.Attendance);
|
||||||
|
Assert.Equal(HomeworkStatus.MissingOpen, entry.Homework);
|
||||||
|
Assert.True(entry.HomeworkMissing);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SitzplatzBewertung_ArchiviertOhneSitzung_LegtKeineNeueSitzungAn()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var sessions = new FakeSessions([]);
|
||||||
|
|
||||||
|
var vm = new SeatAssessmentViewModel(sessions, new FakeEntries(), new FakeAspects(),
|
||||||
|
groupId, Guid.NewGuid(), "Beispiel, Anna", canEdit: false);
|
||||||
|
vm.SetRatingByNumber(5);
|
||||||
|
|
||||||
|
Assert.False(vm.CanEdit);
|
||||||
|
Assert.Empty(sessions.GetByGroup(groupId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ public class App : Application
|
|||||||
var dash = Services.GetRequiredService<DashboardViewModel>();
|
var dash = Services.GetRequiredService<DashboardViewModel>();
|
||||||
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id);
|
||||||
dash.OnNavigateToStudent = id => main.NavigateToStudent(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
|
// StudentList → StudentDetail + Anlegen
|
||||||
var sl = Services.GetRequiredService<StudentListViewModel>();
|
var sl = Services.GetRequiredService<StudentListViewModel>();
|
||||||
@@ -109,7 +110,7 @@ public class App : Application
|
|||||||
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
|
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
|
||||||
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
||||||
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
|
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
|
||||||
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 5);
|
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ShowAddStudentDialog()
|
private static async Task ShowAddStudentDialog()
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ public static class AppBootstrapper
|
|||||||
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
services.AddSingleton(_ => new SchoolCalendarSettingsService(appData));
|
||||||
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
services.AddSingleton(_ => new PeriodScheduleService(appData));
|
||||||
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
services.AddSingleton(_ => new WorkloadSettingsService(appData));
|
||||||
|
services.AddSingleton(_ => new DashboardSettingsService(appData));
|
||||||
services.AddSingleton(_ => new LetterTemplateService(appData));
|
services.AddSingleton(_ => new LetterTemplateService(appData));
|
||||||
|
|
||||||
// ── KI-Unterstützung (4.5.9, optional – nur wenn in den Einstellungen aktiviert) ──────
|
// ── 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 ISubjectRepository _subjects;
|
||||||
private readonly ILessonRepository _lessons;
|
private readonly ILessonRepository _lessons;
|
||||||
private readonly IExamRepository _exams;
|
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 IWorkTaskRepository _tasks;
|
||||||
private readonly IParticipationSessionRepository _participationSessions;
|
private readonly IParticipationSessionRepository _participationSessions;
|
||||||
private readonly IParticipationRepository _participationEntries;
|
private readonly IParticipationRepository _participationEntries;
|
||||||
@@ -25,14 +29,18 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
private readonly PeriodScheduleService _periodSchedule;
|
private readonly PeriodScheduleService _periodSchedule;
|
||||||
private readonly AttendanceBalanceService _attendanceBalance;
|
private readonly AttendanceBalanceService _attendanceBalance;
|
||||||
private readonly SchoolYearService _sy;
|
private readonly SchoolYearService _sy;
|
||||||
|
private readonly DashboardSettingsService _dashboardSettings;
|
||||||
|
|
||||||
private const int OpenExcuseMaxAgeDays = 21;
|
private const int OpenExcuseMaxAgeDays = 21;
|
||||||
private const int SupportPlanDueWithinDays = 14;
|
private const int SupportPlanDueWithinDays = 14;
|
||||||
|
private const int UpcomingWithinDays = 30;
|
||||||
|
|
||||||
[ObservableProperty] private string _greeting = "";
|
[ObservableProperty] private string _greeting = "";
|
||||||
[ObservableProperty] private string _currentDate = "";
|
[ObservableProperty] private string _currentDate = "";
|
||||||
[ObservableProperty] private string _currentSchoolYear = "";
|
[ObservableProperty] private string _currentSchoolYear = "";
|
||||||
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
|
[ObservableProperty] private DateOnly _calendarMonth = FirstOfMonth(DateTime.Today);
|
||||||
|
[ObservableProperty] private string _selectedDayLabel = "";
|
||||||
|
[ObservableProperty] private bool _isDashboardSettingsOpen;
|
||||||
|
|
||||||
public string CalendarMonthLabel => CalendarMonth.ToString("MMMM yyyy", De);
|
public string CalendarMonthLabel => CalendarMonth.ToString("MMMM yyyy", De);
|
||||||
|
|
||||||
@@ -43,6 +51,11 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
|
||||||
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
|
||||||
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { 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"];
|
public static string[] CalendarWeekdayHeaders { get; } = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||||
|
|
||||||
// Navigation-Callback – wird von App.axaml.cs verdrahtet
|
// 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
|
// 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; }
|
||||||
|
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,
|
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,
|
IParticipationRepository participationEntries, IStudentRepository students,
|
||||||
IDocumentationRepository documentation, ITimetableSlotRepository timetableSlots,
|
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;
|
_groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams; _tasks = tasks;
|
||||||
|
_examResults = examResults; _grades = grades; _reportGrades = reportGrades; _memberships = memberships;
|
||||||
_participationSessions = participationSessions; _participationEntries = participationEntries;
|
_participationSessions = participationSessions; _participationEntries = participationEntries;
|
||||||
_students = students; _documentation = documentation;
|
_students = students; _documentation = documentation;
|
||||||
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
|
_timetableSlots = timetableSlots; _periodSchedule = periodSchedule;
|
||||||
_attendanceBalance = attendanceBalance; _sy = sy;
|
_attendanceBalance = attendanceBalance; _sy = sy; _dashboardSettings = dashboardSettings;
|
||||||
|
LoadDashboardCards();
|
||||||
Load();
|
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 static DateOnly FirstOfMonth(DateTime d) => new(d.Year, d.Month, 1);
|
||||||
|
|
||||||
private void Load()
|
private void Load()
|
||||||
@@ -123,6 +155,9 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
LoadOpenExcuses(groups.Values.ToList(), today);
|
LoadOpenExcuses(groups.Values.ToList(), today);
|
||||||
LoadAttendanceWarnings(today);
|
LoadAttendanceWarnings(today);
|
||||||
LoadSupportPlanReviews(today);
|
LoadSupportPlanReviews(today);
|
||||||
|
LoadUpcomingDates(groups, today);
|
||||||
|
LoadOpenCorrections(groups, today);
|
||||||
|
LoadAlerts(groups, today);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fehlzeiten-Warnung (5.2.3) ────────────────────────────────────────────
|
// ── 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)
|
[RelayCommand] private void OpenStudentAttendance(AttendanceWarningItem? item)
|
||||||
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
{ if (item is not null) OnNavigateToStudent?.Invoke(item.StudentId); }
|
||||||
|
|
||||||
@@ -240,8 +443,8 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
var agg = Agg(lesson.Date);
|
var agg = Agg(lesson.Date);
|
||||||
agg.HasLesson = true;
|
agg.HasLesson = true;
|
||||||
if (g.IsOwnClass) agg.IsOwnClassDay = true;
|
if (g.IsOwnClass) agg.IsOwnClassDay = true;
|
||||||
agg.Details.Add($"Unterricht: {g.Name}" +
|
agg.Details.Add(new CalendarEventItem(CalendarEventKind.Lesson, lesson.Date,
|
||||||
(string.IsNullOrWhiteSpace(lesson.Topic) ? "" : $" – {lesson.Topic}"));
|
g.Name, lesson.Topic, g.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var exam in _exams.GetByGroup(g.Id).Where(e => e.Date >= gridStart && e.Date <= gridEnd))
|
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);
|
var agg = Agg(exam.Date);
|
||||||
agg.HasExam = true;
|
agg.HasExam = true;
|
||||||
if (g.IsOwnClass) agg.IsOwnClassDay = 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?.HasLesson ?? false, agg?.HasExam ?? false, agg?.IsOwnClassDay ?? false,
|
||||||
agg?.Details ?? []));
|
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]
|
[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 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 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();
|
[RelayCommand] private void Refresh() => Load();
|
||||||
|
|
||||||
private class DayAgg
|
private class DayAgg
|
||||||
@@ -296,7 +533,7 @@ public partial class DashboardViewModel : ObservableObject
|
|||||||
public bool HasLesson;
|
public bool HasLesson;
|
||||||
public bool HasExam;
|
public bool HasExam;
|
||||||
public bool IsOwnClassDay;
|
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 int DayNumber { get; }
|
||||||
public bool IsCurrentMonth { get; }
|
public bool IsCurrentMonth { get; }
|
||||||
public bool IsToday { get; }
|
public bool IsToday { get; }
|
||||||
@@ -378,16 +617,103 @@ public class CalendarDayCell
|
|||||||
public bool HasExam { get; }
|
public bool HasExam { get; }
|
||||||
public bool IsOwnClassDay { get; }
|
public bool IsOwnClassDay { get; }
|
||||||
public string Tooltip { get; }
|
public string Tooltip { get; }
|
||||||
|
public IReadOnlyList<CalendarEventItem> Events { get; }
|
||||||
|
|
||||||
internal CalendarDayCell(DateOnly date, bool isCurrentMonth, bool isToday,
|
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;
|
DayNumber = date.Day;
|
||||||
IsCurrentMonth = isCurrentMonth;
|
IsCurrentMonth = isCurrentMonth;
|
||||||
IsToday = isToday;
|
IsToday = isToday;
|
||||||
HasLesson = hasLesson;
|
HasLesson = hasLesson;
|
||||||
HasExam = hasExam;
|
HasExam = hasExam;
|
||||||
IsOwnClassDay = isOwnClassDay;
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -243,6 +243,11 @@ public partial class GroupDetailViewModel : ObservableObject
|
|||||||
PlanningTab = planningTab;
|
PlanningTab = planningTab;
|
||||||
CompetencyOverviewTab = competencyOverviewTab;
|
CompetencyOverviewTab = competencyOverviewTab;
|
||||||
SeatingPlanTab = seatingPlanTab;
|
SeatingPlanTab = seatingPlanTab;
|
||||||
|
SeatingPlanTab.OnAssessmentChanged = () =>
|
||||||
|
{
|
||||||
|
ParticipationTab.LoadSessions();
|
||||||
|
ParticipationTab.RefreshCurrentGrid();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadGroup(Guid id)
|
public void LoadGroup(Guid id)
|
||||||
|
|||||||
@@ -108,11 +108,11 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
|
|
||||||
public void LoadSessions()
|
public void LoadSessions()
|
||||||
{
|
{
|
||||||
|
var selectedId = SelectedSession?.Id;
|
||||||
Sessions.Clear();
|
Sessions.Clear();
|
||||||
foreach (var s in _sessions.GetByGroup(_groupId))
|
foreach (var s in _sessions.GetByGroup(_groupId))
|
||||||
Sessions.Add(new ParticipationSessionItem(s));
|
Sessions.Add(new ParticipationSessionItem(s));
|
||||||
if (SelectedSession is null && Sessions.Any())
|
SelectedSession = Sessions.FirstOrDefault(s => s.Id == selectedId) ?? Sessions.FirstOrDefault();
|
||||||
SelectedSession = Sessions[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedSessionChanged(ParticipationSessionItem? value)
|
partial void OnSelectedSessionChanged(ParticipationSessionItem? value)
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
private readonly ISeatingPlanRepository _plans;
|
private readonly ISeatingPlanRepository _plans;
|
||||||
private readonly IStudentRepository _students;
|
private readonly IStudentRepository _students;
|
||||||
private readonly IGroupMembershipRepository _memberships;
|
private readonly IGroupMembershipRepository _memberships;
|
||||||
|
private readonly IParticipationSessionRepository _sessions;
|
||||||
|
private readonly IParticipationRepository _participation;
|
||||||
|
private readonly IParticipationAspectRepository _aspects;
|
||||||
private Guid _groupId;
|
private Guid _groupId;
|
||||||
private SeatingPlan? _currentPlan;
|
private SeatingPlan? _currentPlan;
|
||||||
private bool _isReadOnly;
|
private bool _isReadOnly;
|
||||||
@@ -25,19 +28,26 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
|
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
|
||||||
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
|
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
|
||||||
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
|
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
|
||||||
|
public ObservableCollection<StudentSeatOption> UnassignedStudents { get; } = [];
|
||||||
|
|
||||||
public bool HasPlans => Plans.Count > 0;
|
public bool HasPlans => Plans.Count > 0;
|
||||||
public bool HasSelectedPlan => _currentPlan is not null;
|
public bool HasSelectedPlan => _currentPlan is not null;
|
||||||
public bool IsEditable => !_isReadOnly;
|
public bool IsEditable => !_isReadOnly;
|
||||||
public Func<SeatingPlan?, Task<SeatingPlan?>>? OnEditPlan { get; set; }
|
public Func<SeatingPlan?, Task<SeatingPlan?>>? OnEditPlan { get; set; }
|
||||||
public Func<SeatingPlanSummary, Task<bool>>? OnConfirmDelete { get; set; }
|
public Func<SeatingPlanSummary, Task<bool>>? OnConfirmDelete { get; set; }
|
||||||
|
public Func<SeatAssessmentViewModel, Task>? OnAssessStudent { get; set; }
|
||||||
|
public Action? OnAssessmentChanged { get; set; }
|
||||||
|
|
||||||
public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students,
|
public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students,
|
||||||
IGroupMembershipRepository memberships)
|
IGroupMembershipRepository memberships, IParticipationSessionRepository sessions,
|
||||||
|
IParticipationRepository participation, IParticipationAspectRepository aspects)
|
||||||
{
|
{
|
||||||
_plans = plans;
|
_plans = plans;
|
||||||
_students = students;
|
_students = students;
|
||||||
_memberships = memberships;
|
_memberships = memberships;
|
||||||
|
_sessions = sessions;
|
||||||
|
_participation = participation;
|
||||||
|
_aspects = aspects;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
|
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
|
||||||
@@ -87,6 +97,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
Seats.Clear();
|
Seats.Clear();
|
||||||
if (plan is null)
|
if (plan is null)
|
||||||
{
|
{
|
||||||
|
UnassignedStudents.Clear();
|
||||||
PlanColumns = 1;
|
PlanColumns = 1;
|
||||||
PlanTitle = "";
|
PlanTitle = "";
|
||||||
PlanSubtitle = "";
|
PlanSubtitle = "";
|
||||||
@@ -124,6 +135,49 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
other.SetSelectionSilently(StudentSeatOption.Empty);
|
other.SetSelectionSilently(StudentSeatOption.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SaveSeatAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAssignmentSummary()
|
||||||
|
{
|
||||||
|
var assigned = Seats.Count(s => s.SelectedOption.StudentId.HasValue);
|
||||||
|
var total = StudentOptions.Count - 1;
|
||||||
|
AssignmentSummary = $"{assigned} von {total} Schülern zugeordnet";
|
||||||
|
var assignedIds = Seats.Where(s => s.SelectedOption.StudentId.HasValue)
|
||||||
|
.Select(s => s.SelectedOption.StudentId!.Value).ToHashSet();
|
||||||
|
UnassignedStudents.Clear();
|
||||||
|
foreach (var option in StudentOptions.Where(o => o.StudentId.HasValue && !assignedIds.Contains(o.StudentId.Value)))
|
||||||
|
UnassignedStudents.Add(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target)
|
||||||
|
{
|
||||||
|
if (!IsEditable || source == target || !source.SelectedOption.StudentId.HasValue) return;
|
||||||
|
var targetOption = target.SelectedOption;
|
||||||
|
target.SetSelectionSilently(source.SelectedOption);
|
||||||
|
source.SetSelectionSilently(targetOption);
|
||||||
|
SaveSeatAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AssignStudent(StudentSeatOption student, SeatCellViewModel target)
|
||||||
|
{
|
||||||
|
if (!IsEditable || !student.StudentId.HasValue) return;
|
||||||
|
foreach (var other in Seats.Where(s => s != target && s.SelectedOption.StudentId == student.StudentId))
|
||||||
|
other.SetSelectionSilently(StudentSeatOption.Empty);
|
||||||
|
target.SetSelectionSilently(student);
|
||||||
|
SaveSeatAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearSeat(SeatCellViewModel seat)
|
||||||
|
{
|
||||||
|
if (!IsEditable || !seat.SelectedOption.StudentId.HasValue) return;
|
||||||
|
seat.SetSelectionSilently(StudentSeatOption.Empty);
|
||||||
|
SaveSeatAssignments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveSeatAssignments()
|
||||||
|
{
|
||||||
|
if (_currentPlan is null) return;
|
||||||
_currentPlan.Assignments = Seats
|
_currentPlan.Assignments = Seats
|
||||||
.Where(s => s.SelectedOption.StudentId.HasValue)
|
.Where(s => s.SelectedOption.StudentId.HasValue)
|
||||||
.Select(s => new SeatAssignment
|
.Select(s => new SeatAssignment
|
||||||
@@ -136,11 +190,13 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
UpdateAssignmentSummary();
|
UpdateAssignmentSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateAssignmentSummary()
|
public async Task AssessStudent(SeatCellViewModel seat)
|
||||||
{
|
{
|
||||||
var assigned = Seats.Count(s => s.SelectedOption.StudentId.HasValue);
|
if (!seat.SelectedOption.StudentId.HasValue || OnAssessStudent is null) return;
|
||||||
var total = StudentOptions.Count - 1;
|
var assessment = new SeatAssessmentViewModel(_sessions, _participation, _aspects,
|
||||||
AssignmentSummary = $"{assigned} von {total} Schülern zugeordnet";
|
_groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable);
|
||||||
|
await OnAssessStudent(assessment);
|
||||||
|
OnAssessmentChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanEdit))]
|
[RelayCommand(CanExecute = nameof(CanEdit))]
|
||||||
@@ -203,11 +259,14 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
private bool _suppressChange;
|
private bool _suppressChange;
|
||||||
|
|
||||||
[ObservableProperty] private StudentSeatOption _selectedOption;
|
[ObservableProperty] private StudentSeatOption _selectedOption;
|
||||||
|
[ObservableProperty] private bool _isDropTarget;
|
||||||
public int Row { get; }
|
public int Row { get; }
|
||||||
public int Column { get; }
|
public int Column { get; }
|
||||||
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
|
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
|
||||||
public ObservableCollection<StudentSeatOption> Options { get; }
|
public ObservableCollection<StudentSeatOption> Options { get; }
|
||||||
public bool CanEdit { get; }
|
public bool CanEdit { get; }
|
||||||
|
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
||||||
|
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
|
||||||
|
|
||||||
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
||||||
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit)
|
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit)
|
||||||
@@ -222,6 +281,8 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
|
|
||||||
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
||||||
{
|
{
|
||||||
|
OnPropertyChanged(nameof(IsOccupied));
|
||||||
|
OnPropertyChanged(nameof(StudentName));
|
||||||
if (!_suppressChange) _onChanged(this);
|
if (!_suppressChange) _onChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +294,281 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class SeatAssessmentViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly IParticipationRepository _entries;
|
||||||
|
private readonly ParticipationEntry? _entry;
|
||||||
|
private readonly bool _canEdit;
|
||||||
|
|
||||||
|
[ObservableProperty] private int _selectedAspectIndex;
|
||||||
|
[ObservableProperty] private string _attendanceLabel = "Noch nicht kontrolliert";
|
||||||
|
[ObservableProperty] private string _homeworkLabel = "Keine Hausaufgabe aufgegeben";
|
||||||
|
|
||||||
|
public string StudentName { get; }
|
||||||
|
public string SessionDisplay { get; }
|
||||||
|
public bool CanEdit => _canEdit && _entry is not null;
|
||||||
|
public string ReadOnlyHint => _entry is null
|
||||||
|
? "Für heute existiert keine Sitzung. In einer archivierten Gruppe kann keine neue angelegt werden."
|
||||||
|
: "Archivierte Lerngruppe – Bewertung nur ansehen.";
|
||||||
|
public ObservableCollection<SeatAssessmentAspectRow> AspectRows { get; } = [];
|
||||||
|
public ObservableCollection<SeatAttendanceChoice> AttendanceChoices { get; } = [];
|
||||||
|
public ObservableCollection<SeatHomeworkChoice> HomeworkChoices { get; } = [];
|
||||||
|
|
||||||
|
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
|
||||||
|
IParticipationRepository entries, IParticipationAspectRepository aspects,
|
||||||
|
Guid groupId, Guid studentId, string studentName, bool canEdit)
|
||||||
|
{
|
||||||
|
_entries = entries;
|
||||||
|
_canEdit = canEdit;
|
||||||
|
StudentName = studentName;
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var session = sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
|
||||||
|
if (session is null && canEdit)
|
||||||
|
{
|
||||||
|
session = new ParticipationSession
|
||||||
|
{
|
||||||
|
GroupId = groupId,
|
||||||
|
Date = today,
|
||||||
|
Comment = "Sitzplan",
|
||||||
|
};
|
||||||
|
sessions.Save(session);
|
||||||
|
}
|
||||||
|
SessionDisplay = session is null ? "Keine Sitzung für heute" : $"{session.Date:dd.MM.yyyy} · {session.Comment}";
|
||||||
|
_entry = session is null ? null
|
||||||
|
: entries.GetBySessionAndStudent(session.Id, studentId)
|
||||||
|
?? new ParticipationEntry { SessionId = session.Id, StudentId = studentId };
|
||||||
|
|
||||||
|
var aspectDefinitions = aspects.GetDefaults().Concat(aspects.GetByGroup(groupId)).ToList();
|
||||||
|
if (aspectDefinitions.Count == 0) aspectDefinitions = DefaultParticipationAspects.All.Select(a => new ParticipationAspect
|
||||||
|
{
|
||||||
|
Key = a.Key, Label = a.Label, ValueType = a.ValueType, MaxPoints = a.MaxPoints,
|
||||||
|
}).ToList();
|
||||||
|
foreach (var (aspect, index) in aspectDefinitions.Select((a, i) => (a, i)))
|
||||||
|
{
|
||||||
|
var value = _entry?.Ratings.FirstOrDefault(r => r.Key == aspect.Key)?.Value;
|
||||||
|
AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating));
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildAttendanceChoices();
|
||||||
|
BuildHomeworkChoices();
|
||||||
|
RefreshStatusChoices();
|
||||||
|
SelectAspect(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildAttendanceChoices()
|
||||||
|
{
|
||||||
|
AttendanceChoices.Add(new("✓", "Anwesend", "Strg+1", AttendanceStatus.Present, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("?", "Entschuldigung offen", "Strg+2", AttendanceStatus.ExcusePending, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("⊘", "Entschuldigt", "Strg+5", AttendanceStatus.Excused, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("◇", "Schulveranstaltung", "Strg+7", AttendanceStatus.OtherSchoolEvent, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("✕", "Geschwänzt", "Strg+9", AttendanceStatus.Truant, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("!", "Unentschuldigt", "Strg+0", AttendanceStatus.Unexcused, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("·", "Nicht kontrolliert", "Strg+X", null, SetAttendance));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildHomeworkChoices()
|
||||||
|
{
|
||||||
|
HomeworkChoices.Add(new("✓", "Gemacht", "⌥1", HomeworkStatus.Completed, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("◐", "Teilweise", "⌥3", HomeworkStatus.PartiallyCompleted, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("◕", "Rest nachgereicht", "⌥4", HomeworkStatus.PartialSubmittedLate, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("◒", "Rest fehlt", "⌥5", HomeworkStatus.PartialMissingOverdue, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("!", "Nicht gemacht", "⌥7", HomeworkStatus.MissingOpen, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("↺", "Nachgereicht", "⌥8", HomeworkStatus.SubmittedLate, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("✕", "Nicht nachgereicht", "⌥0", HomeworkStatus.MissingOverdue, SetHomework));
|
||||||
|
HomeworkChoices.Add(new("·", "Keine aufgegeben", "⌥X", null, SetHomework));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SelectAspect(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= AspectRows.Count) return;
|
||||||
|
SelectedAspectIndex = index;
|
||||||
|
foreach (var row in AspectRows) row.IsActive = row.Index == index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveAspect(int delta)
|
||||||
|
{
|
||||||
|
if (AspectRows.Count == 0) return;
|
||||||
|
SelectAspect(Math.Clamp(SelectedAspectIndex + delta, 0, AspectRows.Count - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetRatingByNumber(int number)
|
||||||
|
{
|
||||||
|
if (!CanEdit) return;
|
||||||
|
var row = AspectRows.ElementAtOrDefault(SelectedAspectIndex);
|
||||||
|
if (row is null) return;
|
||||||
|
if (row.ValueType == AspectValueType.Points)
|
||||||
|
row.ApplyValue(Math.Clamp(number, 0, row.MaxPoints));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var steps = ParticipationRatingScale.Steps(row.ValueType);
|
||||||
|
if (number >= 1 && number <= steps.Count) row.ApplyValue(steps[number - 1].Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AdjustCurrentRating(int delta)
|
||||||
|
{
|
||||||
|
if (!CanEdit) return;
|
||||||
|
var row = AspectRows.ElementAtOrDefault(SelectedAspectIndex);
|
||||||
|
row?.Adjust(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearCurrentRating()
|
||||||
|
{
|
||||||
|
if (!CanEdit) return;
|
||||||
|
AspectRows.ElementAtOrDefault(SelectedAspectIndex)?.ApplyValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyAttendanceShortcut(int? digit, bool clear)
|
||||||
|
{
|
||||||
|
if (!CanEdit) return;
|
||||||
|
var status = clear ? null : digit switch
|
||||||
|
{
|
||||||
|
1 => AttendanceStatus.Present, 2 => AttendanceStatus.ExcusePending,
|
||||||
|
5 => AttendanceStatus.Excused, 7 => AttendanceStatus.OtherSchoolEvent,
|
||||||
|
9 => AttendanceStatus.Truant, 0 => AttendanceStatus.Unexcused,
|
||||||
|
_ => (AttendanceStatus?)null,
|
||||||
|
};
|
||||||
|
if (clear || digit is 0 or 1 or 2 or 5 or 7 or 9) SetAttendance(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyHomeworkShortcut(int? digit, bool clear)
|
||||||
|
{
|
||||||
|
if (!CanEdit) return;
|
||||||
|
var status = clear ? null : digit switch
|
||||||
|
{
|
||||||
|
1 => HomeworkStatus.Completed, 3 => HomeworkStatus.PartiallyCompleted,
|
||||||
|
4 => HomeworkStatus.PartialSubmittedLate, 5 => HomeworkStatus.PartialMissingOverdue,
|
||||||
|
7 => HomeworkStatus.MissingOpen, 8 => HomeworkStatus.SubmittedLate,
|
||||||
|
0 => HomeworkStatus.MissingOverdue, _ => (HomeworkStatus?)null,
|
||||||
|
};
|
||||||
|
if (clear || digit is 0 or 1 or 3 or 4 or 5 or 7 or 8) SetHomework(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyRating(string key, int? value)
|
||||||
|
{
|
||||||
|
if (!CanEdit || _entry is null) return;
|
||||||
|
var existing = _entry.Ratings.FirstOrDefault(r => r.Key == key);
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
if (existing is not null) _entry.Ratings.Remove(existing);
|
||||||
|
}
|
||||||
|
else if (existing is null) _entry.Ratings.Add(new AspectRating { Key = key, Value = value.Value });
|
||||||
|
else existing.Value = value.Value;
|
||||||
|
_entries.Save(_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAttendance(AttendanceStatus? status)
|
||||||
|
{
|
||||||
|
if (!CanEdit || _entry is null) return;
|
||||||
|
_entry.Attendance = status;
|
||||||
|
_entries.Save(_entry);
|
||||||
|
RefreshStatusChoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetHomework(HomeworkStatus? status)
|
||||||
|
{
|
||||||
|
if (!CanEdit || _entry is null) return;
|
||||||
|
_entry.Homework = status;
|
||||||
|
_entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(status);
|
||||||
|
_entries.Save(_entry);
|
||||||
|
RefreshStatusChoices();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshStatusChoices()
|
||||||
|
{
|
||||||
|
AttendanceLabel = AttendanceDisplay.Label(_entry?.Attendance);
|
||||||
|
HomeworkLabel = HomeworkDisplay.Label(_entry is null ? null : HomeworkDisplay.Effective(_entry));
|
||||||
|
foreach (var choice in AttendanceChoices) choice.IsSelected = choice.Status == _entry?.Attendance;
|
||||||
|
var homework = _entry is null ? null : HomeworkDisplay.Effective(_entry);
|
||||||
|
foreach (var choice in HomeworkChoices) choice.IsSelected = choice.Status == homework;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class SeatAssessmentAspectRow : ObservableObject
|
||||||
|
{
|
||||||
|
private readonly Action<string, int?> _apply;
|
||||||
|
[ObservableProperty] private bool _isActive;
|
||||||
|
[ObservableProperty] private int? _value;
|
||||||
|
public int Index { get; }
|
||||||
|
public string Key { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public AspectValueType ValueType { get; }
|
||||||
|
public int MaxPoints { get; }
|
||||||
|
public string Shortcut => Index switch { 0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => "" };
|
||||||
|
public string DisplayValue => ParticipationRatingScale.DisplayLabel(ValueType, Value);
|
||||||
|
public ObservableCollection<SeatRatingChoice> Choices { get; } = [];
|
||||||
|
|
||||||
|
public SeatAssessmentAspectRow(int index, ParticipationAspect aspect, int? value,
|
||||||
|
Action<string, int?> apply)
|
||||||
|
{
|
||||||
|
Index = index; Key = aspect.Key; Label = aspect.Label; ValueType = aspect.ValueType;
|
||||||
|
MaxPoints = aspect.MaxPoints; _value = value; _apply = apply;
|
||||||
|
var steps = ValueType == AspectValueType.Points
|
||||||
|
? Enumerable.Range(0, Math.Min(MaxPoints, 9) + 1).Select(v => (v, v.ToString())).ToList()
|
||||||
|
: ParticipationRatingScale.Steps(ValueType).ToList();
|
||||||
|
foreach (var (step, i) in steps.Select((s, i) => (s, i)))
|
||||||
|
Choices.Add(new SeatRatingChoice(step.Item2, ValueType == AspectValueType.Points ? step.Item1.ToString() : (i + 1).ToString(),
|
||||||
|
step.Item1, step.Item1 == value, ApplyValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyValue(int? value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
OnPropertyChanged(nameof(DisplayValue));
|
||||||
|
foreach (var choice in Choices) choice.IsSelected = choice.Value == value;
|
||||||
|
_apply(Key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Adjust(int delta)
|
||||||
|
{
|
||||||
|
if (ValueType == AspectValueType.Points)
|
||||||
|
{
|
||||||
|
ApplyValue(Math.Clamp((Value ?? (delta > 0 ? -1 : MaxPoints + 1)) + delta, 0, MaxPoints));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var steps = ParticipationRatingScale.Steps(ValueType).Select(s => s.Value).ToList();
|
||||||
|
if (steps.Count == 0) return;
|
||||||
|
var index = Value.HasValue ? steps.IndexOf(Value.Value) : (delta > 0 ? -1 : steps.Count);
|
||||||
|
ApplyValue(steps[Math.Clamp(index + delta, 0, steps.Count - 1)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand] private void Clear() => ApplyValue(null);
|
||||||
|
[RelayCommand] private void Increment() => Adjust(1);
|
||||||
|
[RelayCommand] private void Decrement() => Adjust(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class SeatRatingChoice(string label, string shortcut, int value, bool isSelected,
|
||||||
|
Action<int?> apply) : ObservableObject
|
||||||
|
{
|
||||||
|
public string Label { get; } = label;
|
||||||
|
public string Shortcut { get; } = shortcut;
|
||||||
|
public int Value { get; } = value;
|
||||||
|
[ObservableProperty] private bool _isSelected = isSelected;
|
||||||
|
[RelayCommand] private void Apply() => apply(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class SeatAttendanceChoice(string symbol, string label, string shortcut,
|
||||||
|
AttendanceStatus? status, Action<AttendanceStatus?> apply) : ObservableObject
|
||||||
|
{
|
||||||
|
public string Symbol { get; } = symbol;
|
||||||
|
public string Label { get; } = label;
|
||||||
|
public string Shortcut { get; } = shortcut;
|
||||||
|
public AttendanceStatus? Status { get; } = status;
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
[RelayCommand] private void Apply() => apply(Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class SeatHomeworkChoice(string symbol, string label, string shortcut,
|
||||||
|
HomeworkStatus? status, Action<HomeworkStatus?> apply) : ObservableObject
|
||||||
|
{
|
||||||
|
public string Symbol { get; } = symbol;
|
||||||
|
public string Label { get; } = label;
|
||||||
|
public string Shortcut { get; } = shortcut;
|
||||||
|
public HomeworkStatus? Status { get; } = status;
|
||||||
|
[ObservableProperty] private bool _isSelected;
|
||||||
|
[RelayCommand] private void Apply() => apply(Status);
|
||||||
|
}
|
||||||
|
|
||||||
public partial class SeatingPlanDialogViewModel : ObservableObject
|
public partial class SeatingPlanDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly ISeatingPlanRepository _plans;
|
private readonly ISeatingPlanRepository _plans;
|
||||||
@@ -256,8 +592,8 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
|
|||||||
_groupId = groupId;
|
_groupId = groupId;
|
||||||
_editingPlan = editingPlan;
|
_editingPlan = editingPlan;
|
||||||
if (editingPlan is null) return;
|
if (editingPlan is null) return;
|
||||||
Name = editingPlan.Name;
|
Name = editingPlan.Name ?? "";
|
||||||
Room = editingPlan.Room;
|
Room = editingPlan.Room ?? "";
|
||||||
Rows = editingPlan.Rows;
|
Rows = editingPlan.Rows;
|
||||||
Columns = editingPlan.Columns;
|
Columns = editingPlan.Columns;
|
||||||
}
|
}
|
||||||
@@ -281,8 +617,8 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
|
|||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
|
|
||||||
var plan = _editingPlan ?? new SeatingPlan { GroupId = _groupId };
|
var plan = _editingPlan ?? new SeatingPlan { GroupId = _groupId };
|
||||||
plan.Name = Name.Trim();
|
plan.Name = Name?.Trim() ?? "";
|
||||||
plan.Room = Room.Trim();
|
plan.Room = Room?.Trim() ?? "";
|
||||||
plan.Rows = decimal.ToInt32(Rows);
|
plan.Rows = decimal.ToInt32(Rows);
|
||||||
plan.Columns = decimal.ToInt32(Columns);
|
plan.Columns = decimal.ToInt32(Columns);
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -9,21 +9,56 @@
|
|||||||
<Setter Property="Foreground" Value="Red"/>
|
<Setter Property="Foreground" Value="Red"/>
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
</Style>
|
</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>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<ScrollViewer Padding="24">
|
<ScrollViewer Padding="24">
|
||||||
<StackPanel Spacing="20">
|
<StackPanel Spacing="20">
|
||||||
|
|
||||||
<!-- Begrüßung -->
|
<!-- Begrüßung -->
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
|
<TextBlock Text="{Binding Greeting}" FontSize="14" Opacity="0.6"/>
|
||||||
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
|
<TextBlock Text="{Binding CurrentDate}" FontSize="24" FontWeight="SemiBold"/>
|
||||||
</StackPanel>
|
</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 -->
|
<!-- 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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -63,7 +98,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Offene Aufgaben -->
|
<!-- 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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -88,7 +124,8 @@
|
|||||||
|
|
||||||
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
<!-- Kalender: feste Position direkt unter Heute/Aufgaben, damit die wachsende
|
||||||
Lerngruppen-Liste darunter ihn nicht nach unten verdrängt. -->
|
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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
@@ -150,11 +187,14 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:CalendarDayCell">
|
<DataTemplate x:DataType="vm:CalendarDayCell">
|
||||||
|
<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}"
|
<Border Classes="daycell" Classes.ownclass="{Binding IsOwnClassDay}"
|
||||||
Classes.haslesson="{Binding HasLesson}"
|
Classes.haslesson="{Binding HasLesson}"
|
||||||
Classes.outside="{Binding !IsCurrentMonth}"
|
Classes.outside="{Binding !IsCurrentMonth}"
|
||||||
Width="32" Height="32" Margin="1" CornerRadius="6"
|
Classes.selected="{Binding IsSelected}"
|
||||||
ToolTip.Tip="{Binding Tooltip}">
|
Width="32" Height="32" CornerRadius="6">
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
<TextBlock Classes="daynum" Classes.today="{Binding IsToday}"
|
||||||
Text="{Binding DayNumber}" FontSize="12"
|
Text="{Binding DayNumber}" FontSize="12"
|
||||||
@@ -164,6 +204,7 @@
|
|||||||
IsVisible="{Binding HasExam}"/>
|
IsVisible="{Binding HasExam}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
</Button>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
@@ -183,11 +224,35 @@
|
|||||||
<TextBlock Text="Meine Klasse" FontSize="10" Opacity="0.6"/>
|
<TextBlock Text="Meine Klasse" FontSize="10" Opacity="0.6"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</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>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Offene Entschuldigungen: neben dem Kalender, ebenfalls feste Position -->
|
<!-- 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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -220,7 +285,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Fehlzeiten-Warnung (5.2.3) -->
|
<!-- 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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -249,7 +315,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Förderplan-Wiedervorlage (5.3.2) -->
|
<!-- 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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
@@ -278,8 +345,113 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Meine Lerngruppen: wächst mit der Zeit, deshalb ganz unten und volle Breite -->
|
<!-- Anstehende Termine (9.3) -->
|
||||||
<Border Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2"
|
<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}"
|
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||||
CornerRadius="8" Padding="16">
|
CornerRadius="8" Padding="16">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
|
|||||||
@@ -117,31 +117,36 @@
|
|||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="1"/>
|
CommandParameter="1"/>
|
||||||
|
<Button Content="🪑 Sitzpläne"
|
||||||
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
|
Padding="12,9"
|
||||||
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
|
CommandParameter="2"/>
|
||||||
<Button Content="📝 Klausuren"
|
<Button Content="📝 Klausuren"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="3"/>
|
CommandParameter="4"/>
|
||||||
<Button Content="🔢 Notenübersicht"
|
<Button Content="🔢 Notenübersicht"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="4"/>
|
CommandParameter="5"/>
|
||||||
<Button Content="📅 Unterrichtsplanung"
|
<Button Content="📅 Unterrichtsplanung"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="5"/>
|
CommandParameter="6"/>
|
||||||
<Button Content="🎯 Kompetenzübersicht"
|
<Button Content="🎯 Kompetenzübersicht"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="6"/>
|
CommandParameter="7"/>
|
||||||
<Button Content="📋 Dokumentation"
|
<Button Content="📋 Dokumentation"
|
||||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Left"
|
||||||
Padding="12,9"
|
Padding="12,9"
|
||||||
Command="{Binding NavigateToSectionCommand}"
|
Command="{Binding NavigateToSectionCommand}"
|
||||||
CommandParameter="7"/>
|
CommandParameter="8"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
x:Class="LehrerApp.Desktop.Views.Groups.SeatAssessmentDialog"
|
||||||
|
x:DataType="vm:SeatAssessmentViewModel"
|
||||||
|
Title="Sitzplatz-Schnelleingabe" Width="660" Height="700"
|
||||||
|
MinWidth="560" MinHeight="560" CanResize="True"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Button.choice">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="8,5"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.choice.selected">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight2}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.aspectrow">
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.aspectrow.active">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight3}"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="Auto,*,Auto" Margin="20">
|
||||||
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,14">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding StudentName}" FontSize="22" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding SessionDisplay}" FontSize="12" Opacity="0.55"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="Schließen" Click="OnClose" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="1">
|
||||||
|
<StackPanel Spacing="16" IsEnabled="{Binding CanEdit}">
|
||||||
|
<Border Background="#FFF3CD" CornerRadius="6" Padding="10"
|
||||||
|
IsVisible="{Binding CanEdit, Converter={x:Static BoolConverters.Not}}">
|
||||||
|
<TextBlock Text="{Binding ReadOnlyHint}" Foreground="#92400E" TextWrapping="Wrap"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<TextBlock Text="MITARBEIT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding AspectRows}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SeatAssessmentAspectRow">
|
||||||
|
<Border Classes="aspectrow" Classes.active="{Binding IsActive}"
|
||||||
|
CornerRadius="7" Padding="9" Margin="0,3">
|
||||||
|
<Grid ColumnDefinitions="150,*">
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock FontWeight="SemiBold">
|
||||||
|
<Run Text="["/><Run Text="{Binding Shortcut}"/><Run Text="] "/><Run Text="{Binding Label}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding DisplayValue}" FontSize="11" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
|
||||||
|
<ItemsControl ItemsSource="{Binding Choices}">
|
||||||
|
<ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SeatRatingChoice">
|
||||||
|
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||||
|
Command="{Binding ApplyCommand}" Margin="2">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{Binding Label}" HorizontalAlignment="Center" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding Shortcut}" HorizontalAlignment="Center" FontSize="9" Opacity="0.5"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<Button Classes="choice" Content="−" Command="{Binding DecrementCommand}" Margin="2"/>
|
||||||
|
<Button Classes="choice" Content="+" Command="{Binding IncrementCommand}" Margin="2"/>
|
||||||
|
<Button Classes="choice" Content="Löschen" Command="{Binding ClearCommand}" Margin="2"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<TextBlock Text="ANWESENHEIT" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding AttendanceLabel}" FontSize="12"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding AttendanceChoices}">
|
||||||
|
<ItemsControl.ItemsPanel><ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SeatAttendanceChoice">
|
||||||
|
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||||
|
Command="{Binding ApplyCommand}" Margin="2">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<TextBlock Text="{Binding Symbol}" FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="{Binding Label}"/>
|
||||||
|
<TextBlock Text="{Binding Shortcut}" FontSize="9" Opacity="0.5" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<TextBlock Text="HAUSAUFGABEN" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="{Binding HomeworkLabel}" FontSize="12"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding HomeworkChoices}">
|
||||||
|
<ItemsControl.ItemsPanel><ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:SeatHomeworkChoice">
|
||||||
|
<Button Classes="choice" Classes.selected="{Binding IsSelected}"
|
||||||
|
Command="{Binding ApplyCommand}" Margin="2">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<TextBlock Text="{Binding Symbol}" FontWeight="Bold"/>
|
||||||
|
<TextBlock Text="{Binding Label}"/>
|
||||||
|
<TextBlock Text="{Binding Shortcut}" FontSize="9" Opacity="0.5" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Margin="0,12,0,0" FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||||
|
Text="Mitarbeit: Q/W/E/R/T Aspekt · 1–5 Bewertung · +/− anpassen · Backspace löschen · ←/→ Aspekt | Anwesenheit: Strg+1/2/5/7/9/0, Strg+X | Hausaufgaben: ⌥+1/3/4/5/7/8/0, ⌥+X | Esc schließen"/>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
public partial class SeatAssessmentDialog : Window
|
||||||
|
{
|
||||||
|
public SeatAssessmentDialog()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel, handledEventsToo: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnOpened(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnOpened(e);
|
||||||
|
Focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPreviewKeyDown(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not SeatAssessmentViewModel vm) return;
|
||||||
|
var digit = DigitFromEvent(e);
|
||||||
|
var clear = e.Key == Key.X || e.PhysicalKey == PhysicalKey.X;
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||||
|
{
|
||||||
|
if (clear || digit is not null) vm.ApplyAttendanceShortcut(digit, clear);
|
||||||
|
e.Handled = clear || digit is not null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.KeyModifiers.HasFlag(KeyModifiers.Alt))
|
||||||
|
{
|
||||||
|
if (clear || digit is not null) vm.ApplyHomeworkShortcut(digit, clear);
|
||||||
|
e.Handled = clear || digit is not null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (digit is not null)
|
||||||
|
{
|
||||||
|
vm.SetRatingByNumber(digit.Value);
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Q: vm.SelectAspect(0); e.Handled = true; break;
|
||||||
|
case Key.W: vm.SelectAspect(1); e.Handled = true; break;
|
||||||
|
case Key.E: vm.SelectAspect(2); e.Handled = true; break;
|
||||||
|
case Key.R: vm.SelectAspect(3); e.Handled = true; break;
|
||||||
|
case Key.T: vm.SelectAspect(4); e.Handled = true; break;
|
||||||
|
case Key.Left: vm.MoveAspect(-1); e.Handled = true; break;
|
||||||
|
case Key.Right: vm.MoveAspect(1); e.Handled = true; break;
|
||||||
|
case Key.OemPlus or Key.Add: vm.AdjustCurrentRating(1); e.Handled = true; break;
|
||||||
|
case Key.OemMinus or Key.Subtract: vm.AdjustCurrentRating(-1); e.Handled = true; break;
|
||||||
|
case Key.Back: vm.ClearCurrentRating(); e.Handled = true; break;
|
||||||
|
case Key.Escape: Close(); e.Handled = true; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? DigitFromEvent(KeyEventArgs e) => e.PhysicalKey switch
|
||||||
|
{
|
||||||
|
PhysicalKey.Digit0 => 0, PhysicalKey.Digit1 => 1, PhysicalKey.Digit2 => 2,
|
||||||
|
PhysicalKey.Digit3 => 3, PhysicalKey.Digit4 => 4, PhysicalKey.Digit5 => 5,
|
||||||
|
PhysicalKey.Digit6 => 6, PhysicalKey.Digit7 => 7, PhysicalKey.Digit8 => 8,
|
||||||
|
PhysicalKey.Digit9 => 9,
|
||||||
|
_ => e.Key switch
|
||||||
|
{
|
||||||
|
Key.D0 or Key.NumPad0 => 0, Key.D1 or Key.NumPad1 => 1,
|
||||||
|
Key.D2 or Key.NumPad2 => 2, Key.D3 or Key.NumPad3 => 3,
|
||||||
|
Key.D4 or Key.NumPad4 => 4, Key.D5 or Key.NumPad5 => 5,
|
||||||
|
Key.D6 or Key.NumPad6 => 6, Key.D7 or Key.NumPad7 => 7,
|
||||||
|
Key.D8 or Key.NumPad8 => 8, Key.D9 or Key.NumPad9 => 9,
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnClose(object? sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
@@ -3,6 +3,21 @@
|
|||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanTabView"
|
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanTabView"
|
||||||
x:DataType="vm:SeatingPlanTabViewModel">
|
x:DataType="vm:SeatingPlanTabViewModel">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Border.seat">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltHighBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.seat.occupied">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.seat.droptarget">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight2}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
<Grid ColumnDefinitions="260,*">
|
<Grid ColumnDefinitions="260,*">
|
||||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
BorderThickness="0,0,1,0" Padding="16">
|
BorderThickness="0,0,1,0" Padding="16">
|
||||||
@@ -51,8 +66,11 @@
|
|||||||
<TextBlock Text="{Binding PlanTitle}" FontSize="22" FontWeight="SemiBold"/>
|
<TextBlock Text="{Binding PlanTitle}" FontSize="22" FontWeight="SemiBold"/>
|
||||||
<TextBlock Text="{Binding PlanSubtitle}" Opacity="0.65"/>
|
<TextBlock Text="{Binding PlanSubtitle}" Opacity="0.65"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding AssignmentSummary}" VerticalAlignment="Bottom"
|
<StackPanel Grid.Column="1" VerticalAlignment="Bottom">
|
||||||
FontSize="12" Opacity="0.6"/>
|
<TextBlock Text="{Binding AssignmentSummary}" HorizontalAlignment="Right" FontSize="12" Opacity="0.6"/>
|
||||||
|
<TextBlock Text="Ziehen: Platz ändern · Klicken: bewerten" HorizontalAlignment="Right"
|
||||||
|
FontSize="11" Opacity="0.5"/>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
|
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
|
||||||
@@ -70,24 +88,60 @@
|
|||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:SeatCellViewModel">
|
<DataTemplate x:DataType="vm:SeatCellViewModel">
|
||||||
<Border Width="190" MinHeight="76" Margin="5" Padding="10"
|
<Border Classes="seat" Classes.occupied="{Binding IsOccupied}"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
Classes.droptarget="{Binding IsDropTarget}"
|
||||||
BorderThickness="1" CornerRadius="7">
|
Width="190" MinHeight="76" Margin="5" Padding="10"
|
||||||
|
CornerRadius="7"
|
||||||
|
DragDrop.AllowDrop="True"
|
||||||
|
DragDrop.DragEnter="OnSeatDragEnter"
|
||||||
|
DragDrop.DragLeave="OnSeatDragLeave"
|
||||||
|
DragDrop.DragOver="OnSeatDragOver"
|
||||||
|
DragDrop.Drop="OnSeatDrop"
|
||||||
|
PointerPressed="OnDragSourcePressed"
|
||||||
|
PointerMoved="OnDragSourceMoved"
|
||||||
|
PointerReleased="OnDragSourceReleased"
|
||||||
|
Tapped="OnSeatTapped">
|
||||||
<StackPanel Spacing="5">
|
<StackPanel Spacing="5">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
||||||
<ComboBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"
|
<TextBlock Grid.Column="1" Text="⠿" Opacity="0.45" IsVisible="{Binding IsOccupied}"/>
|
||||||
IsEnabled="{Binding CanEdit}" HorizontalAlignment="Stretch">
|
</Grid>
|
||||||
<ComboBox.ItemTemplate>
|
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
|
||||||
<DataTemplate x:DataType="vm:StudentSeatOption">
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
<TextBlock Text="{Binding DisplayName}"/>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
|
<Border DragDrop.AllowDrop="True" DragDrop.DragOver="OnUnassignedDragOver"
|
||||||
|
DragDrop.Drop="OnUnassignedDrop"
|
||||||
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="7" Padding="12" Margin="5,8">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="NICHT ZUGEORDNET" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Text="Schüler hierher ziehen, um einen Platz zu leeren."
|
||||||
|
FontSize="11" Opacity="0.5"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding UnassignedStudents}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:StudentSeatOption">
|
||||||
|
<Border Background="{DynamicResource SystemAccentColorLight2}" CornerRadius="5"
|
||||||
|
Padding="9,5" Margin="0,0,6,6"
|
||||||
|
PointerPressed="OnDragSourcePressed"
|
||||||
|
PointerMoved="OnDragSourceMoved"
|
||||||
|
PointerReleased="OnDragSourceReleased">
|
||||||
|
<TextBlock Text="{Binding DisplayName}" FontSize="12"/>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Alle Schüler sind zugeordnet." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !UnassignedStudents.Count}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Threading;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using LehrerApp.Desktop.Views.Shared;
|
using LehrerApp.Desktop.Views.Shared;
|
||||||
@@ -7,6 +10,15 @@ namespace LehrerApp.Desktop.Views.Groups;
|
|||||||
|
|
||||||
public partial class SeatingPlanTabView : UserControl
|
public partial class SeatingPlanTabView : UserControl
|
||||||
{
|
{
|
||||||
|
private object? _dragCandidate;
|
||||||
|
private PointerPressedEventArgs? _dragTrigger;
|
||||||
|
private Avalonia.Point _pressPosition;
|
||||||
|
private SeatCellViewModel? _draggedSeat;
|
||||||
|
private StudentSeatOption? _draggedStudent;
|
||||||
|
private SeatCellViewModel? _pendingDropTarget;
|
||||||
|
private bool _pendingClearSeat;
|
||||||
|
private DateTime _ignoreTapUntil;
|
||||||
|
|
||||||
public SeatingPlanTabView() => InitializeComponent();
|
public SeatingPlanTabView() => InitializeComponent();
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
@@ -16,9 +28,128 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
{
|
{
|
||||||
vm.OnEditPlan = ShowPlanDialog;
|
vm.OnEditPlan = ShowPlanDialog;
|
||||||
vm.OnConfirmDelete = ShowDeleteConfirmDialog;
|
vm.OnConfirmDelete = ShowDeleteConfirmDialog;
|
||||||
|
vm.OnAssessStudent = ShowAssessmentDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDragSourcePressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Control control ||
|
||||||
|
!e.GetCurrentPoint(control).Properties.IsLeftButtonPressed) return;
|
||||||
|
var candidate = control.DataContext;
|
||||||
|
if (candidate is SeatCellViewModel { IsOccupied: false } or null) return;
|
||||||
|
if (DataContext is not SeatingPlanTabViewModel { IsEditable: true }) return;
|
||||||
|
_dragCandidate = candidate;
|
||||||
|
_dragTrigger = e;
|
||||||
|
_pressPosition = e.GetPosition(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnDragSourceMoved(object? sender, PointerEventArgs e)
|
||||||
|
{
|
||||||
|
if (_dragCandidate is null || _dragTrigger is null ||
|
||||||
|
!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
|
||||||
|
var current = e.GetPosition(this);
|
||||||
|
if (Math.Abs(current.X - _pressPosition.X) < 6 && Math.Abs(current.Y - _pressPosition.Y) < 6) return;
|
||||||
|
|
||||||
|
_draggedSeat = _dragCandidate as SeatCellViewModel;
|
||||||
|
_draggedStudent = _dragCandidate as StudentSeatOption;
|
||||||
|
_pendingDropTarget = null;
|
||||||
|
_pendingClearSeat = false;
|
||||||
|
_dragCandidate = null;
|
||||||
|
var trigger = _dragTrigger;
|
||||||
|
_dragTrigger = null;
|
||||||
|
var data = new DataTransfer();
|
||||||
|
data.Add(DataTransferItem.CreateText("LehrerApp-Sitzplatz"));
|
||||||
|
var effect = await DragDrop.DoDragDropAsync(trigger, data, DragDropEffects.Move);
|
||||||
|
var draggedSeat = _draggedSeat;
|
||||||
|
var draggedStudent = _draggedStudent;
|
||||||
|
var dropTarget = _pendingDropTarget;
|
||||||
|
var clearSeat = _pendingClearSeat;
|
||||||
|
_draggedSeat = null;
|
||||||
|
_draggedStudent = null;
|
||||||
|
_pendingDropTarget = null;
|
||||||
|
_pendingClearSeat = false;
|
||||||
|
_ignoreTapUntil = DateTime.UtcNow.AddMilliseconds(250);
|
||||||
|
|
||||||
|
// Never mutate an ItemsControl while Avalonia is still processing its native
|
||||||
|
// drop event. Assigning an unassigned student removes the dragged source item.
|
||||||
|
if ((effect & DragDropEffects.Move) == 0 || DataContext is not SeatingPlanTabViewModel vm) return;
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
if (clearSeat && draggedSeat is not null) vm.ClearSeat(draggedSeat);
|
||||||
|
else if (dropTarget is not null && draggedSeat is not null) vm.MoveSeat(draggedSeat, dropTarget);
|
||||||
|
else if (dropTarget is not null && draggedStudent is not null) vm.AssignStudent(draggedStudent, dropTarget);
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDragSourceReleased(object? sender, PointerReleasedEventArgs e)
|
||||||
|
{
|
||||||
|
_dragCandidate = null;
|
||||||
|
_dragTrigger = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSeatDragEnter(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Border { DataContext: SeatCellViewModel target } && CanDropOn(target))
|
||||||
|
target.IsDropTarget = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSeatDragLeave(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is Border { DataContext: SeatCellViewModel target }) target.IsDropTarget = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSeatDragOver(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
e.DragEffects = sender is Border { DataContext: SeatCellViewModel target } && CanDropOn(target)
|
||||||
|
? DragDropEffects.Move : DragDropEffects.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSeatDrop(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Border { DataContext: SeatCellViewModel target }) return;
|
||||||
|
target.IsDropTarget = false;
|
||||||
|
if (!CanDropOn(target))
|
||||||
|
{
|
||||||
|
e.DragEffects = DragDropEffects.None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_pendingDropTarget = target;
|
||||||
|
_pendingClearSeat = false;
|
||||||
|
e.DragEffects = DragDropEffects.Move;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanDropOn(SeatCellViewModel target) =>
|
||||||
|
DataContext is SeatingPlanTabViewModel { IsEditable: true }
|
||||||
|
&& ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null);
|
||||||
|
|
||||||
|
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>
|
||||||
|
e.DragEffects = _draggedSeat is not null ? DragDropEffects.Move : DragDropEffects.None;
|
||||||
|
|
||||||
|
private void OnUnassignedDrop(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (_draggedSeat is not null)
|
||||||
|
{
|
||||||
|
_pendingDropTarget = null;
|
||||||
|
_pendingClearSeat = true;
|
||||||
|
e.DragEffects = DragDropEffects.Move;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnSeatTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow < _ignoreTapUntil || sender is not Border { DataContext: SeatCellViewModel seat }
|
||||||
|
|| !seat.IsOccupied || DataContext is not SeatingPlanTabViewModel vm) return;
|
||||||
|
await vm.AssessStudent(seat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowAssessmentDialog(SeatAssessmentViewModel vm)
|
||||||
|
{
|
||||||
|
var dialog = new SeatAssessmentDialog { DataContext = vm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is not null) await dialog.ShowDialog(owner);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SeatingPlan?> ShowPlanDialog(SeatingPlan? plan)
|
private async Task<SeatingPlan?> ShowPlanDialog(SeatingPlan? plan)
|
||||||
{
|
{
|
||||||
if (DataContext is not SeatingPlanTabViewModel vm) return null;
|
if (DataContext is not SeatingPlanTabViewModel vm) return null;
|
||||||
|
|||||||
@@ -1151,10 +1151,14 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
|||||||
die bereits vorhandenen getrennten Adressfelder. DOCX-Vorlagen verwenden Word-
|
die bereits vorhandenen getrennten Adressfelder. DOCX-Vorlagen verwenden Word-
|
||||||
Inhaltssteuerelemente mit dokumentierten Tags wie `Letter.Salutation`, `Contact.Address`,
|
Inhaltssteuerelemente mit dokumentierten Tags wie `Letter.Salutation`, `Contact.Address`,
|
||||||
`Student.FirstName` und `Group.Name`.
|
`Student.FirstName` und `Group.Name`.
|
||||||
- [ ] **7.1.5** Sitzplan je Gruppe (Raster mit Drag & Drop), Sprung von Sitzplatz zur Bewertung.
|
- [x] **7.1.5** Sitzplan je Gruppe (Raster mit Drag & Drop), Sprung von Sitzplatz zur Bewertung.
|
||||||
Grundfunktion umgesetzt: mehrere benannte Sitzpläne je Lerngruppe und Raum, konfigurierbares
|
Mehrere benannte Sitzpläne je Lerngruppe und Raum besitzen ein konfigurierbares Raster.
|
||||||
Raster sowie direkte Schülerzuordnung pro Platz. Offen bleiben Drag & Drop und der Sprung von
|
Schüler werden aus „Nicht zugeordnet“ auf Plätze gezogen, zwischen belegten Plätzen
|
||||||
einem Sitzplatz in die Mitarbeitsbewertung.
|
getauscht oder durch Ablegen im freien Bereich wieder entfernt. Ein Klick auf einen
|
||||||
|
belegten Sitz öffnet die kompakte Sitzplatz-Schnelleingabe für alle aktiven
|
||||||
|
Mitarbeitsaspekte, Anwesenheit und Hausaufgaben. Die Eingabe unterstützt Maus sowie
|
||||||
|
Tastenkürzel und schreibt in die heutige Mitarbeitssitzung; falls noch keine existiert,
|
||||||
|
wird einmalig eine Sitzung „Sitzplan“ für heute angelegt.
|
||||||
|
|
||||||
### 7.2 Gruppen
|
### 7.2 Gruppen
|
||||||
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
|
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
|
||||||
@@ -1260,20 +1264,36 @@ Hervorhebung "eigene Klasse" über `LearningGroup.IsOwnClass`, feste Kartenbreit
|
|||||||
Stunde, für den vollständigen Tagesüberblick bleibt der Stundenplan zuständig.
|
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.
|
- [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"
|
**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
|
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
|
(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.
|
Schritt morgens eher die Mitarbeitserfassung als die Planung.
|
||||||
- [ ] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
|
- [x] **9.3** Kachel "Anstehende Termine": Klausuren, Förderplan-Überprüfungen, Abgabefristen.
|
||||||
- [ ] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
|
Bündelt geplante Klausuren, aktive Förderplan-Wiedervorlagen und offene Aufgaben mit
|
||||||
- [ ] **9.5** Kachel "Auffälligkeiten": Fehlzeitenüberschreitungen, Notenabfall, Versetzungsgefährdung.
|
Fälligkeitsdatum für die nächsten 30 Tage; überfällige Einträge bleiben sichtbar und alle
|
||||||
- [ ] **9.6** Dashboard-Kacheln ein-/ausblendbar und in der Reihenfolge konfigurierbar.
|
Einträge springen zur passenden Lerngruppe bzw. zum Schüler.
|
||||||
- [ ] **9.7** Automatische Aktualisierung beim Zurücknavigieren (aktuell nur manueller Refresh).
|
- [x] **9.4** Kachel "Offene Korrekturen" mit Fortschritt (x von y Klausuren bewertet).
|
||||||
- [ ] **9.8** Kalender-Detailansicht: Tag im Monatskalender anklickbar/auswählbar, zeigt in
|
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
|
einem angrenzenden Feld die Termine dieses Tages (Unterricht, Klausuren, perspektivisch
|
||||||
Konferenzen/Sondertermine) mit Details und Sprungmöglichkeit in die jeweilige Ansicht.
|
Konferenzen/Sondertermine) mit Details und Sprungmöglichkeit in die jeweilige Ansicht.
|
||||||
Sinnvoll erst, wenn weitere Terminarten existieren — Abhängigkeit zu Kapitel 4
|
Unterricht und Klausuren werden jetzt im Detailbereich der Kalenderkachel angezeigt und
|
||||||
(Unterrichtsplanung/Termine) sowie ggf. einem neuen Termine-Modell.
|
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