added teacher-focused lesson sheet with direct class and teacher timetable links

This commit is contained in:
2026-09-24 23:33:13 +02:00
parent de866b12b4
commit e21560ed3d
14 changed files with 306 additions and 33 deletions
@@ -10,12 +10,17 @@ class McTimetableTeacher {
final String displayName; final String displayName;
final String? originalShortName; final String? originalShortName;
final String? originalDisplayName; final String? originalDisplayName;
// Webuntis element ids; null from servers that predate them.
final int? id;
final int? originalId;
McTimetableTeacher({ McTimetableTeacher({
required this.shortName, required this.shortName,
required this.displayName, required this.displayName,
this.originalShortName, this.originalShortName,
this.originalDisplayName, this.originalDisplayName,
this.id,
this.originalId,
}); });
factory McTimetableTeacher.fromJson(Map<String, dynamic> json) => factory McTimetableTeacher.fromJson(Map<String, dynamic> json) =>
@@ -45,6 +50,8 @@ class McTimetableEntry {
final String? substitutionText; final String? substitutionText;
final String? lessonText; final String? lessonText;
final String? infoText; final String? infoText;
// Index-aligned with [classNames]; null from servers that predate it.
final List<int>? classIds;
McTimetableEntry({ McTimetableEntry({
required this.id, required this.id,
@@ -60,6 +67,7 @@ class McTimetableEntry {
required this.substitutionText, required this.substitutionText,
required this.lessonText, required this.lessonText,
required this.infoText, required this.infoText,
this.classIds,
}); });
factory McTimetableEntry.fromJson(Map<String, dynamic> json) => factory McTimetableEntry.fromJson(Map<String, dynamic> json) =>
@@ -12,6 +12,8 @@ McTimetableTeacher _$McTimetableTeacherFromJson(Map<String, dynamic> json) =>
displayName: json['displayName'] as String, displayName: json['displayName'] as String,
originalShortName: json['originalShortName'] as String?, originalShortName: json['originalShortName'] as String?,
originalDisplayName: json['originalDisplayName'] as String?, originalDisplayName: json['originalDisplayName'] as String?,
id: (json['id'] as num?)?.toInt(),
originalId: (json['originalId'] as num?)?.toInt(),
); );
Map<String, dynamic> _$McTimetableTeacherToJson(McTimetableTeacher instance) => Map<String, dynamic> _$McTimetableTeacherToJson(McTimetableTeacher instance) =>
@@ -20,6 +22,8 @@ Map<String, dynamic> _$McTimetableTeacherToJson(McTimetableTeacher instance) =>
'displayName': instance.displayName, 'displayName': instance.displayName,
'originalShortName': instance.originalShortName, 'originalShortName': instance.originalShortName,
'originalDisplayName': instance.originalDisplayName, 'originalDisplayName': instance.originalDisplayName,
'id': instance.id,
'originalId': instance.originalId,
}; };
McTimetableEntry _$McTimetableEntryFromJson(Map<String, dynamic> json) => McTimetableEntry _$McTimetableEntryFromJson(Map<String, dynamic> json) =>
@@ -43,6 +47,9 @@ McTimetableEntry _$McTimetableEntryFromJson(Map<String, dynamic> json) =>
substitutionText: json['substitutionText'] as String?, substitutionText: json['substitutionText'] as String?,
lessonText: json['lessonText'] as String?, lessonText: json['lessonText'] as String?,
infoText: json['infoText'] as String?, infoText: json['infoText'] as String?,
classIds: (json['classIds'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
); );
Map<String, dynamic> _$McTimetableEntryToJson(McTimetableEntry instance) => Map<String, dynamic> _$McTimetableEntryToJson(McTimetableEntry instance) =>
@@ -60,6 +67,7 @@ Map<String, dynamic> _$McTimetableEntryToJson(McTimetableEntry instance) =>
'substitutionText': instance.substitutionText, 'substitutionText': instance.substitutionText,
'lessonText': instance.lessonText, 'lessonText': instance.lessonText,
'infoText': instance.infoText, 'infoText': instance.infoText,
'classIds': instance.classIds,
}; };
TimetableGetWeekResponse _$TimetableGetWeekResponseFromJson( TimetableGetWeekResponse _$TimetableGetWeekResponseFromJson(
+18 -1
View File
@@ -36,6 +36,7 @@ import 'routing/app_routes.dart';
import 'share_intent/share_intent_listener.dart'; import 'share_intent/share_intent_listener.dart';
import 'state/app/modules/account/bloc/account_bloc.dart'; import 'state/app/modules/account/bloc/account_bloc.dart';
import 'state/app/modules/account/bloc/account_state.dart'; import 'state/app/modules/account/bloc/account_state.dart';
import 'state/app/modules/app_modules.dart';
import 'state/app/modules/breaker/bloc/breaker_bloc.dart'; import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
import 'state/app/modules/capabilities/bloc/capabilities_cubit.dart'; import 'state/app/modules/capabilities/bloc/capabilities_cubit.dart';
import 'state/app/modules/chat/bloc/chat_bloc.dart'; import 'state/app/modules/chat/bloc/chat_bloc.dart';
@@ -295,7 +296,9 @@ class _MainState extends State<Main> {
unawaited( unawaited(
context.read<CapabilitiesCubit>().load().then((_) { context.read<CapabilitiesCubit>().load().then((_) {
if (!mounted) return; if (!mounted) return;
_syncPush(settingsCubit, context.read<CapabilitiesCubit>()); final capabilities = context.read<CapabilitiesCubit>();
_syncPush(settingsCubit, capabilities);
_applyRoleDefaults(settingsCubit, capabilities);
}), }),
); );
unawaited(context.read<NextcloudCapabilitiesCubit>().load()); unawaited(context.read<NextcloudCapabilitiesCubit>().load());
@@ -331,6 +334,19 @@ class _MainState extends State<Main> {
); );
} }
/// userType is only known once capabilities are loaded.
void _applyRoleDefaults(
SettingsCubit settings,
CapabilitiesCubit capabilities,
) {
if (AppModule.applyTeacherDefaults(
settings.val().modulesSettings,
isTeacher: capabilities.isTeacher,
)) {
settings.val(write: true);
}
}
/// Background credential check: a 401 means the password was rotated /// Background credential check: a 401 means the password was rotated
/// server-side, so the validator wipes the local session and flips the /// server-side, so the validator wipes the local session and flips the
/// account bloc to `loggedOut` (sending the user to the login screen). /// account bloc to `loggedOut` (sending the user to the login screen).
@@ -424,6 +440,7 @@ class _MainState extends State<Main> {
capabilitiesCubit.load().then((_) { capabilitiesCubit.load().then((_) {
if (!mounted) return; if (!mounted) return;
_syncPush(settingsCubit, capabilitiesCubit); _syncPush(settingsCubit, capabilitiesCubit);
_applyRoleDefaults(settingsCubit, capabilitiesCubit);
}), }),
); );
unawaited( unawaited(
+21
View File
@@ -188,6 +188,27 @@ class AppModule {
return order; return order;
} }
/// Modules that are of no use to teachers and start out hidden for them.
static const Set<Modules> hiddenForTeachersByDefault = {
Modules.absenceReport,
};
/// Hides [hiddenForTeachersByDefault] once for teacher accounts; the teacher
/// can re-enable them in the module settings. Returns whether [settings]
/// changed.
static bool applyTeacherDefaults(
ModulesSettings settings, {
required bool isTeacher,
}) {
if (!isTeacher || settings.teacherDefaultsApplied) return false;
settings.hiddenModules = {
...settings.hiddenModules,
...hiddenForTeachersByDefault,
}.toList();
settings.teacherDefaultsApplied = true;
return true;
}
// The settings list displays a capability-filtered subset, so reorder // The settings list displays a capability-filtered subset, so reorder
// indices refer to that subset; the move is applied there and merged back // indices refer to that subset; the move is applied there and merged back
// into the full order (non-displayed modules keep their relative slots). // into the full order (non-displayed modules keep their relative slots).
+4
View File
@@ -10,12 +10,16 @@ class ModulesSettings {
List<Modules> hiddenModules; List<Modules> hiddenModules;
bool autoFillBottomBar; bool autoFillBottomBar;
int fixedBottomBarSlots; int fixedBottomBarSlots;
// Set once the teacher module defaults were applied, so a module the teacher
// re-enables afterwards stays visible.
bool teacherDefaultsApplied;
ModulesSettings({ ModulesSettings({
required this.moduleOrder, required this.moduleOrder,
required this.hiddenModules, required this.hiddenModules,
this.autoFillBottomBar = true, this.autoFillBottomBar = true,
this.fixedBottomBarSlots = 3, this.fixedBottomBarSlots = 3,
this.teacherDefaultsApplied = false,
}); });
factory ModulesSettings.fromJson(Map<String, dynamic> json) => factory ModulesSettings.fromJson(Map<String, dynamic> json) =>
+2
View File
@@ -16,6 +16,7 @@ ModulesSettings _$ModulesSettingsFromJson(Map<String, dynamic> json) =>
.toList(), .toList(),
autoFillBottomBar: json['autoFillBottomBar'] as bool? ?? true, autoFillBottomBar: json['autoFillBottomBar'] as bool? ?? true,
fixedBottomBarSlots: (json['fixedBottomBarSlots'] as num?)?.toInt() ?? 3, fixedBottomBarSlots: (json['fixedBottomBarSlots'] as num?)?.toInt() ?? 3,
teacherDefaultsApplied: json['teacherDefaultsApplied'] as bool? ?? false,
); );
Map<String, dynamic> _$ModulesSettingsToJson( Map<String, dynamic> _$ModulesSettingsToJson(
@@ -27,6 +28,7 @@ Map<String, dynamic> _$ModulesSettingsToJson(
.toList(), .toList(),
'autoFillBottomBar': instance.autoFillBottomBar, 'autoFillBottomBar': instance.autoFillBottomBar,
'fixedBottomBarSlots': instance.fixedBottomBarSlots, 'fixedBottomBarSlots': instance.fixedBottomBarSlots,
'teacherDefaultsApplied': instance.teacherDefaultsApplied,
}; };
const _$ModulesEnumMap = { const _$ModulesEnumMap = {
@@ -73,5 +73,6 @@ class LessonMerger {
substitutionText: source.substitutionText, substitutionText: source.substitutionText,
lessonText: source.lessonText, lessonText: source.lessonText,
infoText: source.infoText, infoText: source.infoText,
classIds: source.classIds,
); );
} }
@@ -240,12 +240,14 @@ class TimetableAppointmentFactory {
String _locationLabel(McTimetableEntry lesson) { String _locationLabel(McTimetableEntry lesson) {
final roomName = final roomName =
collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt'; collapseWhitespace(lesson.rooms.firstOrNull) ?? 'Unbekannt';
// Klassenlose Einträge (Aufsichten etc.) fallen auf den Lehrer zurück. if (showClassInsteadOfTeacher) {
final secondLine = // Klassenlose Einträge (Aufsichten etc.) zeigen nur den Raum — die
(showClassInsteadOfTeacher ? lesson.classLabel : null) ?? // Lehrkraft wäre hier die Plan-Inhaberin selbst.
_teacherLabel(lesson.teachers.firstOrNull) ?? final classLabel = lesson.classLabel;
'Unbekannt'; return classLabel == null ? roomName : '$roomName\n$classLabel';
return '$roomName\n$secondLine'; }
final teacher = _teacherLabel(lesson.teachers.firstOrNull) ?? 'Unbekannt';
return '$roomName\n$teacher';
} }
/// Backend serves teachers with their full display name ("Stefan Müller"), /// Backend serves teachers with their full display name ("Stefan Müller"),
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
import '../../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../../state/app/modules/timetable/bloc/timetable_state.dart';
import '../data/arbitrary_appointment.dart'; import '../data/arbitrary_appointment.dart';
import 'custom_event_sheet.dart'; import 'custom_event_sheet.dart';
@@ -12,6 +13,8 @@ class AppointmentDetailsDispatcher {
TimetableState? state, TimetableState? state,
Appointment appointment, { Appointment appointment, {
bool canEditSubjectColor = false, bool canEditSubjectColor = false,
bool teacherPlan = false,
void Function(TimetableElementRef element)? onOpenElement,
}) { }) {
final id = appointment.id; final id = appointment.id;
if (id is! ArbitraryAppointment) return; if (id is! ArbitraryAppointment) return;
@@ -23,6 +26,8 @@ class AppointmentDetailsDispatcher {
appointment, appointment,
entry, entry,
canEditSubjectColor: canEditSubjectColor, canEditSubjectColor: canEditSubjectColor,
teacherPlan: teacherPlan,
onOpenElement: onOpenElement,
), ),
custom: (event) => CustomEventSheet.show(context, event), custom: (event) => CustomEventSheet.show(context, event),
); );
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart'; import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../extensions/date_time.dart'; import '../../../../extensions/date_time.dart';
import '../../../../extensions/text.dart'; import '../../../../extensions/text.dart';
@@ -21,6 +22,8 @@ class LessonSheet {
Appointment appointment, Appointment appointment,
McTimetableEntry lesson, { McTimetableEntry lesson, {
bool canEditSubjectColor = false, bool canEditSubjectColor = false,
bool teacherPlan = false,
void Function(TimetableElementRef element)? onOpenElement,
}) { }) {
if (state == null) return; if (state == null) return;
@@ -72,7 +75,7 @@ class LessonSheet {
) )
: null, : null,
), ),
children: (_) => <Widget>[ children: (sheetContext) => <Widget>[
ListTile( ListTile(
leading: const Icon(Icons.notifications_active), leading: const Icon(Icons.notifications_active),
title: Text('Status: ${_statusLabel(lesson.status)}'), title: Text('Status: ${_statusLabel(lesson.status)}'),
@@ -88,13 +91,14 @@ class LessonSheet {
.toList(), .toList(),
), ),
_roomTile(context, lesson), _roomTile(context, lesson),
_teacherTile(context, lesson), ..._peopleTiles(
if (lesson.classNames.isNotEmpty) context,
_listTile( lesson,
icon: Icons.people, opener: onOpenElement == null
label: lesson.classNames.length == 1 ? 'Klasse' : 'Klassen', ? null
entries: lesson.classNames.map(_line).toList(), : _ElementOpener(sheetContext, onOpenElement),
), teacherPlan: teacherPlan,
),
..._optionalTextTiles(lesson), ..._optionalTextTiles(lesson),
DebugTile(context).jsonData(lesson.toJson()), DebugTile(context).jsonData(lesson.toJson()),
], ],
@@ -128,7 +132,59 @@ class LessonSheet {
); );
} }
static Widget _teacherTile(BuildContext context, McTimetableEntry lesson) { // Aus Lehrersicht ist die Klasse die relevante Angabe, die Lehrkraft ist
// meist die Plan-Inhaberin selbst — daher dort Klasse zuerst.
static List<Widget> _peopleTiles(
BuildContext context,
McTimetableEntry lesson, {
required _ElementOpener? opener,
required bool teacherPlan,
}) {
final classTile = _classTile(lesson, opener);
final teacherTile = _teacherTile(
context,
lesson,
opener,
teacherPlan: teacherPlan,
);
return teacherPlan ? [?classTile, teacherTile] : [teacherTile, ?classTile];
}
static Widget? _classTile(McTimetableEntry lesson, _ElementOpener? opener) {
final names = lesson.classNames;
if (names.isEmpty) return null;
const tooltip = 'Stundenplan der Klasse öffnen';
final ids = lesson.classIds?.length == names.length
? lesson.classIds
: null;
TimetableElementRef? element(int i) => ids == null
? null
: (type: TimetableElementType.schoolClass, id: ids[i], label: names[i]);
return _listTile(
icon: Icons.people,
label: names.length == 1 ? 'Klasse' : 'Klassen',
entries: names.map(_line).toList(),
trailing: names.length == 1
? opener?.button(tooltip: tooltip, element: element(0))
: null,
wrapRow: (i, row) => _openable(opener, row, tooltip, element(i)),
);
}
static Widget _openable(
_ElementOpener? opener,
Widget label,
String tooltip,
TimetableElementRef? element,
) => opener?.row(label: label, tooltip: tooltip, element: element) ?? label;
static Widget _teacherTile(
BuildContext context,
McTimetableEntry lesson,
_ElementOpener? opener, {
required bool teacherPlan,
}) {
if (lesson.teachers.isEmpty) { if (lesson.teachers.isEmpty) {
return const ListTile( return const ListTile(
leading: Icon(Icons.person), leading: Icon(Icons.person),
@@ -146,12 +202,17 @@ class LessonSheet {
} }
final label = teachers.length == 1 ? 'Lehrkraft' : 'Lehrkräfte'; final label = teachers.length == 1 ? 'Lehrkraft' : 'Lehrkräfte';
const tooltip = 'Stundenplan der Lehrkraft öffnen';
// Einzelne, reguläre Lehrkraft kompakt in der Titelzeile. // Einzelne, reguläre Lehrkraft kompakt in der Titelzeile.
if (teachers.length == 1 && teachers.first.isPlain) { if (teachers.length == 1 && teachers.first.isPlain) {
return ListTile( return ListTile(
leading: const Icon(Icons.person), leading: const Icon(Icons.person),
title: Text('$label: ${teachers.first.after}'), title: Text('$label: ${teachers.first.after}'),
// Im Lehrerplan ist das die Plan-Inhaberin selbst.
trailing: teacherPlan
? null
: opener?.button(tooltip: tooltip, element: teachers.first.target),
); );
} }
@@ -160,7 +221,10 @@ class LessonSheet {
title: Text(label), title: Text(label),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [for (final t in teachers) t.buildRow(context)], children: [
for (final t in teachers)
_openable(opener, t.buildRow(context), tooltip, t.target),
],
), ),
); );
} }
@@ -207,6 +271,7 @@ class LessonSheet {
required String label, required String label,
required List<String> entries, required List<String> entries,
Widget? trailing, Widget? trailing,
Widget Function(int index, Widget row)? wrapRow,
}) { }) {
if (entries.length == 1) { if (entries.length == 1) {
return ListTile( return ListTile(
@@ -220,7 +285,10 @@ class LessonSheet {
title: Text(label), title: Text(label),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: entries.map<Widget>(Text.new).toList(), children: [
for (final (i, entry) in entries.indexed)
wrapRow?.call(i, Text(entry)) ?? Text(entry),
],
), ),
trailing: trailing, trailing: trailing,
); );
@@ -333,7 +401,15 @@ class _TeacherDisplay {
/// Aktuelle Lehrkraft. Leer bei ersatzlosem Entfall. /// Aktuelle Lehrkraft. Leer bei ersatzlosem Entfall.
final String after; final String after;
const _TeacherDisplay._({this.before = '', this.after = ''}); /// Lehrkraft, deren Stundenplan sich öffnen lässt: die aktuelle, bei
/// ersatzlosem Entfall die ursprüngliche. Null, wenn beide unbekannt sind.
final TimetableElementRef? target;
const _TeacherDisplay._({
this.before = '',
this.after = '',
this.target,
});
bool get isPlain => before.isEmpty; bool get isPlain => before.isEmpty;
@@ -345,14 +421,32 @@ class _TeacherDisplay {
t.originalShortName ?? '', t.originalShortName ?? '',
t.originalDisplayName ?? '', t.originalDisplayName ?? '',
); );
final target = _target(t, currentKnown: current.isNotEmpty);
// Kein (abweichendes) Original → reguläre Lehrkraft. // Kein (abweichendes) Original → reguläre Lehrkraft.
if (original.isEmpty || original == current) { if (original.isEmpty || original == current) {
return _TeacherDisplay._(after: current.isEmpty ? '?' : current); return _TeacherDisplay._(
after: current.isEmpty ? '?' : current,
target: target,
);
} }
// Original vorhanden → ersetzte/entfallende Lehrkraft vorn, aktuelle // Original vorhanden → ersetzte/entfallende Lehrkraft vorn, aktuelle
// Lehrkraft (falls vorhanden) als Ersatz dahinter. // Lehrkraft (falls vorhanden) als Ersatz dahinter.
return _TeacherDisplay._(before: original, after: current); return _TeacherDisplay._(
before: original,
after: current,
target: target,
);
}
static TimetableElementRef? _target(
McTimetableTeacher t, {
required bool currentKnown,
}) {
final id = currentKnown ? t.id : t.originalId;
if (id == null) return null;
final label = currentKnown ? t.displayName : t.originalDisplayName;
return (type: TimetableElementType.teacher, id: id, label: label ?? '');
} }
/// Eine Bullet-Zeile je Lehrkraft: ersetzte Person durchgestrichen, bei /// Eine Bullet-Zeile je Lehrkraft: ersetzte Person durchgestrichen, bei
@@ -404,3 +498,58 @@ class _TeacherDisplay {
return short; return short;
} }
} }
/// Baut die Bedienelemente, die aus dem Sheet heraus einen fremden
/// Stundenplan öffnen. Ohne Element-Id (älterer Server) entfallen sie.
class _ElementOpener {
static const IconData icon = Icons.calendar_view_week_outlined;
final BuildContext sheetContext;
final void Function(TimetableElementRef element) onOpen;
const _ElementOpener(this.sheetContext, this.onOpen);
void _open(TimetableElementRef element) {
Navigator.of(sheetContext).pop();
onOpen(element);
}
/// Trailing-Button für einzeilige Tiles, analog zum Raumplan-Button.
Widget? button({
required String tooltip,
required TimetableElementRef? element,
}) {
if (element == null) return null;
return IconButton(
icon: const Icon(icon),
tooltip: tooltip,
onPressed: () => _open(element),
);
}
/// Kompakte, vollständig antippbare Zeile für Aufzählungen — ein
/// 48px-Button pro Eintrag würde die Liste auseinanderziehen.
Widget row({
required Widget label,
required String tooltip,
required TimetableElementRef? element,
}) {
if (element == null) return label;
final muted = Theme.of(sheetContext).colorScheme.onSurfaceVariant;
return Semantics(
button: true,
label: tooltip,
child: InkWell(
onTap: () => _open(element),
borderRadius: BorderRadius.circular(6),
child: Row(
children: [
Expanded(child: label),
const SizedBox(width: 8),
Icon(icon, size: 18, color: muted),
],
),
),
);
}
}
+17 -5
View File
@@ -46,6 +46,10 @@ class _TimetableState extends State<Timetable> {
if (guardDemoAction(context)) return; if (guardDemoAction(context)) return;
final ref = await AppRoutes.openElementPicker(context); final ref = await AppRoutes.openElementPicker(context);
if (!mounted || ref == null) return; if (!mounted || ref == null) return;
_openElement(ref);
}
void _openElement(TimetableElementRef ref) {
setState(() => _selected = ref); setState(() => _selected = ref);
} }
@@ -103,6 +107,7 @@ class _TimetableState extends State<Timetable> {
final atToday = innerState != null && _isOnInitialWeek(innerState); final atToday = innerState != null && _isOnInitialWeek(innerState);
final capabilities = context.watch<CapabilitiesCubit>(); final capabilities = context.watch<CapabilitiesCubit>();
final canViewForeign = capabilities.canViewForeignTimetables; final canViewForeign = capabilities.canViewForeignTimetables;
final teacherPlan = capabilities.isTeacher;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen // Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
@@ -162,10 +167,12 @@ class _TimetableState extends State<Timetable> {
state, state,
apt, apt,
canEditSubjectColor: true, canEditSubjectColor: true,
teacherPlan: teacherPlan,
onOpenElement: canViewForeign ? _openElement : null,
), ),
onCreateEvent: _onCreateEventAt, onCreateEvent: _onCreateEventAt,
customEvents: state.customEvents?.events ?? const [], customEvents: state.customEvents?.events ?? const [],
showClassInsteadOfTeacher: capabilities.isTeacher, showClassInsteadOfTeacher: teacherPlan,
), ),
), ),
); );
@@ -179,6 +186,7 @@ class _TimetableState extends State<Timetable> {
final canViewForeign = context final canViewForeign = context
.watch<CapabilitiesCubit>() .watch<CapabilitiesCubit>()
.canViewForeignTimetables; .canViewForeignTimetables;
final teacherPlan = selected.type == TimetableElementType.teacher;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Siehe _buildOwnPlan: den scroll-under-Farbwechsel unterdrücken, weil // Siehe _buildOwnPlan: den scroll-under-Farbwechsel unterdrücken, weil
@@ -214,11 +222,15 @@ class _TimetableState extends State<Timetable> {
key: _calendarKey, key: _calendarKey,
state: state, state: state,
onWeekChanged: bloc.changeWeek, onWeekChanged: bloc.changeWeek,
onAppointmentTap: (apt) => onAppointmentTap: (apt) => AppointmentDetailsDispatcher.show(
AppointmentDetailsDispatcher.show(context, state, apt), context,
state,
apt,
teacherPlan: teacherPlan,
onOpenElement: canViewForeign ? _openElement : null,
),
customEvents: const [], customEvents: const [],
showClassInsteadOfTeacher: showClassInsteadOfTeacher: teacherPlan,
selected.type == TimetableElementType.teacher,
), ),
), ),
), ),
+6 -5
View File
@@ -350,11 +350,12 @@ class WidgetDataMapper {
} }
final teacher = lesson.teachers.firstOrNull; final teacher = lesson.teachers.firstOrNull;
// Lehrerpläne: Klasse in den Teacher-Slot mappen, damit die nativen // Lehrerpläne: Klasse in den Teacher-Slot mappen, damit die nativen
// Renderer unverändert bleiben. Klassenlose Einträge (Aufsichten) behalten // Renderer unverändert bleiben. Klassenlose Einträge (Aufsichten) lassen
// den Lehrer als Fallback. // den Slot leer — die Lehrkraft wäre die Plan-Inhaberin selbst.
final classLabel = showClassInsteadOfTeacher ? lesson.classLabel : null; final teacherName = showClassInsteadOfTeacher
final teacherName = classLabel ?? teacher?.shortName; ? lesson.classLabel
final originalTeacher = classLabel != null : teacher?.shortName;
final originalTeacher = showClassInsteadOfTeacher
? null ? null
: teacher?.originalShortName; : teacher?.originalShortName;
return WidgetLesson( return WidgetLesson(
+43
View File
@@ -92,4 +92,47 @@ void main() {
); );
}); });
}); });
group('applyTeacherDefaults', () {
test('hides the absence report once for teachers', () {
final settings = settingsWith(Modules.values);
expect(AppModule.applyTeacherDefaults(settings, isTeacher: true), isTrue);
expect(settings.hiddenModules, [Modules.absenceReport]);
expect(settings.teacherDefaultsApplied, isTrue);
});
test('keeps a module the teacher re-enabled afterwards', () {
final settings = settingsWith(Modules.values);
AppModule.applyTeacherDefaults(settings, isTeacher: true);
settings.hiddenModules.remove(Modules.absenceReport);
expect(
AppModule.applyTeacherDefaults(settings, isTeacher: true),
isFalse,
);
expect(settings.hiddenModules, isEmpty);
});
test('leaves non-teachers untouched', () {
final settings = settingsWith(Modules.values);
expect(
AppModule.applyTeacherDefaults(settings, isTeacher: false),
isFalse,
);
expect(settings.hiddenModules, isEmpty);
expect(settings.teacherDefaultsApplied, isFalse);
});
test('does not duplicate an already hidden module', () {
final settings = ModulesSettings(
moduleOrder: Modules.values,
hiddenModules: [Modules.absenceReport, Modules.files],
);
AppModule.applyTeacherDefaults(settings, isTeacher: true);
expect(settings.hiddenModules, [Modules.absenceReport, Modules.files]);
});
});
} }
@@ -65,10 +65,10 @@ void main() {
); );
}); });
test('falls back to the teacher when the entry has no class', () { test('shows only the room when the entry has no class', () {
expect( expect(
_location(showClassInsteadOfTeacher: true, classNames: const []), _location(showClassInsteadOfTeacher: true, classNames: const []),
'A101\nMüller', 'A101',
); );
}); });
}); });