Files
LehrerApp/LehrerApp.Desktop/ViewModels/Planning/TimetableSlotDialogViewModel.cs
T
adminandClaude Sonnet 5 888df39f37 Fix: ArgumentException im Stundenplan bei gleichnamigen Lerngruppen
TimetableSlotDialogViewModel baute die Gruppenauswahl als Dictionary,
geschlüsselt nach LearningGroup.Name. Bei zwei Lerngruppen mit demselben
Namen (dieselbe Klasse in zwei Fächern unterrichtet, z.B. zwei "10c") warf
ToDictionary eine ArgumentException, der Zuweisen-Dialog ließ sich gar
nicht mehr öffnen.

Behoben durch eindeutige Anzeige-Labels statt des rohen Namens: bei einem
Namenskonflikt wird das Fach angehängt ("10c (Chemie)" vs. "10c
(Mathematik)"), mit nummeriertem Fallback für den Restfall gleicher Name
und gleiches Fach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 00:27:14 +02:00

111 lines
4.2 KiB
C#

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
namespace LehrerApp.Desktop.ViewModels.Planning;
/// <summary>Zuweisen/Bearbeiten/Entfernen eines Stundenplan-Termins (4.3.3).</summary>
public partial class TimetableSlotDialogViewModel : ObservableObject
{
private readonly ITimetableSlotRepository _slots;
private readonly Dictionary<string, Guid> _groupIdsByLabel;
private readonly TimetableSlot? _editing;
public DayOfWeek Weekday { get; }
public int PeriodNumber { get; }
public string WeekdayLabel { get; }
public string DialogTitle { get; }
public bool IsEditing => _editing is not null;
[ObservableProperty] private string _selectedGroupName = "";
[ObservableProperty] private string _room = "";
[ObservableProperty] private string _groupError = "";
public string[] GroupOptions { get; }
/// null = unverändert/Abbruch, sonst das neue/aktualisierte Ergebnis.
public TimetableSlot? Result { get; private set; }
public bool Deleted { get; private set; }
public TimetableSlotDialogViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
ISubjectRepository subjects, SchoolYearService schoolYear, DayOfWeek weekday, int periodNumber,
TimetableSlot? editing)
{
_slots = slots;
_editing = editing;
Weekday = weekday;
PeriodNumber = periodNumber;
WeekdayLabel = weekday switch
{
DayOfWeek.Monday => "Montag", DayOfWeek.Tuesday => "Dienstag",
DayOfWeek.Wednesday => "Mittwoch", DayOfWeek.Thursday => "Donnerstag",
DayOfWeek.Friday => "Freitag", _ => weekday.ToString(),
};
DialogTitle = $"{WeekdayLabel}, {periodNumber}. Stunde";
var availableGroups = groups.GetBySchoolYear(schoolYear.CurrentSchoolYear()).OrderBy(g => g.Name).ToList();
var subjectNames = subjects.GetAll().ToDictionary(s => s.Id, s => s.Name);
var nameCounts = availableGroups.GroupBy(g => g.Name).ToDictionary(g => g.Key, g => g.Count());
// Der Gruppenname allein ist nicht eindeutig: dieselbe Klasse in mehreren Fächern hat
// mehrere LearningGroup-Datensätze mit demselben Namen. Bei einem Namenskonflikt wird
// deshalb das Fach angehängt ("10c (Chemie)") — ohne diese Absicherung würde die
// Dictionary-Befüllung unten mit einer ArgumentException abstürzen (echter Nutzer-Fehler).
_groupIdsByLabel = new Dictionary<string, Guid>();
var labeledGroups = new List<(string Label, Guid Id)>();
foreach (var g in availableGroups)
{
var label = nameCounts[g.Name] > 1
? $"{g.Name} ({(g.SubjectId is { } sid && subjectNames.TryGetValue(sid, out var sn) ? sn : "ohne Fach")})"
: g.Name;
var uniqueLabel = label;
var suffix = 2;
while (_groupIdsByLabel.ContainsKey(uniqueLabel)) uniqueLabel = $"{label} ({suffix++})";
_groupIdsByLabel[uniqueLabel] = g.Id;
labeledGroups.Add((uniqueLabel, g.Id));
}
GroupOptions = labeledGroups.Select(x => x.Label).ToArray();
if (editing is not null)
{
SelectedGroupName = labeledGroups.FirstOrDefault(x => x.Id == editing.GroupId).Label ?? "";
Room = editing.Room ?? "";
}
}
[RelayCommand]
private void Save()
{
GroupError = "";
if (string.IsNullOrWhiteSpace(SelectedGroupName) || !_groupIdsByLabel.TryGetValue(SelectedGroupName, out var groupId))
{
GroupError = "Bitte eine Gruppe auswählen.";
return;
}
var slot = _editing ?? new TimetableSlot { Weekday = Weekday, PeriodNumber = PeriodNumber };
slot.GroupId = groupId;
slot.Room = string.IsNullOrWhiteSpace(Room) ? null : Room.Trim();
try
{
_slots.Save(slot);
Result = slot;
}
catch (InvalidOperationException ex)
{
GroupError = ex.Message;
}
}
[RelayCommand]
private void Delete()
{
if (_editing is null) return;
_slots.Delete(_editing.Id);
Deleted = true;
}
}