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 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 = ""; public ObservableCollection Plans { get; } = []; public ObservableCollection Seats { get; } = []; public ObservableCollection StudentOptions { get; } = []; public bool HasPlans => Plans.Count > 0; public bool HasSelectedPlan => _currentPlan is not null; public bool IsEditable => !_isReadOnly; public Func>? OnEditPlan { get; set; } public Func>? OnConfirmDelete { get; set; } public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students, IGroupMembershipRepository memberships) { _plans = plans; _students = students; _memberships = memberships; } public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) => new(_plans, _groupId, plan); public void Initialize(Guid groupId, bool isReadOnly) { _groupId = groupId; _isReadOnly = isReadOnly; 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) { PlanColumns = 1; PlanTitle = ""; PlanSubtitle = ""; AssignmentSummary = ""; } 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"; 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, IsEditable)); } UpdateAssignmentSummary(); } OnPropertyChanged(nameof(HasSelectedPlan)); NotifyCommands(); } private void OnSeatChanged(SeatCellViewModel changed) { if (_currentPlan is null || !IsEditable) 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); } _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(); } private void UpdateAssignmentSummary() { var assigned = Seats.Count(s => s.SelectedOption.StudentId.HasValue); var total = StudentOptions.Count - 1; AssignmentSummary = $"{assigned} von {total} Schülern zugeordnet"; } [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(); } private bool CanEdit() => IsEditable; private bool CanEditSelected() => IsEditable && _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 _onChanged; private bool _suppressChange; [ObservableProperty] private StudentSeatOption _selectedOption; public int Row { get; } public int Column { get; } public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}"; public ObservableCollection Options { get; } public bool CanEdit { get; } public SeatCellViewModel(int row, int column, ObservableCollection options, StudentSeatOption selectedOption, Action onChanged, bool canEdit) { Row = row; Column = column; Options = options; _selectedOption = selectedOption; _onChanged = onChanged; CanEdit = canEdit; } partial void OnSelectedOptionChanged(StudentSeatOption value) { if (!_suppressChange) _onChanged(this); } public void SetSelectionSilently(StudentSeatOption option) { _suppressChange = true; SelectedOption = option; _suppressChange = false; } } 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 string _nameError = ""; [ObservableProperty] private string _layoutError = ""; public SeatingPlan? Result { get; private set; } 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; if (editingPlan is null) return; Name = editingPlan.Name; Room = editingPlan.Room; Rows = editingPlan.Rows; Columns = editingPlan.Columns; } [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); try { _plans.Save(plan); Result = plan; } catch (InvalidOperationException ex) { NameError = ex.Message; } } }