feat: Geräte-Pairing-UI + Einstellungen-Neustrukturierung
- Sync-Settings-Tab: neuer Bereich "Gerät koppeln" (10.3.1), nutzt den bereits vorhandenen, bisher nirgends verdrahteten SnapshotService. Ein Gerät erzeugt per CreatePairingCode einen verschlüsselten DB-Snapshot + Einmal-Code, das zweite Gerät übernimmt per RedeemPairingCode Datenbank und Sync-Schlüssel. Bestätigungsdialog vor dem Einlösen, da die lokale Datenbank dabei vollständig ersetzt wird (alter Stand wird automatisch als Backup gesichert). - Einstellungen: TabPlacement von Top auf Left umgestellt (vertikale Liste statt langem horizontalem Balken) und die 13 Tabs in drei logische Gruppen sortiert (Fachliches / Zeitplanung / System). - Neuer SettingsTab-Enum ersetzt rohe int-Tab-Indizes bei MainWindowViewModel.NavigateToSettings/ TimetableViewModel.OnNavigateToSettings. Dabei einen bestehenden Bug gefunden und mitbehoben: das Zahnrad im Stundenplan öffnete über den hartcodierten Index 7 tatsächlich "Datenschutz" statt des laut Kommentar/Tooltip beabsichtigten "Ferien & Feiertage". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Planning;
|
using LehrerApp.Desktop.ViewModels.Planning;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.Tests;
|
namespace LehrerApp.Desktop.Tests;
|
||||||
@@ -151,12 +152,12 @@ public sealed class TimetableViewModelTests
|
|||||||
public void OpenSettings_RuftOnNavigateToSettingsAuf()
|
public void OpenSettings_RuftOnNavigateToSettingsAuf()
|
||||||
{
|
{
|
||||||
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
var vm = BuildViewModel(new FakeTimetableSlots(), new FakeGroups([]));
|
||||||
var called = false;
|
SettingsTab? navigatedTab = null;
|
||||||
vm.OnNavigateToSettings = () => called = true;
|
vm.OnNavigateToSettings = tab => navigatedTab = tab;
|
||||||
|
|
||||||
vm.OpenSettingsCommand.Execute(null);
|
vm.OpenSettingsCommand.Execute(null);
|
||||||
|
|
||||||
Assert.True(called);
|
Assert.Equal(SettingsTab.Holidays, navigatedTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ────────────────────
|
// ── "Heute"-Ansicht: Wochenraster (Nutzer-Feedback, zweite Iteration) ────────────────────
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class App : Application
|
|||||||
|
|
||||||
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
|
// Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage")
|
||||||
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
var timetable = Services.GetRequiredService<TimetableViewModel>();
|
||||||
timetable.OnNavigateToSettings = () => main.NavigateToSettings(7);
|
timetable.OnNavigateToSettings = tab => main.NavigateToSettings(tab);
|
||||||
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6);
|
timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,11 +116,11 @@ public partial class MainWindowViewModel : ObservableObject
|
|||||||
vm.LoadGroup(groupId); // dann Daten laden
|
vm.LoadGroup(groupId); // dann Daten laden
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NavigateToSettings(int initialTab = 0)
|
public void NavigateToSettings(SettingsTab initialTab = SettingsTab.Subjects)
|
||||||
{
|
{
|
||||||
ActiveNavItem = NavItem.Settings;
|
ActiveNavItem = NavItem.Settings;
|
||||||
var vm = _services.GetRequiredService<SettingsViewModel>();
|
var vm = _services.GetRequiredService<SettingsViewModel>();
|
||||||
vm.ActiveTabIndex = initialTab;
|
vm.ActiveTabIndex = (int)initialTab;
|
||||||
CurrentPage = vm;
|
CurrentPage = vm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
using LehrerApp.Core.Services;
|
using LehrerApp.Core.Services;
|
||||||
|
using LehrerApp.Desktop.ViewModels.Settings;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Planning;
|
namespace LehrerApp.Desktop.ViewModels.Planning;
|
||||||
@@ -78,7 +79,7 @@ public partial class TimetableViewModel : ObservableObject
|
|||||||
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
|
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
|
||||||
public Action<Guid>? OnNavigateToGroup { get; set; }
|
public Action<Guid>? OnNavigateToGroup { get; set; }
|
||||||
public Func<Task>? OnAddSubstitution { get; set; }
|
public Func<Task>? OnAddSubstitution { get; set; }
|
||||||
public Action? OnNavigateToSettings { get; set; }
|
public Action<SettingsTab>? OnNavigateToSettings { get; set; }
|
||||||
|
|
||||||
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
public TimetableViewModel(ITimetableSlotRepository slots, IGroupRepository groups,
|
||||||
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
|
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
|
||||||
@@ -234,7 +235,7 @@ public partial class TimetableViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenSettings() => OnNavigateToSettings?.Invoke();
|
private void OpenSettings() => OnNavigateToSettings?.Invoke(SettingsTab.Holidays);
|
||||||
|
|
||||||
// ── "Heute": Wochenraster (Nutzer-Feedback) — wie das Bearbeiten-Raster, aber nur Anzeige ──
|
// ── "Heute": Wochenraster (Nutzer-Feedback) — wie das Bearbeiten-Raster, aber nur Anzeige ──
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,28 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace LehrerApp.Desktop.ViewModels.Settings;
|
namespace LehrerApp.Desktop.ViewModels.Settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reihenfolge MUSS exakt der Reihenfolge der ContentPage-Elemente in SettingsView.axaml
|
||||||
|
/// entsprechen (ActiveTabIndex bindet per Index, nicht per Name) — beim Umsortieren eines
|
||||||
|
/// Tabs in der XAML immer auch hier den Enum-Wert verschieben.
|
||||||
|
/// </summary>
|
||||||
|
public enum SettingsTab
|
||||||
|
{
|
||||||
|
Subjects = 0,
|
||||||
|
ShorthandCodes = 1,
|
||||||
|
Competencies = 2,
|
||||||
|
GradingKeyTemplates = 3,
|
||||||
|
GradingScheme = 4,
|
||||||
|
LetterTemplates = 5,
|
||||||
|
Holidays = 6,
|
||||||
|
PeriodSchedule = 7,
|
||||||
|
SupervisionDuties = 8,
|
||||||
|
Security = 9,
|
||||||
|
Privacy = 10,
|
||||||
|
Sync = 11,
|
||||||
|
Ai = 12,
|
||||||
|
}
|
||||||
|
|
||||||
// ── Haupt-ViewModel ───────────────────────────────────────────────────────────
|
// ── Haupt-ViewModel ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public partial class SettingsViewModel : ObservableObject
|
public partial class SettingsViewModel : ObservableObject
|
||||||
@@ -176,6 +198,20 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
|
|
||||||
public ObservableCollection<SyncConflictListItem> SyncConflicts { get; } = [];
|
public ObservableCollection<SyncConflictListItem> SyncConflicts { get; } = [];
|
||||||
|
|
||||||
|
// ── Geräte-Pairing (10.3.1: Schlüsselübertragung auf ein zweites Gerät) ───
|
||||||
|
//
|
||||||
|
// SnapshotService ist nur registriert, wenn bereits eine Server-URL konfiguriert ist (siehe
|
||||||
|
// AppBootstrapper) — daher optional/nullable statt eines Pflicht-Konstruktorparameters.
|
||||||
|
|
||||||
|
[ObservableProperty] private string _pairingCode = "";
|
||||||
|
[ObservableProperty] private string _pairingCodeInput = "";
|
||||||
|
[ObservableProperty] private string _pairingStatus = "";
|
||||||
|
[ObservableProperty] private bool _pairingBusy;
|
||||||
|
|
||||||
|
/// Vom Code-Behind gesetzt: zeigt einen Bestätigungsdialog, bevor die lokale Datenbank durch
|
||||||
|
/// den Stand des anderen Geräts ersetzt wird.
|
||||||
|
public Func<Task<bool>>? OnConfirmPairingRestore { get; set; }
|
||||||
|
|
||||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||||
@@ -187,6 +223,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
private readonly SyncSettingsService _syncSettings;
|
private readonly SyncSettingsService _syncSettings;
|
||||||
private readonly SyncAuthService _syncAuth;
|
private readonly SyncAuthService _syncAuth;
|
||||||
private readonly EventQueue _eventQueue;
|
private readonly EventQueue _eventQueue;
|
||||||
|
private readonly SnapshotService? _snapshotService;
|
||||||
private readonly CompetencyCatalogImportService _catalogImport;
|
private readonly CompetencyCatalogImportService _catalogImport;
|
||||||
|
|
||||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||||
@@ -198,7 +235,8 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
|
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
|
||||||
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
|
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
|
||||||
AiSettingsService aiSettings, AiPlanningService aiPlanning,
|
AiSettingsService aiSettings, AiPlanningService aiPlanning,
|
||||||
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue)
|
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
|
||||||
|
SnapshotService? snapshotService = null)
|
||||||
{
|
{
|
||||||
_subjects = subjects;
|
_subjects = subjects;
|
||||||
_domainRepo = domainRepo;
|
_domainRepo = domainRepo;
|
||||||
@@ -223,6 +261,7 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
_syncSettings = syncSettings;
|
_syncSettings = syncSettings;
|
||||||
_syncAuth = syncAuth;
|
_syncAuth = syncAuth;
|
||||||
_eventQueue = eventQueue;
|
_eventQueue = eventQueue;
|
||||||
|
_snapshotService = snapshotService;
|
||||||
_catalogImport = new CompetencyCatalogImportService(domainRepo);
|
_catalogImport = new CompetencyCatalogImportService(domainRepo);
|
||||||
LoadSubjects();
|
LoadSubjects();
|
||||||
LoadShorthandCodes();
|
LoadShorthandCodes();
|
||||||
@@ -463,6 +502,72 @@ public partial class SettingsViewModel : ObservableObject
|
|||||||
SyncConflicts.Remove(item);
|
SyncConflicts.Remove(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Geräte-Pairing: Code erzeugen / einlösen ─────────────────────────────
|
||||||
|
//
|
||||||
|
// CreateAndUploadAsync lädt einen verschlüsselten Snapshot der lokalen Datenbank samt
|
||||||
|
// Sync-Schlüssel hoch, RestoreFromCodeAsync ersetzt auf dem ZWEITEN Gerät die dortige
|
||||||
|
// Datenbank vollständig durch diesen Snapshot (vorheriger Stand wird automatisch als
|
||||||
|
// .backup-Datei gesichert, siehe SnapshotService) — deshalb vor dem Einlösen ein
|
||||||
|
// Bestätigungsdialog wie bei RestoreBackup.
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task CreatePairingCode()
|
||||||
|
{
|
||||||
|
if (_snapshotService is null) return;
|
||||||
|
PairingBusy = true;
|
||||||
|
PairingCode = "";
|
||||||
|
PairingStatus = "";
|
||||||
|
void OnProgress(SnapshotProgress p) => PairingStatus = p.Message;
|
||||||
|
_snapshotService.ProgressChanged += OnProgress;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _snapshotService.CreateAndUploadAsync();
|
||||||
|
PairingCode = result.Code;
|
||||||
|
PairingStatus = $"Gültig bis {result.ExpiresAt:dd.MM.yyyy HH:mm} — auf dem anderen Gerät eingeben.";
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or InvalidOperationException)
|
||||||
|
{
|
||||||
|
PairingStatus = $"Fehlgeschlagen: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_snapshotService.ProgressChanged -= OnProgress;
|
||||||
|
PairingBusy = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task RedeemPairingCode()
|
||||||
|
{
|
||||||
|
if (_snapshotService is null) return;
|
||||||
|
if (string.IsNullOrWhiteSpace(PairingCodeInput)) { PairingStatus = "Bitte Code eingeben."; return; }
|
||||||
|
if (OnConfirmPairingRestore is not null && !await OnConfirmPairingRestore()) return;
|
||||||
|
|
||||||
|
PairingBusy = true;
|
||||||
|
PairingStatus = "";
|
||||||
|
void OnProgress(SnapshotProgress p) => PairingStatus = p.Message;
|
||||||
|
_snapshotService.ProgressChanged += OnProgress;
|
||||||
|
|
||||||
|
// LiteDB hält beim Öffnen einen exklusiven Dateilock — muss vor dem Überschreiben
|
||||||
|
// geschlossen sein. Läuft danach ein Fehler (falscher Code, Server nicht erreichbar), ist
|
||||||
|
// _dbContext bereits disposed und die App nicht mehr sicher weiter benutzbar, ohne dass
|
||||||
|
// die Datenbankdatei selbst angefasst wurde (RestoreFromCodeAsync schreibt sie erst ganz
|
||||||
|
// am Ende) — deshalb in JEDEM Fall (Erfolg wie Fehlschlag) neu starten, nicht nur bei
|
||||||
|
// Erfolg. Ein Neustart öffnet dann wieder dieselbe, unveränderte Datenbank.
|
||||||
|
_dbContext.Dispose();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _snapshotService.RestoreFromCodeAsync(PairingCodeInput.Trim(), AppBootstrapper.DbPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is SnapshotNotFoundException or InvalidOperationException or HttpRequestException)
|
||||||
|
{
|
||||||
|
// _dbContext ist bereits disposed, PairingStatus ist aber ein reines ViewModel-Feld
|
||||||
|
// ohne DB-Zugriff - das Setzen ist unabhängig davon noch sicher.
|
||||||
|
PairingStatus = $"Fehlgeschlagen: {ex.Message}";
|
||||||
|
}
|
||||||
|
AppBootstrapper.RestartApplication();
|
||||||
|
}
|
||||||
|
|
||||||
// ── Stundenraster: Laden / Speichern ─────────────────────────────────────
|
// ── Stundenraster: Laden / Speichern ─────────────────────────────────────
|
||||||
|
|
||||||
private void LoadPeriodTimes()
|
private void LoadPeriodTimes()
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
|
|
||||||
<shared:PageHeader Grid.Row="0" Title="Einstellungen" Margin="32,28,32,0"/>
|
<shared:PageHeader Grid.Row="0" Title="Einstellungen" Margin="32,28,32,0"/>
|
||||||
|
|
||||||
<TabbedPage Grid.Row="1" TabPlacement="Top" SelectedIndex="{Binding ActiveTabIndex}">
|
<TabbedPage Grid.Row="1" TabPlacement="Left" SelectedIndex="{Binding ActiveTabIndex}">
|
||||||
|
<!-- ═══ Fachliches ═══ -->
|
||||||
|
|
||||||
|
|
||||||
<!-- Tab: Fächer -->
|
<!-- Tab: Fächer -->
|
||||||
<ContentPage Header="Fächer">
|
<ContentPage Header="Fächer">
|
||||||
@@ -282,6 +284,50 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Notenschema -->
|
||||||
|
<ContentPage Header="Notenschema">
|
||||||
|
<ContentPage.Resources>
|
||||||
|
<DataTemplate x:Key="GradingSchemeTemplate" DataType="vm:GradingSchemeEditItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="{Binding Label}" FontWeight="SemiBold" FontSize="14"/>
|
||||||
|
<Grid ColumnDefinitions="Auto,90,Auto,90,Auto,90" ColumnSpacing="6">
|
||||||
|
<TextBlock Grid.Column="0" Text="Klausuren %" VerticalAlignment="Center" FontSize="12"/>
|
||||||
|
<NumericUpDown Grid.Column="1" Value="{Binding ExamsPercent}" Minimum="0" Maximum="100"
|
||||||
|
FormatString="0.#" ShowButtonSpinner="False"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="Mitarbeit %" VerticalAlignment="Center" FontSize="12"/>
|
||||||
|
<NumericUpDown Grid.Column="3" Value="{Binding ParticipationPercent}" Minimum="0" Maximum="100"
|
||||||
|
FormatString="0.#" ShowButtonSpinner="False"/>
|
||||||
|
<TextBlock Grid.Column="4" Text="Sonstige %" VerticalAlignment="Center" FontSize="12"/>
|
||||||
|
<NumericUpDown Grid.Column="5" Value="{Binding OtherPercent}" Minimum="0" Maximum="100"
|
||||||
|
FormatString="0.#" ShowButtonSpinner="False"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" Foreground="Green" FontSize="12"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Grid.Column="1" Content="Speichern" Command="{Binding SaveCommand}"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ContentPage.Resources>
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||||
|
|
||||||
|
<TextBlock Text="Voreinstellung der Gewichtung für die Zeugnisnote (Klausuren / Mitarbeit / Sonstige), je Gruppentyp. Kann pro Lerngruppe überschrieben werden."
|
||||||
|
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
|
<ContentControl Content="{Binding ClassScheme}" ContentTemplate="{StaticResource GradingSchemeTemplate}"/>
|
||||||
|
<ContentControl Content="{Binding CourseScheme}" ContentTemplate="{StaticResource GradingSchemeTemplate}"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Word-Briefvorlagen (7.1.4 / 11.5) -->
|
<!-- Tab: Word-Briefvorlagen (7.1.4 / 11.5) -->
|
||||||
<ContentPage Header="Briefvorlagen">
|
<ContentPage Header="Briefvorlagen">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
@@ -362,50 +408,162 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Notenschema -->
|
<!-- ═══ Zeitplanung ═══ -->
|
||||||
<ContentPage Header="Notenschema">
|
|
||||||
<ContentPage.Resources>
|
<!-- Tab: Ferien & Feiertage (4.3.5) -->
|
||||||
<DataTemplate x:Key="GradingSchemeTemplate" DataType="vm:GradingSchemeEditItem">
|
<ContentPage Header="Ferien & Feiertage">
|
||||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="6" Padding="14,12">
|
|
||||||
<StackPanel Spacing="8">
|
|
||||||
<TextBlock Text="{Binding Label}" FontWeight="SemiBold" FontSize="14"/>
|
|
||||||
<Grid ColumnDefinitions="Auto,90,Auto,90,Auto,90" ColumnSpacing="6">
|
|
||||||
<TextBlock Grid.Column="0" Text="Klausuren %" VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
<NumericUpDown Grid.Column="1" Value="{Binding ExamsPercent}" Minimum="0" Maximum="100"
|
|
||||||
FormatString="0.#" ShowButtonSpinner="False"/>
|
|
||||||
<TextBlock Grid.Column="2" Text="Mitarbeit %" VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
<NumericUpDown Grid.Column="3" Value="{Binding ParticipationPercent}" Minimum="0" Maximum="100"
|
|
||||||
FormatString="0.#" ShowButtonSpinner="False"/>
|
|
||||||
<TextBlock Grid.Column="4" Text="Sonstige %" VerticalAlignment="Center" FontSize="12"/>
|
|
||||||
<NumericUpDown Grid.Column="5" Value="{Binding OtherPercent}" Minimum="0" Maximum="100"
|
|
||||||
FormatString="0.#" ShowButtonSpinner="False"/>
|
|
||||||
</Grid>
|
|
||||||
<TextBlock Text="{Binding ValidationMessage}" Foreground="Red" FontSize="12"
|
|
||||||
IsVisible="{Binding ValidationMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<TextBlock Grid.Column="0" Text="{Binding StatusMessage}" Foreground="Green" FontSize="12"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Button Grid.Column="1" Content="Speichern" Command="{Binding SaveCommand}"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ContentPage.Resources>
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||||
|
|
||||||
<TextBlock Text="Voreinstellung der Gewichtung für die Zeugnisnote (Klausuren / Mitarbeit / Sonstige), je Gruppentyp. Kann pro Lerngruppe überschrieben werden."
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Bundesland (für Feiertage)" FontSize="13" FontWeight="SemiBold"/>
|
||||||
|
<ComboBox ItemsSource="{Binding StateOptions}" SelectedItem="{Binding SelectedStateName}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Schulferien" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="Werden im Stundenplan als unterrichtsfreie Tage angezeigt."
|
||||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
<ContentControl Content="{Binding ClassScheme}" ContentTemplate="{StaticResource GradingSchemeTemplate}"/>
|
<ItemsControl ItemsSource="{Binding SchoolHolidayEntries}">
|
||||||
<ContentControl Content="{Binding CourseScheme}" ContentTemplate="{StaticResource GradingSchemeTemplate}"/>
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:SchoolHolidayItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="0,7">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding RangeDisplay}" FontSize="12" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSchoolHolidayCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Noch keine Schulferien hinterlegt." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !SchoolHolidayEntries.Count}"/>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBox Text="{Binding NewHolidayName}" PlaceholderText="Name (z.B. Sommerferien)"/>
|
||||||
|
<TextBlock Text="{Binding HolidayNameError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding HolidayNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Grid ColumnDefinitions="*,8,*">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding NewHolidayStartText}" PlaceholderText="Beginn TT.MM.JJJJ"/>
|
||||||
|
<TextBox Grid.Column="2" Text="{Binding NewHolidayEndText}" PlaceholderText="Ende TT.MM.JJJJ"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding HolidayDateError}" Foreground="Red" FontSize="11"
|
||||||
|
IsVisible="{Binding HolidayDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Content="+ Schulferien hinzufügen" Command="{Binding AddSchoolHolidayCommand}"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Stundenraster (4.2.2 Nachtrag) -->
|
||||||
|
<ContentPage Header="Stundenraster">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
|
||||||
|
|
||||||
|
<TextBlock Text="Uhrzeiten der Einzelstunden" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="Grundlage für die Zeitbedarf-Rückmeldung im Verlaufsplan-Editor. Nicht alle Stunden müssen eingetragen sein — für unkonfigurierte Stunden bleibt die Rückmeldung dort einfach aus."/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="70,*,8,*" Margin="0,4,0,0">
|
||||||
|
<TextBlock Grid.Column="1" Text="Beginn" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="Ende" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
||||||
|
</Grid>
|
||||||
|
<ItemsControl ItemsSource="{Binding PeriodTimes}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:PeriodTimeEditItem">
|
||||||
|
<Grid ColumnDefinitions="70,*,8,*" Margin="0,3">
|
||||||
|
<TextBlock Grid.Column="0" Text="{Binding PeriodLabel}" FontSize="13" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding StartText}" PlaceholderText="HH:MM"/>
|
||||||
|
<TextBox Grid.Column="3" Text="{Binding EndText}" PlaceholderText="HH:MM"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding PeriodTimesError}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding PeriodTimesError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Content="Speichern" Command="{Binding SavePeriodTimesCommand}" HorizontalAlignment="Left"/>
|
||||||
|
<TextBlock Text="{Binding PeriodTimesStatus}" Foreground="Green" FontSize="12"
|
||||||
|
IsVisible="{Binding PeriodTimesStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: Aufsichten (4.3 Nachtrag) -->
|
||||||
|
<ContentPage Header="Aufsichten">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
||||||
|
|
||||||
|
<TextBlock Text="Wiederkehrende Pausenaufsicht" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="Wird im Stundenplan zwischen den betroffenen Stunden angezeigt. Einmalige Vertretungsaufsichten trägst du direkt im Stundenplan (Heute-Ansicht) ein, nicht hier."/>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{Binding SupervisionDuties}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="vm:SupervisionDutyItem">
|
||||||
|
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
|
BorderThickness="0,0,0,1" Padding="0,7">
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0">
|
||||||
|
<TextBlock FontSize="13" FontWeight="SemiBold">
|
||||||
|
<Run Text="{Binding WeekdayLabel}"/><Run Text=" — "/><Run Text="{Binding PeriodLabel}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding Location}" FontSize="12" Opacity="0.6"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSupervisionDutyCommand}"
|
||||||
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<TextBlock Text="Noch keine Aufsicht hinterlegt." Classes="emptyhint"
|
||||||
|
IsVisible="{Binding !SupervisionDuties.Count}"/>
|
||||||
|
|
||||||
|
<Separator Margin="0,4"/>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,8,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Text="Wochentag" FontSize="12" Opacity="0.7"/>
|
||||||
|
<ComboBox ItemsSource="{Binding WeekdayOptions}" SelectedItem="{Binding NewDutyWeekdayName}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Text="Nach Stunde (0 = davor)" FontSize="12" Opacity="0.7"/>
|
||||||
|
<NumericUpDown Value="{Binding NewDutyAfterPeriod}" Minimum="0" Maximum="10" FormatString="0"
|
||||||
|
Width="140" ShowButtonSpinner="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Ort / Bezeichnung" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding NewDutyLocation}" PlaceholderText="z.B. Pausenhof"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="{Binding NewDutyError}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding NewDutyError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Content="+ Aufsicht hinzufügen" Command="{Binding AddSupervisionDutyCommand}"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- ═══ System ═══ -->
|
||||||
|
|
||||||
<!-- Tab: Sicherheit (13.3) -->
|
<!-- Tab: Sicherheit (13.3) -->
|
||||||
<ContentPage Header="Sicherheit">
|
<ContentPage Header="Sicherheit">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
@@ -570,195 +728,6 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
<!-- Tab: Ferien & Feiertage (4.3.5) -->
|
|
||||||
<ContentPage Header="Ferien & Feiertage">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<TextBlock Text="Bundesland (für Feiertage)" FontSize="13" FontWeight="SemiBold"/>
|
|
||||||
<ComboBox ItemsSource="{Binding StateOptions}" SelectedItem="{Binding SelectedStateName}"
|
|
||||||
HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Separator Margin="0,4"/>
|
|
||||||
|
|
||||||
<TextBlock Text="Schulferien" FontSize="16" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock Text="Werden im Stundenplan als unterrichtsfreie Tage angezeigt."
|
|
||||||
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
|
|
||||||
|
|
||||||
<ItemsControl ItemsSource="{Binding SchoolHolidayEntries}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:SchoolHolidayItem">
|
|
||||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="0,7">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock Text="{Binding RangeDisplay}" FontSize="12" Opacity="0.6"/>
|
|
||||||
</StackPanel>
|
|
||||||
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSchoolHolidayCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Noch keine Schulferien hinterlegt." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !SchoolHolidayEntries.Count}"/>
|
|
||||||
|
|
||||||
<Separator Margin="0,4"/>
|
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<TextBox Text="{Binding NewHolidayName}" PlaceholderText="Name (z.B. Sommerferien)"/>
|
|
||||||
<TextBlock Text="{Binding HolidayNameError}" Foreground="Red" FontSize="11"
|
|
||||||
IsVisible="{Binding HolidayNameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Grid ColumnDefinitions="*,8,*">
|
|
||||||
<TextBox Grid.Column="0" Text="{Binding NewHolidayStartText}" PlaceholderText="Beginn TT.MM.JJJJ"/>
|
|
||||||
<TextBox Grid.Column="2" Text="{Binding NewHolidayEndText}" PlaceholderText="Ende TT.MM.JJJJ"/>
|
|
||||||
</Grid>
|
|
||||||
<TextBlock Text="{Binding HolidayDateError}" Foreground="Red" FontSize="11"
|
|
||||||
IsVisible="{Binding HolidayDateError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Button Content="+ Schulferien hinzufügen" Command="{Binding AddSchoolHolidayCommand}"
|
|
||||||
HorizontalAlignment="Left"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
||||||
<!-- Tab: Stundenraster (4.2.2 Nachtrag) -->
|
|
||||||
<ContentPage Header="Stundenraster">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
|
|
||||||
|
|
||||||
<TextBlock Text="Uhrzeiten der Einzelstunden" FontSize="16" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
|
||||||
Text="Grundlage für die Zeitbedarf-Rückmeldung im Verlaufsplan-Editor. Nicht alle Stunden müssen eingetragen sein — für unkonfigurierte Stunden bleibt die Rückmeldung dort einfach aus."/>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="70,*,8,*" Margin="0,4,0,0">
|
|
||||||
<TextBlock Grid.Column="1" Text="Beginn" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
|
||||||
<TextBlock Grid.Column="3" Text="Ende" FontSize="11" FontWeight="Bold" Opacity="0.5"/>
|
|
||||||
</Grid>
|
|
||||||
<ItemsControl ItemsSource="{Binding PeriodTimes}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:PeriodTimeEditItem">
|
|
||||||
<Grid ColumnDefinitions="70,*,8,*" Margin="0,3">
|
|
||||||
<TextBlock Grid.Column="0" Text="{Binding PeriodLabel}" FontSize="13" VerticalAlignment="Center"/>
|
|
||||||
<TextBox Grid.Column="1" Text="{Binding StartText}" PlaceholderText="HH:MM"/>
|
|
||||||
<TextBox Grid.Column="3" Text="{Binding EndText}" PlaceholderText="HH:MM"/>
|
|
||||||
</Grid>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
|
|
||||||
<TextBlock Text="{Binding PeriodTimesError}" Foreground="Red" FontSize="12"
|
|
||||||
IsVisible="{Binding PeriodTimesError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Button Content="Speichern" Command="{Binding SavePeriodTimesCommand}" HorizontalAlignment="Left"/>
|
|
||||||
<TextBlock Text="{Binding PeriodTimesStatus}" Foreground="Green" FontSize="12"
|
|
||||||
IsVisible="{Binding PeriodTimesStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
||||||
<!-- Tab: Aufsichten (4.3 Nachtrag) -->
|
|
||||||
<ContentPage Header="Aufsichten">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="480">
|
|
||||||
|
|
||||||
<TextBlock Text="Wiederkehrende Pausenaufsicht" FontSize="16" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
|
||||||
Text="Wird im Stundenplan zwischen den betroffenen Stunden angezeigt. Einmalige Vertretungsaufsichten trägst du direkt im Stundenplan (Heute-Ansicht) ein, nicht hier."/>
|
|
||||||
|
|
||||||
<ItemsControl ItemsSource="{Binding SupervisionDuties}">
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate DataType="vm:SupervisionDutyItem">
|
|
||||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
|
||||||
BorderThickness="0,0,0,1" Padding="0,7">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<StackPanel Grid.Column="0">
|
|
||||||
<TextBlock FontSize="13" FontWeight="SemiBold">
|
|
||||||
<Run Text="{Binding WeekdayLabel}"/><Run Text=" — "/><Run Text="{Binding PeriodLabel}"/>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Text="{Binding Location}" FontSize="12" Opacity="0.6"/>
|
|
||||||
</StackPanel>
|
|
||||||
<Button Grid.Column="1" Content="×" FontSize="14" Padding="9,3"
|
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).RemoveSupervisionDutyCommand}"
|
|
||||||
CommandParameter="{Binding}"/>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
<TextBlock Text="Noch keine Aufsicht hinterlegt." Classes="emptyhint"
|
|
||||||
IsVisible="{Binding !SupervisionDuties.Count}"/>
|
|
||||||
|
|
||||||
<Separator Margin="0,4"/>
|
|
||||||
|
|
||||||
<Grid ColumnDefinitions="*,8,Auto">
|
|
||||||
<StackPanel Grid.Column="0" Spacing="4">
|
|
||||||
<TextBlock Text="Wochentag" FontSize="12" Opacity="0.7"/>
|
|
||||||
<ComboBox ItemsSource="{Binding WeekdayOptions}" SelectedItem="{Binding NewDutyWeekdayName}"
|
|
||||||
HorizontalAlignment="Stretch"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Grid.Column="2" Spacing="4">
|
|
||||||
<TextBlock Text="Nach Stunde (0 = davor)" FontSize="12" Opacity="0.7"/>
|
|
||||||
<NumericUpDown Value="{Binding NewDutyAfterPeriod}" Minimum="0" Maximum="10" FormatString="0"
|
|
||||||
Width="140" ShowButtonSpinner="True"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<TextBlock Text="Ort / Bezeichnung" FontSize="12" Opacity="0.7"/>
|
|
||||||
<TextBox Text="{Binding NewDutyLocation}" PlaceholderText="z.B. Pausenhof"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Text="{Binding NewDutyError}" Foreground="Red" FontSize="12"
|
|
||||||
IsVisible="{Binding NewDutyError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Button Content="+ Aufsicht hinzufügen" Command="{Binding AddSupervisionDutyCommand}"
|
|
||||||
HorizontalAlignment="Left"/>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
||||||
<!-- Tab: KI-Unterstützung (4.5.9) -->
|
|
||||||
<ContentPage Header="KI-Unterstützung">
|
|
||||||
<ScrollViewer>
|
|
||||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
|
|
||||||
|
|
||||||
<TextBlock Text="KI-gestützte Planungsunterstützung" FontSize="16" FontWeight="SemiBold"/>
|
|
||||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
|
||||||
Text="Ermöglicht KI-Vorschläge für Unterrichtseinheiten über einen Zwischendienst auf dem eigenen Server (kein API-Schlüssel im Client). Jede Anfrage verbraucht Guthaben."/>
|
|
||||||
|
|
||||||
<CheckBox Content="KI-Unterstützung aktivieren" IsChecked="{Binding AiEnabled}"/>
|
|
||||||
|
|
||||||
<StackPanel Spacing="8" IsVisible="{Binding !AiIsLoggedIn}">
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<TextBlock Text="Benutzername" FontSize="12" Opacity="0.7"/>
|
|
||||||
<TextBox Text="{Binding AiUsername}"/>
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<TextBlock Text="Passwort" FontSize="12" Opacity="0.7"/>
|
|
||||||
<TextBox Text="{Binding AiPassword}" PasswordChar="●"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Text="{Binding AiLoginError}" Foreground="Red" FontSize="12"
|
|
||||||
IsVisible="{Binding AiLoginError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
|
||||||
<Button Content="Anmelden" Command="{Binding AiLoginCommand}" HorizontalAlignment="Left"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Spacing="8" IsVisible="{Binding AiIsLoggedIn}">
|
|
||||||
<TextBlock FontSize="13" FontWeight="SemiBold">
|
|
||||||
<Run Text="Angemeldet als: "/><Run Text="{Binding AiUsername}"/>
|
|
||||||
</TextBlock>
|
|
||||||
<TextBlock Text="{Binding AiBalanceDisplay}" FontSize="13"/>
|
|
||||||
<Button Content="Abmelden" Command="{Binding AiLogoutCommand}" HorizontalAlignment="Left"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentPage>
|
|
||||||
|
|
||||||
<!-- Tab: Synchronisation (Kapitel 10) -->
|
<!-- Tab: Synchronisation (Kapitel 10) -->
|
||||||
<ContentPage Header="Synchronisation">
|
<ContentPage Header="Synchronisation">
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
@@ -806,6 +775,35 @@
|
|||||||
<TextBlock FontSize="11" Opacity="0.5" TextWrapping="Wrap"
|
<TextBlock FontSize="11" Opacity="0.5" TextWrapping="Wrap"
|
||||||
Text="Speichern/Anmelden startet die App neu, damit die Änderung wirksam wird."/>
|
Text="Speichern/Anmelden startet die App neu, damit die Änderung wirksam wird."/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="10" Margin="0,10,0,0" IsVisible="{Binding SyncIsLoggedIn}">
|
||||||
|
<Separator/>
|
||||||
|
<TextBlock Text="Gerät koppeln" FontSize="14" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="Login allein reicht nicht: Verschlüsselt wird lokal mit einem eigenen Schlüssel je Gerät, der nie über den Server läuft. Für ein zweites Gerät hier einen Code erzeugen — auf dem anderen Gerät eingeben, um Datenbank und Schlüssel zu übernehmen."/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Dieses Gerät als Quelle" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||||
|
<Button Content="Code erzeugen" Command="{Binding CreatePairingCodeCommand}"
|
||||||
|
IsEnabled="{Binding !PairingBusy}" HorizontalAlignment="Left"/>
|
||||||
|
<TextBlock Text="{Binding PairingCode}" FontSize="20" FontWeight="Bold"
|
||||||
|
FontFamily="Monospace" IsVisible="{Binding PairingCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="Code von einem anderen Gerät einlösen" FontSize="12" FontWeight="SemiBold" Opacity="0.7"/>
|
||||||
|
<TextBlock FontSize="11" Opacity="0.5" TextWrapping="Wrap"
|
||||||
|
Text="Ersetzt die Datenbank auf DIESEM Gerät vollständig — nur auf einem Gerät ohne eigene Daten verwenden."/>
|
||||||
|
<Grid ColumnDefinitions="*,8,Auto">
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding PairingCodeInput}" PlaceholderText="TIGER-42-BLAU"/>
|
||||||
|
<Button Grid.Column="2" Content="Einlösen" Command="{Binding RedeemPairingCodeCommand}"
|
||||||
|
IsEnabled="{Binding !PairingBusy}"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding PairingStatus}" FontSize="12" TextWrapping="Wrap"
|
||||||
|
IsVisible="{Binding PairingStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Text="Konflikte" FontSize="14" FontWeight="SemiBold" Margin="0,10,0,0"
|
<TextBlock Text="Konflikte" FontSize="14" FontWeight="SemiBold" Margin="0,10,0,0"
|
||||||
IsVisible="{Binding SyncConflicts.Count}"/>
|
IsVisible="{Binding SyncConflicts.Count}"/>
|
||||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
@@ -836,6 +834,42 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|
||||||
|
<!-- Tab: KI-Unterstützung (4.5.9) -->
|
||||||
|
<ContentPage Header="KI-Unterstützung">
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
|
||||||
|
|
||||||
|
<TextBlock Text="KI-gestützte Planungsunterstützung" FontSize="16" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||||
|
Text="Ermöglicht KI-Vorschläge für Unterrichtseinheiten über einen Zwischendienst auf dem eigenen Server (kein API-Schlüssel im Client). Jede Anfrage verbraucht Guthaben."/>
|
||||||
|
|
||||||
|
<CheckBox Content="KI-Unterstützung aktivieren" IsChecked="{Binding AiEnabled}"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="8" IsVisible="{Binding !AiIsLoggedIn}">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Benutzername" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding AiUsername}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Text="Passwort" FontSize="12" Opacity="0.7"/>
|
||||||
|
<TextBox Text="{Binding AiPassword}" PasswordChar="●"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="{Binding AiLoginError}" Foreground="Red" FontSize="12"
|
||||||
|
IsVisible="{Binding AiLoginError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<Button Content="Anmelden" Command="{Binding AiLoginCommand}" HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="8" IsVisible="{Binding AiIsLoggedIn}">
|
||||||
|
<TextBlock FontSize="13" FontWeight="SemiBold">
|
||||||
|
<Run Text="Angemeldet als: "/><Run Text="{Binding AiUsername}"/>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Text="{Binding AiBalanceDisplay}" FontSize="13"/>
|
||||||
|
<Button Content="Abmelden" Command="{Binding AiLogoutCommand}" HorizontalAlignment="Left"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentPage>
|
||||||
</TabbedPage>
|
</TabbedPage>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public partial class SettingsView : UserControl
|
|||||||
vm.OnConfirmRestore = ShowRestoreConfirmDialog;
|
vm.OnConfirmRestore = ShowRestoreConfirmDialog;
|
||||||
vm.OnAppLockChanged = () => App.Services.GetRequiredService<AppLockViewModel>().ApplyConfig();
|
vm.OnAppLockChanged = () => App.Services.GetRequiredService<AppLockViewModel>().ApplyConfig();
|
||||||
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
|
vm.OnConfirmHardDelete = ShowHardDeleteConfirmDialog;
|
||||||
|
vm.OnConfirmPairingRestore = ShowPairingRestoreConfirmDialog;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +51,21 @@ public partial class SettingsView : UserControl
|
|||||||
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ShowPairingRestoreConfirmDialog()
|
||||||
|
{
|
||||||
|
var info = new ConfirmDialogInfo
|
||||||
|
{
|
||||||
|
Title = "Gerät koppeln?",
|
||||||
|
Message = "Die lokale Datenbank auf diesem Gerät wird vollständig durch den Stand des anderen " +
|
||||||
|
"Geräts ersetzt. Der bisherige Stand wird automatisch als Backup gesichert. " +
|
||||||
|
"Die App wird danach automatisch neu gestartet.",
|
||||||
|
ConfirmText = "Ersetzen",
|
||||||
|
};
|
||||||
|
var dialog = new ConfirmDialog { DataContext = info };
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
return owner is not null && await dialog.ShowDialog<bool>(owner);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnImportClick(object? sender, RoutedEventArgs e)
|
private async void OnImportClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var topLevel = TopLevel.GetTopLevel(this);
|
var topLevel = TopLevel.GetTopLevel(this);
|
||||||
|
|||||||
@@ -1317,6 +1317,17 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
|
|||||||
RestartApplication`, gleiches Muster wie bei DB-Passwort/AppLock-Änderungen) — `SyncEngine`/
|
RestartApplication`, gleiches Muster wie bei DB-Passwort/AppLock-Änderungen) — `SyncEngine`/
|
||||||
`SnapshotService` werden nur einmalig beim Start registriert, es gibt keinen
|
`SnapshotService` werden nur einmalig beim Start registriert, es gibt keinen
|
||||||
Live-Re-Registrierungspfad.
|
Live-Re-Registrierungspfad.
|
||||||
|
|
||||||
|
**Nachtrag (Einstellungen-Reorg):** Der Tab-Balken war mit 13 Tabs oben (`TabPlacement="Top"`)
|
||||||
|
unübersichtlich lang geworden und wird mit weiteren Sync-Features noch länger. Umgestellt auf
|
||||||
|
`TabPlacement="Left"` (vertikale Liste, skaliert besser) und die Tabs in drei logische Gruppen
|
||||||
|
sortiert (Fachliches / Zeitplanung / System). Neuer Enum `SettingsTab` in
|
||||||
|
`SettingsViewModel.cs` ersetzt die bisherigen rohen `int`-Tab-Indizes bei
|
||||||
|
`MainWindowViewModel.NavigateToSettings`/`TimetableViewModel.OnNavigateToSettings` — beim
|
||||||
|
Umsortieren dabei ein bereits vorhandener Bug aufgefallen und mitbehoben: das Zahnrad-Symbol
|
||||||
|
im Stundenplan öffnete über den hartcodierten Index `7` tatsächlich den Tab "Datenschutz"
|
||||||
|
statt des in Kommentar/Tooltip beabsichtigten "Ferien & Feiertage" — mit benanntem Enum kann
|
||||||
|
diese Klasse von Fehler nicht mehr auftreten.
|
||||||
- [x] **10.1.2** Verbindungstest mit klarer Fehlermeldung (nicht erreichbar / Token ungültig).
|
- [x] **10.1.2** Verbindungstest mit klarer Fehlermeldung (nicht erreichbar / Token ungültig).
|
||||||
|
|
||||||
**Umsetzung:** `SyncAuthService.TestConnectionAsync` unterscheidet drei Zustände (erreichbar
|
**Umsetzung:** `SyncAuthService.TestConnectionAsync` unterscheidet drei Zustände (erreichbar
|
||||||
@@ -1431,7 +1442,16 @@ die Docker-Verifikation unter 10.2.4 (kein Docker im Entwicklungsstand verfügba
|
|||||||
Pfad explizit außerhalb des von der Plattform verwalteten Checkouts sind sicher.
|
Pfad explizit außerhalb des von der Plattform verwalteten Checkouts sind sicher.
|
||||||
|
|
||||||
### 10.3 Verschlüsselung
|
### 10.3 Verschlüsselung
|
||||||
- [ ] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase).
|
- [x] **10.3.1** Schlüsselübertragung auf ein zweites Gerät (QR-Code oder Passphrase).
|
||||||
|
|
||||||
|
**Umsetzung:** Der Backend-Mechanismus (`SnapshotService`/`SyncCrypto`, Einmal-Code-Pairing)
|
||||||
|
existierte bereits, war aber an keiner Stelle im Client verdrahtet. Neuer Bereich "Gerät
|
||||||
|
koppeln" im Sync-Settings-Tab: ein Gerät erzeugt per `CreatePairingCode` einen verschlüsselten
|
||||||
|
DB-Snapshot + Code (Format `WORT-ZZ-WORT`, 24h gültig), das zweite Gerät gibt den Code über
|
||||||
|
`RedeemPairingCode` ein und übernimmt Datenbank + Sync-Schlüssel. Bestätigungsdialog vor dem
|
||||||
|
Einlösen (überschreibt die lokale Datenbank vollständig, alter Stand wird automatisch als
|
||||||
|
Backup gesichert). Kein QR-Code (nur Passphrase-Code) — für zwei eigene Geräte per Hand
|
||||||
|
abtippen ausreichend, QR-Code wäre erst für eine Companion-App relevant.
|
||||||
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
- [ ] **10.3.2** Warnung und Wiederherstellungspfad bei verlorenem Schlüssel.
|
||||||
- [x] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen —
|
- [x] **10.3.3** Prüfen, welche Daten unverschlüsselt über `PlainEventStore` laufen —
|
||||||
personenbezogene Daten dürfen das nicht. `Grade`/`ExamResult` (beide mit `StudentId` plus
|
personenbezogene Daten dürfen das nicht. `Grade`/`ExamResult` (beide mit `StudentId` plus
|
||||||
|
|||||||
Reference in New Issue
Block a user