added teacher-focused lesson sheet with direct class and teacher timetable links
This commit is contained in:
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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 '../../../../extensions/date_time.dart';
|
||||
import '../../../../extensions/text.dart';
|
||||
@@ -21,6 +22,8 @@ class LessonSheet {
|
||||
Appointment appointment,
|
||||
McTimetableEntry lesson, {
|
||||
bool canEditSubjectColor = false,
|
||||
bool teacherPlan = false,
|
||||
void Function(TimetableElementRef element)? onOpenElement,
|
||||
}) {
|
||||
if (state == null) return;
|
||||
|
||||
@@ -72,7 +75,7 @@ class LessonSheet {
|
||||
)
|
||||
: null,
|
||||
),
|
||||
children: (_) => <Widget>[
|
||||
children: (sheetContext) => <Widget>[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_active),
|
||||
title: Text('Status: ${_statusLabel(lesson.status)}'),
|
||||
@@ -88,13 +91,14 @@ class LessonSheet {
|
||||
.toList(),
|
||||
),
|
||||
_roomTile(context, lesson),
|
||||
_teacherTile(context, lesson),
|
||||
if (lesson.classNames.isNotEmpty)
|
||||
_listTile(
|
||||
icon: Icons.people,
|
||||
label: lesson.classNames.length == 1 ? 'Klasse' : 'Klassen',
|
||||
entries: lesson.classNames.map(_line).toList(),
|
||||
),
|
||||
..._peopleTiles(
|
||||
context,
|
||||
lesson,
|
||||
opener: onOpenElement == null
|
||||
? null
|
||||
: _ElementOpener(sheetContext, onOpenElement),
|
||||
teacherPlan: teacherPlan,
|
||||
),
|
||||
..._optionalTextTiles(lesson),
|
||||
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) {
|
||||
return const ListTile(
|
||||
leading: Icon(Icons.person),
|
||||
@@ -146,12 +202,17 @@ class LessonSheet {
|
||||
}
|
||||
|
||||
final label = teachers.length == 1 ? 'Lehrkraft' : 'Lehrkräfte';
|
||||
const tooltip = 'Stundenplan der Lehrkraft öffnen';
|
||||
|
||||
// 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}'),
|
||||
// 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),
|
||||
subtitle: Column(
|
||||
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 List<String> entries,
|
||||
Widget? trailing,
|
||||
Widget Function(int index, Widget row)? wrapRow,
|
||||
}) {
|
||||
if (entries.length == 1) {
|
||||
return ListTile(
|
||||
@@ -220,7 +285,10 @@ class LessonSheet {
|
||||
title: Text(label),
|
||||
subtitle: Column(
|
||||
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,
|
||||
);
|
||||
@@ -333,7 +401,15 @@ class _TeacherDisplay {
|
||||
/// Aktuelle Lehrkraft. Leer bei ersatzlosem Entfall.
|
||||
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;
|
||||
|
||||
@@ -345,14 +421,32 @@ class _TeacherDisplay {
|
||||
t.originalShortName ?? '',
|
||||
t.originalDisplayName ?? '',
|
||||
);
|
||||
final target = _target(t, currentKnown: current.isNotEmpty);
|
||||
|
||||
// Kein (abweichendes) Original → reguläre Lehrkraft.
|
||||
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
|
||||
// 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
|
||||
@@ -404,3 +498,58 @@ class _TeacherDisplay {
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user