Stundenverlaufsplan: Viewer und Alternativpfad-Katalog (Kapitel 4.2 Nachtrag)
Schreibgeschützter LessonViewerDialog (größere Schrift, ohne Bearbeitungs-/Verlängern-/ Verschieben-Funktion) für den Einsatz während des Unterrichtens, erreichbar über "Anzeigen" im Stunden-Toolbar. Alternative Unterrichtsabläufe (z.B. Kurzversion bei Zeitmangel) laufen jetzt über einen echten Katalog (neues Modell AlternativeLessonPath: Name + Beschreibung) statt Freitext direkt an der Phase: im Verlaufsplan-Editor eine kompakte, farbig unterstützte Checkbox statt einer durchgehend sichtbaren Eingabespalte, Zuordnung/Neuanlage über einen eigenen Dialog. Der Viewer gruppiert Phasen entsprechend und zeigt die hinterlegte Beschreibung. Schema-Migration v3→v4 führt bestehende Freitextwerte verlustfrei in Katalogeinträge über. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,9 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
public ObservableCollection<LessonSummary> Lessons { get; } = [];
|
||||
|
||||
/// Aus den Material-/Kurzsymbol-Werten aller bereits vorhandenen Stunden-Phasen der Gruppe
|
||||
/// zusammengestellt (4.2.2 Autovervollständigung im Verlaufsplan-Editor).
|
||||
/// zusammengestellt (4.2.2 Autovervollständigung im Verlaufsplan-Editor). Alternative Abläufe
|
||||
/// kommen seit dem Katalog-Redesign nicht mehr aus der Stundenhistorie, sondern direkt aus
|
||||
/// IAlternativeLessonPathRepository (siehe LessonDialogViewModel).
|
||||
public List<string> KnownMaterials { get; private set; } = [];
|
||||
public List<string> KnownShorthands { get; private set; } = [];
|
||||
|
||||
@@ -53,6 +55,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
public Func<Lesson, List<string>, List<string>, Task<bool>>? OnEditLesson { get; set; }
|
||||
public Func<LessonSummary, Task<bool>>? OnConfirmDeleteLesson { get; set; }
|
||||
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
|
||||
public Func<Lesson, Task>? OnShowLesson { get; set; }
|
||||
|
||||
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
||||
IGroupRepository groups, ISubjectRepository subjects,
|
||||
@@ -120,6 +123,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
|
||||
partial void OnSelectedLessonChanged(LessonSummary? value)
|
||||
{
|
||||
ShowLessonCommand.NotifyCanExecuteChanged();
|
||||
EditLessonCommand.NotifyCanExecuteChanged();
|
||||
DeleteLessonCommand.NotifyCanExecuteChanged();
|
||||
MoveLessonCommand.NotifyCanExecuteChanged();
|
||||
@@ -209,6 +213,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
Activity = p.Activity,
|
||||
Material = p.Material,
|
||||
Shorthand = p.Shorthand,
|
||||
AlternativePathId = p.AlternativePathId,
|
||||
})],
|
||||
Homework = lesson.Homework,
|
||||
Reflection = null,
|
||||
@@ -233,6 +238,16 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
if (await OnEditLesson(SelectedLesson.Model, KnownMaterials, KnownShorthands)) LoadUnits();
|
||||
}
|
||||
|
||||
/// Schreibgeschützte Anzeige des Verlaufsplans für den Einsatz im Unterricht (kein
|
||||
/// Bearbeitungsrisiko). Bewusst ohne Live-Anpassung (Verlängern/Verschieben während des
|
||||
/// Haltens) — das gehört zur zurückgestellten Live-Unterrichtsmodus-Ideensammlung (TODO.md).
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task ShowLesson()
|
||||
{
|
||||
if (OnShowLesson is null || SelectedLesson is null) return;
|
||||
await OnShowLesson(SelectedLesson.Model);
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task DeleteLesson()
|
||||
{
|
||||
@@ -542,6 +557,7 @@ public partial class UnitDialogViewModel : ObservableObject
|
||||
public partial class LessonDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly IAlternativeLessonPathRepository _alternativePaths;
|
||||
private readonly Guid _unitId;
|
||||
private readonly Guid _groupId;
|
||||
private readonly Lesson? _editingLesson;
|
||||
@@ -563,15 +579,21 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
public string[] ShorthandSuggestions { get; }
|
||||
public ObservableCollection<PhaseStepEditItem> Phases { get; } = [];
|
||||
|
||||
/// Vom Code-Behind gesetzt (Fenster als Owner für den Zuweisen-Dialog): fragt nach dem
|
||||
/// alternativen Ablauf, dem eine Phase zugeordnet werden soll (Auswahl oder Neuanlage
|
||||
/// per Combobox-Dialog). null zurückgegeben = abgebrochen.
|
||||
public Func<Guid?, Task<AlternativeLessonPath?>>? OnPickAlternativePath { get; set; }
|
||||
|
||||
public Lesson? Result { get; private set; }
|
||||
public string DialogTitle => _editingLesson is null ? "Neue Stunde anlegen" : "Stunde bearbeiten";
|
||||
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
||||
|
||||
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
||||
Guid unitId, Guid groupId, List<string> materialSuggestions, List<string> shorthandHistorySuggestions,
|
||||
Lesson? editingLesson)
|
||||
IAlternativeLessonPathRepository alternativePaths, Guid unitId, Guid groupId,
|
||||
List<string> materialSuggestions, List<string> shorthandHistorySuggestions, Lesson? editingLesson)
|
||||
{
|
||||
_lessons = lessons; _unitId = unitId; _groupId = groupId; _editingLesson = editingLesson;
|
||||
_lessons = lessons; _alternativePaths = alternativePaths;
|
||||
_unitId = unitId; _groupId = groupId; _editingLesson = editingLesson;
|
||||
MaterialSuggestions = [.. materialSuggestions];
|
||||
|
||||
// Vorschläge kommen sowohl aus dem gepflegten Kürzel-Katalog (Einstellungen) als auch aus
|
||||
@@ -620,9 +642,19 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
item.OnRemove = RemovePhase;
|
||||
item.OnMoveUp = MovePhaseUp;
|
||||
item.OnMoveDown = MovePhaseDown;
|
||||
item.OnAssignAlternativePath = AssignAlternativePath;
|
||||
if (source?.AlternativePathId is Guid pathId)
|
||||
item.SetAlternativePath(_alternativePaths.GetById(pathId));
|
||||
Phases.Add(item);
|
||||
}
|
||||
|
||||
private async Task AssignAlternativePath(PhaseStepEditItem item)
|
||||
{
|
||||
if (OnPickAlternativePath is null) { item.SetAlternativePath(null); return; }
|
||||
var picked = await OnPickAlternativePath(item.AlternativePathId);
|
||||
item.SetAlternativePath(picked);
|
||||
}
|
||||
|
||||
private void RemovePhase(PhaseStepEditItem item) { Phases.Remove(item); RecomputeTimes(); }
|
||||
|
||||
private void MovePhaseUp(PhaseStepEditItem item)
|
||||
@@ -701,6 +733,9 @@ public partial class LessonDialogViewModel : ObservableObject
|
||||
|
||||
public partial class PhaseStepEditItem : ObservableObject
|
||||
{
|
||||
private static readonly string[] AlternativePathPalette =
|
||||
["#7F77DD", "#1D9E75", "#D85A30", "#D4537E", "#378ADD", "#639922", "#EF9F27"];
|
||||
|
||||
[ObservableProperty] private string _name = "";
|
||||
[ObservableProperty] private int _durationMinutes = 5;
|
||||
[ObservableProperty] private string _activity = "";
|
||||
@@ -708,13 +743,53 @@ public partial class PhaseStepEditItem : ObservableObject
|
||||
[ObservableProperty] private string _shorthand = "";
|
||||
[ObservableProperty] private string _computedTimeDisplay = "";
|
||||
|
||||
/// Checkbox-Zustand im Editor: unchecked→checked öffnet den Zuweisen-Dialog
|
||||
/// (<see cref="OnAssignAlternativePath"/>); checked→unchecked entfernt die Zuordnung.
|
||||
/// Änderungen, die von <see cref="SetAlternativePath"/> selbst kommen, lösen das nicht erneut aus.
|
||||
[ObservableProperty] private bool _hasAlternativePath;
|
||||
[ObservableProperty] private string _alternativePathName = "";
|
||||
[ObservableProperty] private string _alternativePathColorHex = "#9E9E9E";
|
||||
|
||||
private bool _suppressAlternativePathToggle;
|
||||
|
||||
public Guid? AlternativePathId { get; private set; }
|
||||
|
||||
public Action? OnChanged { get; set; }
|
||||
public Action<PhaseStepEditItem>? OnRemove { get; set; }
|
||||
public Action<PhaseStepEditItem>? OnMoveUp { get; set; }
|
||||
public Action<PhaseStepEditItem>? OnMoveDown { get; set; }
|
||||
public Func<PhaseStepEditItem, Task>? OnAssignAlternativePath { get; set; }
|
||||
|
||||
partial void OnDurationMinutesChanged(int value) => OnChanged?.Invoke();
|
||||
|
||||
partial void OnHasAlternativePathChanged(bool value)
|
||||
{
|
||||
if (_suppressAlternativePathToggle) return;
|
||||
if (value) _ = OnAssignAlternativePath?.Invoke(this);
|
||||
else SetAlternativePath(null);
|
||||
}
|
||||
|
||||
/// Wird sowohl beim Laden einer bestehenden Zuordnung als auch nach dem Zuweisen-Dialog
|
||||
/// aufgerufen (auch mit null bei Abbruch/Entfernen) — setzt Id/Anzeigename/Farbe konsistent
|
||||
/// und unterdrückt dabei das erneute Öffnen des Dialogs über <see cref="OnHasAlternativePathChanged"/>.
|
||||
public void SetAlternativePath(AlternativeLessonPath? path)
|
||||
{
|
||||
_suppressAlternativePathToggle = true;
|
||||
AlternativePathId = path?.Id;
|
||||
AlternativePathName = path?.Name ?? "";
|
||||
AlternativePathColorHex = path is null ? "#9E9E9E" : ColorFor(path.Name);
|
||||
HasAlternativePath = path is not null;
|
||||
_suppressAlternativePathToggle = false;
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
private static string ColorFor(string name)
|
||||
{
|
||||
var hash = 0;
|
||||
foreach (var c in name) hash = hash * 31 + c;
|
||||
return AlternativePathPalette[Math.Abs(hash) % AlternativePathPalette.Length];
|
||||
}
|
||||
|
||||
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
||||
[RelayCommand] private void MoveUp() => OnMoveUp?.Invoke(this);
|
||||
[RelayCommand] private void MoveDown() => OnMoveDown?.Invoke(this);
|
||||
@@ -726,9 +801,61 @@ public partial class PhaseStepEditItem : ObservableObject
|
||||
Activity = Activity.Trim(),
|
||||
Material = Material.Trim(),
|
||||
Shorthand = Shorthand.Trim(),
|
||||
AlternativePathId = AlternativePathId,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Dialog: Alternativen Ablauf zuweisen/anlegen (4.2.2 Nachtrag) ────────────
|
||||
|
||||
public partial class AlternativePathDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAlternativeLessonPathRepository _repo;
|
||||
|
||||
[ObservableProperty] private AlternativeLessonPath? _selectedPath;
|
||||
[ObservableProperty] private string _newName = "";
|
||||
[ObservableProperty] private string _newDescription = "";
|
||||
[ObservableProperty] private string _newNameError = "";
|
||||
[ObservableProperty] private string _selectionError = "";
|
||||
|
||||
public ObservableCollection<AlternativeLessonPath> Available { get; } = [];
|
||||
public AlternativeLessonPath? Result { get; private set; }
|
||||
|
||||
public AlternativePathDialogViewModel(IAlternativeLessonPathRepository repo, Guid? currentId)
|
||||
{
|
||||
_repo = repo;
|
||||
foreach (var p in repo.GetAll()) Available.Add(p);
|
||||
if (currentId is Guid id) SelectedPath = Available.FirstOrDefault(p => p.Id == id);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CreateNew()
|
||||
{
|
||||
NewNameError = "";
|
||||
if (string.IsNullOrWhiteSpace(NewName)) { NewNameError = "Name erforderlich."; return; }
|
||||
|
||||
var entry = new AlternativeLessonPath
|
||||
{
|
||||
Name = NewName.Trim(),
|
||||
Description = string.IsNullOrWhiteSpace(NewDescription) ? null : NewDescription.Trim(),
|
||||
};
|
||||
try { _repo.Save(entry); }
|
||||
catch (InvalidOperationException ex) { NewNameError = ex.Message; return; }
|
||||
catch (ArgumentException ex) { NewNameError = ex.Message; return; }
|
||||
|
||||
Available.Add(entry);
|
||||
SelectedPath = entry;
|
||||
NewName = ""; NewDescription = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
SelectionError = "";
|
||||
if (SelectedPath is null) { SelectionError = "Bitte einen Ablauf auswählen oder neu anlegen."; return; }
|
||||
Result = SelectedPath;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialog: Stunde verschieben (4.2.4) ────────────────────────────────────────
|
||||
|
||||
public partial class MoveLessonDialogViewModel : ObservableObject
|
||||
@@ -794,3 +921,84 @@ public partial class CopyUnitDialogViewModel : ObservableObject
|
||||
Result = new CopyUnitTarget(SelectedGroup!.Id, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Verlaufsplan-Ansicht (schreibgeschützt, für den Unterrichtseinsatz) ──────
|
||||
|
||||
/// Eine Phasen-Zeile in der schreibgeschützten Ansicht — reine Anzeige, keine Bearbeitung.
|
||||
public record PhaseViewItem(string Name, int DurationMinutes, string TimeDisplay,
|
||||
string Activity, string Material, string Shorthand);
|
||||
|
||||
/// Eine Gruppe von Phasen mit demselben <see cref="LessonPhaseStep.AlternativePathId"/>
|
||||
/// ("Hauptweg" bei null). Jede Gruppe bekommt ihre eigene kumulierte Zeitberechnung ab
|
||||
/// Lesson.StartTime — eine Alternative zeigt also "so würde die Uhr laufen, wenn man diesen Weg
|
||||
/// von Stundenbeginn an nimmt", nicht ab einer gemeinsamen Verzweigungsstelle im Hauptweg.
|
||||
public record PhaseGroupViewItem(string Label, bool IsMainPath, string? Description, List<PhaseViewItem> Phases);
|
||||
|
||||
public class LessonViewerViewModel
|
||||
{
|
||||
private const string MainPathLabel = "Hauptweg";
|
||||
|
||||
public string DateDisplay { get; }
|
||||
public string Topic { get; }
|
||||
public string StatusLabel { get; }
|
||||
public string StartTimeDisplay { get; }
|
||||
public string? Homework { get; }
|
||||
public string? Reflection { get; }
|
||||
public List<PhaseGroupViewItem> PhaseGroups { get; }
|
||||
public bool HasAlternatives { get; }
|
||||
|
||||
public LessonViewerViewModel(Lesson lesson, IAlternativeLessonPathRepository alternativePaths)
|
||||
{
|
||||
DateDisplay = lesson.Date.ToString("dd.MM.yyyy");
|
||||
Topic = lesson.Topic;
|
||||
StatusLabel = LessonStatusDisplay.ToName(lesson.Status);
|
||||
StartTimeDisplay = lesson.StartTime?.ToString("HH:mm") ?? "–";
|
||||
Homework = lesson.Homework;
|
||||
Reflection = lesson.Reflection;
|
||||
|
||||
var order = new List<string>();
|
||||
var descriptions = new Dictionary<string, string?>();
|
||||
var byLabel = new Dictionary<string, List<LessonPhaseStep>>();
|
||||
foreach (var p in lesson.Phases)
|
||||
{
|
||||
string label; string? description = null;
|
||||
if (p.AlternativePathId is Guid pathId)
|
||||
{
|
||||
var path = alternativePaths.GetById(pathId);
|
||||
label = path?.Name ?? "Unbekannter Ablauf";
|
||||
description = path?.Description;
|
||||
}
|
||||
else label = MainPathLabel;
|
||||
|
||||
if (!byLabel.TryGetValue(label, out var list))
|
||||
{
|
||||
list = [];
|
||||
byLabel[label] = list;
|
||||
descriptions[label] = description;
|
||||
order.Add(label);
|
||||
}
|
||||
list.Add(p);
|
||||
}
|
||||
|
||||
// Hauptweg immer zuerst, unabhängig davon, in welcher Reihenfolge Phasen angelegt wurden.
|
||||
var orderedLabels = order.OrderBy(l => l == MainPathLabel ? 0 : 1).ToList();
|
||||
PhaseGroups = orderedLabels
|
||||
.Select(label => new PhaseGroupViewItem(
|
||||
label, label == MainPathLabel, descriptions[label], BuildPhaseViewItems(byLabel[label], lesson.StartTime)))
|
||||
.ToList();
|
||||
HasAlternatives = PhaseGroups.Count > 1;
|
||||
}
|
||||
|
||||
private static List<PhaseViewItem> BuildPhaseViewItems(List<LessonPhaseStep> steps, TimeOnly? startTime)
|
||||
{
|
||||
var cursor = startTime;
|
||||
var items = new List<PhaseViewItem>();
|
||||
foreach (var p in steps)
|
||||
{
|
||||
var timeDisplay = cursor is { } c ? $"ab {c:HH:mm}" : "";
|
||||
items.Add(new PhaseViewItem(p.Name, p.DurationMinutes, timeDisplay, p.Activity, p.Material, p.Shorthand));
|
||||
if (cursor is { } cc) cursor = cc.AddMinutes(p.DurationMinutes);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user