Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
T

688 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class SeatingPlanTabViewModel : ObservableObject
{
private readonly ISeatingPlanRepository _plans;
private readonly IStudentRepository _students;
private readonly IGroupMembershipRepository _memberships;
private readonly IParticipationSessionRepository _sessions;
private readonly IParticipationRepository _participation;
private readonly IParticipationAspectRepository _aspects;
private Guid _groupId;
private SeatingPlan? _currentPlan;
private bool _isReadOnly;
[ObservableProperty] private SeatingPlanSummary? _selectedPlan;
[ObservableProperty] private int _planColumns = 1;
[ObservableProperty] private string _planTitle = "";
[ObservableProperty] private string _planSubtitle = "";
[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<SeatCellViewModel> Seats { get; } = [];
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
public ObservableCollection<StudentSeatOption> UnassignedStudents { get; } = [];
public bool HasPlans => Plans.Count > 0;
public bool HasSelectedPlan => _currentPlan is not null;
public bool IsEditable => !_isReadOnly;
public bool CanEditLayout => IsEditable && IsEditMode;
public Func<SeatingPlan?, Task<SeatingPlan?>>? OnEditPlan { 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,
IGroupMembershipRepository memberships, IParticipationSessionRepository sessions,
IParticipationRepository participation, IParticipationAspectRepository aspects)
{
_plans = plans;
_students = students;
_memberships = memberships;
_sessions = sessions;
_participation = participation;
_aspects = aspects;
}
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
new(_plans, _groupId, plan);
public void Initialize(Guid groupId, bool isReadOnly)
{
_groupId = groupId;
_isReadOnly = isReadOnly;
IsEditMode = false;
LoadStudentOptions();
ReloadPlans();
OnPropertyChanged(nameof(IsEditable));
NotifyCommands();
}
private void LoadStudentOptions()
{
StudentOptions.Clear();
StudentOptions.Add(StudentSeatOption.Empty);
var today = DateOnly.FromDateTime(DateTime.Today);
var memberships = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
foreach (var student in _students.GetByGroup(_groupId)
.Where(s => memberships.TryGetValue(s.Id, out var membership)
&& GroupMembershipService.IsActiveOn(membership, today))
.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
StudentOptions.Add(new StudentSeatOption(student.Id, $"{student.LastName}, {student.FirstName}"));
}
private void ReloadPlans(Guid? selectId = null)
{
selectId ??= SelectedPlan?.Id;
Plans.Clear();
foreach (var plan in _plans.GetByGroup(_groupId))
Plans.Add(new SeatingPlanSummary(plan));
SelectedPlan = Plans.FirstOrDefault(p => p.Id == selectId) ?? Plans.FirstOrDefault();
if (SelectedPlan is null) LoadPlan(null);
OnPropertyChanged(nameof(HasPlans));
NotifyCommands();
}
partial void OnSelectedPlanChanged(SeatingPlanSummary? value) =>
LoadPlan(value is null ? null : _plans.GetById(value.Id));
private void LoadPlan(SeatingPlan? plan)
{
_currentPlan = plan;
Seats.Clear();
if (plan is null)
{
UnassignedStudents.Clear();
PlanColumns = 1;
PlanTitle = "";
PlanSubtitle = "";
AssignmentSummary = "";
IsBoardAtTop = true;
IsBoardAtBottom = false;
ColumnGapWidths = [];
}
else
{
PlanColumns = plan.Columns;
PlanTitle = plan.Name;
PlanSubtitle = string.IsNullOrWhiteSpace(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 column = 0; column < plan.Columns; column++)
{
var assignment = plan.Assignments.FirstOrDefault(a => a.Row == row && a.Column == column);
var option = assignment is null
? StudentSeatOption.Empty
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
?? StudentSeatOption.Empty;
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, CanEditLayout));
}
UpdateAssignmentSummary();
}
OnPropertyChanged(nameof(HasSelectedPlan));
NotifyCommands();
}
private void OnSeatChanged(SeatCellViewModel changed)
{
if (_currentPlan is null || !CanEditLayout) return;
if (changed.SelectedOption.StudentId is Guid studentId)
{
foreach (var other in Seats.Where(s => s != changed && s.SelectedOption.StudentId == studentId))
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 (!CanEditLayout || 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 (!CanEditLayout || !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 (!CanEditLayout || !seat.SelectedOption.StudentId.HasValue) return;
seat.SetSelectionSilently(StudentSeatOption.Empty);
SaveSeatAssignments();
}
private void SaveSeatAssignments()
{
if (_currentPlan is null) return;
_currentPlan.Assignments = Seats
.Where(s => s.SelectedOption.StudentId.HasValue)
.Select(s => new SeatAssignment
{
Row = s.Row,
Column = s.Column,
StudentId = s.SelectedOption.StudentId!.Value,
}).ToList();
_plans.Save(_currentPlan);
UpdateAssignmentSummary();
}
public async Task AssessStudent(SeatCellViewModel seat)
{
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);
await OnAssessStudent(assessment);
OnAssessmentChanged?.Invoke();
}
[RelayCommand(CanExecute = nameof(CanEdit))]
private async Task AddPlan()
{
if (OnEditPlan is null) return;
var plan = await OnEditPlan(null);
if (plan is not null) ReloadPlans(plan.Id);
}
[RelayCommand(CanExecute = nameof(CanEditSelected))]
private async Task EditPlan()
{
if (_currentPlan is null || OnEditPlan is null) return;
var plan = await OnEditPlan(_currentPlan);
if (plan is not null) ReloadPlans(plan.Id);
}
[RelayCommand(CanExecute = nameof(CanEditSelected))]
private async Task DeletePlan()
{
if (SelectedPlan is null || OnConfirmDelete is null || !await OnConfirmDelete(SelectedPlan)) return;
_plans.Delete(SelectedPlan.Id);
ReloadPlans();
}
partial void OnIsEditModeChanged(bool value)
{
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()
{
AddPlanCommand.NotifyCanExecuteChanged();
EditPlanCommand.NotifyCanExecuteChanged();
DeletePlanCommand.NotifyCanExecuteChanged();
}
}
public sealed class SeatingPlanSummary
{
public Guid Id { get; }
public string Name { get; }
public string RoomDisplay { get; }
public SeatingPlanSummary(SeatingPlan plan)
{
Id = plan.Id;
Name = plan.Name;
RoomDisplay = string.IsNullOrWhiteSpace(plan.Room) ? "Kein Raum" : $"Raum {plan.Room}";
}
}
public sealed record StudentSeatOption(Guid? StudentId, string DisplayName)
{
public static StudentSeatOption Empty { get; } = new(null, "— frei —");
}
public partial class SeatCellViewModel : ObservableObject
{
private readonly Action<SeatCellViewModel> _onChanged;
private bool _suppressChange;
[ObservableProperty] private StudentSeatOption _selectedOption;
[ObservableProperty] private bool _isDropTarget;
public int Row { get; }
public int Column { get; }
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
public ObservableCollection<StudentSeatOption> Options { get; }
[ObservableProperty] private bool _canEdit;
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)
{
Row = row;
Column = column;
Options = options;
_selectedOption = selectedOption;
_onChanged = onChanged;
_canEdit = canEdit;
}
partial void OnSelectedOptionChanged(StudentSeatOption value)
{
OnPropertyChanged(nameof(IsOccupied));
OnPropertyChanged(nameof(StudentName));
if (!_suppressChange) _onChanged(this);
}
public void SetSelectionSilently(StudentSeatOption option)
{
_suppressChange = true;
SelectedOption = option;
_suppressChange = false;
}
}
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("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));
}
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
{
private readonly ISeatingPlanRepository _plans;
private readonly Guid _groupId;
private readonly SeatingPlan? _editingPlan;
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _room = "";
[ObservableProperty] private decimal _rows = 4;
[ObservableProperty] private decimal _columns = 4;
[ObservableProperty] private bool _isBoardAtBottom;
[ObservableProperty] private string _nameError = "";
[ObservableProperty] private string _layoutError = "";
public SeatingPlan? Result { get; private set; }
public ObservableCollection<ColumnGapEditor> ColumnGaps { get; } = [];
public string DialogTitle => _editingPlan is null ? "Neuen Sitzplan anlegen" : "Sitzplan bearbeiten";
public string SaveButtonText => _editingPlan is null ? "Anlegen" : "Speichern";
public SeatingPlanDialogViewModel(ISeatingPlanRepository plans, Guid groupId, SeatingPlan? editingPlan)
{
_plans = plans;
_groupId = groupId;
_editingPlan = editingPlan;
RebuildColumnGaps(decimal.ToInt32(Columns));
if (editingPlan is null) return;
Name = editingPlan.Name ?? "";
Room = editingPlan.Room ?? "";
Rows = editingPlan.Rows;
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]
private void Save()
{
NameError = "";
LayoutError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Name))
{
NameError = "Bitte einen Namen eingeben.";
valid = false;
}
if (Rows is < 1 or > 10 || Columns is < 1 or > 10)
{
LayoutError = "Reihen und Plätze müssen zwischen 1 und 10 liegen.";
valid = false;
}
if (!valid) return;
var plan = _editingPlan ?? new SeatingPlan { GroupId = _groupId };
plan.Name = Name?.Trim() ?? "";
plan.Room = Room?.Trim() ?? "";
plan.Rows = decimal.ToInt32(Rows);
plan.Columns = decimal.ToInt32(Columns);
plan.IsBoardAtBottom = IsBoardAtBottom;
plan.ColumnGapWidths = ColumnGaps.Select(g => decimal.ToDouble(g.Width)).ToList();
try
{
_plans.Save(plan);
Result = plan;
}
catch (InvalidOperationException ex)
{
NameError = ex.Message;
}
}
}
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;
}