Baustein 9: Konflikt-Review-UI (Kapitel 10)
Minimale Liste im Tab "Synchronisation" - Entitaet, Zeitpunkt, welche Seite ConflictResolver gewaehlt hat, mit "Gesehen"-Aktion. Kein Feld-Diff fuer v1: die Payloads sind clientseitig verschluesselt, ein Diff wuerde ohnehin nur rohes JSON zeigen. Neu EventQueue.MarkReviewed(id) - bisher gab es AddConflict/ GetUnreviewed/ConflictCount, aber keinen Weg, einen Konflikt als gesehen zu markieren. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ using LehrerApp.Core.Services;
|
||||
using LehrerApp.Data;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using LehrerApp.Desktop.ViewModels.Planning;
|
||||
using LehrerApp.Sync;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
@@ -173,6 +174,8 @@ public partial class SettingsViewModel : ObservableObject
|
||||
[ObservableProperty] private bool _syncIsLoggedIn;
|
||||
[ObservableProperty] private string _syncConnectionStatus = "";
|
||||
|
||||
public ObservableCollection<SyncConflictListItem> SyncConflicts { get; } = [];
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
private readonly ISchoolHolidayRepository _schoolHolidays;
|
||||
@@ -183,6 +186,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
private readonly AiPlanningService _aiPlanning;
|
||||
private readonly SyncSettingsService _syncSettings;
|
||||
private readonly SyncAuthService _syncAuth;
|
||||
private readonly EventQueue _eventQueue;
|
||||
private readonly CompetencyCatalogImportService _catalogImport;
|
||||
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||
@@ -194,7 +198,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
|
||||
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
|
||||
AiSettingsService aiSettings, AiPlanningService aiPlanning,
|
||||
SyncSettingsService syncSettings, SyncAuthService syncAuth)
|
||||
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
@@ -218,6 +222,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
_aiPlanning = aiPlanning;
|
||||
_syncSettings = syncSettings;
|
||||
_syncAuth = syncAuth;
|
||||
_eventQueue = eventQueue;
|
||||
_catalogImport = new CompetencyCatalogImportService(domainRepo);
|
||||
LoadSubjects();
|
||||
LoadShorthandCodes();
|
||||
@@ -236,6 +241,7 @@ public partial class SettingsViewModel : ObservableObject
|
||||
LoadLetterTemplates();
|
||||
LoadAiSettings();
|
||||
LoadSyncSettings();
|
||||
LoadSyncConflicts();
|
||||
}
|
||||
|
||||
// ── Word-Briefvorlagen: Import und Validierung ──────────────────────────
|
||||
@@ -436,6 +442,27 @@ public partial class SettingsViewModel : ObservableObject
|
||||
AppBootstrapper.RestartApplication();
|
||||
}
|
||||
|
||||
// ── Synchronisation: Konflikte ────────────────────────────────────────────
|
||||
//
|
||||
// Zeigt, was ConflictResolver bereits entschieden hat (welche Seite gewonnen hat) — kein
|
||||
// Feld-Diff für v1, die Payloads sind clientseitig verschlüsselt und würden hier ohnehin nur
|
||||
// rohes JSON zeigen. Minimal: Entität, Zeitpunkt, Ergebnis, "gesehen"-Aktion.
|
||||
|
||||
private void LoadSyncConflicts()
|
||||
{
|
||||
SyncConflicts.Clear();
|
||||
foreach (var c in _eventQueue.GetUnreviewed().OrderByDescending(c => c.DetectedAt))
|
||||
SyncConflicts.Add(new SyncConflictListItem(c));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void MarkConflictReviewed(SyncConflictListItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
_eventQueue.MarkReviewed(item.Id);
|
||||
SyncConflicts.Remove(item);
|
||||
}
|
||||
|
||||
// ── Stundenraster: Laden / Speichern ─────────────────────────────────────
|
||||
|
||||
private void LoadPeriodTimes()
|
||||
@@ -1295,6 +1322,19 @@ public class ExpiredDocumentItem(Documentation d, string studentName)
|
||||
public string CreatedAtDisplay { get; } = d.CreatedAt.ToLocalTime().ToString("dd.MM.yyyy");
|
||||
}
|
||||
|
||||
public class SyncConflictListItem(ConflictEntry c)
|
||||
{
|
||||
public Guid Id { get; } = c.Id;
|
||||
public string EntityDisplay { get; } = $"{c.RemoteEvent.EntityType} ({c.RemoteEvent.EntityId[..Math.Min(8, c.RemoteEvent.EntityId.Length)]}…)";
|
||||
public string DetectedAtDisplay { get; } = c.DetectedAt.ToLocalTime().ToString("dd.MM.yyyy HH:mm");
|
||||
public string ResolutionDisplay { get; } = c.Resolution switch
|
||||
{
|
||||
"LocalWon" => "Lokale Änderung übernommen (dieses Gerät)",
|
||||
"RemoteWon" => "Änderung vom anderen Gerät übernommen",
|
||||
_ => c.Resolution,
|
||||
};
|
||||
}
|
||||
|
||||
public class SchoolHolidayItem(SchoolHoliday h)
|
||||
{
|
||||
public Guid Id { get; } = h.Id;
|
||||
|
||||
@@ -806,6 +806,32 @@
|
||||
<TextBlock FontSize="11" Opacity="0.5" TextWrapping="Wrap"
|
||||
Text="Speichern/Anmelden startet die App neu, damit die Änderung wirksam wird."/>
|
||||
|
||||
<TextBlock Text="Konflikte" FontSize="14" FontWeight="SemiBold" Margin="0,10,0,0"
|
||||
IsVisible="{Binding SyncConflicts.Count}"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
IsVisible="{Binding SyncConflicts.Count}"
|
||||
Text="Ein anderes Gerät hat dieselbe Änderung gleichzeitig gemacht — hier steht, welche Seite jeweils übernommen wurde."/>
|
||||
<ItemsControl ItemsSource="{Binding SyncConflicts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:SyncConflictListItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="0,0,0,1" Padding="0,7">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="2">
|
||||
<TextBlock Text="{Binding EntityDisplay}" FontSize="13"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6">
|
||||
<Run Text="{Binding DetectedAtDisplay}"/><Run Text=" · "/><Run Text="{Binding ResolutionDisplay}"/>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Gesehen" FontSize="12" Padding="10,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:SettingsViewModel)DataContext).MarkConflictReviewedCommand}"
|
||||
CommandParameter="{Binding}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
Reference in New Issue
Block a user