From 8128cede21cae76778266b3a55b92738bb0aab7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Thu, 16 Jul 2026 21:44:08 +0200 Subject: [PATCH] refactored teacher display in lesson details to better handle substitutions and duplicates --- .../pages/timetable/details/lesson_sheet.dart | 132 ++++++++++++++---- 1 file changed, 107 insertions(+), 25 deletions(-) diff --git a/lib/view/pages/timetable/details/lesson_sheet.dart b/lib/view/pages/timetable/details/lesson_sheet.dart index c528a17..5f0d5f4 100644 --- a/lib/view/pages/timetable/details/lesson_sheet.dart +++ b/lib/view/pages/timetable/details/lesson_sheet.dart @@ -87,7 +87,7 @@ class LessonSheet { .toList(), ), _roomTile(context, lesson), - _teacherTile(lesson), + _teacherTile(context, lesson), if (lesson.classNames.isNotEmpty) _listTile( icon: Icons.people, @@ -127,7 +127,7 @@ class LessonSheet { ); } - static Widget _teacherTile(McTimetableEntry lesson) { + static Widget _teacherTile(BuildContext context, McTimetableEntry lesson) { if (lesson.teachers.isEmpty) { return const ListTile( leading: Icon(Icons.person), @@ -135,33 +135,32 @@ class LessonSheet { ); } - final entries = lesson.teachers.map((t) { - final shortName = t.shortName.isEmpty ? '?' : t.shortName; - final longName = t.displayName.trim(); - final orgShort = (t.originalShortName ?? '').trim(); - final orgLong = (t.originalDisplayName ?? '').trim(); + // Webuntis liefert entfallende Lehrkräfte bei Vertretungen teils mehrfach — + // über die Anzeigewerte deduplizieren. + final seen = {}; + final teachers = <_TeacherDisplay>[]; + for (final t in lesson.teachers) { + final display = _TeacherDisplay.from(t); + if (seen.add(display.dedupKey)) teachers.add(display); + } - final subLines = []; - if (longName.isNotEmpty && longName != shortName) { - subLines.add(longName); - } - if (orgShort.isNotEmpty) { - final label = orgLong.isEmpty || orgLong == orgShort - ? orgShort - : '$orgShort · $orgLong'; - subLines.add('ehemals $label'); - } + final label = teachers.length == 1 ? 'Lehrkraft' : 'Lehrkräfte'; - return ( - main: shortName, - sub: subLines.isEmpty ? null : subLines.join('\n'), + // Einzelne, reguläre Lehrkraft kompakt in der Titelzeile. + if (teachers.length == 1 && teachers.first.isPlain) { + return ListTile( + leading: const Icon(Icons.person), + title: Text('$label: ${teachers.first.after}'), ); - }).toList(); + } - return _listTileWithSubs( - icon: Icons.person, - label: lesson.teachers.length == 1 ? 'Lehrkraft' : 'Lehrkräfte', - entries: entries, + return ListTile( + leading: const Icon(Icons.person), + title: Text(label), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [for (final t in teachers) t.buildRow(context)], + ), ); } @@ -318,3 +317,86 @@ class LessonSheet { } } } + +/// Aufbereitete Darstellung einer einzelnen Lehrkraft aus einem +/// Webuntis-Element. Trennt die drei Fälle regulär / Vertretung / Entfall, +/// damit die Ansicht statt eines nackten `?` einen sprechenden Namen zeigt. +class _TeacherDisplay { + /// Ersetzte bzw. entfallende Lehrkraft — steht vorn und wird durchgestrichen. + /// Leer bei einer regulären Lehrkraft. + final String before; + + /// Aktuelle Lehrkraft. Leer bei ersatzlosem Entfall. + final String after; + + const _TeacherDisplay._({this.before = '', this.after = ''}); + + bool get isPlain => before.isEmpty; + + String get dedupKey => '$before|$after'; + + factory _TeacherDisplay.from(McTimetableTeacher t) { + final current = _formatName(t.shortName, t.displayName); + final original = _formatName( + t.originalShortName ?? '', + t.originalDisplayName ?? '', + ); + + // Kein (abweichendes) Original → reguläre Lehrkraft. + if (original.isEmpty || original == current) { + return _TeacherDisplay._(after: current.isEmpty ? '?' : current); + } + // Original vorhanden → ersetzte/entfallende Lehrkraft vorn, aktuelle + // Lehrkraft (falls vorhanden) als Ersatz dahinter. + return _TeacherDisplay._(before: original, after: current); + } + + /// Eine Bullet-Zeile je Lehrkraft: ersetzte Person durchgestrichen, bei + /// Ersatz ein Pfeil auf die neue Person. Bricht bei Bedarf sauber um. + Widget buildRow(BuildContext context) { + final muted = Theme.of(context).colorScheme.onSurfaceVariant; + final spans = []; + if (before.isNotEmpty) { + spans.add( + TextSpan( + text: before, + style: TextStyle( + decoration: TextDecoration.lineThrough, + color: muted, + ), + ), + ); + } + if (after.isNotEmpty) { + if (before.isNotEmpty) { + spans.add(TextSpan(text: ' → ', style: TextStyle(color: muted))); + } + spans.add(TextSpan(text: after)); + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('• '), + Expanded(child: Text.rich(TextSpan(children: spans))), + ], + ), + ); + } + + /// Kombiniert Kurz- und Langname zu „Langname (Kurzname)“ und lässt leere + /// Teile weg. Liefert `''`, wenn beides leer ist. + static String _formatName(String short, String long) { + short = short.trim(); + long = long.trim(); + // Ein wörtliches „?“ zählt wie ein fehlender Wert. + if (short == '?') short = ''; + if (long == '?') long = ''; + if (long.isNotEmpty && short.isNotEmpty && long != short) { + return '$long ($short)'; + } + if (long.isNotEmpty) return long; + return short; + } +}