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:
@@ -128,6 +128,7 @@ public static class AppBootstrapper
|
||||
services.AddSingleton<ISubjectRepository, SubjectRepository>();
|
||||
services.AddSingleton<ICompetencyDomainRepository, CompetencyDomainRepository>();
|
||||
services.AddSingleton<IShorthandCodeRepository, ShorthandCodeRepository>();
|
||||
services.AddSingleton<IAlternativeLessonPathRepository, AlternativeLessonPathRepository>();
|
||||
services.AddSingleton<IAttachmentStorage, LiteAttachmentStorage>();
|
||||
|
||||
// ── Services ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
xmlns:models="clr-namespace:LehrerApp.Core.Models;assembly=LehrerApp.Core"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.AlternativePathDialog"
|
||||
x:DataType="vm:AlternativePathDialogViewModel"
|
||||
Title="Alternativer Ablauf"
|
||||
Width="440" Height="500" MinWidth="380" MinHeight="380"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
<ScrollViewer Grid.Row="0">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Text="Alternativer Ablauf" Classes="dialogtitle"/>
|
||||
<TextBlock Text="Diese Phase gehört zu einem alternativen Unterrichtsverlauf, z.B. einer Kurzversion bei Zeitmangel."
|
||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Vorhandene Abläufe" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding Available}" SelectedItem="{Binding SelectedPath}"
|
||||
HorizontalAlignment="Stretch" PlaceholderText="Ablauf auswählen">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AlternativeLessonPath">
|
||||
<StackPanel Margin="0,2">
|
||||
<TextBlock Text="{Binding Name}"/>
|
||||
<TextBlock Text="{Binding Description}" FontSize="11" Opacity="0.6" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Text="{Binding SelectionError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding SelectionError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Separator Margin="0,4"/>
|
||||
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Neuen Ablauf anlegen" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBox Text="{Binding NewName}" PlaceholderText="Name, z.B. Kurzversion"/>
|
||||
<TextBlock Text="{Binding NewNameError}" Foreground="Red" FontSize="11"
|
||||
IsVisible="{Binding NewNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<TextBox Text="{Binding NewDescription}" AcceptsReturn="True" Height="64" TextWrapping="Wrap"
|
||||
PlaceholderText="Wann nimmt man diesen Weg? z.B. 'Bei Zeitmangel, wenn Aufgabe 3 nicht mehr passt.'"/>
|
||||
<Button Content="Anlegen und auswählen" Command="{Binding CreateNewCommand}" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnConfirm"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class AlternativePathDialog : Window
|
||||
{
|
||||
public AlternativePathDialog() => InitializeComponent();
|
||||
|
||||
private void OnConfirm(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is AlternativePathDialogViewModel vm && vm.ConfirmCommand.CanExecute(null))
|
||||
{
|
||||
vm.ConfirmCommand.Execute(null);
|
||||
if (vm.Result is not null) Close(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.LessonDialog"
|
||||
x:DataType="vm:LessonDialogViewModel"
|
||||
Title="{Binding DialogTitle}"
|
||||
Width="960" Height="760" MinWidth="720" MinHeight="480"
|
||||
Width="1000" Height="760" MinWidth="780" MinHeight="480"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="*,Auto" Margin="24">
|
||||
@@ -56,12 +56,14 @@
|
||||
</Grid>
|
||||
|
||||
<!-- Tabellenkopf -->
|
||||
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26" Margin="4,0,0,0">
|
||||
<Grid ColumnDefinitions="130,95,120,*,100,95,26,26,26" Margin="4,0,0,0">
|
||||
<TextBlock Grid.Column="0" Text="Phase" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="1" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="1" Text="Pfad" FontSize="11" FontWeight="SemiBold" Opacity="0.6"
|
||||
ToolTip.Tip="Ankreuzen, wenn diese Phase zu einem alternativen Ablauf gehört (z.B. Kurzversion bei Zeitnot) — öffnet die Zuweisung. Phasen mit demselben Ablauf werden im Verlaufsplan-Viewer gruppiert."/>
|
||||
<TextBlock Grid.Column="2" Text="Dauer / Zeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Tätigkeit" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Material" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="5" Text="Kurzsymbol" FontSize="11" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<Separator/>
|
||||
|
||||
@@ -71,34 +73,38 @@
|
||||
<DataTemplate x:DataType="vm:PhaseStepEditItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,8">
|
||||
<Grid ColumnDefinitions="140,130,*,110,100,26,26,26">
|
||||
<Grid ColumnDefinitions="130,95,120,*,100,95,26,26,26">
|
||||
<TextBox Grid.Column="0" Text="{Binding Name}" PlaceholderText="z.B. Erarbeitung"
|
||||
VerticalAlignment="Top" Margin="0,0,6,0"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6"
|
||||
<CheckBox Grid.Column="1" IsChecked="{Binding HasAlternativePath}"
|
||||
Content="{Binding AlternativePathName}" Foreground="{Binding AlternativePathColorHex}"
|
||||
FontSize="12" VerticalAlignment="Top" Margin="0,4,6,0"
|
||||
ToolTip.Tip="Teil eines alternativen Ablaufs (z.B. Kurzversion bei Zeitnot). Ankreuzen zum Zuweisen/Anlegen, abwählen zum Entfernen."/>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"
|
||||
VerticalAlignment="Top" Margin="0,0,6,0">
|
||||
<NumericUpDown Value="{Binding DurationMinutes}" Minimum="0" Maximum="180" Width="62"
|
||||
FormatString="0" ShowButtonSpinner="False" ToolTip.Tip="Dauer in Minuten"/>
|
||||
<TextBlock Text="{Binding ComputedTimeDisplay}" FontSize="12" Opacity="0.6"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBox Grid.Column="2" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
|
||||
<TextBox Grid.Column="3" Text="{Binding Activity}" AcceptsReturn="True" TextWrapping="Wrap"
|
||||
Height="56" VerticalAlignment="Top" Margin="0,0,6,0"
|
||||
PlaceholderText="Lehrer-/Schüler-Tätigkeit"
|
||||
ToolTip.Tip="Bei mehr Text scrollt das Feld intern."/>
|
||||
<AutoCompleteBox Grid.Column="3" Text="{Binding Material}"
|
||||
<AutoCompleteBox Grid.Column="4" Text="{Binding Material}"
|
||||
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).MaterialSuggestions}"
|
||||
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
||||
Margin="0,0,6,0" PlaceholderText="z.B. AB01"/>
|
||||
<AutoCompleteBox Grid.Column="4" Text="{Binding Shorthand}"
|
||||
<AutoCompleteBox Grid.Column="5" Text="{Binding Shorthand}"
|
||||
ItemsSource="{Binding $parent[ItemsControl].((vm:LessonDialogViewModel)DataContext).ShorthandSuggestions}"
|
||||
FilterMode="Contains" MinimumPrefixLength="0" VerticalAlignment="Top"
|
||||
Margin="0,0,6,0" PlaceholderText="z.B. AB001->S, Plenum, LDE"
|
||||
ToolTip.Tip="Freitext für den schnellen Überblick — mal ein Materialfluss-Pfeil (AB001->S), mal nur eine Sozialform (Plenum, LDE)."/>
|
||||
<Button Grid.Column="5" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
|
||||
<Button Grid.Column="6" Content="↑" Command="{Binding MoveUpCommand}" Padding="4,2"
|
||||
VerticalAlignment="Top" ToolTip.Tip="Nach oben"/>
|
||||
<Button Grid.Column="6" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
|
||||
<Button Grid.Column="7" Content="↓" Command="{Binding MoveDownCommand}" Padding="4,2"
|
||||
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Nach unten"/>
|
||||
<Button Grid.Column="7" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
|
||||
<Button Grid.Column="8" Content="✕" Command="{Binding RemoveCommand}" Padding="4,2"
|
||||
VerticalAlignment="Top" Margin="2,0,0,0" ToolTip.Tip="Entfernen"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
@@ -8,6 +11,23 @@ public partial class LessonDialog : Window
|
||||
{
|
||||
public LessonDialog() => InitializeComponent();
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is LessonDialogViewModel vm)
|
||||
vm.OnPickAlternativePath = ShowAlternativePathDialog;
|
||||
}
|
||||
|
||||
private async Task<AlternativeLessonPath?> ShowAlternativePathDialog(Guid? currentId)
|
||||
{
|
||||
var dialogVm = new AlternativePathDialogViewModel(
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(), currentId);
|
||||
var dialog = new AlternativePathDialog { DataContext = dialogVm };
|
||||
|
||||
var ok = await dialog.ShowDialog<bool>(this);
|
||||
return ok ? dialogVm.Result : null;
|
||||
}
|
||||
|
||||
private void OnSave(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is LessonDialogViewModel vm && vm.SaveCommand.CanExecute(null))
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
|
||||
x:Class="LehrerApp.Desktop.Views.Groups.LessonViewerDialog"
|
||||
x:DataType="vm:LessonViewerViewModel"
|
||||
Title="{Binding Topic}"
|
||||
Width="900" Height="700" MinWidth="600" MinHeight="420"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
|
||||
<StackPanel Grid.Row="0" Spacing="4" Margin="0,0,0,16">
|
||||
<TextBlock Text="{Binding Topic}" FontSize="20" FontWeight="SemiBold"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<TextBlock FontSize="13" Opacity="0.7">
|
||||
<Run Text="Datum: "/><Run Text="{Binding DateDisplay}"/>
|
||||
</TextBlock>
|
||||
<TextBlock FontSize="13" Opacity="0.7">
|
||||
<Run Text="Beginn: "/><Run Text="{Binding StartTimeDisplay}"/>
|
||||
</TextBlock>
|
||||
<TextBlock FontSize="13" Opacity="0.7">
|
||||
<Run Text="Status: "/><Run Text="{Binding StatusLabel}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<StackPanel Spacing="0">
|
||||
<Grid ColumnDefinitions="140,80,*,140,130" Margin="4,0,0,6">
|
||||
<TextBlock Grid.Column="0" Text="Phase" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="1" Text="Dauer/Zeit" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="2" Text="Tätigkeit" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="3" Text="Material" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Grid.Column="4" Text="Kurzsymbol" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
</Grid>
|
||||
<Separator/>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding PhaseGroups}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PhaseGroupViewItem">
|
||||
<StackPanel Margin="0,0,0,14">
|
||||
<StackPanel IsVisible="{Binding $parent[ItemsControl].((vm:LessonViewerViewModel)DataContext).HasAlternatives}">
|
||||
<TextBlock Text="{Binding Label}" FontSize="13" FontWeight="SemiBold" Opacity="0.75"
|
||||
Margin="4,10,0,2"/>
|
||||
<TextBlock Text="{Binding Description}" FontSize="12" Opacity="0.55" TextWrapping="Wrap"
|
||||
Margin="4,0,0,6"
|
||||
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding Phases}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PhaseViewItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="4,10">
|
||||
<Grid ColumnDefinitions="140,80,*,140,130">
|
||||
<TextBlock Grid.Column="0" Text="{Binding Name}" FontSize="15" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Top">
|
||||
<TextBlock Text="{Binding DurationMinutes, StringFormat='{}{0} Min.'}" FontSize="14"/>
|
||||
<TextBlock Text="{Binding TimeDisplay}" FontSize="12" Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding Activity}" FontSize="14"
|
||||
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Material}" FontSize="14"
|
||||
TextWrapping="Wrap" VerticalAlignment="Top" Margin="0,0,8,0"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding Shorthand}" FontSize="14"
|
||||
TextWrapping="Wrap" VerticalAlignment="Top"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<StackPanel Spacing="4" Margin="4,16,0,0" IsVisible="{Binding Homework, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="Hausaufgabe" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding Homework}" FontSize="14" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4" Margin="4,16,0,0" IsVisible="{Binding Reflection, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="Reflexion" FontSize="12" FontWeight="SemiBold" Opacity="0.6"/>
|
||||
<TextBlock Text="{Binding Reflection}" FontSize="14" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Button Grid.Row="2" Content="Schließen" HorizontalAlignment="Right" Margin="0,16,0,0" Click="OnClose"/>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
|
||||
public partial class LessonViewerDialog : Window
|
||||
{
|
||||
public LessonViewerDialog() => InitializeComponent();
|
||||
|
||||
private void OnClose(object? s, RoutedEventArgs e) => Close();
|
||||
}
|
||||
@@ -63,6 +63,8 @@
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="+ Stunde" Command="{Binding AddLessonCommand}"/>
|
||||
<Button Content="Anzeigen" Command="{Binding ShowLessonCommand}"
|
||||
ToolTip.Tip="Verlaufsplan schreibgeschützt und größer anzeigen — zum Mitnehmen in den Unterricht."/>
|
||||
<Button Content="Bearbeiten" Command="{Binding EditLessonCommand}"/>
|
||||
<Button Content="Verschieben" Command="{Binding MoveLessonCommand}"/>
|
||||
<Button Content="Status → Durchgeführt" Command="{Binding AdvanceLessonStatusCommand}"/>
|
||||
|
||||
@@ -26,6 +26,7 @@ public partial class PlanningTabView : UserControl
|
||||
ShowLessonDialog(lesson.UnitId, lesson.GroupId, materials, shorthands, editingLesson: lesson);
|
||||
vm.OnConfirmDeleteLesson = ShowDeleteLessonDialog;
|
||||
vm.OnPickMoveTarget = ShowMoveLessonDialog;
|
||||
vm.OnShowLesson = ShowLessonViewerDialog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +79,7 @@ public partial class PlanningTabView : UserControl
|
||||
var dialogVm = new LessonDialogViewModel(
|
||||
App.Services.GetRequiredService<ILessonRepository>(),
|
||||
App.Services.GetRequiredService<IShorthandCodeRepository>(),
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>(),
|
||||
unitId, groupId, materialSuggestions, shorthandHistorySuggestions, editingLesson);
|
||||
|
||||
var dialog = new LessonDialog { DataContext = dialogVm };
|
||||
@@ -111,4 +113,13 @@ public partial class PlanningTabView : UserControl
|
||||
var ok = await dialog.ShowDialog<bool>(owner);
|
||||
return ok ? dialogVm.Result : null;
|
||||
}
|
||||
|
||||
private async Task ShowLessonViewerDialog(Lesson lesson)
|
||||
{
|
||||
var viewerVm = new LessonViewerViewModel(lesson,
|
||||
App.Services.GetRequiredService<IAlternativeLessonPathRepository>());
|
||||
var dialog = new LessonViewerDialog { DataContext = viewerVm };
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
if (owner is not null) await dialog.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user