diff --git a/LehrerApp.Core/Models/Workload.cs b/LehrerApp.Core/Models/Workload.cs
index 2a2d3f1..873f355 100644
--- a/LehrerApp.Core/Models/Workload.cs
+++ b/LehrerApp.Core/Models/Workload.cs
@@ -3,7 +3,14 @@ namespace LehrerApp.Core.Models;
public class Documentation : IHasAttachments
{
public Guid Id { get; set; } = Guid.NewGuid();
+ /// Hauptperson des Eintrags (bei Gruppeneinträgen ).
public Guid StudentId { get; set; }
+ ///
+ /// Weitere betroffene Schüler*innen, z.B. bei einem Vorgang mit mehreren Beteiligten: der Eintrag
+ /// existiert nur einmal, erscheint aber bei jeder dieser Personen (siehe ).
+ /// Enthält nie selbst.
+ ///
+ public List AdditionalStudentIds { get; set; } = [];
public Guid? GroupId { get; set; }
/// Unterrichtssitzung, in der die Beobachtung entstanden ist.
public Guid? ParticipationSessionId { get; set; }
@@ -33,6 +40,14 @@ public class Documentation : IHasAttachments
// Löschen entfernt den Eintrag nicht hart, sondern markiert ihn nur (Nachvollziehbarkeit, 5.1.4).
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
+
+ /// Ob der Eintrag diese Person betrifft — als Hauptperson oder als weitere Beteiligte.
+ public bool Concerns(Guid studentId) =>
+ studentId != Guid.Empty && (StudentId == studentId || AdditionalStudentIds.Contains(studentId));
+
+ /// Hauptperson (falls gesetzt) gefolgt von den weiteren Beteiligten.
+ public IEnumerable InvolvedStudentIds() =>
+ (StudentId == Guid.Empty ? [] : new[] { StudentId }).Concat(AdditionalStudentIds);
}
public class AbsenceData
{
diff --git a/LehrerApp.Data.Tests/RepositoryTests.cs b/LehrerApp.Data.Tests/RepositoryTests.cs
index c6cc065..c39904b 100644
--- a/LehrerApp.Data.Tests/RepositoryTests.cs
+++ b/LehrerApp.Data.Tests/RepositoryTests.cs
@@ -1338,4 +1338,39 @@ public sealed class RepositoryTests
Assert.Null(repo.GetById(extra.Id));
}
+
+ [Fact]
+ public void DocumentationRepository_GetByStudent_FindetGemeinsamenEintragBeiAllenBeteiligten()
+ {
+ using var db = NewInMemoryContext();
+ var repo = new DocumentationRepository(db);
+ Guid anna = Guid.NewGuid(), ben = Guid.NewGuid(), cem = Guid.NewGuid();
+ var shared = new Documentation
+ {
+ StudentId = anna, AdditionalStudentIds = [ben], Title = "Konflikt", Type = DocumentationType.Incident,
+ };
+ repo.Save(shared);
+
+ Assert.Equal(shared.Id, Assert.Single(repo.GetByStudent(anna)).Id);
+ Assert.Equal(shared.Id, Assert.Single(repo.GetByStudent(ben)).Id);
+ Assert.Empty(repo.GetByStudent(cem));
+ Assert.Single(repo.GetByStudentAndType(ben, DocumentationType.Incident));
+ Assert.Empty(repo.GetByStudentAndType(ben, DocumentationType.Conversation));
+ }
+
+ [Fact]
+ public void StudentRepository_GetReferenceSummary_ZaehltAuchGemeinsameDokumentation()
+ {
+ using var db = NewInMemoryContext();
+ var students = new StudentRepository(db);
+ var ben = new Student { FirstName = "Ben", LastName = "Muster" };
+ students.Save(ben);
+ new DocumentationRepository(db).Save(new Documentation
+ {
+ StudentId = Guid.NewGuid(), AdditionalStudentIds = [ben.Id], Title = "Konflikt",
+ });
+
+ Assert.Equal(1, students.GetReferenceSummary(ben.Id).DocumentationEntries);
+ Assert.Throws(() => students.Delete(ben.Id));
+ }
}
diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs
index e938299..fc644f4 100644
--- a/LehrerApp.Data/Repositories/AllRepositories.cs
+++ b/LehrerApp.Data/Repositories/AllRepositories.cs
@@ -34,7 +34,7 @@ public class StudentRepository(LiteDbContext db) : IStudentRepository
db.Grades.Count(g => g.StudentId == studentId),
db.ReportGrades.Count(g => g.StudentId == studentId),
db.ParticipationEntries.Count(e => e.StudentId == studentId),
- db.Documentation.Count(d => d.StudentId == studentId));
+ db.Documentation.FindAll().Count(d => d.Concerns(studentId)));
public void Save(Student s)
{
s.ExternalIds ??= [];
@@ -415,10 +415,13 @@ public class LessonRepository(LiteDbContext db) : ILessonRepository
public class DocumentationRepository(LiteDbContext db) : IDocumentationRepository
{
+ // Filter auf AdditionalStudentIds bewusst im Speicher statt als LiteDB-Ausdruck — die Menge
+ // an Dokumentation pro Lehrkraft ist klein, und so gilt exakt dieselbe Regel wie Documentation.Concerns.
public List GetByStudent(Guid id) =>
- db.Documentation.Find(d => d.StudentId == id && !d.IsDeleted).OrderByDescending(d => d.Date).ToList();
+ db.Documentation.Find(d => !d.IsDeleted).Where(d => d.Concerns(id))
+ .OrderByDescending(d => d.Date).ToList();
public List GetByStudentAndType(Guid sid, DocumentationType type) =>
- db.Documentation.Find(d => d.StudentId == sid && d.Type == type && !d.IsDeleted)
+ db.Documentation.Find(d => d.Type == type && !d.IsDeleted).Where(d => d.Concerns(sid))
.OrderByDescending(d => d.Date).ToList();
public List GetAll() =>
db.Documentation.Find(d => !d.IsDeleted).OrderByDescending(d => d.Date).ToList();
diff --git a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
index 8b1c354..e377194 100644
--- a/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
+++ b/LehrerApp.Desktop.Tests/ClassTeacherViewModelsTests.cs
@@ -1260,6 +1260,18 @@ public sealed class ClassTeacherViewModelsTests
Assert.Empty(result);
}
+ [Fact]
+ public void FilterAvailableDocumentation_BeruecksichtigtWeitereBeteiligte()
+ {
+ Guid ada = Guid.NewGuid(), ben = Guid.NewGuid();
+ var shared = new Documentation { StudentId = Guid.NewGuid(), AdditionalStudentIds = [ben] };
+ var vorgang = new Vorgang { StudentIds = [ada, ben] };
+
+ var result = ClassTeacherCasesViewModel.FilterAvailableDocumentation([shared], vorgang);
+
+ Assert.Equal(shared.Id, Assert.Single(result).Id);
+ }
+
// ── Konfigurierbare Gewichtung + Dämpfung/Rückfall-Warnung nach geschlossenem Vorgang ─────────
[Fact]
diff --git a/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs
index 7f5d394..b17678e 100644
--- a/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/DocumentationDialogViewModelTests.cs
@@ -308,4 +308,89 @@ public sealed class DocumentationDialogViewModelTests
Assert.Null(storage.OpenRead(storageId));
}
+
+ // ── Weitere Beteiligte ("Betrifft außerdem") ─────────────────────────────
+
+ [Fact]
+ public void WeitereBeteiligte_NeuerEintrag_UebernimmtAuswahlOhneHauptperson()
+ {
+ var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
+ var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
+ var cem = new StudentOption(Guid.NewGuid(), "Cem Test");
+ var vm = new DocumentationDialogViewModel(anna.Id, null, new FakeAttachmentStorage(),
+ coStudentOptions: [anna, ben, cem])
+ {
+ Title = "Konflikt", TypeName = "Vorkommnis",
+ };
+ Assert.True(vm.HasCoStudents);
+ Assert.True(vm.CoStudents.Single(c => c.Id == anna.Id).IsPrimary);
+ foreach (var c in vm.CoStudents) c.IsSelected = true;
+
+ vm.SaveCommand.Execute(null);
+
+ Assert.Equal(anna.Id, vm.Result!.StudentId);
+ Assert.Equal([ben.Id, cem.Id], vm.Result.AdditionalStudentIds);
+ }
+
+ [Fact]
+ public void WeitereBeteiligte_NurHauptpersonAlsKandidat_BlendetAuswahlAus()
+ {
+ var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
+ var vm = new DocumentationDialogViewModel(anna.Id, null, new FakeAttachmentStorage(), coStudentOptions: [anna]);
+
+ Assert.False(vm.HasCoStudents);
+ }
+
+ [Fact]
+ public void WeitereBeteiligte_WechselDerHauptperson_VerschiebtAusblendung()
+ {
+ var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
+ var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
+ var vm = new DocumentationDialogViewModel(anna.Id, null, new FakeAttachmentStorage(), [anna, ben],
+ coStudentOptions: [anna, ben])
+ {
+ Title = "Konflikt", TypeName = "Vorkommnis",
+ };
+ vm.CoStudents.Single(c => c.Id == anna.Id).IsSelected = true;
+
+ vm.SelectedStudent = ben;
+ vm.SaveCommand.Execute(null);
+
+ Assert.True(vm.CoStudents.Single(c => c.Id == ben.Id).IsPrimary);
+ Assert.Equal(ben.Id, vm.Result!.StudentId);
+ Assert.Equal([anna.Id], vm.Result.AdditionalStudentIds);
+ }
+
+ [Fact]
+ public void WeitereBeteiligte_BearbeitenOhneKandidaten_BehaeltBestehendeBei()
+ {
+ Guid anna = Guid.NewGuid(), ben = Guid.NewGuid();
+ var existing = new Documentation { StudentId = anna, AdditionalStudentIds = [ben], Title = "Konflikt" };
+ var vm = new DocumentationDialogViewModel(ben, existing, new FakeAttachmentStorage());
+
+ vm.SaveCommand.Execute(null);
+
+ Assert.Equal(anna, vm.Result!.StudentId);
+ Assert.Equal([ben], vm.Result.AdditionalStudentIds);
+ }
+
+ [Fact]
+ public void WeitereBeteiligte_Bearbeiten_AbwahlEntferntNurAngeboteneKandidaten()
+ {
+ var anna = new StudentOption(Guid.NewGuid(), "Anna Beispiel");
+ var ben = new StudentOption(Guid.NewGuid(), "Ben Muster");
+ var outsider = Guid.NewGuid();
+ var existing = new Documentation
+ {
+ StudentId = anna.Id, AdditionalStudentIds = [ben.Id, outsider], Title = "Konflikt",
+ };
+ var vm = new DocumentationDialogViewModel(anna.Id, existing, new FakeAttachmentStorage(),
+ coStudentOptions: [anna, ben]);
+ Assert.True(vm.CoStudents.Single(c => c.Id == ben.Id).IsSelected);
+
+ vm.CoStudents.Single(c => c.Id == ben.Id).IsSelected = false;
+ vm.SaveCommand.Execute(null);
+
+ Assert.Equal([outsider], vm.Result!.AdditionalStudentIds);
+ }
}
diff --git a/LehrerApp.Desktop.Tests/Fakes.cs b/LehrerApp.Desktop.Tests/Fakes.cs
index 978e397..555c365 100644
--- a/LehrerApp.Desktop.Tests/Fakes.cs
+++ b/LehrerApp.Desktop.Tests/Fakes.cs
@@ -320,9 +320,9 @@ public class FakeDocumentation : IDocumentationRepository
private readonly List _all = [];
public void Add(Documentation d) => _all.Add(d);
public List GetByStudent(Guid studentId) =>
- _all.Where(d => d.StudentId == studentId && !d.IsDeleted).ToList();
+ _all.Where(d => d.Concerns(studentId) && !d.IsDeleted).ToList();
public List GetByStudentAndType(Guid studentId, DocumentationType type) =>
- _all.Where(d => d.StudentId == studentId && d.Type == type && !d.IsDeleted).ToList();
+ _all.Where(d => d.Concerns(studentId) && d.Type == type && !d.IsDeleted).ToList();
public List GetAll() => _all.Where(d => !d.IsDeleted).ToList();
public void Save(Documentation doc) { _all.RemoveAll(d => d.Id == doc.Id); _all.Add(doc); }
public void Delete(Guid id) { var d = _all.FirstOrDefault(x => x.Id == id); if (d is not null) d.IsDeleted = true; }
diff --git a/LehrerApp.Desktop.Tests/StudentWarningEvidenceTests.cs b/LehrerApp.Desktop.Tests/StudentWarningEvidenceTests.cs
index 631dedd..e87c4db 100644
--- a/LehrerApp.Desktop.Tests/StudentWarningEvidenceTests.cs
+++ b/LehrerApp.Desktop.Tests/StudentWarningEvidenceTests.cs
@@ -136,6 +136,38 @@ public sealed class StudentWarningEvidenceTests
Assert.False(vm.IsPersisted(draft));
}
+ [Fact]
+ public async Task GemeinsamerVorgang_DokumentationFuerMehrereErscheintAuchBeimZweitenSchueler()
+ {
+ var ben = new Student { FirstName = "Ben", LastName = "Muster" };
+ var students = new FakeStudents([_student, ben]);
+ var vorgaenge = new FakeVorgangRepository();
+ var vorgang = new Vorgang { Title = "Streit", StudentIds = [_student.Id, ben.Id], Tags = ["Konflikte"] };
+ vorgaenge.Add(vorgang);
+ var documentation = new FakeDocumentation();
+ var vm = new StudentVorgaengeViewModel(vorgaenge, documentation, students);
+ vm.Load(_student.Id);
+ List offered = [];
+ vm.OnEditDocumentation = (studentId, coStudents, _) =>
+ {
+ offered = coStudents;
+ return Task.FromResult(new Documentation
+ {
+ StudentId = studentId, AdditionalStudentIds = [ben.Id], Title = "Gespräch mit beiden",
+ });
+ };
+
+ await vm.CreateAndLinkDocumentationCommand.ExecuteAsync(Assert.Single(vm.Cases));
+
+ Assert.Equal([_student.Id, ben.Id], offered.Select(o => o.Id));
+ var benView = new StudentVorgaengeViewModel(vorgaenge, documentation, students);
+ benView.Load(ben.Id);
+ var linked = Assert.Single(Assert.Single(benView.Cases).LinkedDocumentation);
+ Assert.Equal("Gespräch mit beiden", linked.Model.Title);
+ Assert.Contains("Muster", linked.StudentName);
+ Assert.Contains("Beispiel", linked.StudentName);
+ }
+
[Fact]
public async Task VorlageAusVorgangstab_OeffnetEntwurfUndVerknuepftNachSpeichern()
{
@@ -147,7 +179,7 @@ public sealed class StudentWarningEvidenceTests
var vm = new StudentVorgaengeViewModel(vorgaenge, documentation, new FakeStudents([_student]), BuildService());
vm.Load(_student.Id);
var wasDraft = false;
- vm.OnEditDocumentation = (_, editing) =>
+ vm.OnEditDocumentation = (_, _, editing) =>
{
wasDraft = vm.IsNewDocumentationDraft(editing!);
return Task.FromResult(editing);
diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherCasesViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherCasesViewModel.cs
index 052e38a..46ed01a 100644
--- a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherCasesViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherCasesViewModel.cs
@@ -42,7 +42,8 @@ public partial class ClassTeacherCasesViewModel : ObservableObject
public Func, Vorgang?, Task>? OnEditVorgang { get; set; }
public Func>? OnConfirmDeleteVorgang { get; set; }
- public Func, Documentation?, Task>? OnEditDocumentation { get; set; }
+ /// Parameter: Hauptperson, Bezug-Auswahl, Kandidaten für "Betrifft außerdem", bestehender Eintrag.
+ public Func, List, Documentation?, Task>? OnEditDocumentation { get; set; }
public ClassTeacherCasesViewModel(IVorgangRepository vorgaenge, IDocumentationRepository documentation,
IStudentRepository students, UntisReportCacheService cache)
@@ -110,9 +111,9 @@ public partial class ClassTeacherCasesViewModel : ObservableObject
var linkedIds = vorgang.DocumentationIds.ToHashSet();
foreach (var doc in allDocs.Where(d => linkedIds.Contains(d.Id)))
- item.LinkedDocumentation.Add(new DocumentationItem(doc, nameById.GetValueOrDefault(doc.StudentId, "")));
+ item.LinkedDocumentation.Add(new DocumentationItem(doc, InvolvedNames(doc, nameById)));
foreach (var doc in FilterAvailableDocumentation(allDocs, vorgang))
- item.AvailableDocumentation.Add(new DocumentationItem(doc, nameById.GetValueOrDefault(doc.StudentId, "")));
+ item.AvailableDocumentation.Add(new DocumentationItem(doc, InvolvedNames(doc, nameById)));
foreach (var entry in vorgang.ClassRegisterEntries.OrderByDescending(e => e.Date))
item.ClassRegisterRows.Add(new VorgangClassRegisterEntryRow(entry));
@@ -125,6 +126,25 @@ public partial class ClassTeacherCasesViewModel : ObservableObject
private void NotifyState() => OnPropertyChanged(nameof(HasCases));
+ private static string InvolvedNames(Documentation doc, Dictionary nameById) =>
+ string.Join(", ", doc.InvolvedStudentIds().Select(id => nameById.GetValueOrDefault(id, "")).Where(n => n != ""));
+
+ private List VorgangStudentOptions(Vorgang vorgang) =>
+ _rosterStudentOptions.Where(s => vorgang.StudentIds.Contains(s.Id)).ToList();
+
+ /// "Betrifft außerdem"-Kandidaten beim Bearbeiten eines Eintrags: die Beteiligten aller angezeigten
+ /// Vorgänge, in denen der Eintrag verknüpft oder verknüpfbar ist, plus die schon Beteiligten —
+ /// bewusst nicht der ganze Klassen-Roster, damit die Liste kurz bleibt.
+ private List CoStudentOptionsFor(Documentation doc)
+ {
+ var ids = Cases.Select(c => c.Model)
+ .Where(v => v.DocumentationIds.Contains(doc.Id) || v.StudentIds.Any(doc.Concerns))
+ .SelectMany(v => v.StudentIds)
+ .Concat(doc.InvolvedStudentIds())
+ .ToHashSet();
+ return _rosterStudentOptions.Where(s => ids.Contains(s.Id)).ToList();
+ }
+
/// Lädt Fehlzeiten und Klassenbucheinträge des laufenden Schuljahres (aus dem Untis-Cache, der
/// kalte Bereich ist dort dauerhaft gespeichert) und leitet daraus den Stand je Schüler*in ab.
/// Scheitert der Abruf (offline, Sitzung abgelaufen), gibt es eben nur die Standardformulierungen
@@ -188,7 +208,7 @@ public partial class ClassTeacherCasesViewModel : ObservableObject
_documentationDraftId = draft.Id;
Documentation? result;
- try { result = await OnEditDocumentation(option.Id, [option], draft); }
+ try { result = await OnEditDocumentation(option.Id, [option], VorgangStudentOptions(item.Model), draft); }
finally { _documentationDraftId = null; }
if (result is null) return;
_documentation.Save(result);
@@ -242,9 +262,9 @@ public partial class ClassTeacherCasesViewModel : ObservableObject
private async Task CreateAndLinkDocumentation(VorgangItem? item)
{
if (item is null || OnEditDocumentation is null) return;
- var studentOptions = _rosterStudentOptions.Where(s => item.Model.StudentIds.Contains(s.Id)).ToList();
+ var studentOptions = VorgangStudentOptions(item.Model);
var defaultStudentId = item.Model.StudentIds.FirstOrDefault();
- var result = await OnEditDocumentation(defaultStudentId, studentOptions, null);
+ var result = await OnEditDocumentation(defaultStudentId, studentOptions, studentOptions, null);
if (result is null) return;
_documentation.Save(result);
item.Model.DocumentationIds.Add(result.Id);
@@ -259,19 +279,20 @@ public partial class ClassTeacherCasesViewModel : ObservableObject
private async Task EditDocumentation(DocumentationItem doc)
{
if (OnEditDocumentation is null) return;
- var result = await OnEditDocumentation(doc.Model.StudentId, _rosterStudentOptions, doc.Model);
+ var result = await OnEditDocumentation(doc.Model.StudentId, _rosterStudentOptions,
+ CoStudentOptionsFor(doc.Model), doc.Model);
if (result is null) return;
_documentation.Save(result);
await LoadInternal();
}
- /// Dokumentation der Vorgangs-Schüler*innen, die noch nicht mit diesem Vorgang verknüpft ist —
- /// reine, ohne Repository-Zugriff testbare Filterlogik (gleiches Muster wie
- /// ).
+ /// Dokumentation der Vorgangs-Schüler*innen (als Haupt- oder weitere Beteiligte), die noch nicht
+ /// mit diesem Vorgang verknüpft ist — reine, ohne Repository-Zugriff testbare Filterlogik
+ /// (gleiches Muster wie ).
public static List FilterAvailableDocumentation(IReadOnlyList allDocs, Vorgang vorgang)
{
var linkedIds = vorgang.DocumentationIds.ToHashSet();
- return allDocs.Where(d => vorgang.StudentIds.Contains(d.StudentId) && !linkedIds.Contains(d.Id)).ToList();
+ return allDocs.Where(d => vorgang.StudentIds.Any(d.Concerns) && !linkedIds.Contains(d.Id)).ToList();
}
private async Task LinkDocumentation(VorgangItem item, DocumentationItem doc)
diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs
index ce57eeb..f36c5b3 100644
--- a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherDetailsViewModel.cs
@@ -464,7 +464,7 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
var matchedIds = rosterMatches.Select(m => m.StudentId).ToHashSet();
return all
.Where(d => !d.IsDeleted &&
- (matchedIds.Contains(d.StudentId) || IsUnassignedClassDocumentation(d)) &&
+ (d.InvolvedStudentIds().Any(matchedIds.Contains) || IsUnassignedClassDocumentation(d)) &&
d.Date >= start && d.Date <= end)
.Where(d => MatchesOwnDocStudentFilter(d, rosterMatches, studentFilter))
.OrderByDescending(d => d.IsDraft).ThenByDescending(d => d.Date)
@@ -477,8 +477,10 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
if (string.IsNullOrWhiteSpace(studentFilter)) return true;
if (IsUnassignedClassDocumentation(d))
return d.Participants.Any(p => NameContainsSearchTerm(p, studentFilter));
- var name = rosterMatches.FirstOrDefault(m => m.StudentId == d.StudentId).DisplayName;
- return name is not null && NameContainsSearchTerm(name, studentFilter);
+ // Gemeinsame Einträge (weitere Beteiligte) passen, sobald eine der beteiligten Personen passt.
+ return d.InvolvedStudentIds()
+ .Select(id => rosterMatches.FirstOrDefault(m => m.StudentId == id).DisplayName)
+ .Any(name => name is not null && NameContainsSearchTerm(name, studentFilter));
}
private static bool IsUnassignedClassDocumentation(Documentation d) =>
@@ -495,8 +497,13 @@ public partial class ClassTeacherDetailsViewModel : ObservableObject
IReadOnlyList<(Guid StudentId, string DisplayName)> rosterMatches)
{
if (d.StudentId != Guid.Empty)
- return rosterMatches.FirstOrDefault(m => m.StudentId == d.StudentId).DisplayName
- ?? "Unbekannter Schülerbezug";
+ {
+ var names = d.InvolvedStudentIds()
+ .Select(id => rosterMatches.FirstOrDefault(m => m.StudentId == id).DisplayName)
+ .Where(n => n is not null)
+ .ToList();
+ return names.Count > 0 ? string.Join(", ", names) : "Unbekannter Schülerbezug";
+ }
return d.Participants.Count > 0
? $"Ohne festen Bezug · {string.Join(", ", d.Participants)}"
: "Ohne Schülerbezug";
diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs
index 175f2d3..cb164b2 100644
--- a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs
@@ -1179,7 +1179,8 @@ public partial class ClassTeacherOverviewViewModel : ObservableObject
.Where(s => s is not null)
.Select(s => s!.Id)
.ToHashSet();
- var docs = _documentation.GetAll().Where(d => !d.IsDeleted && matchedIds.Contains(d.StudentId)).ToList();
+ var docs = _documentation.GetAll()
+ .Where(d => !d.IsDeleted && d.InvolvedStudentIds().Any(matchedIds.Contains)).ToList();
OwnDocumentationFollowUpCount = docs.Count(d => d.IsDraft);
OwnDocumentationCriticalCount = docs.Count(d =>
d.Tags.Any(t => string.Equals(t, "Kritisch", StringComparison.OrdinalIgnoreCase)));
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs
index a955461..d5880e2 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs
@@ -296,6 +296,7 @@ public partial class GroupOverviewViewModel : ObservableObject
{
DraftDocumentationCount = _students.GetByGroup(_groupId)
.SelectMany(s => _documentation.GetByStudent(s.Id))
+ .DistinctBy(d => d.Id)
.Count(d => d.IsDraft && (d.GroupId is null || d.GroupId == _groupId));
}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
index db33e10..983d2fd 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
@@ -313,7 +313,8 @@ public partial class SeatingPlanTabViewModel : ObservableObject
}
entries.TryGetValue(studentId, out var entry);
var tags = _documentation?.GetByStudent(studentId)
- .FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == SelectedSession.Id)?.Tags ?? [];
+ .FirstOrDefault(d => d.IsDraft && d.StudentId == studentId && d.ParticipationSessionId == SelectedSession.Id)
+ ?.Tags ?? [];
seat.SetLessonData(entry, tags);
}
}
@@ -340,8 +341,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
seat.SelectedOption.StudentId is not Guid studentId) return;
var session = EnsureTodaySession();
if (session is null) return;
+ // Nur Entwürfe mit dieser Person als Hauptperson — ein gemeinsamer Eintrag (weitere Beteiligte)
+ // ist keine persönliche Sitzplan-Notiz.
var draft = _documentation.GetByStudent(studentId)
- .FirstOrDefault(d => d.IsDraft && d.ParticipationSessionId == session.Id);
+ .FirstOrDefault(d => d.IsDraft && d.StudentId == studentId && d.ParticipationSessionId == session.Id);
if (draft is null)
{
draft = new Documentation
diff --git a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Privacy.cs b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Privacy.cs
index 682de6a..7447703 100644
--- a/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Privacy.cs
+++ b/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.Privacy.cs
@@ -36,8 +36,8 @@ public partial class SettingsViewModel
var cutoff = _privacy.RetentionCutoff();
foreach (var d in _documentation.GetAll().Where(d => d.CreatedAt < cutoff))
{
- var student = _students.GetById(d.StudentId);
- ExpiredDocuments.Add(new ExpiredDocumentItem(d, student?.FullName ?? "?"));
+ var names = d.InvolvedStudentIds().Select(id => _students.GetById(id)?.FullName ?? "?").ToList();
+ ExpiredDocuments.Add(new ExpiredDocumentItem(d, names.Count > 0 ? string.Join("; ", names) : "?"));
}
}
diff --git a/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs b/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
index 9757cc9..ae4c03b 100644
--- a/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Students/DocumentationViewModels.cs
@@ -104,6 +104,12 @@ public partial class DocumentationDialogViewModel : ObservableObject
[ObservableProperty] private StudentOption? _selectedStudent;
[ObservableProperty] private string _studentError = "";
+ // Weitere Beteiligte (Documentation.AdditionalStudentIds) — nur angeboten, wenn der Aufrufer
+ // Kandidaten mitgibt (z.B. die übrigen Schüler*innen eines Vorgangs). Die Hauptperson steht
+ // mit in der Liste, wird aber ausgeblendet (CoStudentChoice.IsPrimary).
+ public List CoStudents { get; }
+ public bool HasCoStudents => CoStudents.Any(c => !c.IsPrimary);
+
[ObservableProperty] private string _typeName = DocumentationTypeDisplay.Options[0];
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _title = "";
@@ -163,8 +169,11 @@ public partial class DocumentationDialogViewModel : ObservableObject
public Documentation? Result { get; private set; }
+ /// Kandidaten für "Betrifft außerdem" — ohne Angabe bleiben
+ /// bestehende weitere Beteiligte eines bearbeiteten Eintrags unverändert erhalten.
public DocumentationDialogViewModel(Guid studentId, Documentation? editing, IAttachmentStorage attachmentStorage,
- List? studentOptions = null, Guid? contextGroupId = null, bool isNewDraft = false)
+ List? studentOptions = null, Guid? contextGroupId = null, bool isNewDraft = false,
+ List? coStudentOptions = null)
{
_studentId = studentId;
_editing = editing;
@@ -174,8 +183,17 @@ public partial class DocumentationDialogViewModel : ObservableObject
_attachmentStorage = attachmentStorage;
_contextGroupId = contextGroupId;
StudentOptions = studentOptions ?? [];
+ CoStudents = (coStudentOptions ?? [])
+ .Where(o => o.Id != Guid.Empty)
+ .DistinctBy(o => o.Id)
+ .Select(o => new CoStudentChoice(o.Id, o.Name)
+ {
+ IsSelected = editing?.AdditionalStudentIds.Contains(o.Id) == true,
+ })
+ .ToList();
if (CanPickStudent)
SelectedStudent = StudentOptions.FirstOrDefault(s => s.Id == studentId);
+ UpdatePrimaryCoStudent();
if (editing is null) return;
TypeName = DocumentationTypeDisplay.Label(editing.Type);
@@ -212,6 +230,22 @@ public partial class DocumentationDialogViewModel : ObservableObject
foreach (var tag in editing.Tags) Tags.Add(tag);
}
+ // Beim Bearbeiten bleibt die Hauptperson die des bestehenden Eintrags (Save() übernimmt die
+ // Auswahl dann nicht), sonst folgt sie der Bezug-Auswahl.
+ private Guid PrimaryStudentId =>
+ _editing is not null && !_isNewDraft ? _editing.StudentId
+ : CanPickStudent ? SelectedStudent?.Id ?? Guid.Empty
+ : _editing?.StudentId ?? _studentId;
+
+ partial void OnSelectedStudentChanged(StudentOption? value) => UpdatePrimaryCoStudent();
+
+ private void UpdatePrimaryCoStudent()
+ {
+ var primary = PrimaryStudentId;
+ foreach (var c in CoStudents) c.IsPrimary = c.Id == primary;
+ OnPropertyChanged(nameof(HasCoStudents));
+ }
+
partial void OnTypeNameChanged(string value)
{
OnPropertyChanged(nameof(IsConversation));
@@ -348,6 +382,14 @@ public partial class DocumentationDialogViewModel : ObservableObject
var type = DocumentationTypeDisplay.FromLabel(TypeName);
var effectiveStudentId = CanPickStudent ? SelectedStudent!.Id : _studentId;
Result = _editing ?? new Documentation { StudentId = effectiveStudentId, GroupId = _contextGroupId };
+ // Nicht angebotene weitere Beteiligte bleiben erhalten (z.B. beim Bearbeiten aus dem Schülerdatensatz,
+ // wo keine Kandidaten bekannt sind), angebotene folgen der Auswahl.
+ var offered = CoStudents.Select(c => c.Id).ToHashSet();
+ Result.AdditionalStudentIds = Result.AdditionalStudentIds.Where(id => !offered.Contains(id))
+ .Concat(CoStudents.Where(c => c.IsSelected).Select(c => c.Id))
+ .Where(id => id != Result.StudentId && id != Guid.Empty)
+ .Distinct()
+ .ToList();
Result.Type = type;
Result.Date = date;
Result.Title = Title.Trim();
@@ -413,6 +455,14 @@ public partial class DocumentationDialogViewModel : ObservableObject
}
}
+public partial class CoStudentChoice(Guid id, string name) : ObservableObject
+{
+ public Guid Id { get; } = id;
+ public string Name { get; } = name;
+ [ObservableProperty] private bool _isSelected;
+ [ObservableProperty] private bool _isPrimary;
+}
+
public class AttachmentItem(string storageId, string fileName, long sizeBytes, DateTime uploadedAt)
{
public const string HazardAssessmentSuffix = ".gbu.json";
@@ -458,6 +508,10 @@ public partial class DocumentationItem : ObservableObject
public string OtherGroupLabel { get; }
/// Dimmt Einträge aus anderen Lerngruppen im Gruppen-Tab, ohne sie zu verstecken.
public double ContentOpacity => IsOwnGroup ? 1.0 : 0.55;
+ /// Nur im Schülerdatensatz gefüllt: die übrigen Beteiligten eines gemeinsamen Eintrags
+ /// (Documentation.AdditionalStudentIds bzw. Hauptperson), damit er dort als geteilt erkennbar ist.
+ public string SharedWith { get; init; } = "";
+ public bool IsShared => SharedWith != "";
[ObservableProperty] private bool _isRevealed;
diff --git a/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs b/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs
index 9bada05..315d6d4 100644
--- a/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Students/StudentViewModels.cs
@@ -334,7 +334,12 @@ public partial class StudentDetailViewModel : ObservableObject
if (Student is null) return;
Documentation.Clear();
foreach (var d in _docs.GetByStudent(Student.Id))
- Documentation.Add(new DocumentationItem(d));
+ {
+ var others = d.InvolvedStudentIds().Where(id => id != Student.Id)
+ .Select(id => _students.GetById(id)?.FullName)
+ .Where(n => !string.IsNullOrEmpty(n));
+ Documentation.Add(new DocumentationItem(d) { SharedWith = string.Join("; ", others) });
+ }
}
[RelayCommand]
diff --git a/LehrerApp.Desktop/ViewModels/Students/StudentVorgaengeViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/StudentVorgaengeViewModel.cs
index 4739e6d..3175c9b 100644
--- a/LehrerApp.Desktop/ViewModels/Students/StudentVorgaengeViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Students/StudentVorgaengeViewModel.cs
@@ -39,7 +39,8 @@ public partial class StudentVorgaengeViewModel : ObservableObject
public Func, Vorgang?, Task>? OnEditVorgang { get; set; }
public Func>? OnConfirmDeleteVorgang { get; set; }
- public Func>? OnEditDocumentation { get; set; }
+ /// Parameter: Hauptperson, Kandidaten für "Betrifft außerdem", bestehender Eintrag.
+ public Func, Documentation?, Task>? OnEditDocumentation { get; set; }
/// Rückfrage vor dem automatischen Anlegen eines Vorgangs aus einer Dashboard-Warnung heraus
/// (siehe ) — Parameter ist das gesuchte Tag.
public Func>? OnConfirmCreateForWarning { get; set; }
@@ -89,8 +90,8 @@ public partial class StudentVorgaengeViewModel : ObservableObject
IEnumerable vorgaenge = _vorgaenge.GetByStudent(studentId);
if (OnlyOpen) vorgaenge = vorgaenge.Where(v => v.Status == VorgangStatus.Open);
- // Nur die Dokumentation dieses einen Schülers (nicht GetAll()) — dieses Panel ist die Sicht
- // "von diesem Schüler aus", nicht Vorgangsverwaltung für mehrere Beteiligte.
+ // Nur die Dokumentation, die diesen Schüler betrifft (nicht GetAll()) — dieses Panel ist die
+ // Sicht "von diesem Schüler aus"; gemeinsame Einträge mit weiteren Beteiligten zählen dazu.
var ownDocs = _documentation.GetByStudent(studentId);
foreach (var vorgang in vorgaenge.OrderByDescending(v => v.UpdatedAt))
@@ -107,9 +108,9 @@ public partial class StudentVorgaengeViewModel : ObservableObject
var linkedIds = vorgang.DocumentationIds.ToHashSet();
foreach (var doc in ownDocs.Where(d => linkedIds.Contains(d.Id)))
- item.LinkedDocumentation.Add(new DocumentationItem(doc, _studentName));
+ item.LinkedDocumentation.Add(new DocumentationItem(doc, InvolvedNames(doc)));
foreach (var doc in ClassTeacherCasesViewModel.FilterAvailableDocumentation(ownDocs, vorgang))
- item.AvailableDocumentation.Add(new DocumentationItem(doc, _studentName));
+ item.AvailableDocumentation.Add(new DocumentationItem(doc, InvolvedNames(doc)));
foreach (var entry in vorgang.ClassRegisterEntries.OrderByDescending(e => e.Date))
item.ClassRegisterRows.Add(new VorgangClassRegisterEntryRow(entry));
@@ -118,6 +119,27 @@ public partial class StudentVorgaengeViewModel : ObservableObject
OnPropertyChanged(nameof(HasCases));
}
+ private string StudentName(Guid id) => id == _studentId ? _studentName : _students.GetById(id)?.FullName ?? "";
+
+ private string InvolvedNames(Documentation doc) =>
+ string.Join(", ", doc.InvolvedStudentIds().Select(StudentName).Where(n => n != ""));
+
+ private List VorgangStudentOptions(Vorgang vorgang) => vorgang.StudentIds
+ .Select(id => new StudentOption(id, StudentName(id)))
+ .Where(o => o.Name != "")
+ .ToList();
+
+ /// "Betrifft außerdem"-Kandidaten beim Bearbeiten: Beteiligte der angezeigten Vorgänge, in denen der
+ /// Eintrag verknüpft oder verknüpfbar ist, plus die schon Beteiligten.
+ private List CoStudentOptionsFor(Documentation doc) => Cases.Select(c => c.Model)
+ .Where(v => v.DocumentationIds.Contains(doc.Id) || v.StudentIds.Any(doc.Concerns))
+ .SelectMany(v => v.StudentIds)
+ .Concat(doc.InvolvedStudentIds())
+ .Distinct()
+ .Select(id => new StudentOption(id, StudentName(id)))
+ .Where(o => o.Name != "")
+ .ToList();
+
[RelayCommand]
private async Task AddVorgang()
{
@@ -161,7 +183,7 @@ public partial class StudentVorgaengeViewModel : ObservableObject
private async Task CreateAndLinkDocumentation(VorgangItem? item)
{
if (item is null || OnEditDocumentation is null) return;
- var result = await OnEditDocumentation(_studentId, null);
+ var result = await OnEditDocumentation(_studentId, VorgangStudentOptions(item.Model), null);
if (result is null) return;
_documentation.Save(result);
item.Model.DocumentationIds.Add(result.Id);
@@ -179,7 +201,7 @@ public partial class StudentVorgaengeViewModel : ObservableObject
if (draft is null) return;
_documentationDraftId = draft.Id;
Documentation? result;
- try { result = await OnEditDocumentation(_studentId, draft); }
+ try { result = await OnEditDocumentation(_studentId, VorgangStudentOptions(item.Model), draft); }
finally { _documentationDraftId = null; }
if (result is null) return;
_documentation.Save(result);
@@ -193,7 +215,7 @@ public partial class StudentVorgaengeViewModel : ObservableObject
private async Task EditDocumentation(DocumentationItem doc)
{
if (OnEditDocumentation is null) return;
- var result = await OnEditDocumentation(doc.Model.StudentId, doc.Model);
+ var result = await OnEditDocumentation(doc.Model.StudentId, CoStudentOptionsFor(doc.Model), doc.Model);
if (result is null) return;
_documentation.Save(result);
Load(_studentId);
diff --git a/LehrerApp.Desktop/ViewModels/Students/WebUntisDocumentationComparisonViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/WebUntisDocumentationComparisonViewModel.cs
index 898eaff..6ea0d33 100644
--- a/LehrerApp.Desktop/ViewModels/Students/WebUntisDocumentationComparisonViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Students/WebUntisDocumentationComparisonViewModel.cs
@@ -106,7 +106,7 @@ public partial class WebUntisDocumentationComparisonViewModel : ObservableObject
// Nur nach Schüler*in+Datum erkannt, nicht nach Wortlaut: ein lokaler Eintrag von vor dieser
// Funktion (oder frei formuliert) hat selten denselben Titel wie die WebUntis-Kategorie.
List LocalDocsFor(Guid studentId, DateOnly date) =>
- localDocs.Where(d => d.StudentId == studentId && d.Date == date).ToList();
+ localDocs.Where(d => d.Concerns(studentId) && d.Date == date).ToList();
foreach (var (entry, date) in ordered)
{
@@ -128,15 +128,20 @@ public partial class WebUntisDocumentationComparisonViewModel : ObservableObject
});
}
+ // WebUntis-Klassenbucheinträge sind personenbezogen — ein gemeinsamer lokaler Eintrag
+ // (weitere Beteiligte) wird deshalb je beteiligter Person einzeln abgeglichen.
foreach (var doc in localDocs)
{
if (doc.ExcludeFromWebUntisSync) continue;
- var coveredByReport = Rows.Any(r => r.AssignedStudent?.Id == doc.StudentId && r.Date == doc.Date);
- if (coveredByReport) continue;
- var student = ownStudents.FirstOrDefault(s => s.Id == doc.StudentId);
- if (student is null) continue;
- LocalOnlyRows.Add(new LocalOnlyDocumentationRow(student.FullName, doc.Date,
- doc.GroupId is { } groupId ? _groups.GetById(groupId)?.Name : null, doc.Title, doc.Content));
+ foreach (var studentId in doc.InvolvedStudentIds())
+ {
+ var coveredByReport = Rows.Any(r => r.AssignedStudent?.Id == studentId && r.Date == doc.Date);
+ if (coveredByReport) continue;
+ var student = ownStudents.FirstOrDefault(s => s.Id == studentId);
+ if (student is null) continue;
+ LocalOnlyRows.Add(new LocalOnlyDocumentationRow(student.FullName, doc.Date,
+ doc.GroupId is { } groupId ? _groups.GetById(groupId)?.Name : null, doc.Title, doc.Content));
+ }
}
var unresolved = Rows.Count(x => x.AssignedStudent is null);
diff --git a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherCasesView.axaml.cs b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherCasesView.axaml.cs
index fd71dff..17db53b 100644
--- a/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherCasesView.axaml.cs
+++ b/LehrerApp.Desktop/Views/ClassTeacher/ClassTeacherCasesView.axaml.cs
@@ -51,7 +51,8 @@ public partial class ClassTeacherCasesView : UserControl
}
private async Task ShowDocumentationDialog(
- Guid defaultStudentId, List studentOptions, Documentation? editing)
+ Guid defaultStudentId, List studentOptions, List coStudentOptions,
+ Documentation? editing)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
@@ -59,7 +60,8 @@ public partial class ClassTeacherCasesView : UserControl
var isNewDraft = editing is not null
&& (DataContext as ClassTeacherCasesViewModel)?.IsNewDocumentationDraft(editing) == true;
var vm = new DocumentationDialogViewModel(defaultStudentId, editing,
- App.Services.GetRequiredService(), studentOptions, isNewDraft: isNewDraft);
+ App.Services.GetRequiredService(), studentOptions, isNewDraft: isNewDraft,
+ coStudentOptions: coStudentOptions);
var dialog = new DocumentationDialog { DataContext = vm };
var saved = await dialog.ShowDialog(owner);
if (!saved) vm.DiscardUnsavedAttachments();
diff --git a/LehrerApp.Desktop/Views/Students/DocumentationDialog.axaml b/LehrerApp.Desktop/Views/Students/DocumentationDialog.axaml
index 260e8d5..3eed87d 100644
--- a/LehrerApp.Desktop/Views/Students/DocumentationDialog.axaml
+++ b/LehrerApp.Desktop/Views/Students/DocumentationDialog.axaml
@@ -21,6 +21,20 @@
IsVisible="{Binding StudentError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml b/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml
index aa9a4a6..bc90c72 100644
--- a/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml
+++ b/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml
@@ -299,6 +299,8 @@
+
diff --git a/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml.cs b/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml.cs
index e9b3031..bf6583d 100644
--- a/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Students/StudentDetailView.axaml.cs
@@ -105,7 +105,8 @@ public partial class StudentDetailView : UserControl
var info = new ConfirmDialogInfo
{
Title = "Eintrag löschen?",
- Message = $"\"{item.Model.Title}\" wird als gelöscht markiert und nicht mehr angezeigt.",
+ Message = $"\"{item.Model.Title}\" wird als gelöscht markiert und nicht mehr angezeigt." +
+ (item.IsShared ? $" Der Eintrag verschwindet auch bei {item.SharedWith}." : ""),
ConfirmText = "Löschen",
};
var dialog = new ConfirmDialog { DataContext = info };
diff --git a/LehrerApp.Desktop/Views/Students/StudentVorgaengeTabView.axaml.cs b/LehrerApp.Desktop/Views/Students/StudentVorgaengeTabView.axaml.cs
index e14f488..2d94bdf 100644
--- a/LehrerApp.Desktop/Views/Students/StudentVorgaengeTabView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Students/StudentVorgaengeTabView.axaml.cs
@@ -52,7 +52,8 @@ public partial class StudentVorgaengeTabView : UserControl
return owner is not null && await dialog.ShowDialog(owner);
}
- private async Task ShowDocumentationDialog(Guid studentId, Documentation? editing)
+ private async Task ShowDocumentationDialog(
+ Guid studentId, List coStudentOptions, Documentation? editing)
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return null;
@@ -60,7 +61,8 @@ public partial class StudentVorgaengeTabView : UserControl
var isNewDraft = editing is not null
&& (DataContext as StudentVorgaengeViewModel)?.IsNewDocumentationDraft(editing) == true;
var vm = new DocumentationDialogViewModel(studentId, editing,
- App.Services.GetRequiredService(), isNewDraft: isNewDraft);
+ App.Services.GetRequiredService(), isNewDraft: isNewDraft,
+ coStudentOptions: coStudentOptions);
var dialog = new DocumentationDialog { DataContext = vm };
var saved = await dialog.ShowDialog(owner);
if (!saved) vm.DiscardUnsavedAttachments();
diff --git a/TODO.md b/TODO.md
index 82b1892..a892f02 100644
--- a/TODO.md
+++ b/TODO.md
@@ -2050,6 +2050,31 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
`ClassTeacherStudentFacts.TemplateContext`). Vorlagen im Klassenlehrer-Tab beziehen sich auf
die/den erste/n Beteiligte/n des Vorgangs, der Dokumentations-Dialog bietet dann nur diese Person
an (sonst passten die eingesetzten Zahlen nicht zur gewählten Person).
+- [x] **Vorgang mit mehreren Beteiligten — gemeinsame Dokumentation (September 2026):** Nutzer-
+ Feedback: ein Vorgang kann mehrere Schüler*innen umfassen, ein `Documentation`-Eintrag gehörte aber
+ immer genau einer Person — wer einen gemeinsamen Eintrag (z.B. Gespräch mit beiden
+ Streitparteien) bei jeder Person im Dokumentations-Tab sehen wollte, musste ihn mehrfach mit
+ identischem Inhalt anlegen. **Nutzer-Entscheidung: ein Eintrag, mehrere Personen** (statt
+ automatischer unabhängiger Kopien). `Documentation` bekommt `AdditionalStudentIds` (weitere
+ Beteiligte; `StudentId` bleibt Hauptperson, damit bestehende Daten, Sync-Events und Gruppeneinträge
+ mit `Guid.Empty` unverändert gelten) sowie `Concerns(id)`/`InvolvedStudentIds()` als einzige Regel
+ für "betrifft diese Person". `DocumentationRepository.GetByStudent`/`GetByStudentAndType` filtern
+ danach (im Speicher statt als LiteDB-Ausdruck), dadurch erscheint der Eintrag automatisch im
+ Dokumentations-Tab jeder beteiligten Person, im schülerbezogenen Vorgänge-Tab und im
+ Personendaten-Export. `GetReferenceSummary` zählt gemeinsame Einträge mit, damit eine beteiligte
+ Person nicht hart gelöscht werden kann (sonst bliebe eine verwaiste ID zurück).
+ Der `DocumentationDialog` hat einen neuen Bereich **"Betrifft außerdem"** (Checkboxen, optionaler
+ Parameter `coStudentOptions`, Hauptperson ausgeblendet). Angeboten wird er nur aus den beiden
+ Vorgänge-Tabs heraus, mit den Beteiligten des Vorgangs (beim Bearbeiten: Beteiligte aller
+ angezeigten Vorgänge, in denen der Eintrag verknüpft oder verknüpfbar ist). Beim Bearbeiten ohne
+ Kandidatenliste (z.B. aus dem Schülerdatensatz) bleiben bestehende weitere Beteiligte erhalten,
+ abgewählt werden nur angebotene. Im Schülerdatensatz zeigt ein gemeinsamer Eintrag
+ "👥 gemeinsam mit …", und die Lösch-Rückfrage weist darauf hin, dass er auch bei den anderen
+ verschwindet. Mitgezogen: Klassenlehrer-Dokumentationsliste und -Zähler, "verknüpfbare"
+ Dokumentation im Vorgang, WebUntis-Abgleich (gemeinsamer Eintrag wird je beteiligter Person
+ abgeglichen, da WebUntis-Klassenbucheinträge personenbezogen sind), Löschfristen-Liste (alle Namen).
+ **Bewusst unverändert:** Sitzplan-Entwürfe bleiben persönliche Notizen (nur Hauptperson), der
+ Gruppen-Dokumentations-Tab bietet (noch) keine weiteren Beteiligten an.
**Nachtrag zu 4.3, Bugfix Wochennavigation (September 2026):** Nutzer-Feedback: beim Planen der
nächsten Woche musste nach jedem kurzen Abstecher in einen anderen Bereich der Sidebar wieder