added guardian login with views for their assigned childs
This commit is contained in:
@@ -6,11 +6,14 @@ import '../../../extensions/date_time.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
|
||||
import '../../../state/app/modules/capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../../state/app/modules/foreign_timetable/bloc/foreign_timetable_bloc.dart';
|
||||
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart';
|
||||
import '../../../state/app/modules/timetable/bloc/timetable_state.dart';
|
||||
import '../../../state/app/modules/timetable/policy/timetable_policy.dart';
|
||||
import '../../../state/app/modules/timetable/subject/timetable_subject.dart';
|
||||
import '../../../utils/haptics.dart';
|
||||
import '../../../widget/app_progress_indicator.dart';
|
||||
import '../../../widget/child_switcher.dart';
|
||||
import '../../../widget/demo_restricted.dart';
|
||||
import 'custom_events/custom_event_edit_dialog.dart';
|
||||
import 'details/appointment_details_dispatcher.dart';
|
||||
@@ -26,8 +29,9 @@ class Timetable extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _TimetableState extends State<Timetable> {
|
||||
final GlobalKey<TimetableCalendarViewState> _calendarKey =
|
||||
GlobalKey<TimetableCalendarViewState> _calendarKey =
|
||||
GlobalKey<TimetableCalendarViewState>();
|
||||
TimetableSubject? _calendarSubject;
|
||||
|
||||
/// When non-null the view shows this element's plan inline instead of the
|
||||
/// user's own. Cleared (back to own plan) via the viewing banner.
|
||||
@@ -78,31 +82,39 @@ class _TimetableState extends State<Timetable> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selected = _selected;
|
||||
if (selected == null) return _buildOwnPlan(context);
|
||||
if (selected == null) {
|
||||
final primary = context.watch<TimetableBloc>().subject;
|
||||
if (primary is NoTimetable) return const _NoTimetableView();
|
||||
return _buildPlan<TimetableBloc>(context);
|
||||
}
|
||||
// Scope the foreign bloc to the current selection so switching elements
|
||||
// (or back to the own plan) tears it down and builds a fresh one.
|
||||
return BlocProvider<ForeignTimetableBloc>(
|
||||
return BlocProvider<ScopedTimetableBloc>(
|
||||
key: ValueKey('${selected.type.name}-${selected.id}'),
|
||||
create: (_) => ForeignTimetableBloc(
|
||||
type: selected.type,
|
||||
elementId: selected.id,
|
||||
title: selected.label,
|
||||
),
|
||||
// Builder gives us a context *below* the provider so the foreign bloc is
|
||||
// resolvable inside _buildForeignPlan.
|
||||
create: (_) => ScopedTimetableBloc(subject: ElementTimetable(selected)),
|
||||
// Builder gives us a context *below* the provider so the scoped bloc is
|
||||
// resolvable inside _buildPlan.
|
||||
child: Builder(
|
||||
builder: (context) => _buildForeignPlan(context, selected),
|
||||
builder: (context) => _buildPlan<ScopedTimetableBloc>(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOwnPlan(BuildContext context) {
|
||||
final bloc = context.read<TimetableBloc>();
|
||||
final loadableState = context.watch<TimetableBloc>().state;
|
||||
final innerState = loadableState.data;
|
||||
Widget _buildPlan<B extends TimetableBloc>(BuildContext context) {
|
||||
final bloc = context.read<B>();
|
||||
final subject = bloc.subject;
|
||||
// A new subject (child switch) must not inherit the displayed week of the
|
||||
// previous calendar state.
|
||||
if (subject != _calendarSubject) {
|
||||
_calendarSubject = subject;
|
||||
_calendarKey = GlobalKey<TimetableCalendarViewState>();
|
||||
}
|
||||
final innerState = context.watch<B>().state.data;
|
||||
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
||||
final capabilities = context.watch<CapabilitiesCubit>();
|
||||
final canViewForeign = capabilities.canViewForeignTimetables;
|
||||
final policy = TimetablePolicy.resolve(
|
||||
subject: subject,
|
||||
capabilities: context.watch<CapabilitiesCubit>().state,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Der Kalender scrollt nicht (nur ziehen/reloaden), aber seine internen
|
||||
@@ -111,87 +123,36 @@ class _TimetableState extends State<Timetable> {
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
// Hides itself unless a guardian has more than one child.
|
||||
const ChildSwitcher(),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: 'Zur aktuellen Woche',
|
||||
onPressed: atToday ? null : _jumpToToday,
|
||||
),
|
||||
PopupMenuButton<_CalendarAction>(
|
||||
tooltip: 'Kalendereinträge',
|
||||
icon: const Icon(Icons.edit_calendar_outlined),
|
||||
onSelected: _onAction,
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.addEvent,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereintrag hinzufügen'),
|
||||
leading: Icon(Icons.add),
|
||||
if (policy.canManageCustomEvents)
|
||||
PopupMenuButton<_CalendarAction>(
|
||||
tooltip: 'Kalendereinträge',
|
||||
icon: const Icon(Icons.edit_calendar_outlined),
|
||||
onSelected: _onAction,
|
||||
itemBuilder: (_) => const [
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.addEvent,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereintrag hinzufügen'),
|
||||
leading: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.viewEvents,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereinträge anzeigen'),
|
||||
leading: Icon(Icons.perm_contact_calendar_outlined),
|
||||
PopupMenuItem(
|
||||
value: _CalendarAction.viewEvents,
|
||||
child: ListTile(
|
||||
title: Text('Kalendereinträge anzeigen'),
|
||||
leading: Icon(Icons.perm_contact_calendar_outlined),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (canViewForeign)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_search),
|
||||
tooltip: 'Anderen Stundenplan öffnen',
|
||||
onPressed: _openPicker,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
body: LoadableStateConsumer<TimetableBloc, TimetableState>(
|
||||
// Without this predicate the consumer treats the freshly-initialised
|
||||
// empty TimetableState as "has content" and only shows the error bar
|
||||
// on top — but the calendar view collapses to `SizedBox.shrink()`
|
||||
// while the reference data is missing, leaving the user with a blank
|
||||
// screen. Telling the consumer that "ready" means having reference
|
||||
// data flips it into the proper error-screen path instead.
|
||||
isReady: (state) => state.hasReferenceData,
|
||||
child: (state, _) => TimetableCalendarView(
|
||||
key: _calendarKey,
|
||||
state: state,
|
||||
onWeekChanged: bloc.changeWeek,
|
||||
onAppointmentTap: (apt) => AppointmentDetailsDispatcher.show(
|
||||
context,
|
||||
state,
|
||||
apt,
|
||||
canEditSubjectColor: true,
|
||||
),
|
||||
onCreateEvent: _onCreateEventAt,
|
||||
customEvents: state.customEvents?.events ?? const [],
|
||||
showClassInsteadOfTeacher: capabilities.isTeacher,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildForeignPlan(BuildContext context, TimetableElementRef selected) {
|
||||
final bloc = context.read<ForeignTimetableBloc>();
|
||||
final loadableState = context.watch<ForeignTimetableBloc>().state;
|
||||
final innerState = loadableState.data;
|
||||
final atToday = innerState != null && _isOnInitialWeek(innerState);
|
||||
final canViewForeign = context
|
||||
.watch<CapabilitiesCubit>()
|
||||
.canViewForeignTimetables;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// Siehe _buildOwnPlan: den scroll-under-Farbwechsel unterdrücken, weil
|
||||
// der Kalender nicht scrollt, aber ScrollNotifications feuert.
|
||||
notificationPredicate: (_) => false,
|
||||
title: const Text('Stunden & Vertretungsplan'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.home_outlined),
|
||||
tooltip: 'Zur aktuellen Woche',
|
||||
onPressed: atToday ? null : _jumpToToday,
|
||||
),
|
||||
if (canViewForeign)
|
||||
if (policy.canOpenForeign)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_search),
|
||||
tooltip: 'Anderen Stundenplan öffnen',
|
||||
@@ -201,24 +162,33 @@ class _TimetableState extends State<Timetable> {
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_ViewingBanner(element: selected, onClose: _backToOwnPlan),
|
||||
if (subject case ElementTimetable(:final element))
|
||||
_ViewingBanner(element: element, onClose: _backToOwnPlan),
|
||||
Expanded(
|
||||
child: LoadableStateConsumer<ForeignTimetableBloc, TimetableState>(
|
||||
// Foreign plans never carry custom events, so unlike the own-plan
|
||||
// view we must not require `customEvents` here.
|
||||
isReady: (state) =>
|
||||
state.rooms != null &&
|
||||
state.subjects != null &&
|
||||
state.schoolHolidays != null,
|
||||
child: LoadableStateConsumer<B, TimetableState>(
|
||||
// Without this predicate the consumer treats the freshly-
|
||||
// initialised empty TimetableState as "has content" and only
|
||||
// shows the error bar on top — but the calendar view collapses
|
||||
// to `SizedBox.shrink()` while the reference data is missing,
|
||||
// leaving the user with a blank screen.
|
||||
isReady: (state) => state.isReady(
|
||||
needsCustomEvents: subject.supportsCustomEvents,
|
||||
),
|
||||
child: (state, _) => TimetableCalendarView(
|
||||
key: _calendarKey,
|
||||
state: state,
|
||||
onWeekChanged: bloc.changeWeek,
|
||||
onAppointmentTap: (apt) =>
|
||||
AppointmentDetailsDispatcher.show(context, state, apt),
|
||||
customEvents: const [],
|
||||
showClassInsteadOfTeacher:
|
||||
selected.type == TimetableElementType.teacher,
|
||||
onAppointmentTap: (apt) => AppointmentDetailsDispatcher.show(
|
||||
context,
|
||||
state,
|
||||
apt,
|
||||
canEditSubjectColor: policy.canEditSubjectColors,
|
||||
),
|
||||
onCreateEvent: policy.canManageCustomEvents
|
||||
? _onCreateEventAt
|
||||
: null,
|
||||
customEvents: state.customEvents?.events ?? const [],
|
||||
showClassInsteadOfTeacher: policy.showClassInsteadOfTeacher,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -233,6 +203,23 @@ class _TimetableState extends State<Timetable> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shown instead of a plan when the session has none, i.e. a guardian whose
|
||||
/// children are not known (yet).
|
||||
class _NoTimetableView extends StatelessWidget {
|
||||
const _NoTimetableView();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final capabilities = context.watch<CapabilitiesCubit>().state;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Stunden & Vertretungsplan')),
|
||||
body: capabilities.loaded
|
||||
? const NoChildrenPlaceholder()
|
||||
: const Center(child: AppProgressIndicator.large()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Slim banner shown at the top of the timetable while a foreign element's plan
|
||||
/// is being viewed. Displays which element is shown, lets the user star it, and
|
||||
/// offers a one-tap return to the own plan.
|
||||
|
||||
Reference in New Issue
Block a user