added guardian login with views for their assigned childs
This commit is contained in:
@@ -3,8 +3,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||
|
||||
import '../../../access/access_requirement.dart';
|
||||
import '../../../api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
|
||||
import '../../../routing/app_routes.dart';
|
||||
import '../../../session/session.dart';
|
||||
import '../../../session/session_manager.dart';
|
||||
import '../../../storage/modules_settings.dart';
|
||||
import '../../../view/pages/absence_report/absence_report_view.dart';
|
||||
import '../../../view/pages/files/files.dart';
|
||||
@@ -38,6 +41,16 @@ class AppModule {
|
||||
required this.create,
|
||||
});
|
||||
|
||||
/// Backend identities each module needs. Modules without an entry work for
|
||||
/// every session.
|
||||
static const Map<Modules, Set<AccessRequirement>> requirements = {
|
||||
Modules.talk: {AccessRequirement.nextcloud},
|
||||
Modules.files: {AccessRequirement.nextcloud},
|
||||
};
|
||||
|
||||
static bool isAvailableFor(Modules module, Session? session) =>
|
||||
(requirements[module] ?? const {}).areMetBy(session);
|
||||
|
||||
static Map<Modules, AppModule> modules(
|
||||
BuildContext context, {
|
||||
bool showFiltered = false,
|
||||
@@ -146,6 +159,9 @@ class AppModule {
|
||||
),
|
||||
};
|
||||
|
||||
final session = SessionManager().current;
|
||||
available.removeWhere((key, _) => !isAvailableFor(key, session));
|
||||
|
||||
if (!showFiltered) {
|
||||
available.removeWhere(
|
||||
(key, value) =>
|
||||
@@ -177,9 +193,7 @@ class AppModule {
|
||||
for (final missing in Modules.values) {
|
||||
if (!seen.add(missing)) continue;
|
||||
var insertAt = 0;
|
||||
for (final predecessor in Modules.values.takeWhile(
|
||||
(m) => m != missing,
|
||||
)) {
|
||||
for (final predecessor in Modules.values.takeWhile((m) => m != missing)) {
|
||||
final pos = order.indexOf(predecessor);
|
||||
if (pos >= insertAt) insertAt = pos + 1;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import '../../../../../api/demo/data/demo_capabilities.dart';
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumconnect/queries/get_capabilities/get_capabilities.dart';
|
||||
import '../../../../../session/session.dart';
|
||||
import '../../../../../session/session_manager.dart';
|
||||
import 'capabilities_state.dart';
|
||||
|
||||
/// Holds the current user's mobile capability flags. Hydrated so the last
|
||||
@@ -21,17 +23,17 @@ class CapabilitiesCubit extends HydratedCubit<CapabilitiesState> {
|
||||
|
||||
int? get timetableFutureDays => state.timetableFutureDays;
|
||||
|
||||
/// Teacher accounts get the class shown on timetable tiles instead of their
|
||||
/// own name (see TimetableAppointmentFactory.showClassInsteadOfTeacher).
|
||||
bool get isTeacher => state.userType == 'TEACHER';
|
||||
|
||||
/// Refreshes capabilities from the server. On any failure (endpoint not yet
|
||||
/// live, network error, 4xx) the previously hydrated flags are kept but the
|
||||
/// state is marked `loaded` — a failed fetch never silently grants a
|
||||
/// capability, and an offline launch keeps whatever was cached.
|
||||
Future<void> load() async {
|
||||
if (DemoMode.active) {
|
||||
emit(DemoCapabilities.state());
|
||||
emit(
|
||||
SessionManager().current is GuardianSession
|
||||
? DemoCapabilities.guardianState()
|
||||
: DemoCapabilities.state(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -43,6 +45,7 @@ class CapabilitiesCubit extends HydratedCubit<CapabilitiesState> {
|
||||
timetablePastDays: response.timetablePastDays,
|
||||
timetableFutureDays: response.timetableFutureDays,
|
||||
userType: response.userType,
|
||||
children: response.children,
|
||||
loaded: true,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../../../access/user_role.dart';
|
||||
import '../../../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart';
|
||||
|
||||
part 'capabilities_state.freezed.dart';
|
||||
part 'capabilities_state.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class CapabilitiesState with _$CapabilitiesState {
|
||||
const CapabilitiesState._();
|
||||
|
||||
const factory CapabilitiesState({
|
||||
@Default(false) bool viewForeignTimetables,
|
||||
@Default(false) bool pushNotifications,
|
||||
@@ -12,8 +17,10 @@ abstract class CapabilitiesState with _$CapabilitiesState {
|
||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||
int? timetablePastDays,
|
||||
int? timetableFutureDays,
|
||||
// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||
// Wire value of the user type; read it through [role].
|
||||
String? userType,
|
||||
// Students a guardian may see; empty for all other accounts.
|
||||
@Default(<GuardianChild>[]) List<GuardianChild> children,
|
||||
// Whether a capability response (or a definitive failure) has been
|
||||
// observed at least once this session. Lets the UI distinguish "still
|
||||
// unknown" from "confirmed not allowed".
|
||||
@@ -22,4 +29,6 @@ abstract class CapabilitiesState with _$CapabilitiesState {
|
||||
|
||||
factory CapabilitiesState.fromJson(Map<String, Object?> json) =>
|
||||
_$CapabilitiesStateFromJson(json);
|
||||
|
||||
UserRole get role => UserRole.parse(userType);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: type=lint, type=warning, deprecated_member_use, deprecated_member_use_from_same_package
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'capabilities_state.dart';
|
||||
@@ -9,19 +9,14 @@ part of 'capabilities_state.dart';
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$CapabilitiesState {
|
||||
|
||||
bool get viewForeignTimetables; bool get pushNotifications;// Days into the past/future the timetable may be scrolled. Null = no
|
||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||
int? get timetablePastDays; int? get timetableFutureDays;// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||
String? get userType;// Whether a capability response (or a definitive failure) has been
|
||||
// observed at least once this session. Lets the UI distinguish "still
|
||||
// unknown" from "confirmed not allowed".
|
||||
bool get loaded;
|
||||
bool get viewForeignTimetables; bool get pushNotifications; int? get timetablePastDays; int? get timetableFutureDays; String? get userType; List<GuardianChild> get children; bool get loaded;
|
||||
/// Create a copy of CapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -34,16 +29,21 @@ $CapabilitiesStateCopyWith<CapabilitiesState> get copyWith => _$CapabilitiesStat
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
final _this = this as CapabilitiesState;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CapabilitiesState&&(identical(other.viewForeignTimetables, _this.viewForeignTimetables) || other.viewForeignTimetables == _this.viewForeignTimetables)&&(identical(other.pushNotifications, _this.pushNotifications) || other.pushNotifications == _this.pushNotifications)&&(identical(other.timetablePastDays, _this.timetablePastDays) || other.timetablePastDays == _this.timetablePastDays)&&(identical(other.timetableFutureDays, _this.timetableFutureDays) || other.timetableFutureDays == _this.timetableFutureDays)&&(identical(other.userType, _this.userType) || other.userType == _this.userType)&&const DeepCollectionEquality().equals(other.children, _this.children)&&(identical(other.loaded, _this.loaded) || other.loaded == _this.loaded));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||
int get hashCode {
|
||||
final _this = this as CapabilitiesState;
|
||||
return Object.hash(runtimeType,_this.viewForeignTimetables,_this.pushNotifications,_this.timetablePastDays,_this.timetableFutureDays,_this.userType,const DeepCollectionEquality().hash(_this.children),_this.loaded);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||
final _this = this as CapabilitiesState;
|
||||
return 'CapabilitiesState(viewForeignTimetables: ${_this.viewForeignTimetables}, pushNotifications: ${_this.pushNotifications}, timetablePastDays: ${_this.timetablePastDays}, timetableFutureDays: ${_this.timetableFutureDays}, userType: ${_this.userType}, children: ${_this.children}, loaded: ${_this.loaded})';
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ abstract mixin class $CapabilitiesStateCopyWith<$Res> {
|
||||
factory $CapabilitiesStateCopyWith(CapabilitiesState value, $Res Function(CapabilitiesState) _then) = _$CapabilitiesStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded
|
||||
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, List<GuardianChild> children, bool loaded
|
||||
});
|
||||
|
||||
|
||||
@@ -71,14 +71,15 @@ class _$CapabilitiesStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of CapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? loaded = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? children = null,Object? loaded = null,}) {
|
||||
return _then(CapabilitiesState(
|
||||
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
||||
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
||||
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
||||
as int?,timetableFutureDays: freezed == timetableFutureDays ? _self.timetableFutureDays : timetableFutureDays // ignore: cast_nullable_to_non_nullable
|
||||
as int?,userType: freezed == userType ? _self.userType : userType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
||||
as String?,children: null == children ? _self.children : children // ignore: cast_nullable_to_non_nullable
|
||||
as List<GuardianChild>,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
@@ -164,10 +165,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, List<GuardianChild> children, bool loaded)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CapabilitiesState() when $default != null:
|
||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.loaded);case _:
|
||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.children,_that.loaded);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -185,10 +186,10 @@ return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timeta
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, List<GuardianChild> children, bool loaded) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CapabilitiesState():
|
||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.loaded);case _:
|
||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.children,_that.loaded);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -205,10 +206,10 @@ return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timeta
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, List<GuardianChild> children, bool loaded)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _CapabilitiesState() when $default != null:
|
||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.loaded);case _:
|
||||
return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timetablePastDays,_that.timetableFutureDays,_that.userType,_that.children,_that.loaded);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -219,21 +220,22 @@ return $default(_that.viewForeignTimetables,_that.pushNotifications,_that.timeta
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _CapabilitiesState implements CapabilitiesState {
|
||||
const _CapabilitiesState({this.viewForeignTimetables = false, this.pushNotifications = false, this.timetablePastDays, this.timetableFutureDays, this.userType, this.loaded = false});
|
||||
class _CapabilitiesState extends CapabilitiesState {
|
||||
const _CapabilitiesState({this.viewForeignTimetables = false, this.pushNotifications = false, this.timetablePastDays, this.timetableFutureDays, this.userType, List<GuardianChild> children = const <GuardianChild>[], this.loaded = false}): _children = children,super._();
|
||||
factory _CapabilitiesState.fromJson(Map<String, dynamic> json) => _$CapabilitiesStateFromJson(json);
|
||||
|
||||
@override@JsonKey() final bool viewForeignTimetables;
|
||||
@override@JsonKey() final bool pushNotifications;
|
||||
// Days into the past/future the timetable may be scrolled. Null = no
|
||||
// client-side clamp; the (server-narrowed) school year alone governs.
|
||||
@override final int? timetablePastDays;
|
||||
@override final int? timetableFutureDays;
|
||||
// LDAP user type ('TEACHER' | 'STUDENT' | 'STAFF'); null = unknown.
|
||||
@override final String? userType;
|
||||
// Whether a capability response (or a definitive failure) has been
|
||||
// observed at least once this session. Lets the UI distinguish "still
|
||||
// unknown" from "confirmed not allowed".
|
||||
final List<GuardianChild> _children;
|
||||
@override@JsonKey() List<GuardianChild> get children {
|
||||
if (_children is EqualUnmodifiableListView) return _children;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_children);
|
||||
}
|
||||
|
||||
@override@JsonKey() final bool loaded;
|
||||
|
||||
/// Create a copy of CapabilitiesState
|
||||
@@ -249,16 +251,18 @@ Map<String, dynamic> toJson() {
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CapabilitiesState&&(identical(other.viewForeignTimetables, viewForeignTimetables) || other.viewForeignTimetables == viewForeignTimetables)&&(identical(other.pushNotifications, pushNotifications) || other.pushNotifications == pushNotifications)&&(identical(other.timetablePastDays, timetablePastDays) || other.timetablePastDays == timetablePastDays)&&(identical(other.timetableFutureDays, timetableFutureDays) || other.timetableFutureDays == timetableFutureDays)&&(identical(other.userType, userType) || other.userType == userType)&&const DeepCollectionEquality().equals(other.children, _children)&&(identical(other.loaded, loaded) || other.loaded == loaded));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,loaded);
|
||||
int get hashCode {
|
||||
return Object.hash(runtimeType,viewForeignTimetables,pushNotifications,timetablePastDays,timetableFutureDays,userType,const DeepCollectionEquality().hash(_children),loaded);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, loaded: $loaded)';
|
||||
return 'CapabilitiesState(viewForeignTimetables: $viewForeignTimetables, pushNotifications: $pushNotifications, timetablePastDays: $timetablePastDays, timetableFutureDays: $timetableFutureDays, userType: $userType, children: $children, loaded: $loaded)';
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +273,7 @@ abstract mixin class _$CapabilitiesStateCopyWith<$Res> implements $CapabilitiesS
|
||||
factory _$CapabilitiesStateCopyWith(_CapabilitiesState value, $Res Function(_CapabilitiesState) _then) = __$CapabilitiesStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, bool loaded
|
||||
bool viewForeignTimetables, bool pushNotifications, int? timetablePastDays, int? timetableFutureDays, String? userType, List<GuardianChild> children, bool loaded
|
||||
});
|
||||
|
||||
|
||||
@@ -286,14 +290,15 @@ class __$CapabilitiesStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of CapabilitiesState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? loaded = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? viewForeignTimetables = null,Object? pushNotifications = null,Object? timetablePastDays = freezed,Object? timetableFutureDays = freezed,Object? userType = freezed,Object? children = null,Object? loaded = null,}) {
|
||||
return _then(_CapabilitiesState(
|
||||
viewForeignTimetables: null == viewForeignTimetables ? _self.viewForeignTimetables : viewForeignTimetables // ignore: cast_nullable_to_non_nullable
|
||||
as bool,pushNotifications: null == pushNotifications ? _self.pushNotifications : pushNotifications // ignore: cast_nullable_to_non_nullable
|
||||
as bool,timetablePastDays: freezed == timetablePastDays ? _self.timetablePastDays : timetablePastDays // ignore: cast_nullable_to_non_nullable
|
||||
as int?,timetableFutureDays: freezed == timetableFutureDays ? _self.timetableFutureDays : timetableFutureDays // ignore: cast_nullable_to_non_nullable
|
||||
as int?,userType: freezed == userType ? _self.userType : userType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
||||
as String?,children: null == children ? _self._children : children // ignore: cast_nullable_to_non_nullable
|
||||
as List<GuardianChild>,loaded: null == loaded ? _self.loaded : loaded // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ _CapabilitiesState _$CapabilitiesStateFromJson(Map<String, dynamic> json) =>
|
||||
timetablePastDays: (json['timetablePastDays'] as num?)?.toInt(),
|
||||
timetableFutureDays: (json['timetableFutureDays'] as num?)?.toInt(),
|
||||
userType: json['userType'] as String?,
|
||||
children:
|
||||
(json['children'] as List<dynamic>?)
|
||||
?.map((e) => GuardianChild.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const <GuardianChild>[],
|
||||
loaded: json['loaded'] as bool? ?? false,
|
||||
);
|
||||
|
||||
@@ -23,5 +28,6 @@ Map<String, dynamic> _$CapabilitiesStateToJson(_CapabilitiesState instance) =>
|
||||
'timetablePastDays': instance.timetablePastDays,
|
||||
'timetableFutureDays': instance.timetableFutureDays,
|
||||
'userType': instance.userType,
|
||||
'children': instance.children,
|
||||
'loaded': instance.loaded,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:developer';
|
||||
|
||||
import 'package:flutter_app_badge/flutter_app_badge.dart';
|
||||
|
||||
import '../../../../../access/access_requirement.dart';
|
||||
import '../../../../../api/marianumcloud/talk/chat/get_chat_response.dart';
|
||||
import '../../../../../api/marianumcloud/talk/room/get_room_response.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
@@ -43,6 +44,11 @@ class ChatListBloc
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Set<AccessRequirement> get requirements => const {
|
||||
AccessRequirement.nextcloud,
|
||||
};
|
||||
|
||||
@override
|
||||
ChatListRepository repository() => ChatListRepository();
|
||||
|
||||
@@ -73,6 +79,7 @@ class ChatListBloc
|
||||
}
|
||||
|
||||
Future<void> refresh({bool renew = true, bool silent = false}) async {
|
||||
if (!requirementsMet) return;
|
||||
if (!silent) add(RefetchStarted<ChatListState>());
|
||||
Object? capturedError;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
import '../../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart';
|
||||
|
||||
/// The child a guardian is currently looking at. Shared by every module that
|
||||
/// shows per-child data (timetable, absence report, later messages), so
|
||||
/// switching the child in one place switches it everywhere.
|
||||
class ChildSelectionCubit extends HydratedCubit<String?> {
|
||||
ChildSelectionCubit() : super(null);
|
||||
|
||||
void select(String childId) => emit(childId);
|
||||
|
||||
Future<void> reset() async => emit(null);
|
||||
|
||||
@override
|
||||
String? fromJson(Map<String, dynamic> json) => json['childId'] as String?;
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(String? state) => {'childId': state};
|
||||
}
|
||||
|
||||
/// The selected child if it is still linked, otherwise the first one. Null
|
||||
/// when there are no children.
|
||||
GuardianChild? effectiveChild(
|
||||
List<GuardianChild> children,
|
||||
String? selectedId,
|
||||
) {
|
||||
if (children.isEmpty) return null;
|
||||
for (final child in children) {
|
||||
if (child.id == selectedId) return child;
|
||||
}
|
||||
return children.first;
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import 'dart:developer';
|
||||
|
||||
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 '../../../infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../../timetable/bloc/timetable_event.dart';
|
||||
import '../../timetable/bloc/timetable_state.dart';
|
||||
import '../repository/foreign_timetable_repository.dart';
|
||||
|
||||
/// Drives a foreign element's timetable. Mirrors `TimetableBloc`'s week-loading
|
||||
/// and navigation but loads weeks from the element endpoint, carries no custom
|
||||
/// events, and does not persist (page-scoped, recreated per element). Reuses
|
||||
/// [TimetableState] verbatim so the render pipeline is unchanged; `customEvents`
|
||||
/// stays null (the foreign view's `isReady` predicate ignores it).
|
||||
class ForeignTimetableBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
TimetableEvent,
|
||||
TimetableState,
|
||||
ForeignTimetableRepository
|
||||
> {
|
||||
|
||||
final TimetableElementType type;
|
||||
// Named `elementId` rather than `id` to avoid shadowing HydratedMixin's
|
||||
// `String get id` (the storage key), which a plain `int id` would illegally
|
||||
// override.
|
||||
final int elementId;
|
||||
final String title;
|
||||
|
||||
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
ForeignTimetableBloc({
|
||||
required this.type,
|
||||
required this.elementId,
|
||||
required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
ForeignTimetableRepository repository() => ForeignTimetableRepository();
|
||||
|
||||
@override
|
||||
TimetableState fromNothing() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
return TimetableState(
|
||||
startDate: _startOfWeek(reference),
|
||||
endDate: _endOfWeek(reference),
|
||||
);
|
||||
}
|
||||
|
||||
// Persistence disabled: page-scoped and element-specific, nothing worth
|
||||
// restoring. toJson returns null so nothing is written; fromJson starts fresh.
|
||||
@override
|
||||
Map<String, dynamic>? toJson(LoadableState<TimetableState> state) => null;
|
||||
|
||||
@override
|
||||
LoadableState<TimetableState> fromJson(Map<String, dynamic> json) =>
|
||||
const LoadableState(
|
||||
isLoading: true,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
);
|
||||
|
||||
@override
|
||||
TimetableState fromStorage(Map<String, dynamic> json) => fromNothing();
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(TimetableState state) => null;
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
final initial = innerState ?? fromNothing();
|
||||
|
||||
Object? firstError;
|
||||
void recordError(Object e) {
|
||||
firstError ??= e;
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
_loadCurrentWeek(initial.startDate, initial.endDate, onError: recordError),
|
||||
_loadStaticReferenceData(onError: recordError),
|
||||
]);
|
||||
|
||||
if (firstError != null) throw firstError!;
|
||||
|
||||
add(DataGathered((s) => s));
|
||||
_prefetchAdjacentWeeks(initial.startDate, initial.endDate);
|
||||
}
|
||||
|
||||
void changeWeek(DateTime startDate, DateTime endDate) {
|
||||
final current = innerState ?? fromNothing();
|
||||
if (current.startDate == startDate && current.endDate == endDate) return;
|
||||
add(Emit((s) => s.copyWith(startDate: startDate, endDate: endDate)));
|
||||
_loadCurrentWeek(startDate, endDate);
|
||||
_prefetchAdjacentWeeks(startDate, endDate);
|
||||
}
|
||||
|
||||
void resetWeek() {
|
||||
final reference = DateTime.now().addDays(2);
|
||||
changeWeek(_startOfWeek(reference), _endOfWeek(reference));
|
||||
}
|
||||
|
||||
void refresh() => fetch();
|
||||
|
||||
Future<void> _loadCurrentWeek(
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastWeekRequestStart = requestStart;
|
||||
try {
|
||||
final week = await repo.data.getElementWeek(
|
||||
type,
|
||||
elementId,
|
||||
startDate,
|
||||
endDate,
|
||||
onError: onError,
|
||||
);
|
||||
if (_lastWeekRequestStart.isAfter(requestStart)) return;
|
||||
_writeWeekToCache(startDate, week);
|
||||
} catch (e) {
|
||||
log('getElementWeek error for $startDate–$endDate: $e');
|
||||
onError?.call(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadStaticReferenceData({
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
try {
|
||||
final (rooms, subjects, schoolHolidays, schoolyear) = await (
|
||||
repo.data.getRooms(onError: onError),
|
||||
repo.data.getSubjects(onError: onError),
|
||||
repo.data.getSchoolHolidays(onError: onError),
|
||||
repo.data.getCurrentSchoolyear(onError: onError),
|
||||
).wait;
|
||||
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(
|
||||
rooms: rooms,
|
||||
subjects: subjects,
|
||||
schoolHolidays: schoolHolidays,
|
||||
schoolyear: schoolyear,
|
||||
dataVersion: s.dataVersion + 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final timegrid = await repo.data.getTimegrid();
|
||||
add(
|
||||
Emit(
|
||||
(s) => s.copyWith(timegrid: timegrid, dataVersion: s.dataVersion + 1),
|
||||
),
|
||||
);
|
||||
} catch (_) {
|
||||
// Timegrid load failure falls back to a hardcoded schedule in the UI.
|
||||
}
|
||||
}
|
||||
|
||||
void _prefetchAdjacentWeeks(DateTime start, DateTime end) {
|
||||
_prefetchWeek(start.subtractDays(7), end.subtractDays(7));
|
||||
_prefetchWeek(start.addDays(7), end.addDays(7));
|
||||
}
|
||||
|
||||
void _prefetchWeek(DateTime start, DateTime end) {
|
||||
repo.data
|
||||
.getElementWeek(type, elementId, start, end)
|
||||
.then((week) => _writeWeekToCache(start, week))
|
||||
.catchError((_) {});
|
||||
}
|
||||
|
||||
void _writeWeekToCache(DateTime weekStart, TimetableGetWeekResponse week) {
|
||||
final key = weekStart.weekKey();
|
||||
add(
|
||||
Emit((s) {
|
||||
final updated = Map<String, TimetableGetWeekResponse>.of(s.weekCache);
|
||||
updated[key] = week;
|
||||
return s.copyWith(weekCache: updated, dataVersion: s.dataVersion + 1);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static DateTime _startOfWeek(DateTime reference) {
|
||||
final monday = reference.subtractDays(reference.weekday - 1);
|
||||
return DateTime(monday.year, monday.month, monday.day);
|
||||
}
|
||||
|
||||
static DateTime _endOfWeek(DateTime reference) {
|
||||
final friday = reference.addDays(
|
||||
DateTime.daysPerWeek - reference.weekday - 2,
|
||||
);
|
||||
return DateTime(friday.year, friday.month, friday.day);
|
||||
}
|
||||
}
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_schoolyear/timetable_get_schoolyear_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_timegrid/timetable_get_timegrid_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../timetable/data_provider/timetable_data_provider.dart';
|
||||
|
||||
/// Data access for a foreign element's timetable. The week comes from the
|
||||
/// element-specific endpoint; all reference data (rooms/subjects/holidays/
|
||||
/// school year/timegrid) is school-wide, so it delegates to the existing
|
||||
/// [TimetableDataProvider] (which caches it). Custom events are intentionally
|
||||
/// absent — they are user-private.
|
||||
class ForeignTimetableDataProvider {
|
||||
final TimetableDataProvider _base;
|
||||
|
||||
ForeignTimetableDataProvider([TimetableDataProvider? base])
|
||||
: _base = base ?? TimetableDataProvider();
|
||||
|
||||
Future<TimetableGetWeekResponse> getElementWeek(
|
||||
TimetableElementType type,
|
||||
int id,
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetElementWeek().run(
|
||||
type: type,
|
||||
id: id,
|
||||
from: startDate,
|
||||
until: endDate,
|
||||
);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<TimetableGetRoomsResponse> getRooms({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getRooms(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetSubjectsResponse> getSubjects({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getSubjects(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetHolidaysResponse> getSchoolHolidays({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getSchoolHolidays(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetSchoolyearResponse> getCurrentSchoolyear({
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) => _base.getCurrentSchoolyear(onError: onError, renew: renew);
|
||||
|
||||
Future<TimetableGetTimegridResponse> getTimegrid({bool renew = false}) =>
|
||||
_base.getTimegrid(renew: renew);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import '../../../infrastructure/repository/repository.dart';
|
||||
import '../../timetable/bloc/timetable_state.dart';
|
||||
import '../data_provider/foreign_timetable_data_provider.dart';
|
||||
|
||||
class ForeignTimetableRepository extends Repository<TimetableState> {
|
||||
final ForeignTimetableDataProvider _provider;
|
||||
|
||||
ForeignTimetableRepository([ForeignTimetableDataProvider? provider])
|
||||
: _provider = provider ?? ForeignTimetableDataProvider();
|
||||
|
||||
ForeignTimetableDataProvider get data => _provider;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
import '../../../../../api/demo/data/demo_capabilities.dart';
|
||||
import '../../../../../api/demo/demo_mode.dart';
|
||||
import '../../../../../api/marianumcloud/capabilities/get_nextcloud_capabilities.dart';
|
||||
import '../../../../../session/session_manager.dart';
|
||||
import 'nextcloud_capabilities_state.dart';
|
||||
|
||||
/// Holds the current user's Nextcloud `files_sharing` capabilities. Hydrated so
|
||||
@@ -59,6 +60,7 @@ class NextcloudCapabilitiesCubit
|
||||
/// Refreshes capabilities from the server. On any failure the previously
|
||||
/// hydrated flags are kept but the state is marked `loaded`.
|
||||
Future<void> load() async {
|
||||
if (!SessionManager().hasNextcloud) return;
|
||||
if (DemoMode.active) {
|
||||
emit(DemoNextcloudCapabilities.state());
|
||||
return;
|
||||
|
||||
@@ -4,12 +4,17 @@ import '../../../../../api/marianumconnect/queries/timetable_custom_events/custo
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
|
||||
import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../../extensions/date_time.dart';
|
||||
import '../../../infrastructure/loadable_state/loadable_state.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc.dart';
|
||||
import '../../../infrastructure/utility_widgets/loadable_hydrated_bloc/loadable_hydrated_bloc_event.dart';
|
||||
import '../repository/timetable_repository.dart';
|
||||
import '../subject/timetable_subject.dart';
|
||||
import 'timetable_event.dart';
|
||||
import 'timetable_state.dart';
|
||||
|
||||
/// Drives one [TimetableSubject]'s plan. The same class serves the own plan
|
||||
/// and foreign element plans; everything subject-specific (endpoint,
|
||||
/// persistence, custom events) is derived from [subject].
|
||||
class TimetableBloc
|
||||
extends
|
||||
LoadableHydratedBloc<
|
||||
@@ -17,6 +22,13 @@ class TimetableBloc
|
||||
TimetableState,
|
||||
TimetableRepository
|
||||
> {
|
||||
final TimetableSubject subject;
|
||||
|
||||
TimetableBloc({required this.subject});
|
||||
|
||||
@override
|
||||
String get id => subject.storageId;
|
||||
|
||||
DateTime _lastWeekRequestStart = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
/// Set by [retry] to force the next [gatherData] to bypass cache freshness
|
||||
@@ -59,8 +71,25 @@ class TimetableBloc
|
||||
@override
|
||||
Map<String, dynamic>? toStorage(TimetableState state) => state.toJson();
|
||||
|
||||
@override
|
||||
Map<String, dynamic>? toJson(LoadableState<TimetableState> state) =>
|
||||
subject.persistent ? super.toJson(state) : null;
|
||||
|
||||
@override
|
||||
LoadableState<TimetableState> fromJson(Map<String, dynamic> json) =>
|
||||
subject.persistent
|
||||
? super.fromJson(json)
|
||||
: const LoadableState(
|
||||
isLoading: true,
|
||||
data: null,
|
||||
lastFetch: null,
|
||||
reFetch: null,
|
||||
error: null,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> gatherData() async {
|
||||
if (subject is NoTimetable) return;
|
||||
final initial = innerState ?? fromNothing();
|
||||
final renew = _forceRenew;
|
||||
_forceRenew = false;
|
||||
@@ -75,10 +104,10 @@ class TimetableBloc
|
||||
initial.startDate,
|
||||
initial.endDate,
|
||||
onError: recordError,
|
||||
renew: renew,
|
||||
),
|
||||
_loadStaticReferenceData(onError: recordError, renew: renew),
|
||||
_loadCustomEvents(onError: recordError, renew: renew),
|
||||
if (subject.supportsCustomEvents)
|
||||
_loadCustomEvents(onError: recordError, renew: renew),
|
||||
]);
|
||||
|
||||
if (firstError != null) throw firstError!;
|
||||
@@ -102,17 +131,28 @@ class TimetableBloc
|
||||
|
||||
void refresh() => fetch();
|
||||
|
||||
/// Custom events belong to the signed-in user's own plan only — never to a
|
||||
/// foreign plan or a guardian's view of a child.
|
||||
void _requireCustomEvents() {
|
||||
if (!subject.supportsCustomEvents) {
|
||||
throw StateError('Custom events are not available for $subject');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addCustomEvent(CustomTimetableEvent event) async {
|
||||
_requireCustomEvents();
|
||||
await repo.data.addCustomEvent(event);
|
||||
await _refreshCustomEvents();
|
||||
}
|
||||
|
||||
Future<void> updateCustomEvent(String id, CustomTimetableEvent event) async {
|
||||
_requireCustomEvents();
|
||||
await repo.data.updateCustomEvent(id, event);
|
||||
await _refreshCustomEvents();
|
||||
}
|
||||
|
||||
Future<void> removeCustomEvent(String id) async {
|
||||
_requireCustomEvents();
|
||||
await repo.data.removeCustomEvent(id);
|
||||
await _refreshCustomEvents();
|
||||
}
|
||||
@@ -142,16 +182,15 @@ class TimetableBloc
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
final requestStart = DateTime.now();
|
||||
_lastWeekRequestStart = requestStart;
|
||||
try {
|
||||
final week = await repo.data.getWeek(
|
||||
subject,
|
||||
startDate,
|
||||
endDate,
|
||||
onError: onError,
|
||||
renew: renew,
|
||||
);
|
||||
if (_lastWeekRequestStart.isAfter(requestStart)) return;
|
||||
_writeWeekToCache(startDate, week);
|
||||
@@ -237,7 +276,7 @@ class TimetableBloc
|
||||
|
||||
void _prefetchWeek(DateTime start, DateTime end) {
|
||||
repo.data
|
||||
.getWeek(start, end)
|
||||
.getWeek(subject, start, end)
|
||||
.then((week) => _writeWeekToCache(start, week))
|
||||
.catchError((_) {});
|
||||
}
|
||||
@@ -265,3 +304,11 @@ class TimetableBloc
|
||||
return DateTime(friday.year, friday.month, friday.day);
|
||||
}
|
||||
}
|
||||
|
||||
/// Type token for a page-scoped plan (e.g. a foreign element). Carries no
|
||||
/// logic of its own; the distinct type keeps a page-local provider from
|
||||
/// shadowing the app-wide [TimetableBloc] that sheets and root-navigator pages
|
||||
/// (subject colours, custom events) read.
|
||||
final class ScopedTimetableBloc extends TimetableBloc {
|
||||
ScopedTimetableBloc({required super.subject});
|
||||
}
|
||||
|
||||
@@ -40,9 +40,12 @@ abstract class TimetableState with _$TimetableState {
|
||||
Iterable<McTimetableEntry> getAllKnownLessons() =>
|
||||
weekCache.values.expand((response) => response.entries);
|
||||
|
||||
bool get hasReferenceData =>
|
||||
/// Whether the calendar has everything it needs to render. Custom events
|
||||
/// only exist for subjects that support them; requiring them elsewhere would
|
||||
/// keep foreign plans loading forever.
|
||||
bool isReady({required bool needsCustomEvents}) =>
|
||||
rooms != null &&
|
||||
subjects != null &&
|
||||
schoolHolidays != null &&
|
||||
customEvents != null;
|
||||
(!needsCustomEvents || customEvents != null);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import '../../../../../api/marianumconnect/queries/timetable_custom_events/timet
|
||||
import '../../../../../api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_cache.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_remove.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_custom_events/timetable_custom_events_update.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_child_week/timetable_get_child_week.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_get_element_week.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_rooms/timetable_get_rooms.dart';
|
||||
@@ -21,19 +23,43 @@ import '../../../../../api/marianumconnect/queries/timetable_subject_colors/time
|
||||
import '../../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
|
||||
import '../../../../../api/mhsl/custom_timetable_event/get/get_custom_timetable_event_response.dart';
|
||||
import '../../../../../api/request_cache.dart';
|
||||
import '../subject/timetable_subject.dart';
|
||||
|
||||
/// Pulls the timetable from the Marianum-Connect mobile API. Each endpoint is
|
||||
/// its own HTTP call; this provider exposes the lazy futures so the bloc can
|
||||
/// chain them without seeing the dio layer.
|
||||
/// chain them without seeing the dio layer. Only the week depends on the
|
||||
/// [TimetableSubject]; the reference data is school-wide.
|
||||
class TimetableDataProvider {
|
||||
/// The endpoint serving [subject]'s week. Shared with the widget background
|
||||
/// isolate, which has no bloc.
|
||||
static Future<TimetableGetWeekResponse> fetchWeek(
|
||||
TimetableSubject subject, {
|
||||
required DateTime from,
|
||||
required DateTime until,
|
||||
}) => switch (subject) {
|
||||
OwnTimetable() => TimetableGetWeek().run(from: from, until: until),
|
||||
ElementTimetable(:final element) => TimetableGetElementWeek().run(
|
||||
type: element.type,
|
||||
id: element.id,
|
||||
from: from,
|
||||
until: until,
|
||||
),
|
||||
ChildTimetable(:final childId) => TimetableGetChildWeek().run(
|
||||
childId: childId,
|
||||
from: from,
|
||||
until: until,
|
||||
),
|
||||
NoTimetable() => throw StateError('No timetable subject'),
|
||||
};
|
||||
|
||||
Future<TimetableGetWeekResponse> getWeek(
|
||||
TimetableSubject subject,
|
||||
DateTime startDate,
|
||||
DateTime endDate, {
|
||||
void Function(Object)? onError,
|
||||
bool renew = false,
|
||||
}) async {
|
||||
try {
|
||||
return await TimetableGetWeek().run(from: startDate, until: endDate);
|
||||
return await fetchWeek(subject, from: startDate, until: endDate);
|
||||
} catch (e) {
|
||||
onError?.call(e);
|
||||
rethrow;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import '../../../../../access/user_role.dart';
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
|
||||
import '../../capabilities/bloc/capabilities_state.dart';
|
||||
import '../subject/timetable_subject.dart';
|
||||
|
||||
/// What the timetable view offers for a given subject. Resolved in one place
|
||||
/// so the view never branches on roles or subject types itself.
|
||||
class TimetablePolicy {
|
||||
final bool canManageCustomEvents;
|
||||
final bool canEditSubjectColors;
|
||||
final bool showClassInsteadOfTeacher;
|
||||
final bool canOpenForeign;
|
||||
|
||||
const TimetablePolicy({
|
||||
required this.canManageCustomEvents,
|
||||
required this.canEditSubjectColors,
|
||||
required this.showClassInsteadOfTeacher,
|
||||
required this.canOpenForeign,
|
||||
});
|
||||
|
||||
static TimetablePolicy resolve({
|
||||
required TimetableSubject subject,
|
||||
required CapabilitiesState capabilities,
|
||||
}) => switch (subject) {
|
||||
OwnTimetable() => TimetablePolicy(
|
||||
canManageCustomEvents: true,
|
||||
canEditSubjectColors: true,
|
||||
showClassInsteadOfTeacher: capabilities.role == UserRole.teacher,
|
||||
canOpenForeign: capabilities.viewForeignTimetables,
|
||||
),
|
||||
// Subject colours are the viewer's own, global setting; editing them from
|
||||
// a foreign plan would not refresh that plan, so it is not offered there.
|
||||
ElementTimetable(:final element) => TimetablePolicy(
|
||||
canManageCustomEvents: false,
|
||||
canEditSubjectColors: false,
|
||||
showClassInsteadOfTeacher: element.type == TimetableElementType.teacher,
|
||||
canOpenForeign: capabilities.viewForeignTimetables,
|
||||
),
|
||||
// Custom events are the child's private data; subject colours are the
|
||||
// guardian's own and apply to this (primary) plan directly.
|
||||
ChildTimetable() => TimetablePolicy(
|
||||
canManageCustomEvents: false,
|
||||
canEditSubjectColors: true,
|
||||
showClassInsteadOfTeacher: false,
|
||||
canOpenForeign: capabilities.viewForeignTimetables,
|
||||
),
|
||||
NoTimetable() => const TimetablePolicy(
|
||||
canManageCustomEvents: false,
|
||||
canEditSubjectColors: false,
|
||||
showClassInsteadOfTeacher: false,
|
||||
canOpenForeign: false,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import '../../../../../api/marianumconnect/queries/get_capabilities/guardian_child.dart';
|
||||
import '../../../../../session/session.dart';
|
||||
import '../../children/child_selection_cubit.dart';
|
||||
import '../subject/timetable_subject.dart';
|
||||
|
||||
/// Whose plan the timetable tab shows for the active session.
|
||||
TimetableSubject resolvePrimarySubject({
|
||||
required Session? session,
|
||||
required List<GuardianChild> children,
|
||||
required String? selectedChildId,
|
||||
}) => switch (session) {
|
||||
null => const NoTimetable(),
|
||||
CredentialSession() => const OwnTimetable(),
|
||||
GuardianSession() => switch (effectiveChild(children, selectedChildId)) {
|
||||
null => const NoTimetable(),
|
||||
final child => ChildTimetable(child.id),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../../session/session_manager.dart';
|
||||
import '../../account/bloc/account_bloc.dart';
|
||||
import '../../account/bloc/account_state.dart';
|
||||
import '../../capabilities/bloc/capabilities_cubit.dart';
|
||||
import '../../capabilities/bloc/capabilities_state.dart';
|
||||
import '../../children/child_selection_cubit.dart';
|
||||
import '../bloc/timetable_bloc.dart';
|
||||
import '../subject/timetable_subject.dart';
|
||||
import 'primary_subject_resolver.dart';
|
||||
|
||||
/// Provides the app-wide [TimetableBloc] for the session's primary subject
|
||||
/// (own plan, or the selected child for guardians) and swaps it for a fresh
|
||||
/// instance when that subject changes.
|
||||
///
|
||||
/// Sits above MaterialApp so root-navigator pages (subject colours, custom
|
||||
/// events) reach it. The widget subtree is kept on a swap — only the provided
|
||||
/// instance changes, which BlocBuilder/BlocListener pick up — so switching
|
||||
/// the child does not reset the navigation. A new instance per subject (rather
|
||||
/// than retargeting one bloc) keeps late responses for the previous child out
|
||||
/// of the new child's week cache.
|
||||
class PrimaryTimetableScope extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const PrimaryTimetableScope({required this.child, super.key});
|
||||
|
||||
@override
|
||||
State<PrimaryTimetableScope> createState() => _PrimaryTimetableScopeState();
|
||||
}
|
||||
|
||||
class _PrimaryTimetableScopeState extends State<PrimaryTimetableScope> {
|
||||
late TimetableBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_bloc = TimetableBloc(subject: _resolve());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
TimetableSubject _resolve() => resolvePrimarySubject(
|
||||
session: SessionManager().current,
|
||||
children: context.read<CapabilitiesCubit>().state.children,
|
||||
selectedChildId: context.read<ChildSelectionCubit>().state,
|
||||
);
|
||||
|
||||
void _sync() {
|
||||
final subject = _resolve();
|
||||
if (subject == _bloc.subject) return;
|
||||
final previous = _bloc;
|
||||
setState(() => _bloc = TimetableBloc(subject: subject));
|
||||
// Dependents re-subscribe during the next build; close afterwards.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => previous.close());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<AccountBloc, AccountState>(
|
||||
listenWhen: (a, b) => a.status != b.status,
|
||||
listener: (_, _) => _sync(),
|
||||
),
|
||||
BlocListener<CapabilitiesCubit, CapabilitiesState>(
|
||||
listenWhen: (a, b) => a.children != b.children,
|
||||
listener: (_, _) => _sync(),
|
||||
),
|
||||
BlocListener<ChildSelectionCubit, String?>(listener: (_, _) => _sync()),
|
||||
],
|
||||
child: BlocProvider<TimetableBloc>.value(
|
||||
value: _bloc,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import '../../../../../api/marianumconnect/queries/timetable_get_element_week/timetable_element_type.dart';
|
||||
|
||||
/// Whose timetable a [TimetableBloc] shows. The render pipeline is identical
|
||||
/// for every subject; only the week endpoint, persistence and the
|
||||
/// user-private extras (custom events) differ.
|
||||
sealed class TimetableSubject {
|
||||
const TimetableSubject();
|
||||
|
||||
/// Suffix of the hydrated storage slot. Must be unique per subject so
|
||||
/// subjects never overwrite each other's cached weeks.
|
||||
String get storageId;
|
||||
|
||||
/// Whether the bloc keeps its state across app restarts.
|
||||
bool get persistent;
|
||||
|
||||
/// Custom events are user-private and only exist for the own plan.
|
||||
bool get supportsCustomEvents;
|
||||
}
|
||||
|
||||
/// The signed-in user's own plan (`timetable/me`).
|
||||
final class OwnTimetable extends TimetableSubject {
|
||||
const OwnTimetable();
|
||||
|
||||
// Empty on purpose: keeps the pre-existing storage slot "TimetableBloc", so
|
||||
// updating the app does not drop the cached weeks.
|
||||
@override
|
||||
String get storageId => '';
|
||||
|
||||
@override
|
||||
bool get persistent => true;
|
||||
|
||||
@override
|
||||
bool get supportsCustomEvents => true;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is OwnTimetable;
|
||||
|
||||
@override
|
||||
int get hashCode => (OwnTimetable).hashCode;
|
||||
}
|
||||
|
||||
/// A foreign element picked by the user (teacher, room, class, student).
|
||||
final class ElementTimetable extends TimetableSubject {
|
||||
final TimetableElementRef element;
|
||||
|
||||
const ElementTimetable(this.element);
|
||||
|
||||
@override
|
||||
String get storageId => 'element-${element.type.name}-${element.id}';
|
||||
|
||||
@override
|
||||
bool get persistent => false;
|
||||
|
||||
@override
|
||||
bool get supportsCustomEvents => false;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ElementTimetable &&
|
||||
other.element.type == element.type &&
|
||||
other.element.id == element.id;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(element.type, element.id);
|
||||
}
|
||||
|
||||
/// A guardian's child (`timetable/child/{id}`). Kept across restarts per
|
||||
/// child, so switching between siblings shows the cached plan immediately.
|
||||
final class ChildTimetable extends TimetableSubject {
|
||||
final String childId;
|
||||
|
||||
const ChildTimetable(this.childId);
|
||||
|
||||
@override
|
||||
String get storageId => 'child-$childId';
|
||||
|
||||
@override
|
||||
bool get persistent => true;
|
||||
|
||||
@override
|
||||
bool get supportsCustomEvents => false;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ChildTimetable && other.childId == childId;
|
||||
|
||||
@override
|
||||
int get hashCode => childId.hashCode;
|
||||
}
|
||||
|
||||
/// No plan to show: signed out, or a guardian without (known) children. The
|
||||
/// bloc loads nothing; the view explains why.
|
||||
final class NoTimetable extends TimetableSubject {
|
||||
const NoTimetable();
|
||||
|
||||
@override
|
||||
String get storageId => 'none';
|
||||
|
||||
@override
|
||||
bool get persistent => false;
|
||||
|
||||
@override
|
||||
bool get supportsCustomEvents => false;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is NoTimetable;
|
||||
|
||||
@override
|
||||
int get hashCode => (NoTimetable).hashCode;
|
||||
}
|
||||
Reference in New Issue
Block a user