299 lines
10 KiB
C#
299 lines
10 KiB
C#
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<SeatingPlanSummary> Plans { get; } = [];
|
||
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
|
||
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
|
||
|
||
public bool HasPlans => Plans.Count > 0;
|
||
public bool HasSelectedPlan => _currentPlan is not null;
|
||
public bool IsEditable => !_isReadOnly;
|
||
public Func<SeatingPlan?, Task<SeatingPlan?>>? OnEditPlan { get; set; }
|
||
public Func<SeatingPlanSummary, Task<bool>>? 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<SeatCellViewModel> _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<StudentSeatOption> Options { get; }
|
||
public bool CanEdit { get; }
|
||
|
||
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)
|
||
{
|
||
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;
|
||
}
|
||
}
|
||
}
|