|
|
|
@@ -15,6 +15,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
private readonly IParticipationSessionRepository _sessions;
|
|
|
|
|
private readonly IParticipationRepository _participation;
|
|
|
|
|
private readonly IParticipationAspectRepository _aspects;
|
|
|
|
|
private readonly IDocumentationRepository? _documentation;
|
|
|
|
|
private Guid _groupId;
|
|
|
|
|
private SeatingPlan? _currentPlan;
|
|
|
|
|
private bool _isReadOnly;
|
|
|
|
@@ -28,11 +29,13 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
[ObservableProperty] private bool _isBoardAtBottom;
|
|
|
|
|
[ObservableProperty] private IReadOnlyList<double> _columnGapWidths = [];
|
|
|
|
|
[ObservableProperty] private bool _isEditMode;
|
|
|
|
|
[ObservableProperty] private ParticipationSessionOption? _selectedSession;
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
|
|
|
|
|
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
|
|
|
|
|
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
|
|
|
|
|
public ObservableCollection<StudentSeatOption> UnassignedStudents { get; } = [];
|
|
|
|
|
public ObservableCollection<ParticipationSessionOption> TodaySessions { get; } = [];
|
|
|
|
|
|
|
|
|
|
public bool HasPlans => Plans.Count > 0;
|
|
|
|
|
public bool HasSelectedPlan => _currentPlan is not null;
|
|
|
|
@@ -42,10 +45,12 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
public Func<SeatingPlanSummary, Task<bool>>? OnConfirmDelete { get; set; }
|
|
|
|
|
public Func<SeatAssessmentViewModel, Task>? OnAssessStudent { get; set; }
|
|
|
|
|
public Action? OnAssessmentChanged { get; set; }
|
|
|
|
|
public Action? OnDocumentationChanged { get; set; }
|
|
|
|
|
|
|
|
|
|
public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students,
|
|
|
|
|
IGroupMembershipRepository memberships, IParticipationSessionRepository sessions,
|
|
|
|
|
IParticipationRepository participation, IParticipationAspectRepository aspects)
|
|
|
|
|
IParticipationRepository participation, IParticipationAspectRepository aspects,
|
|
|
|
|
IDocumentationRepository? documentation = null)
|
|
|
|
|
{
|
|
|
|
|
_plans = plans;
|
|
|
|
|
_students = students;
|
|
|
|
@@ -53,6 +58,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
_sessions = sessions;
|
|
|
|
|
_participation = participation;
|
|
|
|
|
_aspects = aspects;
|
|
|
|
|
_documentation = documentation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
|
|
|
|
@@ -63,12 +69,31 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
_groupId = groupId;
|
|
|
|
|
_isReadOnly = isReadOnly;
|
|
|
|
|
IsEditMode = false;
|
|
|
|
|
LoadTodaySessions();
|
|
|
|
|
LoadStudentOptions();
|
|
|
|
|
ReloadPlans();
|
|
|
|
|
OnPropertyChanged(nameof(IsEditable));
|
|
|
|
|
NotifyCommands();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LoadTodaySessions()
|
|
|
|
|
{
|
|
|
|
|
TodaySessions.Clear();
|
|
|
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
|
|
|
var sessions = _sessions.GetByGroup(_groupId).Where(s => s.Date == today)
|
|
|
|
|
.OrderBy(s => s.CreatedAt).ToList();
|
|
|
|
|
if (sessions.Count == 0 && !_isReadOnly)
|
|
|
|
|
{
|
|
|
|
|
var created = new ParticipationSession { GroupId = _groupId, Date = today, Comment = "Sitzplan" };
|
|
|
|
|
_sessions.Save(created);
|
|
|
|
|
sessions.Add(created);
|
|
|
|
|
}
|
|
|
|
|
foreach (var session in sessions) TodaySessions.Add(new ParticipationSessionOption(session));
|
|
|
|
|
SelectedSession = TodaySessions.LastOrDefault();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData();
|
|
|
|
|
|
|
|
|
|
private void LoadStudentOptions()
|
|
|
|
|
{
|
|
|
|
|
StudentOptions.Clear();
|
|
|
|
@@ -133,14 +158,78 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
? StudentSeatOption.Empty
|
|
|
|
|
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
|
|
|
|
|
?? StudentSeatOption.Empty;
|
|
|
|
|
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, CanEditLayout));
|
|
|
|
|
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
|
|
|
|
|
CanEditLayout, ToggleSituationTag, IsEditable));
|
|
|
|
|
}
|
|
|
|
|
UpdateAssignmentSummary();
|
|
|
|
|
RefreshSeatLessonData();
|
|
|
|
|
}
|
|
|
|
|
OnPropertyChanged(nameof(HasSelectedPlan));
|
|
|
|
|
NotifyCommands();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RefreshSeatLessonData()
|
|
|
|
|
{
|
|
|
|
|
if (SelectedSession is null)
|
|
|
|
|
{
|
|
|
|
|
foreach (var seat in Seats) seat.SetLessonData(null, []);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var entries = _participation.GetBySession(SelectedSession.Id)
|
|
|
|
|
.ToDictionary(e => e.StudentId);
|
|
|
|
|
foreach (var seat in Seats)
|
|
|
|
|
{
|
|
|
|
|
if (seat.SelectedOption.StudentId is not Guid studentId)
|
|
|
|
|
{
|
|
|
|
|
seat.SetLessonData(null, []);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
entries.TryGetValue(studentId, out var entry);
|
|
|
|
|
var tags = _documentation?.GetByStudent(studentId)
|
|
|
|
|
.FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id)?.Tags ?? [];
|
|
|
|
|
seat.SetLessonData(entry, tags);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
|
|
|
|
|
{
|
|
|
|
|
if (!IsEditable || _documentation is null || SelectedSession is null ||
|
|
|
|
|
seat.SelectedOption.StudentId is not Guid studentId) return;
|
|
|
|
|
var draft = _documentation.GetByStudent(studentId)
|
|
|
|
|
.FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id);
|
|
|
|
|
if (draft is null)
|
|
|
|
|
{
|
|
|
|
|
draft = new Documentation
|
|
|
|
|
{
|
|
|
|
|
StudentId = studentId, GroupId = _groupId,
|
|
|
|
|
ParticipationSessionId = SelectedSession.Id,
|
|
|
|
|
LessonId = SelectedSession.LessonId,
|
|
|
|
|
Type = DocumentationType.Incident,
|
|
|
|
|
Date = SelectedSession.Date,
|
|
|
|
|
Title = tag,
|
|
|
|
|
IsDraft = true,
|
|
|
|
|
Tags = [tag],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
else if (draft.Tags.Contains(tag))
|
|
|
|
|
{
|
|
|
|
|
draft.Tags.Remove(tag);
|
|
|
|
|
if (draft.Tags.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
_documentation.Delete(draft.Id);
|
|
|
|
|
RefreshSeatLessonData();
|
|
|
|
|
OnDocumentationChanged?.Invoke();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
draft.Title = draft.Tags[0];
|
|
|
|
|
}
|
|
|
|
|
else draft.Tags.Add(tag);
|
|
|
|
|
draft.UpdatedAt = DateTime.UtcNow;
|
|
|
|
|
_documentation.Save(draft);
|
|
|
|
|
RefreshSeatLessonData();
|
|
|
|
|
OnDocumentationChanged?.Invoke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnSeatChanged(SeatCellViewModel changed)
|
|
|
|
|
{
|
|
|
|
|
if (_currentPlan is null || !CanEditLayout) return;
|
|
|
|
@@ -209,8 +298,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
{
|
|
|
|
|
if (!seat.SelectedOption.StudentId.HasValue || OnAssessStudent is null) return;
|
|
|
|
|
var assessment = new SeatAssessmentViewModel(_sessions, _participation, _aspects,
|
|
|
|
|
_groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable);
|
|
|
|
|
_groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable,
|
|
|
|
|
SelectedSession?.Id);
|
|
|
|
|
await OnAssessStudent(assessment);
|
|
|
|
|
RefreshSeatLessonData();
|
|
|
|
|
OnAssessmentChanged?.Invoke();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -241,7 +332,11 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|
|
|
|
partial void OnIsEditModeChanged(bool value)
|
|
|
|
|
{
|
|
|
|
|
OnPropertyChanged(nameof(CanEditLayout));
|
|
|
|
|
foreach (var seat in Seats) seat.CanEdit = CanEditLayout;
|
|
|
|
|
foreach (var seat in Seats)
|
|
|
|
|
{
|
|
|
|
|
seat.CanEdit = CanEditLayout;
|
|
|
|
|
seat.CanRecordLesson = IsEditable && !value;
|
|
|
|
|
}
|
|
|
|
|
NotifyCommands();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -275,10 +370,21 @@ public sealed record StudentSeatOption(Guid? StudentId, string DisplayName)
|
|
|
|
|
public static StudentSeatOption Empty { get; } = new(null, "— frei —");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class ParticipationSessionOption(ParticipationSession session)
|
|
|
|
|
{
|
|
|
|
|
public Guid Id => session.Id;
|
|
|
|
|
public Guid? LessonId => session.LessonId;
|
|
|
|
|
public DateOnly Date => session.Date;
|
|
|
|
|
public string DisplayName => string.IsNullOrWhiteSpace(session.Comment)
|
|
|
|
|
? $"{session.Date:dd.MM.yyyy}"
|
|
|
|
|
: $"{session.Date:dd.MM.yyyy} · {session.Comment}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public partial class SeatCellViewModel : ObservableObject
|
|
|
|
|
{
|
|
|
|
|
private readonly Action<SeatCellViewModel> _onChanged;
|
|
|
|
|
private bool _suppressChange;
|
|
|
|
|
private readonly Action<SeatCellViewModel, string> _toggleSituationTag;
|
|
|
|
|
|
|
|
|
|
[ObservableProperty] private StudentSeatOption _selectedOption;
|
|
|
|
|
[ObservableProperty] private bool _isDropTarget;
|
|
|
|
@@ -287,11 +393,20 @@ public partial class SeatCellViewModel : ObservableObject
|
|
|
|
|
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
|
|
|
|
|
public ObservableCollection<StudentSeatOption> Options { get; }
|
|
|
|
|
[ObservableProperty] private bool _canEdit;
|
|
|
|
|
[ObservableProperty] private double _lessonOpacity = 1.0;
|
|
|
|
|
[ObservableProperty] private string _attendanceBadge = "";
|
|
|
|
|
[ObservableProperty] private string _homeworkBadge = "";
|
|
|
|
|
public ObservableCollection<SituationTagChoice> SituationTags { get; } = [];
|
|
|
|
|
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
|
|
|
|
|
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
|
|
|
|
|
public bool ShowLessonOverview => IsOccupied && !CanEdit;
|
|
|
|
|
[ObservableProperty] private bool _canRecordLesson;
|
|
|
|
|
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
|
|
|
|
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
|
|
|
|
|
|
|
|
|
|
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
|
|
|
|
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit)
|
|
|
|
|
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
|
|
|
|
|
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false)
|
|
|
|
|
{
|
|
|
|
|
Row = row;
|
|
|
|
|
Column = column;
|
|
|
|
@@ -299,21 +414,54 @@ public partial class SeatCellViewModel : ObservableObject
|
|
|
|
|
_selectedOption = selectedOption;
|
|
|
|
|
_onChanged = onChanged;
|
|
|
|
|
_canEdit = canEdit;
|
|
|
|
|
_toggleSituationTag = toggleSituationTag ?? ((_, _) => { });
|
|
|
|
|
_canRecordLesson = canRecordLesson;
|
|
|
|
|
foreach (var tag in SituationTagChoice.DefaultTags)
|
|
|
|
|
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnSelectedOptionChanged(StudentSeatOption value)
|
|
|
|
|
{
|
|
|
|
|
OnPropertyChanged(nameof(IsOccupied));
|
|
|
|
|
OnPropertyChanged(nameof(StudentName));
|
|
|
|
|
OnPropertyChanged(nameof(ShowLessonOverview));
|
|
|
|
|
if (!_suppressChange) _onChanged(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
partial void OnCanEditChanged(bool value) => OnPropertyChanged(nameof(ShowLessonOverview));
|
|
|
|
|
|
|
|
|
|
public void SetSelectionSilently(StudentSeatOption option)
|
|
|
|
|
{
|
|
|
|
|
_suppressChange = true;
|
|
|
|
|
SelectedOption = option;
|
|
|
|
|
_suppressChange = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SetLessonData(ParticipationEntry? entry, IEnumerable<string> tags)
|
|
|
|
|
{
|
|
|
|
|
var attendance = entry?.Attendance;
|
|
|
|
|
AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
|
|
|
|
|
HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
|
|
|
|
|
LessonOpacity = attendance is not null and not AttendanceStatus.Present
|
|
|
|
|
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
|
|
|
|
|
var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (var choice in SituationTags) choice.IsSelected = selected.Contains(choice.Text);
|
|
|
|
|
OnPropertyChanged(nameof(HasAttendanceBadge));
|
|
|
|
|
OnPropertyChanged(nameof(HasHomeworkBadge));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public partial class SituationTagChoice(string text, bool isSelected, Action<string> toggle) : ObservableObject
|
|
|
|
|
{
|
|
|
|
|
public static readonly string[] DefaultTags =
|
|
|
|
|
[
|
|
|
|
|
"Mitarbeit verweigert", "Unterricht gestört", "Streit/Konflikt",
|
|
|
|
|
"Langer Toilettengang", "Material vergessen", "Handynutzung",
|
|
|
|
|
"Besonders hilfsbereit", "Sehr gute Mitarbeit", "Gespräch erforderlich",
|
|
|
|
|
];
|
|
|
|
|
public string Text { get; } = text;
|
|
|
|
|
[ObservableProperty] private bool _isSelected = isSelected;
|
|
|
|
|
[RelayCommand] private void Toggle() => toggle(Text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public partial class SeatAssessmentViewModel : ObservableObject
|
|
|
|
@@ -338,13 +486,14 @@ public partial class SeatAssessmentViewModel : ObservableObject
|
|
|
|
|
|
|
|
|
|
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
|
|
|
|
|
IParticipationRepository entries, IParticipationAspectRepository aspects,
|
|
|
|
|
Guid groupId, Guid studentId, string studentName, bool canEdit)
|
|
|
|
|
Guid groupId, Guid studentId, string studentName, bool canEdit, Guid? sessionId = null)
|
|
|
|
|
{
|
|
|
|
|
_entries = entries;
|
|
|
|
|
_canEdit = canEdit;
|
|
|
|
|
StudentName = studentName;
|
|
|
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
|
|
|
var session = sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
|
|
|
|
|
var session = sessionId.HasValue ? sessions.GetById(sessionId.Value) : null;
|
|
|
|
|
session ??= sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
|
|
|
|
|
if (session is null && canEdit)
|
|
|
|
|
{
|
|
|
|
|
session = new ParticipationSession
|
|
|
|
|