diff --git a/LehrerApp.Api.Tests/AttachmentStoreTests.cs b/LehrerApp.Api.Tests/AttachmentStoreTests.cs
index 85e4b90..cd0d4c1 100644
--- a/LehrerApp.Api.Tests/AttachmentStoreTests.cs
+++ b/LehrerApp.Api.Tests/AttachmentStoreTests.cs
@@ -38,7 +38,8 @@ public sealed class AttachmentStoreTests
await store.StoreAsync("user-1", "shared-id", new MemoryStream([1]));
Assert.Null(store.OpenRead("user-2", "shared-id"));
- Assert.NotNull(store.OpenRead("user-1", "shared-id"));
+ using var user1Attachment = store.OpenRead("user-1", "shared-id");
+ Assert.NotNull(user1Attachment);
}
[Fact]
diff --git a/LehrerApp.Core/Models/Workload.cs b/LehrerApp.Core/Models/Workload.cs
index 430065c..2ef47b0 100644
--- a/LehrerApp.Core/Models/Workload.cs
+++ b/LehrerApp.Core/Models/Workload.cs
@@ -5,6 +5,10 @@ public class Documentation
public Guid Id { get; set; } = Guid.NewGuid();
public Guid StudentId { get; set; }
public Guid? GroupId { get; set; }
+ /// Unterrichtssitzung, in der die Beobachtung entstanden ist.
+ public Guid? ParticipationSessionId { get; set; }
+ /// Optionaler Bezug auf die konkrete geplante Stunde.
+ public Guid? LessonId { get; set; }
public DocumentationType Type { get; set; }
public DateOnly Date { get; set; }
public string Title { get; set; } = "";
@@ -17,6 +21,8 @@ public class Documentation
public List Attachments { get; set; } = [];
/// Freie Labels zur Nachverfolgung, z.B. "Kritisch", "Nacharbeiten" — siehe `DocumentationTagDisplay`.
public List Tags { get; set; } = [];
+ /// Im Unterricht schnell erfasst und noch inhaltlich nachzuarbeiten.
+ public bool IsDraft { get; set; }
public bool IsConfidential { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
diff --git a/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs
index c04ac1d..676ae0e 100644
--- a/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs
@@ -137,6 +137,24 @@ public sealed class DocumentationDialogViewModelTests
Assert.Equal("Gut verlaufen", vm.Result.ParentCallData.Impressions);
}
+ [Fact]
+ public void Save_SitzplanEntwurf_WirdAbgeschlossenUndBehaeltStundenbezug()
+ {
+ var sessionId = Guid.NewGuid();
+ var existing = new Documentation
+ {
+ StudentId = Guid.NewGuid(), Type = DocumentationType.Incident,
+ Date = DateOnly.FromDateTime(DateTime.Today), Title = "Streit/Konflikt",
+ IsDraft = true, ParticipationSessionId = sessionId, Tags = ["Streit/Konflikt"],
+ };
+ var vm = new DocumentationDialogViewModel(existing.StudentId, existing, new FakeAttachmentStorage());
+
+ vm.SaveCommand.Execute(null);
+
+ Assert.False(vm.Result!.IsDraft);
+ Assert.Equal(sessionId, vm.Result.ParticipationSessionId);
+ }
+
[Fact]
public void Save_Elternbrief_UebernimmtVersandUndRueckmeldung()
{
diff --git a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
index 106a5fc..9146bca 100644
--- a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
@@ -146,6 +146,66 @@ public sealed class SeatingPlanViewModelTests
Assert.All(vm.Seats, seat => Assert.True(seat.CanEdit));
}
+ [Fact]
+ public void Ansichtsmodus_ZeigtAnwesenheitUndHausaufgabeUndBlendetAbwesendeAb()
+ {
+ var groupId = Guid.NewGuid();
+ var student = new Student { FirstName = "Anna", LastName = "A" };
+ var session = new ParticipationSession { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today) };
+ var entries = new FakeEntries();
+ entries.Add(new ParticipationEntry
+ {
+ SessionId = session.Id, StudentId = student.Id,
+ Attendance = AttendanceStatus.Excused, Homework = HomeworkStatus.MissingOpen,
+ });
+ var plan = new SeatingPlan
+ {
+ GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
+ Assignments = [new SeatAssignment { StudentId = student.Id }],
+ };
+ var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
+ new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
+ new FakeSessions([session]), entries, new FakeAspects());
+
+ vm.Initialize(groupId, isReadOnly: false);
+
+ var seat = Assert.Single(vm.Seats);
+ Assert.True(seat.HasAttendanceBadge);
+ Assert.True(seat.HasHomeworkBadge);
+ Assert.Equal(0.42, seat.LessonOpacity);
+ }
+
+ [Fact]
+ public void SituationsTags_WerdenProSchuelerUndSitzungInEinemEntwurfZusammengefuehrt()
+ {
+ var groupId = Guid.NewGuid();
+ var student = new Student { FirstName = "Anna", LastName = "A" };
+ var session = new ParticipationSession
+ {
+ GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), LessonId = Guid.NewGuid(),
+ };
+ var docs = new FakeDocumentation();
+ var plan = new SeatingPlan
+ {
+ GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
+ Assignments = [new SeatAssignment { StudentId = student.Id }],
+ };
+ var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
+ new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
+ new FakeSessions([session]), new FakeEntries(), new FakeAspects(), docs);
+ vm.Initialize(groupId, isReadOnly: false);
+ var seat = Assert.Single(vm.Seats);
+
+ seat.SituationTags.Single(t => t.Text == "Mitarbeit verweigert").ToggleCommand.Execute(null);
+ seat.SituationTags.Single(t => t.Text == "Material vergessen").ToggleCommand.Execute(null);
+
+ var draft = Assert.Single(docs.GetByStudent(student.Id));
+ Assert.True(draft.IsDraft);
+ Assert.Equal(session.Id, draft.ParticipationSessionId);
+ Assert.Equal(session.LessonId, draft.LessonId);
+ Assert.Equal(["Mitarbeit verweigert", "Material vergessen"], draft.Tags);
+ }
+
[Fact]
public void Tischabstaende_WerdenBeimBearbeitenProSpaltengrenzeGespeichert()
{
diff --git a/LehrerApp.Desktop/App.axaml.cs b/LehrerApp.Desktop/App.axaml.cs
index 41c380a..9ba92b6 100644
--- a/LehrerApp.Desktop/App.axaml.cs
+++ b/LehrerApp.Desktop/App.axaml.cs
@@ -9,6 +9,7 @@ using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.Views;
+using LehrerApp.Sync;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop;
@@ -66,6 +67,8 @@ public class App : Application
var mainVm = Services.GetRequiredService();
WireCallbacks(mainVm);
var main = new MainWindow { DataContext = mainVm };
+ if (Services.GetService() is { } syncEngine)
+ main.EnableFinalSync(syncEngine);
desktop.MainWindow = main;
if (showImmediately) main.Show();
}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupDocumentationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupDocumentationViewModels.cs
index 6b33731..9dac71e 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupDocumentationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupDocumentationViewModels.cs
@@ -31,6 +31,7 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
[ObservableProperty] private StudentOption _selectedStudentFilter = AllStudentsOption;
[ObservableProperty] private bool _onlyThisGroup;
+ [ObservableProperty] private int _draftCount;
public Func, Documentation?, Task>? OnEditDocumentation { get; set; }
public Func>? OnConfirmDeleteDocumentation { get; set; }
@@ -80,7 +81,10 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
var all = relevantStudentIds
.SelectMany(id => _docs.GetByStudent(id))
.Where(d => !OnlyThisGroup || d.GroupId == _groupId)
- .OrderByDescending(d => d.Date);
+ .OrderByDescending(d => d.IsDraft)
+ .ThenByDescending(d => d.Date)
+ .ToList();
+ DraftCount = all.Count(d => d.IsDraft && (d.GroupId is null || d.GroupId == _groupId));
foreach (var d in all)
{
@@ -91,6 +95,8 @@ public partial class GroupDocumentationTabViewModel : ObservableObject
}
}
+ public void Refresh() => Load();
+
[RelayCommand]
private async Task AddDocumentation()
{
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
index aee4a25..7c0fa49 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
@@ -250,6 +250,7 @@ public partial class GroupDetailViewModel : ObservableObject
ParticipationTab.LoadSessions();
ParticipationTab.RefreshCurrentGrid();
};
+ SeatingPlanTab.OnDocumentationChanged = GroupDocumentationTab.Refresh;
}
public void LoadGroup(Guid id)
diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
index f6c5a75..48099c5 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
@@ -15,6 +15,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
private readonly IParticipationSessionRepository _sessions;
private readonly IParticipationRepository _participation;
private readonly IParticipationAspectRepository _aspects;
+ private readonly IDocumentationRepository? _documentation;
private Guid _groupId;
private SeatingPlan? _currentPlan;
private bool _isReadOnly;
@@ -28,11 +29,13 @@ public partial class SeatingPlanTabViewModel : ObservableObject
[ObservableProperty] private bool _isBoardAtBottom;
[ObservableProperty] private IReadOnlyList _columnGapWidths = [];
[ObservableProperty] private bool _isEditMode;
+ [ObservableProperty] private ParticipationSessionOption? _selectedSession;
public ObservableCollection Plans { get; } = [];
public ObservableCollection Seats { get; } = [];
public ObservableCollection StudentOptions { get; } = [];
public ObservableCollection UnassignedStudents { get; } = [];
+ public ObservableCollection TodaySessions { get; } = [];
public bool HasPlans => Plans.Count > 0;
public bool HasSelectedPlan => _currentPlan is not null;
@@ -42,10 +45,12 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public Func>? OnConfirmDelete { get; set; }
public Func? OnAssessStudent { get; set; }
public Action? OnAssessmentChanged { get; set; }
+ public Action? OnDocumentationChanged { get; set; }
public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students,
IGroupMembershipRepository memberships, IParticipationSessionRepository sessions,
- IParticipationRepository participation, IParticipationAspectRepository aspects)
+ IParticipationRepository participation, IParticipationAspectRepository aspects,
+ IDocumentationRepository? documentation = null)
{
_plans = plans;
_students = students;
@@ -53,6 +58,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
_sessions = sessions;
_participation = participation;
_aspects = aspects;
+ _documentation = documentation;
}
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
@@ -63,12 +69,31 @@ public partial class SeatingPlanTabViewModel : ObservableObject
_groupId = groupId;
_isReadOnly = isReadOnly;
IsEditMode = false;
+ LoadTodaySessions();
LoadStudentOptions();
ReloadPlans();
OnPropertyChanged(nameof(IsEditable));
NotifyCommands();
}
+ private void LoadTodaySessions()
+ {
+ TodaySessions.Clear();
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var sessions = _sessions.GetByGroup(_groupId).Where(s => s.Date == today)
+ .OrderBy(s => s.CreatedAt).ToList();
+ if (sessions.Count == 0 && !_isReadOnly)
+ {
+ var created = new ParticipationSession { GroupId = _groupId, Date = today, Comment = "Sitzplan" };
+ _sessions.Save(created);
+ sessions.Add(created);
+ }
+ foreach (var session in sessions) TodaySessions.Add(new ParticipationSessionOption(session));
+ SelectedSession = TodaySessions.LastOrDefault();
+ }
+
+ partial void OnSelectedSessionChanged(ParticipationSessionOption? value) => RefreshSeatLessonData();
+
private void LoadStudentOptions()
{
StudentOptions.Clear();
@@ -133,14 +158,78 @@ public partial class SeatingPlanTabViewModel : ObservableObject
? StudentSeatOption.Empty
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
?? StudentSeatOption.Empty;
- Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, CanEditLayout));
+ Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
+ CanEditLayout, ToggleSituationTag, IsEditable));
}
UpdateAssignmentSummary();
+ RefreshSeatLessonData();
}
OnPropertyChanged(nameof(HasSelectedPlan));
NotifyCommands();
}
+ private void RefreshSeatLessonData()
+ {
+ if (SelectedSession is null)
+ {
+ foreach (var seat in Seats) seat.SetLessonData(null, []);
+ return;
+ }
+ var entries = _participation.GetBySession(SelectedSession.Id)
+ .ToDictionary(e => e.StudentId);
+ foreach (var seat in Seats)
+ {
+ if (seat.SelectedOption.StudentId is not Guid studentId)
+ {
+ seat.SetLessonData(null, []);
+ continue;
+ }
+ entries.TryGetValue(studentId, out var entry);
+ var tags = _documentation?.GetByStudent(studentId)
+ .FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id)?.Tags ?? [];
+ seat.SetLessonData(entry, tags);
+ }
+ }
+
+ private void ToggleSituationTag(SeatCellViewModel seat, string tag)
+ {
+ if (!IsEditable || _documentation is null || SelectedSession is null ||
+ seat.SelectedOption.StudentId is not Guid studentId) return;
+ var draft = _documentation.GetByStudent(studentId)
+ .FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id);
+ if (draft is null)
+ {
+ draft = new Documentation
+ {
+ StudentId = studentId, GroupId = _groupId,
+ ParticipationSessionId = SelectedSession.Id,
+ LessonId = SelectedSession.LessonId,
+ Type = DocumentationType.Incident,
+ Date = SelectedSession.Date,
+ Title = tag,
+ IsDraft = true,
+ Tags = [tag],
+ };
+ }
+ else if (draft.Tags.Contains(tag))
+ {
+ draft.Tags.Remove(tag);
+ if (draft.Tags.Count == 0)
+ {
+ _documentation.Delete(draft.Id);
+ RefreshSeatLessonData();
+ OnDocumentationChanged?.Invoke();
+ return;
+ }
+ draft.Title = draft.Tags[0];
+ }
+ else draft.Tags.Add(tag);
+ draft.UpdatedAt = DateTime.UtcNow;
+ _documentation.Save(draft);
+ RefreshSeatLessonData();
+ OnDocumentationChanged?.Invoke();
+ }
+
private void OnSeatChanged(SeatCellViewModel changed)
{
if (_currentPlan is null || !CanEditLayout) return;
@@ -209,8 +298,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
{
if (!seat.SelectedOption.StudentId.HasValue || OnAssessStudent is null) return;
var assessment = new SeatAssessmentViewModel(_sessions, _participation, _aspects,
- _groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable);
+ _groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable,
+ SelectedSession?.Id);
await OnAssessStudent(assessment);
+ RefreshSeatLessonData();
OnAssessmentChanged?.Invoke();
}
@@ -241,7 +332,11 @@ public partial class SeatingPlanTabViewModel : ObservableObject
partial void OnIsEditModeChanged(bool value)
{
OnPropertyChanged(nameof(CanEditLayout));
- foreach (var seat in Seats) seat.CanEdit = CanEditLayout;
+ foreach (var seat in Seats)
+ {
+ seat.CanEdit = CanEditLayout;
+ seat.CanRecordLesson = IsEditable && !value;
+ }
NotifyCommands();
}
@@ -275,10 +370,21 @@ public sealed record StudentSeatOption(Guid? StudentId, string DisplayName)
public static StudentSeatOption Empty { get; } = new(null, "— frei —");
}
+public sealed class ParticipationSessionOption(ParticipationSession session)
+{
+ public Guid Id => session.Id;
+ public Guid? LessonId => session.LessonId;
+ public DateOnly Date => session.Date;
+ public string DisplayName => string.IsNullOrWhiteSpace(session.Comment)
+ ? $"{session.Date:dd.MM.yyyy}"
+ : $"{session.Date:dd.MM.yyyy} · {session.Comment}";
+}
+
public partial class SeatCellViewModel : ObservableObject
{
private readonly Action _onChanged;
private bool _suppressChange;
+ private readonly Action _toggleSituationTag;
[ObservableProperty] private StudentSeatOption _selectedOption;
[ObservableProperty] private bool _isDropTarget;
@@ -287,11 +393,20 @@ public partial class SeatCellViewModel : ObservableObject
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
public ObservableCollection Options { get; }
[ObservableProperty] private bool _canEdit;
+ [ObservableProperty] private double _lessonOpacity = 1.0;
+ [ObservableProperty] private string _attendanceBadge = "";
+ [ObservableProperty] private string _homeworkBadge = "";
+ public ObservableCollection SituationTags { get; } = [];
+ public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
+ public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
+ public bool ShowLessonOverview => IsOccupied && !CanEdit;
+ [ObservableProperty] private bool _canRecordLesson;
public bool IsOccupied => SelectedOption.StudentId.HasValue;
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
public SeatCellViewModel(int row, int column, ObservableCollection options,
- StudentSeatOption selectedOption, Action onChanged, bool canEdit)
+ StudentSeatOption selectedOption, Action onChanged, bool canEdit,
+ Action? toggleSituationTag = null, bool canRecordLesson = false)
{
Row = row;
Column = column;
@@ -299,21 +414,54 @@ public partial class SeatCellViewModel : ObservableObject
_selectedOption = selectedOption;
_onChanged = onChanged;
_canEdit = canEdit;
+ _toggleSituationTag = toggleSituationTag ?? ((_, _) => { });
+ _canRecordLesson = canRecordLesson;
+ foreach (var tag in SituationTagChoice.DefaultTags)
+ SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
}
partial void OnSelectedOptionChanged(StudentSeatOption value)
{
OnPropertyChanged(nameof(IsOccupied));
OnPropertyChanged(nameof(StudentName));
+ OnPropertyChanged(nameof(ShowLessonOverview));
if (!_suppressChange) _onChanged(this);
}
+ partial void OnCanEditChanged(bool value) => OnPropertyChanged(nameof(ShowLessonOverview));
+
public void SetSelectionSilently(StudentSeatOption option)
{
_suppressChange = true;
SelectedOption = option;
_suppressChange = false;
}
+
+ public void SetLessonData(ParticipationEntry? entry, IEnumerable tags)
+ {
+ var attendance = entry?.Attendance;
+ AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
+ HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
+ LessonOpacity = attendance is not null and not AttendanceStatus.Present
+ and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
+ var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var choice in SituationTags) choice.IsSelected = selected.Contains(choice.Text);
+ OnPropertyChanged(nameof(HasAttendanceBadge));
+ OnPropertyChanged(nameof(HasHomeworkBadge));
+ }
+}
+
+public partial class SituationTagChoice(string text, bool isSelected, Action toggle) : ObservableObject
+{
+ public static readonly string[] DefaultTags =
+ [
+ "Mitarbeit verweigert", "Unterricht gestört", "Streit/Konflikt",
+ "Langer Toilettengang", "Material vergessen", "Handynutzung",
+ "Besonders hilfsbereit", "Sehr gute Mitarbeit", "Gespräch erforderlich",
+ ];
+ public string Text { get; } = text;
+ [ObservableProperty] private bool _isSelected = isSelected;
+ [RelayCommand] private void Toggle() => toggle(Text);
}
public partial class SeatAssessmentViewModel : ObservableObject
@@ -338,13 +486,14 @@ public partial class SeatAssessmentViewModel : ObservableObject
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
IParticipationRepository entries, IParticipationAspectRepository aspects,
- Guid groupId, Guid studentId, string studentName, bool canEdit)
+ Guid groupId, Guid studentId, string studentName, bool canEdit, Guid? sessionId = null)
{
_entries = entries;
_canEdit = canEdit;
StudentName = studentName;
var today = DateOnly.FromDateTime(DateTime.Today);
- var session = sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
+ var session = sessionId.HasValue ? sessions.GetById(sessionId.Value) : null;
+ session ??= sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today);
if (session is null && canEdit)
{
session = new ParticipationSession
diff --git a/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs b/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
index 32cd0b1..e1b15ae 100644
--- a/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
@@ -339,6 +339,9 @@ public partial class DocumentationDialogViewModel : ObservableObject
Result.Date = date;
Result.Title = Title.Trim();
Result.Content = (Content ?? "").Trim();
+ // Das bewusste Speichern im vollständigen Dialog schließt einen im Sitzplan erzeugten
+ // Schnellentwurf ab. Stunden- und Lesson-Bezug bleiben am bestehenden Objekt erhalten.
+ Result.IsDraft = false;
Result.IsConfidential = IsConfidential;
Result.Participants = type == DocumentationType.Conversation ? Participants.ToList() : [];
Result.AbsenceData = type == DocumentationType.Absence
@@ -421,6 +424,7 @@ public partial class DocumentationItem : ObservableObject
public bool IsConfidential { get; }
public bool IsParentCall { get; }
public bool HasAttachments { get; }
+ public bool IsDraft { get; }
public string StatusLabel { get; }
public List TagChips { get; }
/// Nur im Gruppen-Tab (5.1, GroupDocumentationTabViewModel) gefüllt — die Schüler-Detailansicht
@@ -446,6 +450,7 @@ public partial class DocumentationItem : ObservableObject
IsRevealed = !d.IsConfidential;
IsParentCall = d.Type == DocumentationType.ParentCall;
HasAttachments = d.Attachments.Count > 0;
+ IsDraft = d.IsDraft;
StatusLabel = BuildStatusLabel(d);
TagChips = d.Tags.Select(t => new TagChip(t)).ToList();
StudentName = studentName;
@@ -455,6 +460,7 @@ public partial class DocumentationItem : ObservableObject
private static string BuildStatusLabel(Documentation d) => d.Type switch
{
+ _ when d.IsDraft => "Nacharbeiten",
DocumentationType.ParentCall when d.ParentCallData is { IsConducted: true } pc =>
$"Durchgeführt am {pc.ConductedDate:dd.MM.yyyy}",
DocumentationType.ParentCall => "Noch nicht durchgeführt",
diff --git a/LehrerApp.Desktop/Views/Groups/GroupDocumentationTabView.axaml b/LehrerApp.Desktop/Views/Groups/GroupDocumentationTabView.axaml
index 2f37531..7c06e9b 100644
--- a/LehrerApp.Desktop/Views/Groups/GroupDocumentationTabView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/GroupDocumentationTabView.axaml
@@ -7,7 +7,7 @@
-
+
-
+
+
+
+
@@ -32,6 +37,10 @@
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml b/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml
index 4bc2bf5..f927101 100644
--- a/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml
@@ -66,6 +66,12 @@
+
+
+
+
+ Tapped="OnSeatTapped" Opacity="{Binding LessonOpacity}">
@@ -120,6 +126,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml.cs b/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml.cs
index f2a40a5..289dcf1 100644
--- a/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml.cs
@@ -1,7 +1,9 @@
using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
+using Avalonia.VisualTree;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Shared;
@@ -195,6 +197,9 @@ public partial class SeatingPlanTabView : UserControl
private async void OnSeatTapped(object? sender, TappedEventArgs e)
{
+ if (e.Source is Control source &&
+ (source is Button or Expander || source.FindAncestorOfType