Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a30aab0fa5 | ||
|
|
07788dcfcb |
@@ -543,6 +543,50 @@ public sealed class SeatingPlanViewModelTests
|
||||
Assert.Equal(session.Id, vm.SelectedSession?.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AssessStudent_SitzungBereitsUeberAndereInstanzVerknuepft_LegtKeineZweiteAn()
|
||||
{
|
||||
// Regressionstest: Der Unterrichtsmodus läuft auf einer eigenen, per DI frisch aufgelösten
|
||||
// SeatingPlanTabViewModel-Instanz (TimetableView.ShowTeachingMode), getrennt von der
|
||||
// Instanz im Sitzplan-Tab der Kursübersicht — beide teilen sich nur das Repository. Wurde
|
||||
// (wie hier simuliert) die Sitzung bereits über den Unterrichtsmodus mit der Stunde
|
||||
// verknüpft, bevor im echten Sitzplan-Tab bewertet wird, darf letzterer keine zweite,
|
||||
// unverknüpfte "Sitzplan"-Sitzung für denselben Tag anlegen.
|
||||
var groupId = Guid.NewGuid();
|
||||
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||
var plan = new SeatingPlan
|
||||
{
|
||||
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
|
||||
Assignments = [new SeatAssignment { StudentId = student.Id }],
|
||||
};
|
||||
var sessions = new FakeSessions([]);
|
||||
var memberships = new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]);
|
||||
|
||||
// Instanz A: Sitzplan-Tab der Kursübersicht, bereits initialisiert (kein SelectedSession).
|
||||
var tabVm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
|
||||
memberships, sessions, new FakeEntries(), new FakeAspects());
|
||||
tabVm.Initialize(groupId, isReadOnly: false);
|
||||
tabVm.OnAssessStudent = _ => Task.CompletedTask;
|
||||
|
||||
// Instanz B: eigene, frische Instanz wie im Unterrichtsmodus — verknüpft die Sitzung mit
|
||||
// der Stunde, ohne dass Instanz A davon erfährt.
|
||||
var lesson = new Lesson { GroupId = groupId, Date = DateOnly.FromDateTime(DateTime.Today), Topic = "Redox" };
|
||||
var teachingModeVm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
|
||||
memberships, sessions, new FakeEntries(), new FakeAspects());
|
||||
teachingModeVm.Initialize(groupId, isReadOnly: false);
|
||||
teachingModeVm.SelectOrCreateSessionForLesson(lesson);
|
||||
|
||||
var linked = Assert.Single(sessions.GetByGroup(groupId));
|
||||
Assert.Equal(lesson.Id, linked.LessonId);
|
||||
|
||||
await tabVm.AssessStudent(tabVm.Seats.Single());
|
||||
|
||||
var session = Assert.Single(sessions.GetByGroup(groupId));
|
||||
Assert.Equal(linked.Id, session.Id);
|
||||
Assert.Equal(lesson.Id, session.LessonId);
|
||||
Assert.Equal(session.Id, tabVm.SelectedSession?.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShuffleSeats_ErzeugtNeuenPlanMitVertauschtenPlaetzenUndBehaeltDenUrsprungsplan()
|
||||
{
|
||||
|
||||
@@ -93,14 +93,37 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
||||
/// Legt bei Bedarf die "Sitzung für heute" an, mit der Sitzplan-Bewertungen/-Markierungen
|
||||
/// verknüpft werden — aber erst, wenn tatsächlich etwas bewertet/markiert wird (AssessStudent/
|
||||
/// ToggleSituationTag), nicht schon beim Öffnen des Tabs.
|
||||
///
|
||||
/// Prüft dabei bewusst gegen das Repository und nicht nur gegen das lokal geladene
|
||||
/// <see cref="TodaySessions"/> / <see cref="SelectedSession"/>: Der Unterrichtsmodus
|
||||
/// (<see cref="SelectOrCreateSessionForLesson"/>) läuft auf einer eigenen, per DI frisch
|
||||
/// aufgelösten <see cref="SeatingPlanTabViewModel"/>-Instanz (siehe TimetableView.ShowTeachingMode),
|
||||
/// getrennt von der Instanz, die im Sitzplan-Tab der Kursübersicht hängt. Eine dort bereits
|
||||
/// heute angelegte (ggf. verknüpfte) Sitzung ist dieser Instanz also unbekannt, solange sie nur
|
||||
/// im lokalen Feld nachschaut — das führte dazu, dass eine zweite, unverknüpfte "Sitzplan"-
|
||||
/// Sitzung für denselben Tag entstand, sobald im echten Sitzplan-Tab bewertet wurde.
|
||||
private ParticipationSessionOption? EnsureTodaySession()
|
||||
{
|
||||
if (SelectedSession is not null) return SelectedSession;
|
||||
if (!IsEditable) return null;
|
||||
|
||||
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||
var existing = _sessions.GetByGroup(_groupId).FirstOrDefault(s => s.Date == today);
|
||||
if (existing is not null)
|
||||
{
|
||||
var found = TodaySessions.FirstOrDefault(o => o.Id == existing.Id);
|
||||
if (found is null)
|
||||
{
|
||||
found = new ParticipationSessionOption(existing);
|
||||
TodaySessions.Add(found);
|
||||
}
|
||||
SelectedSession = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
var created = new ParticipationSession
|
||||
{
|
||||
GroupId = _groupId, Date = DateOnly.FromDateTime(DateTime.Today), Comment = "Sitzplan",
|
||||
GroupId = _groupId, Date = today, Comment = "Sitzplan",
|
||||
};
|
||||
_sessions.Save(created);
|
||||
var option = new ParticipationSessionOption(created);
|
||||
|
||||
@@ -1766,6 +1766,42 @@ CSV-Exports der Auswertung (6.3.3) über die gemeinsame Export-Infrastruktur aus
|
||||
nur tatsächlich erfasste `TimeEntry`-Zeilen auswertet, keine `WorkTask`-Metadaten. In der
|
||||
Aufgabenliste und im Dashboard-Widget "Offene Aufgaben" durch ein 🔔-Symbol gekennzeichnet.
|
||||
|
||||
- [ ] **6.1.7** TickTick-Sync für Erinnerungen (Nutzerwunsch): Der Nutzer pflegt Erinnerungen
|
||||
privat bereits in TickTick (Handy-App, eingespielte Routine dort). Ziel: ausgewählte
|
||||
`WorkTask`s (zunächst `TaskKind.Reminder`, 6.1.6) bidirektional mit einer TickTick-Liste
|
||||
abgleichen, damit Fälligkeiten auf dem Handy ankommen und dort abgehakte Erinnerungen auch
|
||||
in LehrerApp als erledigt gelten. Rechercheergebnis zur TickTick Open API
|
||||
(developer.ticktick.com/docs#/openapi, Stand 2026-08-24) unten festgehalten, damit das nicht
|
||||
erneut nachgeschlagen werden muss.
|
||||
- **Auth:** Kein OAuth2-Redirect-Flow nötig — für den Eigengebrauch (ein Nutzer, ein Gerät)
|
||||
reicht ein persönliches API-Token (TickTick-Web: Avatar → Settings → Account → API Token),
|
||||
gesendet als `Authorization: Bearer <token>`. Ein neues Einstellungsfeld "TickTick API
|
||||
Token" genügt, analog zur bereits pro Gerät hinterlegten WebUntis-URL — kein lokaler
|
||||
Redirect-Listener, keine Client-ID/Secret-Registrierung.
|
||||
- **Push (LehrerApp → TickTick):** `POST /open/v1/task` (einzeln) bzw.
|
||||
`POST /open/v1/task/batch` (bis 50 Creates/Updates in einem Call) beim Anlegen/Ändern einer
|
||||
passenden `WorkTask`. Erledigt-Setzen auf TickTick-Seite über
|
||||
`POST /open/v1/project/{projectId}/task/{taskId}/complete` bzw. gebündelt
|
||||
`POST /open/v1/task/completeTasks` (bis 50 Task-IDs).
|
||||
- **Pull erledigt (TickTick → LehrerApp):** `POST /open/v1/task/completed`, gefiltert nach
|
||||
`projectIds` + Zeitfenster (`completedTime`-Range) — liefert bis zu 200 im Poll-Intervall
|
||||
erledigte Tasks der Zielliste in einem Call, kein Abklappern einzelner Task-IDs nötig.
|
||||
- **Pull neu (TickTick → LehrerApp):** `POST /open/v1/task/filter` (Filter nach `projectIds`
|
||||
+ `status`, bis 200 Treffer) oder `GET /open/v1/project/{projectId}/data` (Projekt + aktive
|
||||
Tasks + Columns in einem Call) auf die Zielliste pollen, Ergebnis gegen die lokal
|
||||
gespeicherten Fremd-IDs diffen — neue IDs werden als `WorkTask` angelegt.
|
||||
- **Architektur-Vorbild:** gleiches Muster wie der bestehende WebUntis-Abgleich —
|
||||
framework-freie Diff-Logik nach `LehrerApp.Core/Services/` (analog `UntisDiffService`),
|
||||
HTTP-Aufruf + Timer-Orchestrierung in `LehrerApp.Desktop/Services/` (analog
|
||||
`UntisSyncService`). Zuordnung TickTick-Task-ID ↔ `WorkTask` über ein neues
|
||||
`WorkTask.ExternalId`-Feld, idempotent wie `SubstitutionEntry.ExternalId`
|
||||
(`GetByExternalId`, Update statt Duplikat).
|
||||
- **Offen:** kein Webhook-Support in der API bekannt — nur Polling. Rate-Limits sind in der
|
||||
Doku nicht dokumentiert, vor Produktivbetrieb mit kurzem Poll-Intervall (< 5 Min.) prüfen.
|
||||
Vorher klären: welche `WorkTask`-Teilmenge synct (nur `Reminder` oder auch `WorkItem`?),
|
||||
welche TickTick-Liste ist die Zielliste (fest in den Einstellungen hinterlegt, analog zur
|
||||
WebUntis-URL)?
|
||||
|
||||
**Nachtrag — Pädagogische Klassen-Aufgaben (Nutzer-Feedback):** Wunsch nach einer zweiten,
|
||||
"weniger arbeitszeitrelevant als pädagogisch" gedachten Art von Todo-Item (Beispiele:
|
||||
"Ansage an die Klasse", "Etwas zum Stichtag einsammeln/austeilen"), gleichberechtigt im
|
||||
|
||||
Reference in New Issue
Block a user