diff --git a/school_data_hub_flutter/lib/core/init/init_on_active_env.dart b/school_data_hub_flutter/lib/core/init/init_on_active_env.dart index 1e194af5..0d52e0ac 100644 --- a/school_data_hub_flutter/lib/core/init/init_on_active_env.dart +++ b/school_data_hub_flutter/lib/core/init/init_on_active_env.dart @@ -52,6 +52,9 @@ class InitOnActiveEnv { ); // Register BottomNavManager in active environment scope so it's always available - di.registerSingleton(BottomNavManager()); + di.registerSingleton( + BottomNavManager(), + dispose: (bottomNavManager) => bottomNavManager.dispose(), + ); } } diff --git a/school_data_hub_flutter/lib/features/_schoolday_events/presentation/schoolday_event_list_page/schoolday_event_list_page.dart b/school_data_hub_flutter/lib/features/_schoolday_events/presentation/schoolday_event_list_page/schoolday_event_list_page.dart index 91d85b97..3b953bb6 100644 --- a/school_data_hub_flutter/lib/features/_schoolday_events/presentation/schoolday_event_list_page/schoolday_event_list_page.dart +++ b/school_data_hub_flutter/lib/features/_schoolday_events/presentation/schoolday_event_list_page/schoolday_event_list_page.dart @@ -18,7 +18,6 @@ class SchooldayEventListPage extends WatchingWidget { @override Widget build(BuildContext context) { - final _schooldayEventManager = di(); //-TODO: Filterstate in SchooldayEventSearchbar and SchooldayEventListPageBottomNavBar List pupils = watchValue((PupilsFilter x) => x.filteredPupils); @@ -30,7 +29,8 @@ class SchooldayEventListPage extends WatchingWidget { title: 'Ereignisse', ), body: RefreshIndicator( - onRefresh: () async => _schooldayEventManager.fetchSchooldayEvents(), + onRefresh: () async => + di().fetchSchooldayEvents(), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 700), diff --git a/school_data_hub_flutter/lib/features/app_main_navigation/domain/main_menu_bottom_nav_manager.dart b/school_data_hub_flutter/lib/features/app_main_navigation/domain/main_menu_bottom_nav_manager.dart index 2575d8a7..3500a4a2 100644 --- a/school_data_hub_flutter/lib/features/app_main_navigation/domain/main_menu_bottom_nav_manager.dart +++ b/school_data_hub_flutter/lib/features/app_main_navigation/domain/main_menu_bottom_nav_manager.dart @@ -1,4 +1,7 @@ import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +final _log = Logger('[BottomNavManager]'); class BottomNavManager { final _bottomNavState = ValueNotifier(0); @@ -17,5 +20,12 @@ class BottomNavManager { void setPupilProfileNavPage(int index) { _pupilProfileNavState.value = index; + _log.info('PupilProfileNavPage set to $index'); + } + + void dispose() { + _bottomNavState.dispose(); + _pupilProfileNavState.dispose(); + _log.info('BottomNavManager disposed'); } } diff --git a/school_data_hub_flutter/lib/features/learning_support/presentation/learning_support_list_page/widgets/learning_support_list_card.dart b/school_data_hub_flutter/lib/features/learning_support/presentation/learning_support_list_page/widgets/learning_support_list_card.dart index 6fe9bf7e..d08c77d6 100644 --- a/school_data_hub_flutter/lib/features/learning_support/presentation/learning_support_list_page/widgets/learning_support_list_card.dart +++ b/school_data_hub_flutter/lib/features/learning_support/presentation/learning_support_list_page/widgets/learning_support_list_card.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:school_data_hub_flutter/core/models/datetime_extensions.dart'; import 'package:school_data_hub_flutter/common/theme/app_colors.dart'; import 'package:school_data_hub_flutter/common/widgets/custom_expansion_tile/custom_expansion_tile.dart'; import 'package:school_data_hub_flutter/common/widgets/custom_expansion_tile/custom_expansion_tile_content.dart'; +import 'package:school_data_hub_flutter/core/models/datetime_extensions.dart'; import 'package:school_data_hub_flutter/features/app_main_navigation/domain/main_menu_bottom_nav_manager.dart'; -import 'package:school_data_hub_flutter/features/learning_support/domain/learning_support_helper.dart'; import 'package:school_data_hub_flutter/features/learning_support/presentation/learning_support_list_page/widgets/support_category_status_batches.dart'; import 'package:school_data_hub_flutter/features/learning_support/presentation/learning_support_list_page/widgets/support_goals_list.dart'; import 'package:school_data_hub_flutter/features/pupil/domain/models/pupil_proxy.dart'; @@ -14,8 +13,6 @@ import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profil import 'package:school_data_hub_flutter/features/pupil/presentation/widgets/avatar.dart'; import 'package:watch_it/watch_it.dart'; -final _mainMenuBottomNavManager = di(); - class LearningSupportCard extends WatchingStatefulWidget { final PupilProxy pupil; const LearningSupportCard(this.pupil, {super.key}); @@ -63,12 +60,9 @@ class _LearningSupportCardState extends State { scrollDirection: Axis.horizontal, child: InkWell( onTap: () { - _mainMenuBottomNavManager - .setPupilProfileNavPage( - ProfileNavigationState - .learningSupport - .value, - ); + di().setPupilProfileNavPage( + ProfileNavigationState.learningSupport.value, + ); Navigator.of(context).push( MaterialPageRoute( builder: (ctx) => diff --git a/school_data_hub_flutter/lib/features/pupil/domain/pupil_identity_stream_suscription.dart b/school_data_hub_flutter/lib/features/pupil/domain/pupil_identity_stream_suscription.dart index 514ccdac..30929f1c 100644 --- a/school_data_hub_flutter/lib/features/pupil/domain/pupil_identity_stream_suscription.dart +++ b/school_data_hub_flutter/lib/features/pupil/domain/pupil_identity_stream_suscription.dart @@ -52,6 +52,17 @@ class PupilIdentityStream { .listen( (PupilIdentityDto event) async { final eventSender = event.sender; + + // Defensive null check - validate sender before processing + if (eventSender == null || eventSender.isEmpty) { + _log.severe( + '[${role.name.toUpperCase()}]: Received event with NULL/EMPTY sender! ' + 'Type: ${event.type}, Value: ${event.value}. Skipping malformed event.', + ); + // Skip processing this malformed event + return; + } + _log.info( '[${role.name.toUpperCase()}]: [${event.type}] Received event from $eventSender', ); @@ -94,6 +105,16 @@ class PupilIdentityStream { final targetUser = event.value.isNotEmpty ? event.value : null; + + // Validate user session before sending data + + if (currentUser == null || currentUser.isEmpty) { + _log.severe( + '[${role.name.toUpperCase()}]: Cannot send data - username is null or empty', + ); + break; + } + if (targetUser == null) { // Legacy: no targeting, proceed for any receiver if (onRequestConfirmed != null) { @@ -103,8 +124,7 @@ class PupilIdentityStream { await _client.pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: - di().user!.userInfo!.userName!, + sender: currentUser, dataTimeStamp: di().activeEnv?.lastIdentitiesUpdate, type: 'data', @@ -126,8 +146,7 @@ class PupilIdentityStream { await _client.pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: - di().user!.userInfo!.userName!, + sender: currentUser, type: 'data', dataTimeStamp: di().activeEnv?.lastIdentitiesUpdate, @@ -283,12 +302,21 @@ class PupilIdentityStream { lastIdentitiesUpdate: event.dataTimeStamp?.toUtc(), ); + // Validate user session before sending confirmation + final confirmUser = + di().user?.userInfo?.userName; + if (confirmUser == null || confirmUser.isEmpty) { + _log.severe( + '[${role.name.toUpperCase()}]: Cannot send confirmation - username is null or empty', + ); + break; + } + // Send confirmation await _client.pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: - di().user!.userInfo!.userName!, + sender: confirmUser, type: 'ok', value: '', ), diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/_credit/credit_list_page/widgets/credit_list_card.dart b/school_data_hub_flutter/lib/features/pupil/presentation/_credit/credit_list_page/widgets/credit_list_card.dart index 86309689..efe73802 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/_credit/credit_list_page/widgets/credit_list_card.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/_credit/credit_list_page/widgets/credit_list_card.dart @@ -10,8 +10,6 @@ import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profil import 'package:school_data_hub_flutter/features/pupil/presentation/widgets/avatar.dart'; import 'package:watch_it/watch_it.dart'; -final _mainMenuBottomNavManager = di(); - class CreditListCard extends WatchingWidget { final PupilProxy pupil; const CreditListCard(this.pupil, {super.key}); @@ -57,8 +55,9 @@ class CreditListCard extends WatchingWidget { scrollDirection: Axis.horizontal, child: InkWell( onTap: () { - _mainMenuBottomNavManager - .setPupilProfileNavPage(2); + di().setPupilProfileNavPage( + 2, + ); Navigator.of(context).push( MaterialPageRoute( builder: (ctx) => diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_identity_stream_page/controllers/stream_controller.dart b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_identity_stream_page/controllers/stream_controller.dart index 7acf16d4..9541d6e0 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_identity_stream_page/controllers/stream_controller.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_identity_stream_page/controllers/stream_controller.dart @@ -22,8 +22,6 @@ class PupilIdentityStreamController { final String? importedChannelName; late String channelName; - final String thisUserName = di().user!.userInfo!.userName!; - final _notificationService = di(); // Controller creates and owns the state @@ -107,6 +105,18 @@ class PupilIdentityStreamController { } } + /// Validate user session before sending a message to prevent null sender + String? _validateUserSession() { + final currentUser = di().user?.userInfo?.userName; + if (currentUser == null || currentUser.isEmpty) { + _log.severe( + 'Cannot send message: username is null or empty. User session may have expired.', + ); + return null; + } + return currentUser; + } + /// Reset sender state for new requests void resetSenderForNewRequest() { if (_isDisposed) { @@ -147,11 +157,20 @@ class PupilIdentityStreamController { // Update local state to transmitting _handleRequestConfirmed(); + // Validate user session before sending + final validatedSender = _validateUserSession(); + if (validatedSender == null) { + _log.severe( + 'Cannot confirm transfer for $userName: invalid user session', + ); + return; + } + // Notify receiver of confirmation (backward compatibility) await di().pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: thisUserName, + sender: validatedSender, type: 'confirmed', value: userName, ), @@ -161,7 +180,7 @@ class PupilIdentityStreamController { await di().pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: thisUserName, + sender: validatedSender, type: 'data', dataTimeStamp: di().activeEnv?.lastIdentitiesUpdate, value: '$userName:${dataToSend ?? ''}', @@ -207,6 +226,13 @@ class PupilIdentityStreamController { String userName, { bool isAutoRejection = false, }) async { + // Validate user session before sending + final validatedSender = _validateUserSession(); + if (validatedSender == null) { + _log.severe('Cannot send rejection to $userName: invalid user session'); + return; + } + final rejectionValue = isAutoRejection ? 'auto:$userName' : userName; int retryCount = 0; const maxRetries = 3; @@ -220,7 +246,7 @@ class PupilIdentityStreamController { await di().pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: thisUserName, + sender: validatedSender, type: 'rejected', value: rejectionValue, ), @@ -314,14 +340,21 @@ class PupilIdentityStreamController { void _sendReceiverMessages() { _log.info('Receiver sending joined message and data request...'); + // Validate user session before sending + final validatedSender = _validateUserSession(); + if (validatedSender == null) { + _log.severe('Cannot send receiver messages: invalid user session'); + return; + } + // First send joined message di().pupilIdentity .sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: thisUserName, + sender: validatedSender, type: 'joined', - value: thisUserName, + value: validatedSender, ), ) .then((_) { @@ -345,9 +378,9 @@ class PupilIdentityStreamController { return di().pupilIdentity.sendPupilIdentityMessage( channelName, PupilIdentityDto( - sender: thisUserName, + sender: validatedSender, type: 'request', - value: thisUserName, + value: validatedSender, ), ); }) @@ -557,17 +590,21 @@ class PupilIdentityStreamController { // If receiver is leaving, send close message before disposing if (role == PupilIdentityStreamRole.receiver && state.streamState.isConnected.value) { - // Send close message and ignore errors since we're disposing - di().pupilIdentity - .sendPupilIdentityMessage( - channelName, - PupilIdentityDto( - sender: thisUserName, - type: 'close', - value: thisUserName, - ), - ) - .ignore(); + // Validate user session before sending + final validatedSender = _validateUserSession(); + if (validatedSender != null) { + // Send close message and ignore errors since we're disposing + di().pupilIdentity + .sendPupilIdentityMessage( + channelName, + PupilIdentityDto( + sender: validatedSender, + type: 'close', + value: validatedSender, + ), + ) + .ignore(); + } } _subscription?.cancel(); @@ -581,43 +618,50 @@ class PupilIdentityStreamController { } void stopStream() async { + // Validate user session before sending any messages + final validatedSender = _validateUserSession(); + // If sender is shutting down, notify all receivers if (role == PupilIdentityStreamRole.sender && state.streamState.isConnected.value) { - try { - // Send shutdown message to all connected receivers - await di().pupilIdentity.sendPupilIdentityMessage( - channelName, - PupilIdentityDto( - sender: thisUserName, - type: 'shutdown', - value: 'Sender hat den Stream beendet', - ), - ); - _log.info('Sent shutdown message to all receivers'); + if (validatedSender != null) { + try { + // Send shutdown message to all connected receivers + await di().pupilIdentity.sendPupilIdentityMessage( + channelName, + PupilIdentityDto( + sender: validatedSender, + type: 'shutdown', + value: 'Sender hat den Stream beendet', + ), + ); + _log.info('Sent shutdown message to all receivers'); - // Wait a brief moment to ensure message is sent - await Future.delayed(const Duration(milliseconds: 200)); - } catch (e) { - _log.warning('Failed to send shutdown message: $e'); + // Wait a brief moment to ensure message is sent + await Future.delayed(const Duration(milliseconds: 200)); + } catch (e) { + _log.warning('Failed to send shutdown message: $e'); + } } } // If receiver is leaving, send close message to notify sender if (role == PupilIdentityStreamRole.receiver && state.streamState.isConnected.value) { - try { - await di().pupilIdentity.sendPupilIdentityMessage( - channelName, - PupilIdentityDto( - sender: thisUserName, - type: 'close', - value: thisUserName, - ), - ); - _log.info('Sent close message to sender before leaving'); - } catch (e) { - _log.warning('Failed to send close message: $e'); + if (validatedSender != null) { + try { + await di().pupilIdentity.sendPupilIdentityMessage( + channelName, + PupilIdentityDto( + sender: validatedSender, + type: 'close', + value: validatedSender, + ), + ); + _log.info('Sent close message to sender before leaving'); + } catch (e) { + _log.warning('Failed to send close message: $e'); + } } } diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_navigation.dart b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_navigation.dart index 2cb47ab5..da8a3f04 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_navigation.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_navigation.dart @@ -5,8 +5,6 @@ import 'package:school_data_hub_flutter/common/theme/app_colors.dart'; import 'package:school_data_hub_flutter/features/app_main_navigation/domain/main_menu_bottom_nav_manager.dart'; import 'package:watch_it/watch_it.dart'; -final _mainMenuBottomNavManager = di(); - enum ProfileNavigationState { info(0), language(1), @@ -77,7 +75,7 @@ class PupilProfileNavigation extends WatchingWidget { if (isSelected) { return; } - _mainMenuBottomNavManager.setPupilProfileNavPage(state.value); + di().setPupilProfileNavPage(state.value); }, child: child, ); @@ -124,7 +122,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.info_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.info.value @@ -142,7 +140,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.language_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.language.value @@ -160,7 +158,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.attach_money_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.credit.value @@ -178,7 +176,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.calendar_month_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.attendance.value @@ -196,7 +194,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.warning_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.schooldayEvent.value @@ -222,7 +220,7 @@ class PupilProfileNavigation extends WatchingWidget { 'OGS', style: TextStyle( color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.ogs.value @@ -243,7 +241,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.rule, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.lists.value @@ -261,7 +259,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.fact_check_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.authorization.value @@ -279,7 +277,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.support_rounded, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.learningSupport.value @@ -297,7 +295,7 @@ class PupilProfileNavigation extends WatchingWidget { child: Icon( Icons.lightbulb, color: - _mainMenuBottomNavManager + di() .pupilProfileNavState .value == ProfileNavigationState.learning.value diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/infos_content/pupil_profile_infos_content.dart b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/infos_content/pupil_profile_infos_content.dart index fbe0f087..ea455994 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/infos_content/pupil_profile_infos_content.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/infos_content/pupil_profile_infos_content.dart @@ -7,6 +7,7 @@ import 'package:school_data_hub_flutter/common/theme/app_colors.dart'; import 'package:school_data_hub_flutter/common/widgets/dialogs/confirmation_dialog.dart'; import 'package:school_data_hub_flutter/common/widgets/dialogs/long_textfield_dialog.dart'; import 'package:school_data_hub_flutter/common/widgets/dialogs/short_textfield_dialog.dart'; +import 'package:school_data_hub_flutter/core/init/init_manager.dart'; import 'package:school_data_hub_flutter/core/models/datetime_extensions.dart'; import 'package:school_data_hub_flutter/core/session/hub_session_manager.dart'; import 'package:school_data_hub_flutter/features/matrix/domain/matrix_policy_helper.dart'; @@ -33,10 +34,10 @@ class PupilProfileInfosContent extends WatchingWidget { @override Widget build(BuildContext context) { - final _pupilManager = di(); + final pupilManager = di(); - final _hubSessionManager = di(); - final pupilSiblings = _pupilManager.getSiblings(pupil); + final hubSessionManager = di(); + final pupilSiblings = pupilManager.getSiblings(pupil); watch(pupil); return Container( decoration: BoxDecoration(color: AppColors.pupilProfileBackgroundColor), @@ -188,7 +189,12 @@ class PupilProfileInfosContent extends WatchingWidget { pupil.contact == null || pupil.contact!.isEmpty ? IconButton( onPressed: () async { - if (_isMatrixAuthorized == false) return; + if (_isMatrixAuthorized() == false) { + di().showInformationDialog( + 'Keine Berechtigung. Admin-Rechte erforderlich.', + ); + return; + } final confirm = await confirmationDialog( context: context, title: 'Matrix-Admindaten erstellen', @@ -196,19 +202,21 @@ class PupilProfileInfosContent extends WatchingWidget { 'Möchten Sie die Matrix-Admindaten wirklich erstellen?', ); if (confirm != true) return; - Navigator.of(context).push( - MaterialPageRoute( - builder: (ctx) => NewMatrixUserPage( - pupil: pupil, - matrixId: - MatrixPolicyHelper.generateMatrixId( - isParent: false, - ), - displayName: - '${pupil.firstName} ${pupil.lastName.substring(0, 1).toUpperCase()}. (${pupil.group})', + if (context.mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => NewMatrixUserPage( + pupil: pupil, + matrixId: + MatrixPolicyHelper.generateMatrixId( + isParent: false, + ), + displayName: + '${pupil.firstName} ${pupil.lastName.substring(0, 1).toUpperCase()}. (${pupil.group})', + ), ), - ), - ); + ); + } }, icon: Icon( Icons.add_circle_outline, @@ -218,7 +226,12 @@ class PupilProfileInfosContent extends WatchingWidget { ) : IconButton( onPressed: () async { - if (_isMatrixAuthorized == false) return; + if (_isMatrixAuthorized() == false) { + di().showInformationDialog( + 'Keine Berechtigung. Admin-Rechte erforderlich.', + ); + return; + } final confirmation = await confirmationDialog( context: context, @@ -232,6 +245,9 @@ class PupilProfileInfosContent extends WatchingWidget { context, ); if (logOutDevices == null) return; + if (!di.isRegistered()) { + await InitManager.registerMatrixManagers(); + } final file = await di() .users .resetPasswordAndPrintCredentialsFile( @@ -291,7 +307,7 @@ class PupilProfileInfosContent extends WatchingWidget { ) : TutorInfo( parentsContact: parentsContact, - createdBy: _hubSessionManager.userName!, + createdBy: hubSessionManager.userName!, ), ); }, @@ -301,7 +317,7 @@ class PupilProfileInfosContent extends WatchingWidget { String? pupilSiblingsGroups; if (pupil.family != null) { pupilSiblingsGroups = [ - ..._pupilManager.getSiblings(pupil), + ...pupilManager.getSiblings(pupil), pupil, ].map((e) => e.group).toList().join(); } @@ -329,7 +345,7 @@ class PupilProfileInfosContent extends WatchingWidget { ) : IconButton( onPressed: () async { - if (_isMatrixAuthorized == false) return; + if (_isMatrixAuthorized() == false) return; final confirmation = await confirmationDialog( context: context, title: 'Passwort zurücksetzen', @@ -342,6 +358,9 @@ class PupilProfileInfosContent extends WatchingWidget { context, ); if (logOutDevices == null) return; + if (!di.isRegistered()) { + await InitManager.registerMatrixManagers(); + } final file = await di() .users .resetPasswordAndPrintCredentialsFile( diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/special_info_page/widgets/special_info_card.dart b/school_data_hub_flutter/lib/features/pupil/presentation/special_info_page/widgets/special_info_card.dart index 070f2f19..8d08abec 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/special_info_page/widgets/special_info_card.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/special_info_page/widgets/special_info_card.dart @@ -7,9 +7,6 @@ import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profil import 'package:school_data_hub_flutter/features/pupil/presentation/widgets/avatar.dart'; import 'package:watch_it/watch_it.dart'; -final _mainMenuBottomNavManager = di(); -final _filterStateManager = di(); - class SpecialInfoCard extends WatchingWidget { final PupilProxy pupil; const SpecialInfoCard(this.pupil, {super.key}); @@ -20,8 +17,12 @@ class SpecialInfoCard extends WatchingWidget { surfaceTintColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), elevation: 1.0, - margin: - const EdgeInsets.only(left: 4.0, right: 4.0, top: 4.0, bottom: 4.0), + margin: const EdgeInsets.only( + left: 4.0, + right: 4.0, + top: 4.0, + bottom: 4.0, + ), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -49,15 +50,15 @@ class SpecialInfoCard extends WatchingWidget { scrollDirection: Axis.horizontal, child: InkWell( onTap: () { - _filterStateManager.resetFilters(); - _mainMenuBottomNavManager + di().resetFilters(); + di() .setPupilProfileNavPage(0); - Navigator.of(context) - .push(MaterialPageRoute( - builder: (ctx) => PupilProfilePage( - pupil: pupil, + Navigator.of(context).push( + MaterialPageRoute( + builder: (ctx) => + PupilProfilePage(pupil: pupil), ), - )); + ); }, child: Row( children: [ @@ -111,8 +112,10 @@ class SpecialInfoCard extends WatchingWidget { child: InkWell( onTap: () {}, child: Padding( - padding: - const EdgeInsets.only(right: 8.0, bottom: 15), + padding: const EdgeInsets.only( + right: 8.0, + bottom: 15, + ), child: Text( pupil.specialInformation ?? 'keine Infos', overflow: TextOverflow.ellipsis, diff --git a/school_data_hub_server/lib/src/_features/attendance/endpoints/missed_schoolday_endpoint.dart b/school_data_hub_server/lib/src/_features/attendance/endpoints/missed_schoolday_endpoint.dart index c5ba1c64..57c7e33c 100644 --- a/school_data_hub_server/lib/src/_features/attendance/endpoints/missed_schoolday_endpoint.dart +++ b/school_data_hub_server/lib/src/_features/attendance/endpoints/missed_schoolday_endpoint.dart @@ -16,33 +16,107 @@ class MissedSchooldayEndpoint extends Endpoint { } } - Future postMissedSchoolday( + /// Helper method that handles upsert logic for a single MissedSchoolday record. + /// Returns a record containing the processed MissedSchoolday and the operation type. + Future<({MissedSchoolday record, String operation})> _upsertMissedSchoolday( Session session, MissedSchoolday missedClass) async { - final createdMissedSchoolday = await session.db.insertRow(missedClass); + late MissedSchoolday resultMissedSchoolday; + String operation = 'add'; + + try { + // Try to insert the record (optimistic case - no duplicate) + final createdMissedSchoolday = await session.db.insertRow(missedClass); + resultMissedSchoolday = createdMissedSchoolday; + } on DatabaseQueryException catch (e) { + // Check if this is a duplicate key error (code 23505) + if (e.toString().contains('23505') || + e.toString().contains('duplicate key')) { + // Fetch the existing record with the same schooldayId and pupilId + final existingRecord = await MissedSchoolday.db.findFirstRow( + session, + where: (t) => + t.schooldayId.equals(missedClass.schooldayId) & + t.pupilId.equals(missedClass.pupilId), + ); + + if (existingRecord != null) { + // Update the existing record with new data, preserving the ID + final updatedMissedSchoolday = existingRecord.copyWith( + missedType: missedClass.missedType, + unexcused: missedClass.unexcused, + contacted: missedClass.contacted, + returned: missedClass.returned, + returnedAt: missedClass.returnedAt, + writtenExcuse: missedClass.writtenExcuse, + minutesLate: missedClass.minutesLate, + modifiedBy: missedClass.modifiedBy, + comment: missedClass.comment, + ); + + resultMissedSchoolday = + await session.db.updateRow(updatedMissedSchoolday); + operation = 'update'; + } else { + // If we can't find the existing record, rethrow the original error + rethrow; + } + } else { + // If it's not a duplicate key error, rethrow + rethrow; + } + } + // Fetch the object again with the relation included final missedSchooldayWithRelation = await MissedSchoolday.db.findById( session, - createdMissedSchoolday.id!, + resultMissedSchoolday.id!, include: MissedSchoolday.include( schoolday: Schoolday.include(), ), ); - final newMissedSchooldayDto = MissedSchooldayDto( - missedSchoolday: missedSchooldayWithRelation!, - operation: 'add', + + return (record: missedSchooldayWithRelation!, operation: operation); + } + + Future postMissedSchoolday( + Session session, MissedSchoolday missedClass) async { + final result = await _upsertMissedSchoolday(session, missedClass); + + final missedSchooldayDto = MissedSchooldayDto( + missedSchoolday: result.record, + operation: result.operation, ); - // Send the new missed class to the stream + + // Send the missed class to the stream session.messages.postMessage( 'missed_schooldays_stream', - newMissedSchooldayDto, + missedSchooldayDto, ); - return missedSchooldayWithRelation; + + return result.record; } Future> postMissedSchooldays( Session session, List missedClasses) async { - final createdMissedSchooldays = await session.db.insert(missedClasses); - return createdMissedSchooldays; + final results = []; + + // Process each record individually to handle duplicates gracefully + for (final missedClass in missedClasses) { + final result = await _upsertMissedSchoolday(session, missedClass); + + // Send stream notification for each record + session.messages.postMessage( + 'missed_schooldays_stream', + MissedSchooldayDto( + missedSchoolday: result.record, + operation: result.operation, + ), + ); + + results.add(result.record); + } + + return results; } Future> fetchAllMissedSchooldays(Session session) { diff --git a/school_data_hub_server/lib/src/_features/books/endpoints/library_books/library_books_endpoint.dart b/school_data_hub_server/lib/src/_features/books/endpoints/library_books/library_books_endpoint.dart index b9de516f..d0563bb1 100644 --- a/school_data_hub_server/lib/src/_features/books/endpoints/library_books/library_books_endpoint.dart +++ b/school_data_hub_server/lib/src/_features/books/endpoints/library_books/library_books_endpoint.dart @@ -29,23 +29,69 @@ class LibraryBooksEndpoint extends Endpoint { throw Exception( 'Library book location "${location.location}" not found.'); } - final libraryBook = LibraryBook( - id: null, - bookId: book.id!, - libraryId: libraryId, - available: true, - book: book, - location: libraryBookLocation, - locationId: libraryBookLocation.id!, - ); - final libraryBookInDatabase = await LibraryBook.db - .insertRow(session, libraryBook, transaction: transaction); - await LibraryBook.db.attachRow - .book(session, libraryBookInDatabase, book, transaction: transaction); - await LibraryBook.db.attachRow.location( - session, libraryBookInDatabase, libraryBookLocation, - transaction: transaction); + late LibraryBook libraryBookInDatabase; + + try { + // Try to insert (optimistic case - no duplicate) + final libraryBook = LibraryBook( + id: null, + bookId: book.id!, + libraryId: libraryId, + available: true, + book: book, + location: libraryBookLocation, + locationId: libraryBookLocation.id!, + ); + + libraryBookInDatabase = await LibraryBook.db + .insertRow(session, libraryBook, transaction: transaction); + + await LibraryBook.db.attachRow.book( + session, libraryBookInDatabase, book, + transaction: transaction); + await LibraryBook.db.attachRow.location( + session, libraryBookInDatabase, libraryBookLocation, + transaction: transaction); + } on DatabaseQueryException catch (e) { + // Check if this is a duplicate key error (code 23505) + if (e.toString().contains('23505') || + e.toString().contains('duplicate key')) { + // Find the existing record with the same libraryId + final existingRecord = await LibraryBook.db.findFirstRow( + session, + where: (t) => t.libraryId.equals(libraryId), + transaction: transaction, + ); + + if (existingRecord != null) { + // Update the existing record with new data + final updatedLibraryBook = existingRecord.copyWith( + bookId: book.id!, + locationId: libraryBookLocation.id!, + available: true, + ); + + libraryBookInDatabase = await LibraryBook.db.updateRow( + session, updatedLibraryBook, + transaction: transaction); + + // Update relations + await LibraryBook.db.attachRow.book( + session, libraryBookInDatabase, book, + transaction: transaction); + await LibraryBook.db.attachRow.location( + session, libraryBookInDatabase, libraryBookLocation, + transaction: transaction); + } else { + // If we can't find the existing record, rethrow the original error + rethrow; + } + } else { + // If it's not a duplicate key error, rethrow + rethrow; + } + } if (libraryBookInDatabase.id == null) { throw Exception('Failed to create library book - no ID assigned.'); diff --git a/school_data_hub_server/pubspec.lock b/school_data_hub_server/pubspec.lock index d00acfc0..a492f758 100644 --- a/school_data_hub_server/pubspec.lock +++ b/school_data_hub_server/pubspec.lock @@ -13,18 +13,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f0bb5d1648339c8308cc0b9838d8456b3cfe5c91f9dc1a735b4d003269e5da9a + sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" url: "https://pub.dev" source: hosted - version: "88.0.0" + version: "93.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "0b7b9c329d2879f8f05d6c05b32ee9ec025f39b077864bdb5ac9a7b63418a98f" + sha256: "92584d8f77b0095cb0cc7041dd4baea8bb3ec1a5a5f84037c93b1cf414942633" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "10.0.0" archive: dependency: transitive description: @@ -125,10 +125,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" crypto_keys: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" file: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: "direct main" description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -261,18 +261,18 @@ packages: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.7.2" intl: dependency: transitive description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.20.2" io: dependency: transitive description: @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: jose - sha256: "7955ec5d131960104e81fbf151abacb9d835c16c9e793ed394b2809f28b2198d" + sha256: bd8dd0bee653a78be16f5e2c0387117906f8f5171f7dcc29e3246b6c7cb73918 url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.5" js: dependency: transitive description: @@ -317,18 +317,18 @@ packages: dependency: "direct main" description: name: mailer - sha256: db61f51ea301e8dcbfe5894e037ccd30a6246eb0a7bcfa007aefa2fc11a8f96e + sha256: c3b934c0e800ddc946167c0123a900eba5acd009abb73648d0191a742542f2b4 url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.0" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" meta: dependency: transitive description: @@ -341,18 +341,18 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" mustache_template: dependency: transitive description: name: mustache_template - sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c + sha256: "4326d0002ff58c74b9486990ccbdab08157fca3c996fe9e197aff9d61badf307" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.3" node_preamble: dependency: transitive description: @@ -365,10 +365,10 @@ packages: dependency: transitive description: name: openid_client - sha256: "1d39a829dc770947bf8ec8684a3456743ef0205a777371efe16773a44163eb6a" + sha256: d7c229b849eaf4ff0ac77c22f8f1c4c522f6830b8c58d7b73953514ad6980954 url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.4.9+2" package_config: dependency: transitive description: @@ -405,10 +405,10 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" posix: dependency: transitive description: @@ -421,10 +421,10 @@ packages: dependency: transitive description: name: postgres - sha256: "9aaa6f4872956adef653535a4e2133b167465c1a68c22b9bd0744ef1244e9393" + sha256: fefbbfe749c6130e5096588b9c4459173684c695952cd7636ab19be76f255469 url: "https://pub.dev" source: hosted - version: "3.5.6" + version: "3.5.9" pub_semver: dependency: transitive description: @@ -457,22 +457,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" - sasl_scram: - dependency: transitive - description: - name: sasl_scram - sha256: a47207a436eb650f8fdcf54a2e2587b850dc3caef9973ce01f332b07a6fc9cb9 - url: "https://pub.dev" - source: hosted - version: "0.1.1" - saslprep: - dependency: transitive - description: - name: saslprep - sha256: "3d421d10be9513bf4459c17c5e70e7b8bc718c9fc5ad4ba5eb4f5fd27396f740" - url: "https://pub.dev" - source: hosted - version: "1.0.3" serverpod: dependency: "direct main" description: @@ -577,14 +561,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" stack_trace: dependency: transitive description: @@ -645,26 +621,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.15" typed_data: dependency: transitive description: @@ -681,22 +657,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.5" - unorm_dart: - dependency: transitive - description: - name: unorm_dart - sha256: "8e3870a1caa60bde8352f9597dd3535d8068613269444f8e35ea8925ec84c1f5" - url: "https://pub.dev" - source: hosted - version: "0.3.1+1" uuid: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.2" vm_service: dependency: transitive description: @@ -709,10 +677,10 @@ packages: dependency: transitive description: name: watcher - sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" web: dependency: transitive description: @@ -770,4 +738,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" + dart: ">=3.9.0 <4.0.0"