feat: Anhänge je Stunde, Klausur-Sitzplan mischen, Gefährdungsbeurteilungs-Assistent

Lesson bekommt dieselbe Anhang-Infrastruktur wie Documentation (Material,
Arbeitsblätter, Experimentunterlagen), samt Fix einer Sync-Lücke, die Anhang-
Dateibytes bisher nur für Documentation statt generisch übertragen hat
(IHasAttachments). Sitzplan-Tab bekommt einen "Plätze mischen"-Button für
Klausursitzpläne. Neu: mehrschrittiger Gefährdungsbeurteilungs-Assistent mit
optionalem KI-Entwurf (ai-backend/gbu.php) und PDF-Export, Format bewusst als
JSON-Anhang statt eigener Datenbank-Entität. Details und Architekturentscheidungen
in TODO.md (4.2, 7.1.5, 10.1.8).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 17:13:14 +02:00
co-authored by Claude Sonnet 5
parent 331033db5c
commit 038337997f
29 changed files with 1886 additions and 22 deletions
@@ -398,6 +398,40 @@ public partial class SeatingPlanTabViewModel : ObservableObject
ReloadPlans();
}
/// <summary>
/// Klausur-Sitzplan mischen (14, Nutzer-Idee): erzeugt aus dem aktuell gewählten Plan einen
/// neuen, unabhängigen Plan mit gleichem Raster/Raum, aber zufällig vertauschten Insassen der
/// belegten Plätze (Fisher-Yates via <see cref="Random.Shared"/>) — die Platzkoordinaten selbst
/// bleiben unverändert, nur wer wo sitzt wird neu gewürfelt. Der ursprüngliche Plan bleibt
/// unangetastet erhalten, damit er bei Bedarf für den nächsten regulären Unterricht weiter
/// genutzt werden kann.
/// </summary>
[RelayCommand(CanExecute = nameof(CanEditSelected))]
private void ShuffleSeats()
{
if (_currentPlan is null) return;
var studentIds = _currentPlan.Assignments.Select(a => a.StudentId).ToArray();
Random.Shared.Shuffle(studentIds);
var shuffled = new SeatingPlan
{
GroupId = _currentPlan.GroupId,
Name = $"{_currentPlan.Name} (Klausur gemischt {DateTime.Now:dd.MM. HH:mm})",
Room = _currentPlan.Room,
Rows = _currentPlan.Rows,
Columns = _currentPlan.Columns,
ColumnGapWidths = [.. _currentPlan.ColumnGapWidths],
IsBoardAtBottom = _currentPlan.IsBoardAtBottom,
HiddenSeats = _currentPlan.HiddenSeats.Select(h => new HiddenSeat { Row = h.Row, Column = h.Column }).ToList(),
Assignments = _currentPlan.Assignments
.Select((a, i) => new SeatAssignment { Row = a.Row, Column = a.Column, StudentId = studentIds[i] })
.ToList(),
};
_plans.Save(shuffled);
ReloadPlans(shuffled.Id);
}
partial void OnIsEditModeChanged(bool value)
{
OnPropertyChanged(nameof(CanEditLayout));
@@ -417,6 +451,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
AddPlanCommand.NotifyCanExecuteChanged();
EditPlanCommand.NotifyCanExecuteChanged();
DeletePlanCommand.NotifyCanExecuteChanged();
ShuffleSeatsCommand.NotifyCanExecuteChanged();
}
}