Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a36b1d1d40 | ||
|
|
4510c56bb4 |
@@ -0,0 +1,48 @@
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Desktop.ViewModels.Groups;
|
||||
using Xunit;
|
||||
|
||||
namespace LehrerApp.Desktop.Tests;
|
||||
|
||||
public sealed class QuickInputViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void ZurueckZuVorherigemSchueler_ZeigtBereitsGesetzteBewertung()
|
||||
{
|
||||
var aspects = new List<AspectColumnDef>
|
||||
{
|
||||
new(new ParticipationAspect { Key = "mitarbeit", Label = "Mitarbeit" }),
|
||||
};
|
||||
var rows = new List<ParticipationStudentRow>
|
||||
{
|
||||
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
|
||||
new(Guid.NewGuid(), "Ben", new ParticipationEntry(), aspects, []),
|
||||
};
|
||||
var vm = new QuickInputViewModel(rows, aspects);
|
||||
|
||||
vm.SetRatingByNumber(4); // Scale5, 4. Stufe -> Rohwert 1 ("+"), Anna bewerten
|
||||
vm.NextStudent(); // zu Ben
|
||||
vm.PreviousStudent(); // zurück zu Anna
|
||||
|
||||
Assert.Equal(1, vm.AspectRows[0].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
|
||||
{
|
||||
var aspects = new List<AspectColumnDef>
|
||||
{
|
||||
new(new ParticipationAspect { Key = "a", Label = "A" }),
|
||||
new(new ParticipationAspect { Key = "b", Label = "B" }),
|
||||
};
|
||||
var rows = new List<ParticipationStudentRow>
|
||||
{
|
||||
new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
|
||||
};
|
||||
var vm = new QuickInputViewModel(rows, aspects);
|
||||
|
||||
vm.PreviousAspect(); // von Index 0 rückwärts -> letzter Aspekt (Umlauf)
|
||||
|
||||
Assert.Equal(1, vm.AspectIndex);
|
||||
}
|
||||
}
|
||||
@@ -442,6 +442,19 @@ public sealed class TimetableViewModelTests
|
||||
Assert.DoesNotContain(vm.Cells, c => c.IsSupervisionRow);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_AufsichtEingetragen_BekommtSchmalereZeilenhoeheAlsStundenzeilen()
|
||||
{
|
||||
var duties = new FakeSupervisionDuties();
|
||||
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
|
||||
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties);
|
||||
|
||||
var row = vm.GridRows.Single(r => r.Cells.Any(c => c.IsSupervisionRow && c.Text.Contains("n. 2.")));
|
||||
|
||||
Assert.Equal(22, row.RowHeight);
|
||||
Assert.Contains(vm.GridRows, r => r.RowHeight == 46); // normale Stundenzeilen bleiben unverändert
|
||||
}
|
||||
|
||||
// ── Aufsicht + Vertretung im Wochenraster ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
@@ -457,6 +470,19 @@ public sealed class TimetableViewModelTests
|
||||
Assert.False(mondayCell.IsSubstitutionSupervision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_Wochenraster_AufsichtBekommtSchmalereZeilenhoeheAlsStundenzeilen()
|
||||
{
|
||||
var duties = new FakeSupervisionDuties();
|
||||
duties.Add(new SupervisionDuty { Weekday = DayOfWeek.Monday, AfterPeriod = 2, Location = "Pausenhof" });
|
||||
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]), supervisionDuties: duties);
|
||||
|
||||
var row = vm.WeekRows.Single(r => r.Cells.Any(c => c.IsSupervisionRow && c.Text.Contains("n. 2.")));
|
||||
|
||||
Assert.Equal(22, row.RowHeight);
|
||||
Assert.Contains(vm.WeekRows, r => r.RowHeight == 76);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Load_Wochenraster_VertretungsaufsichtUeberschreibtRegulaereAnzeige()
|
||||
{
|
||||
|
||||
@@ -351,7 +351,6 @@ public partial class ParticipationStudentRow : ObservableObject
|
||||
public Guid StudentId { get; }
|
||||
public string Name { get; }
|
||||
|
||||
private readonly ParticipationEntry _entry;
|
||||
private readonly IReadOnlyList<AspectColumnDef> _aspectDefs;
|
||||
|
||||
public ObservableCollection<RatingCell> Cells { get; } = [];
|
||||
@@ -375,7 +374,6 @@ public partial class ParticipationStudentRow : ObservableObject
|
||||
{
|
||||
StudentId = id;
|
||||
Name = name;
|
||||
_entry = entry;
|
||||
_aspectDefs = aspects;
|
||||
_homework = HomeworkDisplay.Effective(entry);
|
||||
_attendance = entry.Attendance;
|
||||
@@ -397,8 +395,12 @@ public partial class ParticipationStudentRow : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// Liest aus Cells (per SetValue laufend aktuell gehalten), NICHT aus dem ursprünglichen
|
||||
// ParticipationEntry-Snapshot: SetRating() schreibt nur in Cells + feuert OnRatingChanged
|
||||
// (der Callback speichert über eine EIGENE, neu aus dem Repository geladene Entry-Instanz —
|
||||
// der hier ursprünglich referenzierte Entry-Snapshot bekommt diese Änderung nie zu sehen).
|
||||
public int? GetRating(string key) =>
|
||||
_entry.Ratings.FirstOrDefault(r => r.Key == key)?.Value;
|
||||
Cells.FirstOrDefault(c => c.AspectKey == key)?.Value;
|
||||
|
||||
public void SetRating(string key, int? value)
|
||||
{
|
||||
@@ -743,8 +745,8 @@ public partial class QuickInputViewModel : ObservableObject
|
||||
AspectValueType.Points => $"0–9 Punkte eingeben (bis {CurrentAspectMaxPoints()})",
|
||||
_ => "1–5 bewerten",
|
||||
};
|
||||
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste nächster Aspekt · " +
|
||||
"Enter nächster Schüler · Backspace vorheriger · +/− anpassen · Esc schließen";
|
||||
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste/↓ nächster Aspekt · ↑ vorheriger Aspekt · " +
|
||||
"Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Esc schließen";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -862,6 +864,12 @@ public partial class QuickInputViewModel : ObservableObject
|
||||
UpdateCurrentAspect();
|
||||
}
|
||||
|
||||
public void PreviousAspect()
|
||||
{
|
||||
AspectIndex = (AspectIndex - 1 + _aspects.Count) % _aspects.Count;
|
||||
UpdateCurrentAspect();
|
||||
}
|
||||
|
||||
public void NextStudent()
|
||||
{
|
||||
if (StudentIndex >= _rows.Count - 1) return;
|
||||
|
||||
@@ -45,6 +45,7 @@ public partial class TimetableViewModel : ObservableObject
|
||||
{
|
||||
private const int FirstPeriod = 1;
|
||||
private const int LastPeriod = 10;
|
||||
private const int GridColumns = 6; // Label/Kopf-Spalte + 5 Wochentage (Mo-Fr)
|
||||
private static readonly DayOfWeek[] Weekdays =
|
||||
[DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday];
|
||||
private static readonly string[] WeekdayColorPalette =
|
||||
@@ -70,6 +71,15 @@ public partial class TimetableViewModel : ObservableObject
|
||||
public ObservableCollection<TodaySpecialAssignmentItem> TodaySpecialAssignments { get; } = [];
|
||||
public ObservableCollection<UpcomingExamItem> UpcomingExams { get; } = [];
|
||||
|
||||
/// Cells/WeekItems zeilenweise gruppiert (GridColumns Zellen je Zeile), zusätzlich zur
|
||||
/// flachen Liste - Grundlage für die zeilenweise Höhensteuerung in der View (Aufsicht-Zeilen
|
||||
/// deutlich schmaler als Stunden-/Kopfzeilen). Grid.RowDefinitions lässt sich in Avalonia
|
||||
/// nicht per {Binding} setzen (Compiler lehnt jede Binding-Form dafür ab, siehe
|
||||
/// AVLN3000-Fehler „Unable to find suitable setter or adder for property RowDefinitions“) -
|
||||
/// deshalb hier stattdessen echte Unterzeilen mit je eigener, ganz normal bindbarer Height.
|
||||
public ObservableCollection<TimetableRowItem> GridRows { get; } = [];
|
||||
public ObservableCollection<WeekRowItem> WeekRows { get; } = [];
|
||||
|
||||
[ObservableProperty] private int _activeTabIndex;
|
||||
[ObservableProperty] private string _todayLabel = "";
|
||||
[ObservableProperty] private string _weekRangeLabel = "";
|
||||
@@ -325,6 +335,10 @@ public partial class TimetableViewModel : ObservableObject
|
||||
}
|
||||
AddWeekSupervisionRowIfAny(period, dutiesByPeriod, substitutionsThisWeek, dateByWeekday);
|
||||
}
|
||||
|
||||
WeekRows.Clear();
|
||||
foreach (var rowCells in WeekItems.Chunk(GridColumns))
|
||||
WeekRows.Add(new WeekRowItem(rowCells));
|
||||
}
|
||||
|
||||
private void AddWeekSupervisionRowIfAny(int afterPeriod, ILookup<int, SupervisionDuty> dutiesByPeriod,
|
||||
@@ -409,6 +423,10 @@ public partial class TimetableViewModel : ObservableObject
|
||||
}
|
||||
AddGridSupervisionRowIfAny(period, dutiesByPeriod);
|
||||
}
|
||||
|
||||
GridRows.Clear();
|
||||
foreach (var rowCells in Cells.Chunk(GridColumns))
|
||||
GridRows.Add(new TimetableRowItem(rowCells));
|
||||
}
|
||||
|
||||
/// <summary>Aufsicht wird nur in den Einstellungen gepflegt (siehe SettingsView) — im
|
||||
@@ -571,6 +589,18 @@ public class TimetableCellItem
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eine Zeile im Bearbeiten-Raster (GridColumns Zellen). Aufsicht-Zeilen bekommen eine deutlich
|
||||
/// schmalere RowHeight als normale Kopf-/Stundenzeilen - eine echte, per Height ganz normal
|
||||
/// bindbare Eigenschaft je Unterzeile statt eines vollflächigen UniformGrid-Rasters, das allen
|
||||
/// Zeilen zwangsläufig dieselbe Höhe geben würde.
|
||||
/// </summary>
|
||||
public class TimetableRowItem(IReadOnlyList<TimetableCellItem> cells)
|
||||
{
|
||||
public IReadOnlyList<TimetableCellItem> Cells { get; } = cells;
|
||||
public double RowHeight { get; } = cells[0].IsSupervisionRow ? 22 : 46;
|
||||
}
|
||||
|
||||
/// <summary>Zelle im schreibgeschützten Wochenraster der "Heute"-Ansicht.</summary>
|
||||
public class WeekCellItem
|
||||
{
|
||||
@@ -671,6 +701,13 @@ public class WeekCellItem
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Zeile im Wochenraster - siehe TimetableRowItem, dieselbe Rolle für WeekItems.</summary>
|
||||
public class WeekRowItem(IReadOnlyList<WeekCellItem> cells)
|
||||
{
|
||||
public IReadOnlyList<WeekCellItem> Cells { get; } = cells;
|
||||
public double RowHeight { get; } = cells[0].IsSupervisionRow ? 22 : 76;
|
||||
}
|
||||
|
||||
public class HoursWarningItem(string groupName, int assigned, int expected)
|
||||
{
|
||||
public string GroupName { get; } = groupName;
|
||||
|
||||
@@ -47,6 +47,15 @@ public partial class ParticipationQuickInputDialog : Window
|
||||
break;
|
||||
case Key.Back: vm.PreviousStudent(); e.Handled = true; break;
|
||||
|
||||
// Rechtsseitiges Pendant zu Q/W/E/R/T (links)/Enter/Backspace/Leertaste: ohne diese
|
||||
// Fälle fallen Pfeiltasten auf Avalonias Standard-Fokusnavigation durch und der Fokus
|
||||
// springt auf den Schließen-Button — ein Enter danach schließt dann den Dialog statt
|
||||
// zum nächsten Schüler zu springen.
|
||||
case Key.Up: vm.PreviousAspect(); e.Handled = true; break;
|
||||
case Key.Down: vm.NextAspect(); e.Handled = true; break;
|
||||
case Key.Right: vm.NextStudent(); e.Handled = true; break;
|
||||
case Key.Left: vm.PreviousStudent(); e.Handled = true; break;
|
||||
|
||||
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
|
||||
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;
|
||||
|
||||
|
||||
@@ -133,13 +133,23 @@
|
||||
ToolTip.Tip="Einstellungen (Ferien, Aufsichten, Stundenraster)"/>
|
||||
</Grid>
|
||||
|
||||
<ItemsControl ItemsSource="{Binding WeekItems}">
|
||||
<!-- Zeilenweise (WeekRows) statt eines einzigen flachen UniformGrid: so kann jede
|
||||
Zeile ihre eigene Höhe haben (RowHeight je WeekRowItem) - Aufsicht-Zeilen
|
||||
deutlich schmaler als Stunden-/Kopfzeilen. Grid.RowDefinitions lässt sich in
|
||||
Avalonia nicht per {Binding} setzen (AVLN3000 "Unable to find suitable setter
|
||||
or adder"), daher dieser Umweg statt eines einzelnen Grids mit dynamischer
|
||||
RowDefinitions-Bindung. Die innere UniformGrid Columns="6" bleibt unverändert
|
||||
für die Spaltenaufteilung innerhalb je einer Zeile. -->
|
||||
<ItemsControl ItemsSource="{Binding WeekRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WeekRowItem">
|
||||
<ItemsControl ItemsSource="{Binding Cells}" Height="{Binding RowHeight}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:WeekCellItem">
|
||||
<Grid Margin="2" MinHeight="76">
|
||||
<Grid Margin="2">
|
||||
<Border Classes="weekheader" Classes.today="{Binding IsToday}" CornerRadius="4"
|
||||
IsVisible="{Binding IsHeader}">
|
||||
<TextBlock Text="{Binding Text}" FontWeight="SemiBold" FontSize="12" Opacity="0.75"
|
||||
@@ -152,10 +162,11 @@
|
||||
FontSize="10" FontWeight="SemiBold" Opacity="0.55" TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Classes="supervisioncell" Classes.substitution="{Binding IsSubstitutionSupervision}"
|
||||
CornerRadius="4" Padding="4" IsVisible="{Binding IsSupervisionCell}"
|
||||
CornerRadius="4" Padding="4,1" IsVisible="{Binding IsSupervisionCell}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<TextBlock Text="{Binding SupervisionLocation}" FontSize="10" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
<TextBlock Text="{Binding SupervisionLocation}" FontSize="9" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding HasSupervision}"/>
|
||||
</Border>
|
||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||
@@ -163,7 +174,7 @@
|
||||
<Button Background="{Binding ColorHex}" IsVisible="{Binding IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch" CornerRadius="6" Padding="6"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).OpenGroupCommand}"
|
||||
CommandParameter="{Binding GroupId}">
|
||||
<StackPanel Spacing="1">
|
||||
<TextBlock FontSize="11" FontWeight="Bold" Foreground="White" TextWrapping="Wrap">
|
||||
@@ -193,6 +204,9 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
||||
CornerRadius="8" Padding="16" IsVisible="{Binding HoursWarnings.Count}">
|
||||
@@ -237,13 +251,19 @@
|
||||
<ContentPage Header="Bearbeiten">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="10">
|
||||
<ItemsControl ItemsSource="{Binding Cells}">
|
||||
<!-- Zeilenweise (GridRows) statt eines flachen UniformGrid, siehe Kommentar im
|
||||
Wochenraster oben (Heute-Tab) - gleicher Grund (Grid.RowDefinitions lässt sich
|
||||
nicht per {Binding} setzen). -->
|
||||
<ItemsControl ItemsSource="{Binding GridRows}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TimetableRowItem">
|
||||
<ItemsControl ItemsSource="{Binding Cells}" Height="{Binding RowHeight}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TimetableCellItem">
|
||||
<Grid Margin="2" MinHeight="46">
|
||||
<Grid Margin="2">
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsHeader}"
|
||||
FontWeight="SemiBold" FontSize="12" Opacity="0.6"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
@@ -253,11 +273,12 @@
|
||||
<TextBlock Text="{Binding Text}" IsVisible="{Binding IsSupervisionRow}"
|
||||
FontSize="10" FontWeight="SemiBold" Opacity="0.55" TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Border Classes="supervisioncell" CornerRadius="4" Padding="4"
|
||||
<Border Classes="supervisioncell" CornerRadius="4" Padding="4,1"
|
||||
IsVisible="{Binding IsSupervisionCell}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
|
||||
<TextBlock Text="{Binding SupervisionLocation}" FontSize="10" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
<TextBlock Text="{Binding SupervisionLocation}" FontSize="9" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis"
|
||||
IsVisible="{Binding HasSupervision}"/>
|
||||
</Border>
|
||||
<Border Classes="timetablecell" Classes.assigned="{Binding IsAssigned}"
|
||||
@@ -267,7 +288,7 @@
|
||||
IsVisible="{Binding IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" CornerRadius="6"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{Binding Text}" FontSize="11" Foreground="White"
|
||||
TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
@@ -276,7 +297,7 @@
|
||||
IsVisible="{Binding !IsAssigned}"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center" Background="Transparent"
|
||||
Command="{Binding $parent[ItemsControl].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||
Command="{Binding $parent[ItemsControl;1].((vm:TimetableViewModel)DataContext).EditCellCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
<Border Background="#D85A30" CornerRadius="8" Padding="5,1"
|
||||
IsVisible="{Binding HasBadge}"
|
||||
@@ -289,6 +310,9 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
Reference in New Issue
Block a user