Merge branch 'main' of https://git.science-teaching.de/admin/LehrerApp
This commit is contained in:
@@ -60,6 +60,11 @@ public enum AttendanceStatus
|
|||||||
OtherSchoolEvent,
|
OtherSchoolEvent,
|
||||||
// Hinten angefügt, damit die numerischen Werte bereits gespeicherter LiteDB-Einträge stabil bleiben.
|
// Hinten angefügt, damit die numerischen Werte bereits gespeicherter LiteDB-Einträge stabil bleiben.
|
||||||
Present,
|
Present,
|
||||||
|
Late,
|
||||||
|
SignificantlyLate,
|
||||||
|
LeftDuringClass,
|
||||||
|
LearningIsland,
|
||||||
|
Suspended,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ public class SeatingPlan
|
|||||||
public string Room { get; set; } = "";
|
public string Room { get; set; } = "";
|
||||||
public int Rows { get; set; } = 4;
|
public int Rows { get; set; } = 4;
|
||||||
public int Columns { get; set; } = 4;
|
public int Columns { get; set; } = 4;
|
||||||
|
/// <summary>
|
||||||
|
/// Zeigt die Tafel unterhalb des Sitzrasters an. Der Standardwert <c>false</c>
|
||||||
|
/// erhält für bestehende Pläne die bisherige Darstellung oberhalb des Rasters.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsBoardAtBottom { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Zusätzlicher horizontaler Abstand nach jeder Spalte, in Pixeln. Der Eintrag mit Index 0
|
||||||
|
/// liegt zwischen der ersten und zweiten Spalte. Fehlende Einträge bedeuten keinen Abstand.
|
||||||
|
/// </summary>
|
||||||
|
public List<double> ColumnGapWidths { get; set; } = [];
|
||||||
public List<SeatAssignment> Assignments { get; set; } = [];
|
public List<SeatAssignment> Assignments { get; set; } = [];
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ public class AttendanceBalanceService
|
|||||||
.Select(e => e.Status!.Value)
|
.Select(e => e.Status!.Value)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var present = relevant.Count(s => s == AttendanceStatus.Present);
|
var present = relevant.Count(s => s is AttendanceStatus.Present
|
||||||
|
or AttendanceStatus.Late or AttendanceStatus.SignificantlyLate);
|
||||||
var excused = relevant.Count(s => s == AttendanceStatus.Excused);
|
var excused = relevant.Count(s => s == AttendanceStatus.Excused);
|
||||||
var unexcused = relevant.Count(s => s is AttendanceStatus.Unexcused or AttendanceStatus.Truant);
|
var unexcused = relevant.Count(s => s is AttendanceStatus.Unexcused
|
||||||
var schoolEvent = relevant.Count(s => s == AttendanceStatus.OtherSchoolEvent);
|
or AttendanceStatus.Truant or AttendanceStatus.LeftDuringClass);
|
||||||
|
var schoolEvent = relevant.Count(s => s is AttendanceStatus.OtherSchoolEvent
|
||||||
|
or AttendanceStatus.LearningIsland or AttendanceStatus.Suspended);
|
||||||
var pending = relevant.Count(s => s == AttendanceStatus.ExcusePending);
|
var pending = relevant.Count(s => s == AttendanceStatus.ExcusePending);
|
||||||
var total = relevant.Count;
|
var total = relevant.Count;
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,8 @@ public sealed class RepositoryTests
|
|||||||
repo.Save(new SeatingPlan
|
repo.Save(new SeatingPlan
|
||||||
{
|
{
|
||||||
GroupId = group.Id, Name = "Standard", Room = "Physik", Rows = 2, Columns = 3,
|
GroupId = group.Id, Name = "Standard", Room = "Physik", Rows = 2, Columns = 3,
|
||||||
|
IsBoardAtBottom = true,
|
||||||
|
ColumnGapWidths = [0, 90],
|
||||||
Assignments = [new SeatAssignment { Row = 1, Column = 2, StudentId = ben.Id }],
|
Assignments = [new SeatAssignment { Row = 1, Column = 2, StudentId = ben.Id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,7 +276,9 @@ public sealed class RepositoryTests
|
|||||||
|
|
||||||
Assert.Equal(2, plans.Count);
|
Assert.Equal(2, plans.Count);
|
||||||
Assert.Contains(plans, p => p.Room == "B204" && p.Assignments.Single().StudentId == anna.Id);
|
Assert.Contains(plans, p => p.Room == "B204" && p.Assignments.Single().StudentId == anna.Id);
|
||||||
Assert.Contains(plans, p => p.Room == "Physik" && p.Assignments.Single().StudentId == ben.Id);
|
Assert.Contains(plans, p => p.Room == "Physik" && p.IsBoardAtBottom
|
||||||
|
&& p.ColumnGapWidths.SequenceEqual([0, 90])
|
||||||
|
&& p.Assignments.Single().StudentId == ben.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
|
|||||||
plan.Name = plan.Name?.Trim() ?? "";
|
plan.Name = plan.Name?.Trim() ?? "";
|
||||||
plan.Room = plan.Room?.Trim() ?? "";
|
plan.Room = plan.Room?.Trim() ?? "";
|
||||||
plan.Assignments ??= [];
|
plan.Assignments ??= [];
|
||||||
|
plan.ColumnGapWidths ??= [];
|
||||||
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)
|
||||||
@@ -114,6 +115,12 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
|
|||||||
if (db.Groups.FindById(plan.GroupId) is null)
|
if (db.Groups.FindById(plan.GroupId) is null)
|
||||||
throw new InvalidOperationException("Die zugehörige Lerngruppe existiert nicht.");
|
throw new InvalidOperationException("Die zugehörige Lerngruppe existiert nicht.");
|
||||||
|
|
||||||
|
plan.ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
|
||||||
|
.Select(i => i < plan.ColumnGapWidths.Count && double.IsFinite(plan.ColumnGapWidths[i])
|
||||||
|
? Math.Clamp(plan.ColumnGapWidths[i], 0, 300)
|
||||||
|
: 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var duplicateName = db.SeatingPlans.Find(p => p.GroupId == plan.GroupId)
|
var duplicateName = db.SeatingPlans.Find(p => p.GroupId == plan.GroupId)
|
||||||
.FirstOrDefault(p => p.Id != plan.Id
|
.FirstOrDefault(p => p.Id != plan.Id
|
||||||
&& string.Equals(p.Name, plan.Name, StringComparison.OrdinalIgnoreCase)
|
&& string.Equals(p.Name, plan.Name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
|
public sealed class ParticipationSessionEditingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Bearbeitungsdialog_AendertBestehendeSessionOhneVerknuepfungenZuErsetzen()
|
||||||
|
{
|
||||||
|
var session = new ParticipationSession
|
||||||
|
{
|
||||||
|
GroupId = Guid.NewGuid(),
|
||||||
|
Date = new DateOnly(2026, 8, 19),
|
||||||
|
Comment = "Sitzplan",
|
||||||
|
LessonId = Guid.NewGuid(),
|
||||||
|
CompetencyCodes = ["K1"],
|
||||||
|
};
|
||||||
|
var vm = new AddSessionDialogViewModel(session)
|
||||||
|
{
|
||||||
|
DateText = "20.08.2026",
|
||||||
|
Comment = "Elektrische Stromkreise",
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Same(session, vm.Result);
|
||||||
|
Assert.Equal(new DateOnly(2026, 8, 20), session.Date);
|
||||||
|
Assert.Equal("Elektrische Stromkreise", session.Comment);
|
||||||
|
Assert.NotNull(session.LessonId);
|
||||||
|
Assert.Equal(["K1"], session.CompetencyCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BearbeitenBefehl_SpeichertSessionUndAktualisiertAnzeige()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2026/27" };
|
||||||
|
var session = new ParticipationSession
|
||||||
|
{
|
||||||
|
GroupId = group.Id,
|
||||||
|
Date = new DateOnly(2026, 8, 19),
|
||||||
|
Comment = "Sitzplan",
|
||||||
|
};
|
||||||
|
var sessions = new FakeSessions([session]);
|
||||||
|
var vm = new ParticipationTabViewModel(
|
||||||
|
sessions, new FakeEntries(), new FakeAspects(), new FakeStudents([]),
|
||||||
|
new FakeMemberships([]), new FakeGroups([group]), new FakeCompetencyDomains());
|
||||||
|
vm.OnEditSession = existing =>
|
||||||
|
{
|
||||||
|
existing.Comment = "Nachbesprechung Elektrizität";
|
||||||
|
return Task.FromResult<ParticipationSession?>(existing);
|
||||||
|
};
|
||||||
|
vm.Initialize(group.Id, group.SchoolYear);
|
||||||
|
|
||||||
|
await vm.EditSessionCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Equal(session.Id, vm.SelectedSession?.Id);
|
||||||
|
Assert.Equal("Nachbesprechung Elektrizität", sessions.GetById(session.Id)?.Comment);
|
||||||
|
Assert.Contains("Nachbesprechung Elektrizität", vm.SelectedSessionDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,24 @@ namespace LehrerApp.Desktop.Tests;
|
|||||||
|
|
||||||
public sealed class QuickInputViewModelTests
|
public sealed class QuickInputViewModelTests
|
||||||
{
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(AttendanceStatus.Late, false, "Verspätet", "V")]
|
||||||
|
[InlineData(AttendanceStatus.SignificantlyLate, false, "Erheblich verspätet", "V!")]
|
||||||
|
[InlineData(AttendanceStatus.LeftDuringClass, true, "Während des Unterrichts abgängig", "A")]
|
||||||
|
[InlineData(AttendanceStatus.LearningIsland, true, "Lerninsel", "L")]
|
||||||
|
[InlineData(AttendanceStatus.Suspended, true, "Suspendiert", "S")]
|
||||||
|
public void ZusaetzlicheAnwesenheitsstatus_HabenAnzeigeUndPassendeAbwesenheitswertung(
|
||||||
|
AttendanceStatus status, bool isAbsent, string label, string symbol)
|
||||||
|
{
|
||||||
|
var row = new ParticipationStudentRow(Guid.NewGuid(), "Anna",
|
||||||
|
new ParticipationEntry { Attendance = status }, [], []);
|
||||||
|
|
||||||
|
Assert.Equal(isAbsent, row.IsAbsent);
|
||||||
|
Assert.Equal(label, row.AttendanceTooltip);
|
||||||
|
Assert.Equal(symbol, row.AttendanceLabel);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(AttendanceDisplay.Color(status)));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ZurueckZuVorherigemSchueler_ZeigtBereitsGesetzteBewertung()
|
public void ZurueckZuVorherigemSchueler_ZeigtBereitsGesetzteBewertung()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,41 @@
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
|
using LehrerApp.Desktop.Views.Groups;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Tests;
|
namespace LehrerApp.Desktop.Tests;
|
||||||
|
|
||||||
public sealed class SeatingPlanViewModelTests
|
public sealed class SeatingPlanViewModelTests
|
||||||
{
|
{
|
||||||
|
[Fact]
|
||||||
|
public void TafelpositionWirdAusPlanGeladenUndBeimBearbeitenGespeichert()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = groupId,
|
||||||
|
Name = "Standard",
|
||||||
|
Rows = 1,
|
||||||
|
Columns = 1,
|
||||||
|
IsBoardAtBottom = true,
|
||||||
|
};
|
||||||
|
var plans = new FakeSeatingPlans([plan]);
|
||||||
|
var tabVm = new SeatingPlanTabViewModel(
|
||||||
|
plans, new FakeStudents([]), new FakeMemberships([]),
|
||||||
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
|
|
||||||
|
tabVm.Initialize(groupId, isReadOnly: false);
|
||||||
|
|
||||||
|
Assert.True(tabVm.IsBoardAtBottom);
|
||||||
|
Assert.False(tabVm.IsBoardAtTop);
|
||||||
|
|
||||||
|
var dialogVm = tabVm.CreateDialogViewModel(plan);
|
||||||
|
dialogVm.IsBoardAtBottom = false;
|
||||||
|
dialogVm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.False(plans.GetById(plan.Id)!.IsBoardAtBottom);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AuswahlEinesBereitsZugeordnetenSchuelers_VerschiebtIhnAufDenNeuenPlatz()
|
public void AuswahlEinesBereitsZugeordnetenSchuelers_VerschiebtIhnAufDenNeuenPlatz()
|
||||||
{
|
{
|
||||||
@@ -27,6 +57,7 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
|
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
|
||||||
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
vm.Initialize(groupId, isReadOnly: false);
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
vm.IsEditMode = true;
|
||||||
|
|
||||||
vm.Seats[1].SelectedOption = vm.StudentOptions.Single(o => o.StudentId == student.Id);
|
vm.Seats[1].SelectedOption = vm.StudentOptions.Single(o => o.StudentId == student.Id);
|
||||||
|
|
||||||
@@ -75,6 +106,7 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
|
var vm = new SeatingPlanTabViewModel(plans, students, memberships,
|
||||||
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
vm.Initialize(groupId, isReadOnly: false);
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
vm.IsEditMode = true;
|
||||||
|
|
||||||
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
|
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
|
||||||
|
|
||||||
@@ -83,6 +115,78 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count);
|
Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ansichtsmodus_VerhindertVerschiebenUndBlendetBearbeitungsbefehleAus()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
|
||||||
|
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
|
||||||
|
};
|
||||||
|
var vm = new SeatingPlanTabViewModel(
|
||||||
|
new FakeSeatingPlans([plan]), new FakeStudents([student]),
|
||||||
|
new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
|
||||||
|
new FakeSessions([]), new FakeEntries(), new FakeAspects());
|
||||||
|
vm.Initialize(groupId, isReadOnly: false);
|
||||||
|
|
||||||
|
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
|
||||||
|
|
||||||
|
Assert.False(vm.IsEditMode);
|
||||||
|
Assert.False(vm.CanEditLayout);
|
||||||
|
Assert.False(vm.EditPlanCommand.CanExecute(null));
|
||||||
|
Assert.Equal(student.Id, vm.Seats[0].SelectedOption.StudentId);
|
||||||
|
Assert.Null(vm.Seats[1].SelectedOption.StudentId);
|
||||||
|
|
||||||
|
vm.IsEditMode = true;
|
||||||
|
|
||||||
|
Assert.True(vm.CanEditLayout);
|
||||||
|
Assert.True(vm.EditPlanCommand.CanExecute(null));
|
||||||
|
Assert.All(vm.Seats, seat => Assert.True(seat.CanEdit));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Tischabstaende_WerdenBeimBearbeitenProSpaltengrenzeGespeichert()
|
||||||
|
{
|
||||||
|
var groupId = Guid.NewGuid();
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = groupId, Name = "Standard", Rows = 2, Columns = 4,
|
||||||
|
ColumnGapWidths = [0, 80, 20],
|
||||||
|
};
|
||||||
|
var plans = new FakeSeatingPlans([plan]);
|
||||||
|
var vm = new SeatingPlanDialogViewModel(plans, groupId, plan);
|
||||||
|
|
||||||
|
Assert.Equal([0m, 80m, 20m], vm.ColumnGaps.Select(g => g.Width));
|
||||||
|
vm.ColumnGaps[0].Width = 40;
|
||||||
|
vm.SaveCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Equal([40d, 80d, 20d], plans.GetById(plan.Id)!.ColumnGapWidths);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SitzplanPanel_FuegtAbstandNurAnKonfigurierterSpaltengrenzeEin()
|
||||||
|
{
|
||||||
|
var panel = new SeatingPlanPanel
|
||||||
|
{
|
||||||
|
Columns = 3,
|
||||||
|
ColumnGapWidths = [0, 70],
|
||||||
|
};
|
||||||
|
for (var i = 0; i < 6; i++)
|
||||||
|
panel.Children.Add(new Avalonia.Controls.Border { Width = 100, Height = 50 });
|
||||||
|
|
||||||
|
panel.Measure(Avalonia.Size.Infinity);
|
||||||
|
panel.Arrange(new Avalonia.Rect(panel.DesiredSize));
|
||||||
|
|
||||||
|
Assert.Equal(370, panel.DesiredSize.Width);
|
||||||
|
Assert.Equal(100, panel.DesiredSize.Height);
|
||||||
|
Assert.Equal(0, panel.Children[0].Bounds.X);
|
||||||
|
Assert.Equal(100, panel.Children[1].Bounds.X);
|
||||||
|
Assert.Equal(270, panel.Children[2].Bounds.X);
|
||||||
|
Assert.Equal(270, panel.Children[5].Bounds.X);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SitzplatzBewertung_ErstelltHeutigeSitzungUndSpeichertAlleDreiBereiche()
|
public void SitzplatzBewertung_ErstelltHeutigeSitzungUndSpeichertAlleDreiBereiche()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
|
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
|
||||||
|
|
||||||
public Func<Task<ParticipationSession?>>? OnAddSession { get; set; }
|
public Func<Task<ParticipationSession?>>? OnAddSession { get; set; }
|
||||||
|
public Func<ParticipationSession, Task<ParticipationSession?>>? OnEditSession { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnQuickInput { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnStatusQuickInput { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnStatusQuickInput { get; set; }
|
||||||
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
public Func<ParticipationTabViewModel, Task>? OnComputeGrade { get; set; }
|
||||||
@@ -118,6 +119,7 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
partial void OnSelectedSessionChanged(ParticipationSessionItem? value)
|
partial void OnSelectedSessionChanged(ParticipationSessionItem? value)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(SelectedSessionDisplay));
|
OnPropertyChanged(nameof(SelectedSessionDisplay));
|
||||||
|
EditSessionCommand.NotifyCanExecuteChanged();
|
||||||
if (value is null)
|
if (value is null)
|
||||||
{
|
{
|
||||||
StudentRows.Clear();
|
StudentRows.Clear();
|
||||||
@@ -265,6 +267,23 @@ public partial class ParticipationTabViewModel : ObservableObject
|
|||||||
SelectedSession = Sessions.FirstOrDefault(s => s.Id == session.Id);
|
SelectedSession = Sessions.FirstOrDefault(s => s.Id == session.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanEditSession))]
|
||||||
|
private async Task EditSession()
|
||||||
|
{
|
||||||
|
if (SelectedSession is null || OnEditSession is null) return;
|
||||||
|
var session = _sessions.GetById(SelectedSession.Id);
|
||||||
|
if (session is null) return;
|
||||||
|
var edited = await OnEditSession(session);
|
||||||
|
if (edited is null) return;
|
||||||
|
edited.GroupId = _groupId;
|
||||||
|
_sessions.Save(edited);
|
||||||
|
LoadSessions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanEditSession() => !IsReadOnly && SelectedSession is not null;
|
||||||
|
|
||||||
|
partial void OnIsReadOnlyChanged(bool value) => EditSessionCommand.NotifyCanExecuteChanged();
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanQuickInput))]
|
[RelayCommand(CanExecute = nameof(CanQuickInput))]
|
||||||
private async Task QuickInput()
|
private async Task QuickInput()
|
||||||
{
|
{
|
||||||
@@ -363,7 +382,10 @@ public partial class ParticipationStudentRow : ObservableObject
|
|||||||
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
|
||||||
/// Abwesend im Sinne der Mitarbeitsbewertung: eine Bewertung ergibt für diese Stunde keinen
|
/// Abwesend im Sinne der Mitarbeitsbewertung: eine Bewertung ergibt für diese Stunde keinen
|
||||||
/// Sinn, unabhängig davon, ob die Abwesenheit entschuldigt ist oder noch geklärt werden muss.
|
/// Sinn, unabhängig davon, ob die Abwesenheit entschuldigt ist oder noch geklärt werden muss.
|
||||||
public bool IsAbsent => Attendance is not null and not AttendanceStatus.Present;
|
public bool IsAbsent => Attendance is not null
|
||||||
|
and not AttendanceStatus.Present
|
||||||
|
and not AttendanceStatus.Late
|
||||||
|
and not AttendanceStatus.SignificantlyLate;
|
||||||
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
|
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
|
||||||
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
|
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
|
||||||
|
|
||||||
@@ -538,6 +560,11 @@ public static class AttendanceDisplay
|
|||||||
AttendanceStatus.Unexcused => "Krank, unentschuldigt",
|
AttendanceStatus.Unexcused => "Krank, unentschuldigt",
|
||||||
AttendanceStatus.Truant => "Geschwänzt",
|
AttendanceStatus.Truant => "Geschwänzt",
|
||||||
AttendanceStatus.OtherSchoolEvent => "Andere Schulveranstaltung",
|
AttendanceStatus.OtherSchoolEvent => "Andere Schulveranstaltung",
|
||||||
|
AttendanceStatus.Late => "Verspätet",
|
||||||
|
AttendanceStatus.SignificantlyLate => "Erheblich verspätet",
|
||||||
|
AttendanceStatus.LeftDuringClass => "Während des Unterrichts abgängig",
|
||||||
|
AttendanceStatus.LearningIsland => "Lerninsel",
|
||||||
|
AttendanceStatus.Suspended => "Suspendiert",
|
||||||
_ => "Anwesend",
|
_ => "Anwesend",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -550,6 +577,11 @@ public static class AttendanceDisplay
|
|||||||
AttendanceStatus.Unexcused => "!",
|
AttendanceStatus.Unexcused => "!",
|
||||||
AttendanceStatus.Truant => "✕",
|
AttendanceStatus.Truant => "✕",
|
||||||
AttendanceStatus.OtherSchoolEvent => "◇",
|
AttendanceStatus.OtherSchoolEvent => "◇",
|
||||||
|
AttendanceStatus.Late => "V",
|
||||||
|
AttendanceStatus.SignificantlyLate => "V!",
|
||||||
|
AttendanceStatus.LeftDuringClass => "A",
|
||||||
|
AttendanceStatus.LearningIsland => "L",
|
||||||
|
AttendanceStatus.Suspended => "S",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -561,6 +593,11 @@ public static class AttendanceDisplay
|
|||||||
AttendanceStatus.Unexcused => "#D96C00",
|
AttendanceStatus.Unexcused => "#D96C00",
|
||||||
AttendanceStatus.Truant => "#D64545",
|
AttendanceStatus.Truant => "#D64545",
|
||||||
AttendanceStatus.OtherSchoolEvent => "#5277C3",
|
AttendanceStatus.OtherSchoolEvent => "#5277C3",
|
||||||
|
AttendanceStatus.Late => "#D98200",
|
||||||
|
AttendanceStatus.SignificantlyLate => "#D96C00",
|
||||||
|
AttendanceStatus.LeftDuringClass => "#D64545",
|
||||||
|
AttendanceStatus.LearningIsland => "#5277C3",
|
||||||
|
AttendanceStatus.Suspended => "#6B6576",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -698,12 +735,26 @@ public class ParticipationSessionItem
|
|||||||
|
|
||||||
public partial class AddSessionDialogViewModel : ObservableObject
|
public partial class AddSessionDialogViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
private readonly ParticipationSession? _editingSession;
|
||||||
|
|
||||||
[ObservableProperty] private DateOnly _date = DateOnly.FromDateTime(DateTime.Today);
|
[ObservableProperty] private DateOnly _date = DateOnly.FromDateTime(DateTime.Today);
|
||||||
[ObservableProperty] private string _comment = "";
|
[ObservableProperty] private string _comment = "";
|
||||||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||||||
[ObservableProperty] private string _dateTextError = "";
|
[ObservableProperty] private string _dateTextError = "";
|
||||||
|
|
||||||
public ParticipationSession? Result { get; private set; }
|
public ParticipationSession? Result { get; private set; }
|
||||||
|
public bool IsEditing => _editingSession is not null;
|
||||||
|
public string DialogTitle => IsEditing ? "Bewertungszeitpunkt bearbeiten" : "Bewertungszeitpunkt anlegen";
|
||||||
|
public string SaveButtonText => IsEditing ? "Speichern" : "Anlegen";
|
||||||
|
|
||||||
|
public AddSessionDialogViewModel(ParticipationSession? editingSession = null)
|
||||||
|
{
|
||||||
|
_editingSession = editingSession;
|
||||||
|
if (editingSession is null) return;
|
||||||
|
Date = editingSession.Date;
|
||||||
|
DateText = editingSession.Date.ToString("dd.MM.yyyy");
|
||||||
|
Comment = editingSession.Comment ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Save()
|
private void Save()
|
||||||
@@ -715,7 +766,10 @@ public partial class AddSessionDialogViewModel : ObservableObject
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DateTextError = "";
|
DateTextError = "";
|
||||||
Result = new ParticipationSession { Date = date, Comment = Comment.Trim() };
|
var session = _editingSession ?? new ParticipationSession();
|
||||||
|
session.Date = date;
|
||||||
|
session.Comment = Comment.Trim();
|
||||||
|
Result = session;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _planTitle = "";
|
[ObservableProperty] private string _planTitle = "";
|
||||||
[ObservableProperty] private string _planSubtitle = "";
|
[ObservableProperty] private string _planSubtitle = "";
|
||||||
[ObservableProperty] private string _assignmentSummary = "";
|
[ObservableProperty] private string _assignmentSummary = "";
|
||||||
|
[ObservableProperty] private bool _isBoardAtTop = true;
|
||||||
|
[ObservableProperty] private bool _isBoardAtBottom;
|
||||||
|
[ObservableProperty] private IReadOnlyList<double> _columnGapWidths = [];
|
||||||
|
[ObservableProperty] private bool _isEditMode;
|
||||||
|
|
||||||
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
|
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
|
||||||
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
|
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
|
||||||
@@ -33,6 +37,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
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 bool CanEditLayout => IsEditable && IsEditMode;
|
||||||
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 Func<SeatAssessmentViewModel, Task>? OnAssessStudent { get; set; }
|
||||||
@@ -57,6 +62,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
_groupId = groupId;
|
_groupId = groupId;
|
||||||
_isReadOnly = isReadOnly;
|
_isReadOnly = isReadOnly;
|
||||||
|
IsEditMode = false;
|
||||||
LoadStudentOptions();
|
LoadStudentOptions();
|
||||||
ReloadPlans();
|
ReloadPlans();
|
||||||
OnPropertyChanged(nameof(IsEditable));
|
OnPropertyChanged(nameof(IsEditable));
|
||||||
@@ -102,6 +108,9 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
PlanTitle = "";
|
PlanTitle = "";
|
||||||
PlanSubtitle = "";
|
PlanSubtitle = "";
|
||||||
AssignmentSummary = "";
|
AssignmentSummary = "";
|
||||||
|
IsBoardAtTop = true;
|
||||||
|
IsBoardAtBottom = false;
|
||||||
|
ColumnGapWidths = [];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -110,6 +119,12 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
PlanSubtitle = string.IsNullOrWhiteSpace(plan.Room)
|
PlanSubtitle = string.IsNullOrWhiteSpace(plan.Room)
|
||||||
? $"{plan.Rows} × {plan.Columns} Plätze"
|
? $"{plan.Rows} × {plan.Columns} Plätze"
|
||||||
: $"Raum {plan.Room} · {plan.Rows} × {plan.Columns} Plätze";
|
: $"Raum {plan.Room} · {plan.Rows} × {plan.Columns} Plätze";
|
||||||
|
IsBoardAtBottom = plan.IsBoardAtBottom;
|
||||||
|
IsBoardAtTop = !plan.IsBoardAtBottom;
|
||||||
|
var savedGapWidths = plan.ColumnGapWidths ?? [];
|
||||||
|
ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
|
||||||
|
.Select(i => i < savedGapWidths.Count ? savedGapWidths[i] : 0)
|
||||||
|
.ToArray();
|
||||||
for (var row = 0; row < plan.Rows; row++)
|
for (var row = 0; row < plan.Rows; row++)
|
||||||
for (var column = 0; column < plan.Columns; column++)
|
for (var column = 0; column < plan.Columns; column++)
|
||||||
{
|
{
|
||||||
@@ -118,7 +133,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
? StudentSeatOption.Empty
|
? StudentSeatOption.Empty
|
||||||
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
|
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
|
||||||
?? StudentSeatOption.Empty;
|
?? StudentSeatOption.Empty;
|
||||||
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, IsEditable));
|
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, CanEditLayout));
|
||||||
}
|
}
|
||||||
UpdateAssignmentSummary();
|
UpdateAssignmentSummary();
|
||||||
}
|
}
|
||||||
@@ -128,7 +143,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
|
|
||||||
private void OnSeatChanged(SeatCellViewModel changed)
|
private void OnSeatChanged(SeatCellViewModel changed)
|
||||||
{
|
{
|
||||||
if (_currentPlan is null || !IsEditable) return;
|
if (_currentPlan is null || !CanEditLayout) return;
|
||||||
if (changed.SelectedOption.StudentId is Guid studentId)
|
if (changed.SelectedOption.StudentId is Guid studentId)
|
||||||
{
|
{
|
||||||
foreach (var other in Seats.Where(s => s != changed && s.SelectedOption.StudentId == studentId))
|
foreach (var other in Seats.Where(s => s != changed && s.SelectedOption.StudentId == studentId))
|
||||||
@@ -152,7 +167,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
|
|
||||||
public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target)
|
public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target)
|
||||||
{
|
{
|
||||||
if (!IsEditable || source == target || !source.SelectedOption.StudentId.HasValue) return;
|
if (!CanEditLayout || source == target || !source.SelectedOption.StudentId.HasValue) return;
|
||||||
var targetOption = target.SelectedOption;
|
var targetOption = target.SelectedOption;
|
||||||
target.SetSelectionSilently(source.SelectedOption);
|
target.SetSelectionSilently(source.SelectedOption);
|
||||||
source.SetSelectionSilently(targetOption);
|
source.SetSelectionSilently(targetOption);
|
||||||
@@ -161,7 +176,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
|
|
||||||
public void AssignStudent(StudentSeatOption student, SeatCellViewModel target)
|
public void AssignStudent(StudentSeatOption student, SeatCellViewModel target)
|
||||||
{
|
{
|
||||||
if (!IsEditable || !student.StudentId.HasValue) return;
|
if (!CanEditLayout || !student.StudentId.HasValue) return;
|
||||||
foreach (var other in Seats.Where(s => s != target && s.SelectedOption.StudentId == student.StudentId))
|
foreach (var other in Seats.Where(s => s != target && s.SelectedOption.StudentId == student.StudentId))
|
||||||
other.SetSelectionSilently(StudentSeatOption.Empty);
|
other.SetSelectionSilently(StudentSeatOption.Empty);
|
||||||
target.SetSelectionSilently(student);
|
target.SetSelectionSilently(student);
|
||||||
@@ -170,7 +185,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
|
|
||||||
public void ClearSeat(SeatCellViewModel seat)
|
public void ClearSeat(SeatCellViewModel seat)
|
||||||
{
|
{
|
||||||
if (!IsEditable || !seat.SelectedOption.StudentId.HasValue) return;
|
if (!CanEditLayout || !seat.SelectedOption.StudentId.HasValue) return;
|
||||||
seat.SetSelectionSilently(StudentSeatOption.Empty);
|
seat.SetSelectionSilently(StudentSeatOption.Empty);
|
||||||
SaveSeatAssignments();
|
SaveSeatAssignments();
|
||||||
}
|
}
|
||||||
@@ -223,8 +238,15 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
ReloadPlans();
|
ReloadPlans();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanEdit() => IsEditable;
|
partial void OnIsEditModeChanged(bool value)
|
||||||
private bool CanEditSelected() => IsEditable && _currentPlan is not null;
|
{
|
||||||
|
OnPropertyChanged(nameof(CanEditLayout));
|
||||||
|
foreach (var seat in Seats) seat.CanEdit = CanEditLayout;
|
||||||
|
NotifyCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanEdit() => IsEditable && (IsEditMode || !HasPlans);
|
||||||
|
private bool CanEditSelected() => CanEditLayout && _currentPlan is not null;
|
||||||
|
|
||||||
private void NotifyCommands()
|
private void NotifyCommands()
|
||||||
{
|
{
|
||||||
@@ -264,7 +286,7 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
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; }
|
[ObservableProperty] private bool _canEdit;
|
||||||
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
||||||
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
|
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
|
||||||
|
|
||||||
@@ -276,7 +298,7 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
Options = options;
|
Options = options;
|
||||||
_selectedOption = selectedOption;
|
_selectedOption = selectedOption;
|
||||||
_onChanged = onChanged;
|
_onChanged = onChanged;
|
||||||
CanEdit = canEdit;
|
_canEdit = canEdit;
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
||||||
@@ -363,6 +385,11 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|||||||
AttendanceChoices.Add(new("◇", "Schulveranstaltung", "Strg+7", AttendanceStatus.OtherSchoolEvent, SetAttendance));
|
AttendanceChoices.Add(new("◇", "Schulveranstaltung", "Strg+7", AttendanceStatus.OtherSchoolEvent, SetAttendance));
|
||||||
AttendanceChoices.Add(new("✕", "Geschwänzt", "Strg+9", AttendanceStatus.Truant, SetAttendance));
|
AttendanceChoices.Add(new("✕", "Geschwänzt", "Strg+9", AttendanceStatus.Truant, SetAttendance));
|
||||||
AttendanceChoices.Add(new("!", "Unentschuldigt", "Strg+0", AttendanceStatus.Unexcused, SetAttendance));
|
AttendanceChoices.Add(new("!", "Unentschuldigt", "Strg+0", AttendanceStatus.Unexcused, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("V", "Verspätet", "", AttendanceStatus.Late, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("V!", "Erheblich verspätet", "", AttendanceStatus.SignificantlyLate, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("A", "Im Unterricht abgängig", "", AttendanceStatus.LeftDuringClass, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("L", "Lerninsel", "", AttendanceStatus.LearningIsland, SetAttendance));
|
||||||
|
AttendanceChoices.Add(new("S", "Suspendiert", "", AttendanceStatus.Suspended, SetAttendance));
|
||||||
AttendanceChoices.Add(new("·", "Nicht kontrolliert", "Strg+X", null, SetAttendance));
|
AttendanceChoices.Add(new("·", "Nicht kontrolliert", "Strg+X", null, SetAttendance));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,10 +606,12 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _room = "";
|
[ObservableProperty] private string _room = "";
|
||||||
[ObservableProperty] private decimal _rows = 4;
|
[ObservableProperty] private decimal _rows = 4;
|
||||||
[ObservableProperty] private decimal _columns = 4;
|
[ObservableProperty] private decimal _columns = 4;
|
||||||
|
[ObservableProperty] private bool _isBoardAtBottom;
|
||||||
[ObservableProperty] private string _nameError = "";
|
[ObservableProperty] private string _nameError = "";
|
||||||
[ObservableProperty] private string _layoutError = "";
|
[ObservableProperty] private string _layoutError = "";
|
||||||
|
|
||||||
public SeatingPlan? Result { get; private set; }
|
public SeatingPlan? Result { get; private set; }
|
||||||
|
public ObservableCollection<ColumnGapEditor> ColumnGaps { get; } = [];
|
||||||
public string DialogTitle => _editingPlan is null ? "Neuen Sitzplan anlegen" : "Sitzplan bearbeiten";
|
public string DialogTitle => _editingPlan is null ? "Neuen Sitzplan anlegen" : "Sitzplan bearbeiten";
|
||||||
public string SaveButtonText => _editingPlan is null ? "Anlegen" : "Speichern";
|
public string SaveButtonText => _editingPlan is null ? "Anlegen" : "Speichern";
|
||||||
|
|
||||||
@@ -591,11 +620,26 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
|
|||||||
_plans = plans;
|
_plans = plans;
|
||||||
_groupId = groupId;
|
_groupId = groupId;
|
||||||
_editingPlan = editingPlan;
|
_editingPlan = editingPlan;
|
||||||
|
RebuildColumnGaps(decimal.ToInt32(Columns));
|
||||||
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;
|
||||||
|
IsBoardAtBottom = editingPlan.IsBoardAtBottom;
|
||||||
|
var savedGapWidths = editingPlan.ColumnGapWidths ?? [];
|
||||||
|
for (var i = 0; i < ColumnGaps.Count && i < savedGapWidths.Count; i++)
|
||||||
|
ColumnGaps[i].Width = (decimal)savedGapWidths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnColumnsChanged(decimal value) => RebuildColumnGaps(decimal.ToInt32(value));
|
||||||
|
|
||||||
|
private void RebuildColumnGaps(int columns)
|
||||||
|
{
|
||||||
|
var previous = ColumnGaps.ToDictionary(g => g.AfterColumn, g => g.Width);
|
||||||
|
ColumnGaps.Clear();
|
||||||
|
for (var afterColumn = 1; afterColumn < columns; afterColumn++)
|
||||||
|
ColumnGaps.Add(new ColumnGapEditor(afterColumn, previous.GetValueOrDefault(afterColumn)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
@@ -621,6 +665,8 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
|
|||||||
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);
|
||||||
|
plan.IsBoardAtBottom = IsBoardAtBottom;
|
||||||
|
plan.ColumnGapWidths = ColumnGaps.Select(g => decimal.ToDouble(g.Width)).ToList();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_plans.Save(plan);
|
_plans.Save(plan);
|
||||||
@@ -632,3 +678,10 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class ColumnGapEditor(int afterColumn, decimal width) : ObservableObject
|
||||||
|
{
|
||||||
|
public int AfterColumn { get; } = afterColumn;
|
||||||
|
public string Label => $"Nach Platz {AfterColumn}";
|
||||||
|
[ObservableProperty] private decimal _width = width;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.AddSessionDialog"
|
x:Class="LehrerApp.Desktop.Views.Groups.AddSessionDialog"
|
||||||
x:DataType="vm:AddSessionDialogViewModel"
|
x:DataType="vm:AddSessionDialogViewModel"
|
||||||
Title="Bewertungszeitpunkt anlegen"
|
Title="{Binding DialogTitle}"
|
||||||
Width="380" SizeToContent="Height"
|
Width="380" SizeToContent="Height"
|
||||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
CanResize="False" WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
<StackPanel Grid.Row="0" Spacing="14">
|
<StackPanel Grid.Row="0" Spacing="14">
|
||||||
<TextBlock Text="Neuer Bewertungszeitpunkt" Classes="dialogtitle"/>
|
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Datum *" FontSize="12" Opacity="0.7"/>
|
||||||
@@ -20,13 +20,14 @@
|
|||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Kommentar (optional)" FontSize="12" Opacity="0.7"/>
|
<TextBlock Text="Kommentar (optional)" FontSize="12" Opacity="0.7"/>
|
||||||
<TextBox Text="{Binding Comment}" PlaceholderText="z. B. Stunde 12 – Säure-Base-Reaktion"/>
|
<TextBox Text="{Binding Comment}" PlaceholderText="z. B. Stunde 12 – Säure-Base-Reaktion"
|
||||||
|
x:Name="CommentBox"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
<Button Grid.Column="2" Content="Anlegen" HorizontalAlignment="Stretch" Click="OnSave"/>
|
<Button Grid.Column="2" Content="{Binding SaveButtonText}" HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -11,8 +11,17 @@ public partial class AddSessionDialog : Window
|
|||||||
protected override void OnOpened(EventArgs e)
|
protected override void OnOpened(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnOpened(e);
|
base.OnOpened(e);
|
||||||
|
if (DataContext is AddSessionDialogViewModel { IsEditing: true })
|
||||||
|
{
|
||||||
|
var commentBox = this.FindControl<TextBox>("CommentBox");
|
||||||
|
commentBox?.Focus();
|
||||||
|
commentBox?.SelectAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
this.FindControl<TextBox>("DateBox")?.Focus();
|
this.FindControl<TextBox>("DateBox")?.Focus();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnSave(object? s, RoutedEventArgs e)
|
private void OnSave(object? s, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<Grid Grid.Row="2" RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
<Grid Grid.Row="2" RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||||||
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="3">
|
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="3">
|
||||||
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||||
Text="Anwesenheit: 1 anwesend · 2 Entschuldigung offen · 5 krank/entschuldigt · 7 andere Schulveranstaltung · 9 geschwänzt · 0 unentschuldigt · X nicht kontrolliert"/>
|
Text="Anwesenheit: 1 anwesend · 2 Entschuldigung offen · 5 krank/entschuldigt · 7 andere Schulveranstaltung · 9 geschwänzt · 0 unentschuldigt · X nicht kontrolliert · seltene Status per Klick im Menü"/>
|
||||||
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
|
||||||
Text="Hausaufgaben mit Alt/⌥: 1 gemacht · 3 teilweise/Rest offen · 4 Rest nachgereicht · 5 Rest nicht nachgereicht · 7 nicht gemacht/offen · 8 vollständig nachgereicht · 0 endgültig nicht nachgereicht · X keine aufgegeben"/>
|
Text="Hausaufgaben mit Alt/⌥: 1 gemacht · 3 teilweise/Rest offen · 4 Rest nachgereicht · 5 Rest nicht nachgereicht · 7 nicht gemacht/offen · 8 vollständig nachgereicht · 0 endgültig nicht nachgereicht · X keine aufgegeben"/>
|
||||||
<TextBlock FontSize="10" Opacity="0.55"
|
<TextBlock FontSize="10" Opacity="0.55"
|
||||||
|
|||||||
@@ -67,6 +67,13 @@ public partial class AttendanceHomeworkQuickInputDialog : Window
|
|||||||
AddAttendanceItem(flyout, row, "[7] ◇ Andere Schulveranstaltung", AttendanceStatus.OtherSchoolEvent);
|
AddAttendanceItem(flyout, row, "[7] ◇ Andere Schulveranstaltung", AttendanceStatus.OtherSchoolEvent);
|
||||||
AddAttendanceItem(flyout, row, "[9] ✕ Geschwänzt", AttendanceStatus.Truant);
|
AddAttendanceItem(flyout, row, "[9] ✕ Geschwänzt", AttendanceStatus.Truant);
|
||||||
AddAttendanceItem(flyout, row, "[0] ! Unentschuldigt", AttendanceStatus.Unexcused);
|
AddAttendanceItem(flyout, row, "[0] ! Unentschuldigt", AttendanceStatus.Unexcused);
|
||||||
|
flyout.Items.Add(new Separator());
|
||||||
|
AddAttendanceItem(flyout, row, "V Verspätet", AttendanceStatus.Late);
|
||||||
|
AddAttendanceItem(flyout, row, "V! Erheblich verspätet", AttendanceStatus.SignificantlyLate);
|
||||||
|
AddAttendanceItem(flyout, row, "A Während des Unterrichts abgängig", AttendanceStatus.LeftDuringClass);
|
||||||
|
AddAttendanceItem(flyout, row, "L Lerninsel", AttendanceStatus.LearningIsland);
|
||||||
|
AddAttendanceItem(flyout, row, "S Suspendiert", AttendanceStatus.Suspended);
|
||||||
|
flyout.Items.Add(new Separator());
|
||||||
AddAttendanceItem(flyout, row, "[X] · Nicht kontrolliert", null);
|
AddAttendanceItem(flyout, row, "[X] · Nicht kontrolliert", null);
|
||||||
button.Flyout = flyout;
|
button.Flyout = flyout;
|
||||||
|
|
||||||
|
|||||||
@@ -53,13 +53,16 @@
|
|||||||
Text="{Binding SelectedSessionDisplay}"
|
Text="{Binding SelectedSessionDisplay}"
|
||||||
FontSize="13" FontWeight="SemiBold"
|
FontSize="13" FontWeight="SemiBold"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
|
||||||
IsVisible="{Binding HasCompetencyCatalog}">
|
<Button Content="✎ Sitzung bearbeiten" Command="{Binding EditSessionCommand}"
|
||||||
|
IsVisible="{Binding !IsReadOnly}" FontSize="11" Padding="8,3"/>
|
||||||
<ToggleButton Content="◇ Kompetenzen"
|
<ToggleButton Content="◇ Kompetenzen"
|
||||||
IsChecked="{Binding CompetencyTagsVisible}"
|
IsChecked="{Binding CompetencyTagsVisible}"
|
||||||
|
IsVisible="{Binding HasCompetencyCatalog}"
|
||||||
FontSize="11" Padding="8,3"/>
|
FontSize="11" Padding="8,3"/>
|
||||||
<ToggleButton Content="◈ Schüler-Bewertungen"
|
<ToggleButton Content="◈ Schüler-Bewertungen"
|
||||||
IsChecked="{Binding StudentCompetencyRatingsVisible}"
|
IsChecked="{Binding StudentCompetencyRatingsVisible}"
|
||||||
|
IsVisible="{Binding HasCompetencyCatalog}"
|
||||||
FontSize="11" Padding="8,3"/>
|
FontSize="11" Padding="8,3"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
{
|
{
|
||||||
_vm = vm;
|
_vm = vm;
|
||||||
vm.OnAddSession = ShowAddSessionDialog;
|
vm.OnAddSession = ShowAddSessionDialog;
|
||||||
|
vm.OnEditSession = ShowEditSessionDialog;
|
||||||
vm.OnQuickInput = ShowQuickInputDialog;
|
vm.OnQuickInput = ShowQuickInputDialog;
|
||||||
vm.OnStatusQuickInput = ShowStatusQuickInputDialog;
|
vm.OnStatusQuickInput = ShowStatusQuickInputDialog;
|
||||||
vm.OnComputeGrade = ShowComputeGradeDialog;
|
vm.OnComputeGrade = ShowComputeGradeDialog;
|
||||||
@@ -283,6 +284,17 @@ public partial class ParticipationTabView : UserControl
|
|||||||
return ok ? vm.Result : null;
|
return ok ? vm.Result : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<LehrerApp.Core.Models.ParticipationSession?> ShowEditSessionDialog(
|
||||||
|
LehrerApp.Core.Models.ParticipationSession session)
|
||||||
|
{
|
||||||
|
var vm = new AddSessionDialogViewModel(session);
|
||||||
|
var dialog = new AddSessionDialog { DataContext = vm };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner is null) return null;
|
||||||
|
var ok = await dialog.ShowDialog<bool>(owner);
|
||||||
|
return ok ? vm.Result : null;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ShowQuickInputDialog(ParticipationTabViewModel tabVm)
|
private async Task ShowQuickInputDialog(ParticipationTabViewModel tabVm)
|
||||||
{
|
{
|
||||||
if (tabVm.StudentRows.Count == 0) return;
|
if (tabVm.StudentRows.Count == 0) return;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanDialog"
|
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanDialog"
|
||||||
x:DataType="vm:SeatingPlanDialogViewModel"
|
x:DataType="vm:SeatingPlanDialogViewModel"
|
||||||
Title="{Binding DialogTitle}" Width="480" Height="470"
|
Title="{Binding DialogTitle}" Width="620" Height="680" MinHeight="560"
|
||||||
CanResize="False" WindowStartupLocation="CenterOwner">
|
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||||
|
<ScrollViewer>
|
||||||
<StackPanel Spacing="16">
|
<StackPanel Spacing="16">
|
||||||
<StackPanel Spacing="3">
|
<StackPanel Spacing="3">
|
||||||
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
|
||||||
@@ -40,12 +41,39 @@
|
|||||||
<TextBlock Text="{Binding LayoutError}" Foreground="Red" FontSize="11"
|
<TextBlock Text="{Binding LayoutError}" Foreground="Red" FontSize="11"
|
||||||
IsVisible="{Binding LayoutError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
IsVisible="{Binding LayoutError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Tafelposition" FontSize="12" Opacity="0.7"/>
|
||||||
|
<CheckBox Content="Tafel unterhalb der Sitzplätze anzeigen"
|
||||||
|
IsChecked="{Binding IsBoardAtBottom}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="7">
|
||||||
|
<TextBlock Text="Abstände zwischen den Tischen" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBlock Text="Breite 0 bedeutet: kein zusätzlicher Abstand. So lassen sich z. B. Tischgruppen und Mittelgänge abbilden."
|
||||||
|
FontSize="11" Opacity="0.55" TextWrapping="Wrap"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding ColumnGaps}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ColumnGapEditor">
|
||||||
|
<StackPanel Width="125" Margin="0,0,10,8" Spacing="3">
|
||||||
|
<TextBlock Text="{Binding Label}" FontSize="11" Opacity="0.65"/>
|
||||||
|
<NumericUpDown Value="{Binding Width}" Minimum="0" Maximum="300"
|
||||||
|
Increment="10" FormatString="0 px"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||||
CornerRadius="6" Padding="12">
|
CornerRadius="6" Padding="12">
|
||||||
<TextBlock Text="Beim Verkleinern des Rasters entfallen Zuordnungen außerhalb der neuen Größe."
|
<TextBlock Text="Beim Verkleinern des Rasters entfallen Zuordnungen außerhalb der neuen Größe."
|
||||||
FontSize="12" Opacity="0.7" TextWrapping="Wrap"/>
|
FontSize="12" Opacity="0.7" TextWrapping="Wrap"/>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views.Groups;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ordnet Sitzplätze in einem Raster an und fügt zwischen ausgewählten Spalten zusätzliche
|
||||||
|
/// transparente Breite ein. Anders als ein UniformGrid kann der Abstand je Spaltengrenze variieren.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SeatingPlanPanel : Panel
|
||||||
|
{
|
||||||
|
public static readonly StyledProperty<int> ColumnsProperty =
|
||||||
|
AvaloniaProperty.Register<SeatingPlanPanel, int>(nameof(Columns), 1);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IReadOnlyList<double>> ColumnGapWidthsProperty =
|
||||||
|
AvaloniaProperty.Register<SeatingPlanPanel, IReadOnlyList<double>>(
|
||||||
|
nameof(ColumnGapWidths), Array.Empty<double>());
|
||||||
|
|
||||||
|
public int Columns
|
||||||
|
{
|
||||||
|
get => GetValue(ColumnsProperty);
|
||||||
|
set => SetValue(ColumnsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<double> ColumnGapWidths
|
||||||
|
{
|
||||||
|
get => GetValue(ColumnGapWidthsProperty);
|
||||||
|
set => SetValue(ColumnGapWidthsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SeatingPlanPanel() =>
|
||||||
|
AffectsMeasure<SeatingPlanPanel>(ColumnsProperty, ColumnGapWidthsProperty);
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
foreach (var child in Children)
|
||||||
|
child.Measure(Size.Infinity);
|
||||||
|
|
||||||
|
if (Children.Count == 0) return default;
|
||||||
|
var columns = Math.Max(1, Columns);
|
||||||
|
var rows = (int)Math.Ceiling((double)Children.Count / columns);
|
||||||
|
var cellWidth = Children.Max(child => child.DesiredSize.Width);
|
||||||
|
var cellHeight = Children.Max(child => child.DesiredSize.Height);
|
||||||
|
return new Size(cellWidth * columns + GapWidth(columns), cellHeight * rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Size ArrangeOverride(Size finalSize)
|
||||||
|
{
|
||||||
|
if (Children.Count == 0) return finalSize;
|
||||||
|
var columns = Math.Max(1, Columns);
|
||||||
|
var cellWidth = Children.Max(child => child.DesiredSize.Width);
|
||||||
|
var cellHeight = Children.Max(child => child.DesiredSize.Height);
|
||||||
|
|
||||||
|
for (var index = 0; index < Children.Count; index++)
|
||||||
|
{
|
||||||
|
var column = index % columns;
|
||||||
|
var row = index / columns;
|
||||||
|
var child = Children[index];
|
||||||
|
var x = column * cellWidth + GapWidthBefore(column);
|
||||||
|
var childX = x + Math.Max(0, (cellWidth - child.DesiredSize.Width) / 2);
|
||||||
|
var childY = row * cellHeight + Math.Max(0, (cellHeight - child.DesiredSize.Height) / 2);
|
||||||
|
child.Arrange(new Rect(childX, childY, child.DesiredSize.Width, child.DesiredSize.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GapWidth(int columns) =>
|
||||||
|
Enumerable.Range(0, Math.Max(0, columns - 1)).Sum(GapAt);
|
||||||
|
|
||||||
|
private double GapWidthBefore(int column) =>
|
||||||
|
Enumerable.Range(0, Math.Max(0, column)).Sum(GapAt);
|
||||||
|
|
||||||
|
private double GapAt(int index) => index < ColumnGapWidths.Count
|
||||||
|
? Math.Clamp(ColumnGapWidths[index], 0, 300)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||||
|
xmlns:groups="clr-namespace:LehrerApp.Desktop.Views.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>
|
<UserControl.Styles>
|
||||||
@@ -37,7 +38,7 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
<StackPanel Grid.Row="2" Spacing="8" Margin="0,12,0,0">
|
<StackPanel Grid.Row="2" Spacing="8" Margin="0,12,0,0" IsVisible="{Binding IsEditMode}">
|
||||||
<Button Content="+ Sitzplan" Command="{Binding AddPlanCommand}" HorizontalAlignment="Stretch"/>
|
<Button Content="+ Sitzplan" Command="{Binding AddPlanCommand}" HorizontalAlignment="Stretch"/>
|
||||||
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding HasSelectedPlan}">
|
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding HasSelectedPlan}">
|
||||||
<Button Grid.Column="0" Content="Bearbeiten" Command="{Binding EditPlanCommand}"
|
<Button Grid.Column="0" Content="Bearbeiten" Command="{Binding EditPlanCommand}"
|
||||||
@@ -60,30 +61,41 @@
|
|||||||
HorizontalAlignment="Center"/>
|
HorizontalAlignment="Center"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*" IsVisible="{Binding HasSelectedPlan}" Margin="24">
|
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,Auto" IsVisible="{Binding HasSelectedPlan}" Margin="24">
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="0,0,0,18">
|
<Grid Grid.Row="0" Grid.ColumnSpan="2" ColumnDefinitions="*,Auto" Margin="0,0,0,18">
|
||||||
<StackPanel Spacing="3">
|
<StackPanel Spacing="3">
|
||||||
<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>
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Bottom">
|
<StackPanel Grid.Column="1" VerticalAlignment="Bottom" Spacing="4">
|
||||||
|
<ToggleSwitch Content="Bearbeitungsmodus" IsChecked="{Binding IsEditMode}"
|
||||||
|
IsVisible="{Binding IsEditable}" HorizontalAlignment="Right"/>
|
||||||
<TextBlock Text="{Binding AssignmentSummary}" HorizontalAlignment="Right" 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"
|
<TextBlock Text="Ziehen: Platz ändern · Klicken: bewerten" HorizontalAlignment="Right"
|
||||||
FontSize="11" Opacity="0.5"/>
|
FontSize="11" Opacity="0.5" IsVisible="{Binding IsEditMode}"/>
|
||||||
|
<TextBlock Text="Klicken: bewerten" HorizontalAlignment="Right"
|
||||||
|
FontSize="11" Opacity="0.5" IsVisible="{Binding IsEditMode, Converter={x:Static BoolConverters.Not}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
|
<ScrollViewer x:Name="RoomScrollViewer" Grid.Row="1" Grid.Column="0"
|
||||||
VerticalScrollBarVisibility="Auto">
|
Margin="0,0,16,0"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
DragDrop.AllowDrop="True"
|
||||||
|
DragDrop.DragOver="OnRoomDragOver"
|
||||||
|
DragDrop.DragLeave="OnRoomDragLeave">
|
||||||
<StackPanel Spacing="14" HorizontalAlignment="Center">
|
<StackPanel Spacing="14" HorizontalAlignment="Center">
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||||
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center">
|
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center"
|
||||||
|
IsVisible="{Binding IsBoardAtTop}">
|
||||||
<TextBlock Text="Tafel / Vorderseite" FontWeight="SemiBold" Opacity="0.75"/>
|
<TextBlock Text="Tafel / Vorderseite" FontWeight="SemiBold" Opacity="0.75"/>
|
||||||
</Border>
|
</Border>
|
||||||
<ItemsControl ItemsSource="{Binding Seats}" HorizontalAlignment="Center">
|
<ItemsControl ItemsSource="{Binding Seats}" HorizontalAlignment="Center">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
<UniformGrid Columns="{Binding PlanColumns}"/>
|
<groups:SeatingPlanPanel Columns="{Binding PlanColumns}"
|
||||||
|
ColumnGapWidths="{Binding ColumnGapWidths}"/>
|
||||||
</ItemsPanelTemplate>
|
</ItemsPanelTemplate>
|
||||||
</ItemsControl.ItemsPanel>
|
</ItemsControl.ItemsPanel>
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
@@ -104,7 +116,7 @@
|
|||||||
<StackPanel Spacing="5">
|
<StackPanel Spacing="5">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
||||||
<TextBlock Grid.Column="1" Text="⠿" Opacity="0.45" IsVisible="{Binding IsOccupied}"/>
|
<TextBlock Grid.Column="1" Text="⠿" Opacity="0.45" IsVisible="{Binding CanEdit}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
|
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
@@ -113,38 +125,52 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
|
||||||
|
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center"
|
||||||
|
IsVisible="{Binding IsBoardAtBottom}">
|
||||||
|
<TextBlock Text="Tafel / Vorderseite" FontWeight="SemiBold" Opacity="0.75"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
<Border DragDrop.AllowDrop="True" DragDrop.DragOver="OnUnassignedDragOver"
|
<!-- Die Schülerliste hat einen eigenen Scrollbereich und bleibt dadurch auch bei
|
||||||
|
großen Räumen neben den aktuell sichtbaren Sitzplätzen erreichbar. -->
|
||||||
|
<Border Grid.Row="1" Grid.Column="1"
|
||||||
|
Width="220" IsVisible="{Binding IsEditMode}"
|
||||||
|
DragDrop.AllowDrop="True" DragDrop.DragOver="OnUnassignedDragOver"
|
||||||
DragDrop.Drop="OnUnassignedDrop"
|
DragDrop.Drop="OnUnassignedDrop"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
BorderThickness="1" CornerRadius="7" Padding="12" Margin="5,8">
|
BorderThickness="1" CornerRadius="7" Padding="12">
|
||||||
<StackPanel Spacing="8">
|
<Grid RowDefinitions="Auto,Auto,*">
|
||||||
<TextBlock Text="NICHT ZUGEORDNET" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
|
<TextBlock Grid.Row="0" Text="NICHT ZUGEORDNET" FontSize="10"
|
||||||
<TextBlock Text="Schüler hierher ziehen, um einen Platz zu leeren."
|
FontWeight="Bold" Opacity="0.5"/>
|
||||||
FontSize="11" Opacity="0.5"/>
|
<TextBlock Grid.Row="1" Margin="0,5,0,10"
|
||||||
|
Text="Schüler auf einen Platz ziehen. Belegte Plätze hierher ziehen, um sie zu leeren."
|
||||||
|
TextWrapping="Wrap" FontSize="11" Opacity="0.5"/>
|
||||||
|
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto"
|
||||||
|
HorizontalScrollBarVisibility="Disabled">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
<ItemsControl ItemsSource="{Binding UnassignedStudents}">
|
<ItemsControl ItemsSource="{Binding UnassignedStudents}">
|
||||||
<ItemsControl.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:StudentSeatOption">
|
<DataTemplate x:DataType="vm:StudentSeatOption">
|
||||||
<Border Background="{DynamicResource SystemAccentColorLight2}" CornerRadius="5"
|
<Border Background="{DynamicResource SystemAccentColorLight2}" CornerRadius="5"
|
||||||
Padding="9,5" Margin="0,0,6,6"
|
Padding="9,7"
|
||||||
PointerPressed="OnDragSourcePressed"
|
PointerPressed="OnDragSourcePressed"
|
||||||
PointerMoved="OnDragSourceMoved"
|
PointerMoved="OnDragSourceMoved"
|
||||||
PointerReleased="OnDragSourceReleased">
|
PointerReleased="OnDragSourceReleased">
|
||||||
<TextBlock Text="{Binding DisplayName}" FontSize="12"/>
|
<TextBlock Text="{Binding DisplayName}" FontSize="12" TextTrimming="CharacterEllipsis"/>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
<TextBlock Text="Alle Schüler sind zugeordnet." Classes="emptyhint"
|
<TextBlock Text="Alle Schüler sind zugeordnet." Classes="emptyhint"
|
||||||
|
TextWrapping="Wrap"
|
||||||
IsVisible="{Binding !UnassignedStudents.Count}"/>
|
IsVisible="{Binding !UnassignedStudents.Count}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ namespace LehrerApp.Desktop.Views.Groups;
|
|||||||
|
|
||||||
public partial class SeatingPlanTabView : UserControl
|
public partial class SeatingPlanTabView : UserControl
|
||||||
{
|
{
|
||||||
|
private const double AutoScrollEdgeSize = 72;
|
||||||
|
private const double AutoScrollStep = 14;
|
||||||
|
|
||||||
private object? _dragCandidate;
|
private object? _dragCandidate;
|
||||||
private PointerPressedEventArgs? _dragTrigger;
|
private PointerPressedEventArgs? _dragTrigger;
|
||||||
private Avalonia.Point _pressPosition;
|
private Avalonia.Point _pressPosition;
|
||||||
@@ -18,8 +21,15 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
private SeatCellViewModel? _pendingDropTarget;
|
private SeatCellViewModel? _pendingDropTarget;
|
||||||
private bool _pendingClearSeat;
|
private bool _pendingClearSeat;
|
||||||
private DateTime _ignoreTapUntil;
|
private DateTime _ignoreTapUntil;
|
||||||
|
private readonly DispatcherTimer _autoScrollTimer;
|
||||||
|
private Avalonia.Vector _autoScrollDirection;
|
||||||
|
|
||||||
public SeatingPlanTabView() => InitializeComponent();
|
public SeatingPlanTabView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_autoScrollTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(30) };
|
||||||
|
_autoScrollTimer.Tick += OnAutoScrollTick;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
{
|
{
|
||||||
@@ -38,7 +48,7 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
!e.GetCurrentPoint(control).Properties.IsLeftButtonPressed) return;
|
!e.GetCurrentPoint(control).Properties.IsLeftButtonPressed) return;
|
||||||
var candidate = control.DataContext;
|
var candidate = control.DataContext;
|
||||||
if (candidate is SeatCellViewModel { IsOccupied: false } or null) return;
|
if (candidate is SeatCellViewModel { IsOccupied: false } or null) return;
|
||||||
if (DataContext is not SeatingPlanTabViewModel { IsEditable: true }) return;
|
if (DataContext is not SeatingPlanTabViewModel { CanEditLayout: true }) return;
|
||||||
_dragCandidate = candidate;
|
_dragCandidate = candidate;
|
||||||
_dragTrigger = e;
|
_dragTrigger = e;
|
||||||
_pressPosition = e.GetPosition(this);
|
_pressPosition = e.GetPosition(this);
|
||||||
@@ -69,6 +79,7 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
_draggedStudent = null;
|
_draggedStudent = null;
|
||||||
_pendingDropTarget = null;
|
_pendingDropTarget = null;
|
||||||
_pendingClearSeat = false;
|
_pendingClearSeat = false;
|
||||||
|
StopAutoScroll();
|
||||||
_ignoreTapUntil = DateTime.UtcNow.AddMilliseconds(250);
|
_ignoreTapUntil = DateTime.UtcNow.AddMilliseconds(250);
|
||||||
|
|
||||||
// Never mutate an ItemsControl while Avalonia is still processing its native
|
// Never mutate an ItemsControl while Avalonia is still processing its native
|
||||||
@@ -107,6 +118,7 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
|
|
||||||
private void OnSeatDrop(object? sender, DragEventArgs e)
|
private void OnSeatDrop(object? sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
|
StopAutoScroll();
|
||||||
if (sender is not Border { DataContext: SeatCellViewModel target }) return;
|
if (sender is not Border { DataContext: SeatCellViewModel target }) return;
|
||||||
target.IsDropTarget = false;
|
target.IsDropTarget = false;
|
||||||
if (!CanDropOn(target))
|
if (!CanDropOn(target))
|
||||||
@@ -120,7 +132,7 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool CanDropOn(SeatCellViewModel target) =>
|
private bool CanDropOn(SeatCellViewModel target) =>
|
||||||
DataContext is SeatingPlanTabViewModel { IsEditable: true }
|
DataContext is SeatingPlanTabViewModel { CanEditLayout: true }
|
||||||
&& ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null);
|
&& ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null);
|
||||||
|
|
||||||
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>
|
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>
|
||||||
@@ -128,6 +140,7 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
|
|
||||||
private void OnUnassignedDrop(object? sender, DragEventArgs e)
|
private void OnUnassignedDrop(object? sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
|
StopAutoScroll();
|
||||||
if (_draggedSeat is not null)
|
if (_draggedSeat is not null)
|
||||||
{
|
{
|
||||||
_pendingDropTarget = null;
|
_pendingDropTarget = null;
|
||||||
@@ -136,6 +149,50 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnRoomDragOver(object? sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (_draggedSeat is null && _draggedStudent is null)
|
||||||
|
{
|
||||||
|
StopAutoScroll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var position = e.GetPosition(RoomScrollViewer);
|
||||||
|
_autoScrollDirection = new Avalonia.Vector(
|
||||||
|
GetAutoScrollDirection(position.X, RoomScrollViewer.Bounds.Width),
|
||||||
|
GetAutoScrollDirection(position.Y, RoomScrollViewer.Bounds.Height));
|
||||||
|
|
||||||
|
if (_autoScrollDirection == default)
|
||||||
|
StopAutoScroll();
|
||||||
|
else if (!_autoScrollTimer.IsEnabled)
|
||||||
|
_autoScrollTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoomDragLeave(object? sender, DragEventArgs e) => StopAutoScroll();
|
||||||
|
|
||||||
|
private void OnAutoScrollTick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var maxX = Math.Max(0, RoomScrollViewer.Extent.Width - RoomScrollViewer.Viewport.Width);
|
||||||
|
var maxY = Math.Max(0, RoomScrollViewer.Extent.Height - RoomScrollViewer.Viewport.Height);
|
||||||
|
var offset = RoomScrollViewer.Offset;
|
||||||
|
RoomScrollViewer.Offset = new Avalonia.Vector(
|
||||||
|
Math.Clamp(offset.X + _autoScrollDirection.X * AutoScrollStep, 0, maxX),
|
||||||
|
Math.Clamp(offset.Y + _autoScrollDirection.Y * AutoScrollStep, 0, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double GetAutoScrollDirection(double position, double viewportSize)
|
||||||
|
{
|
||||||
|
if (position < AutoScrollEdgeSize) return -1;
|
||||||
|
if (position > viewportSize - AutoScrollEdgeSize) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StopAutoScroll()
|
||||||
|
{
|
||||||
|
_autoScrollDirection = default;
|
||||||
|
_autoScrollTimer.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnSeatTapped(object? sender, TappedEventArgs e)
|
private async void OnSeatTapped(object? sender, TappedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DateTime.UtcNow < _ignoreTapUntil || sender is not Border { DataContext: SeatCellViewModel seat }
|
if (DateTime.UtcNow < _ignoreTapUntil || sender is not Border { DataContext: SeatCellViewModel seat }
|
||||||
|
|||||||
@@ -22,16 +22,22 @@ public sealed class AttendanceBalanceServiceTests
|
|||||||
(new DateOnly(2025, 9, 5), AttendanceStatus.Truant),
|
(new DateOnly(2025, 9, 5), AttendanceStatus.Truant),
|
||||||
(new DateOnly(2025, 9, 6), AttendanceStatus.OtherSchoolEvent),
|
(new DateOnly(2025, 9, 6), AttendanceStatus.OtherSchoolEvent),
|
||||||
(new DateOnly(2025, 9, 7), AttendanceStatus.ExcusePending),
|
(new DateOnly(2025, 9, 7), AttendanceStatus.ExcusePending),
|
||||||
|
(new DateOnly(2025, 9, 8), AttendanceStatus.Late),
|
||||||
|
(new DateOnly(2025, 9, 9), AttendanceStatus.SignificantlyLate),
|
||||||
|
(new DateOnly(2025, 9, 10), AttendanceStatus.LeftDuringClass),
|
||||||
|
(new DateOnly(2025, 9, 11), AttendanceStatus.LearningIsland),
|
||||||
|
(new DateOnly(2025, 9, 12), AttendanceStatus.Suspended),
|
||||||
};
|
};
|
||||||
|
|
||||||
var balance = _service.Calculate(entries, From, To);
|
var balance = _service.Calculate(entries, From, To);
|
||||||
|
|
||||||
Assert.Equal(7, balance.TotalChecked);
|
Assert.Equal(12, balance.TotalChecked);
|
||||||
Assert.Equal(2, balance.Present);
|
Assert.Equal(4, balance.Present);
|
||||||
Assert.Equal(1, balance.Excused);
|
Assert.Equal(1, balance.Excused);
|
||||||
Assert.Equal(2, balance.Unexcused); // Unexcused + Truant
|
Assert.Equal(3, balance.Unexcused); // Unexcused + Truant + im Unterricht abgängig
|
||||||
Assert.Equal(1, balance.SchoolEvent);
|
Assert.Equal(3, balance.SchoolEvent); // Schulveranstaltung + Lerninsel + Suspendierung
|
||||||
Assert.Equal(1, balance.ExcusePending);
|
Assert.Equal(1, balance.ExcusePending);
|
||||||
|
Assert.Equal(41.7, balance.AbsenceRatePercent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user