diff --git a/school_data_hub_flutter/.cursorrules b/school_data_hub_flutter/.cursorrules deleted file mode 100644 index 54857f9a..00000000 --- a/school_data_hub_flutter/.cursorrules +++ /dev/null @@ -1,291 +0,0 @@ -# Cursor Rules for School Data Hub Flutter Project - -Please think out loud and provide -- Overview of completed work -- Rationale for decision -- next steps checklist - -## Context Usage Guidelines - -### Default Context Sources -- **ALWAYS** check `cursor_docs/` folder first when answering questions about: - - Package usage patterns (watch_it, get_it, serverpod, etc.) - - Package-specific best practices and conventions - - API documentation for packages used in this project - - How packages are integrated in this specific codebase - -### How to Use cursor_docs -- When a question involves a package that has documentation in `cursor_docs/`, search and reference that documentation -- Explicitly cite the specific file when using information from `cursor_docs/` (e.g., "According to `cursor_docs/watch_it_readme.md`...") -- Prefer information from `cursor_docs/` over general package documentation when available, as these files contain project-specific context - -### Context Priority Order -1. Project codebase (`lib/`, etc.) - actual implementation patterns -2. `cursor_docs/` folder - project-specific package documentation -3. General package documentation (pub.dev, GitHub) - fallback if not in cursor_docs - -### Available cursor_docs Files -- `cursor_docs/watch_it_readme.md` - watch_it package documentation and usage patterns -- `cursor_docs/get_it.md` - get_it package documentation and usage patterns - -## Dart and flutter rules - -- Don't use print() statements!!! Implement or use instead final _log = Logger(); -- Always prefer WatchingWidget and createOnce functions for local state over StatefulWidget -- Always prefer using watch functions over ListenableBuilder -- always use package import even for our project files - - Comply to the always_use_package_imports rule -- prefer composability breaking down presentation files: - - creating/ using a "widgets" folder in the page/widget folder - - creating the broken-down widget in a new file in the widgets folder -- important: don't create model classes without asking! Reason and request it before you do it! -- Prefer using a WatchingWidget creating listenables with the createOnce function over stateful widgets! - -## Project Overview -This is a Flutter application for School Data Hub with Serverpod backend integration. The project uses modern Flutter development practices and follows a feature-based architecture. - -## Architecture Guidelines - -### Project Structure -- `lib/features/` - Feature-based modules with their own models, views, and controllers -- `lib/core/` - Core utilities, constants, and shared functionality -- `lib/common/` - Common widgets and utilities used across features -- `lib/app_utils/` - Application-level utilities and helpers -- `lib/l10n/` - Localization files -- every feature contains `data`, `domain` and `presentation` folders. - -### Code Organization -- Use feature-based architecture with clear separation of concerns -- -- Follow the repository pattern for data access -- Implement proper state management using the watch_it package -- Use dependency injection for service management - -### Handling DateTime values -- The server works with utc format. Every datetime landing in the server should be in utc format. -- The managers must work with utc format too because there objects are created and updated. -- The presentation layer must be responsible for the conversion .toLocal(). -- Managers receiving DateTime objects from the presentation layer must convert them to utc before manipulating / creating objects. -- This is valid for every manager, common or feature -- This has high priority and you are allowed to tackle this if it comes you across. - -## Package Documentation and Usage Guidelines - -- include context from the packages where relevant from the `cursor_docs`folder in the file with the package name. -- -### Core Dependencies - -#### serverpod_flutter (2.9.1) -- **Purpose**: Backend integration with Serverpod -- **Usage**: Use for API calls, real-time communication, and data synchronization -- **Best Practices**: - - Always handle connection states - - Use proper error handling for network calls - - Implement retry logic for failed requests - -#### school_data_hub_client -- **Purpose**: Local client package for School Data Hub -- **Usage**: Import and use for local data operations -- **Note**: This is a local path dependency - -#### watch_it (1.7.0) -- **Purpose**: Dependency injection and state management -- **Usage**: - - Register services with `GetIt.instance.registerSingleton()` - - Implement proper disposal of services - -#### flutter_secure_storage (10.0.0-beta.1) -- **Purpose**: Secure storage for sensitive data -- **Usage**: Store authentication tokens, encryption keys, and sensitive user data -- **Best Practices**: - - Always handle encryption/decryption errors - - Use proper key management - - Clear sensitive data on logout - -### UI and Navigation - -#### gap (3.0.1) -- **Purpose**: Consistent spacing between widgets -- **Usage**: Use `Gap()` widget for consistent vertical/horizontal spacing -- **Best Practices**: Define spacing constants in your theme - -#### carousel_slider (5.0.0) -- **Purpose**: Image carousel and slider functionality -- **Usage**: Display multiple images or content in a scrollable format -- **Best Practices**: Implement proper loading states and error handling - -#### table_calendar (3.1.3) & calendar_date_picker2 (2.0.1) -- **Purpose**: Calendar functionality for date selection -- **Usage**: Date picking, event display, and calendar views -- **Best Practices**: Handle timezone differences and date formatting - -### File and Media Handling - -#### file_picker (10.1.9) -- **Purpose**: File selection from device -- **Usage**: Allow users to pick files from their device -- **Best Practices**: - - Validate file types and sizes - - Handle permissions properly - - Show loading states during file operations - -#### image_picker (1.1.2) -- **Purpose**: Image selection from camera or gallery -- **Usage**: Capture or select images -- **Best Practices**: - - Request camera permissions - - Compress images for better performance - - Handle image orientation - -#### crop_image (1.0.16) & custom_image_crop (0.1.1) -- **Purpose**: Image cropping functionality -- **Usage**: Allow users to crop selected images -- **Best Practices**: Maintain aspect ratios and provide preview - -#### mobile_scanner (7.0.1) & qr_flutter (4.1.0) & qr_code_vision (0.1.2) -- **Purpose**: QR code scanning and generation -- **Usage**: - - `mobile_scanner`: Scan QR codes using camera - - `qr_flutter`: Generate QR codes - - `qr_code_vision`: Read QR codes from images (Windows compatibility) -- **Best Practices**: Handle camera permissions and provide fallback options - -### Data and Security - -#### encrypt (5.0.3) & cryptography_plus (2.7.1) -- **Purpose**: Data encryption and security -- **Usage**: Encrypt sensitive data before storage or transmission -- **Best Practices**: - - Use strong encryption algorithms - - Secure key management - - Never store encryption keys in plain text - -#### json_annotation (4.9.0) & json_serializable (6.8.0) -- **Purpose**: JSON serialization/deserialization -- **Usage**: Convert Dart objects to/from JSON -- **Best Practices**: - - Use `@JsonSerializable()` annotation - - Run `flutter packages pub run build_runner build` after changes - - Handle null values properly - -#### jwt_decoder (2.0.1) -- **Purpose**: JWT token decoding -- **Usage**: Extract information from JWT tokens -- **Best Practices**: Validate token expiration and signature - -### PDF and Printing - -#### pdf (3.11.3) & printing (5.14.2) -- **Purpose**: PDF generation and printing -- **Usage**: Create PDF documents and print functionality -- **Best Practices**: - - Handle large documents efficiently - - Provide progress indicators - - Test on different platforms - -### Network and HTTP - -#### dio (5.8.0+1) -- **Purpose**: HTTP client for network requests -- **Usage**: Make HTTP requests with advanced features -- **Best Practices**: - - Configure interceptors for authentication - - Handle timeouts and retries - - Implement proper error handling - -#### url_launcher (6.3.1) -- **Purpose**: Launch URLs in browser or apps -- **Usage**: Open external links and deep links -- **Best Practices**: Validate URLs before launching - -### Platform Specific - -#### window_manager (0.5.0) -- **Purpose**: Desktop window management -- **Usage**: Control window size, position, and behavior on desktop -- **Best Practices**: Respect user preferences and accessibility settings - -#### flutter_secure_storage (10.0.0-beta.1) -- **Purpose**: Platform-specific secure storage -- **Usage**: Store sensitive data securely on each platform -- **Best Practices**: Handle platform differences gracefully - -### Development and Build - -#### flutter_launcher_icons (0.14.3) -- **Purpose**: Generate app icons -- **Usage**: Configure app icons for different platforms -- **Best Practices**: Provide high-resolution source images - -#### shorebird_code_push (2.0.4) -- **Purpose**: Code push updates for Flutter apps -- **Usage**: Deploy updates without app store approval -- **Best Practices**: Test updates thoroughly before deployment - -## Coding Standards - -### Dart/Flutter Best Practices -- Use `const` constructors where possible -- Implement proper error handling with try-catch blocks -- Use meaningful variable and function names -- Add documentation comments for public APIs -- Follow Flutter's style guide - -### Watch_it Best Practices -- **NEVER use watch methods conditionally** - this is bad practice and can cause issues with the reactive system -- Always call watch methods at the top level of the build method, not inside conditional statements -- Use proper null checks and default values instead of conditional watching -- Example of bad practice: `if (condition) watchPropertyValue(...)` -- Example of good practice: `final value = watchPropertyValue(...) ?? defaultValue` - -### State Management -- Use `watch_it` for dependency injection -- Avoid listenable builders -- Implement proper state management patterns -- Avoid global state when possible -- Use `ChangeNotifier` or similar for reactive UI updates - -### Error Handling -- Always handle async operations with proper error handling -- Provide user-friendly error messages -- Log errors appropriately using the `logging` package -- Implement retry mechanisms for network operations - -### Performance -- Use `ListView.builder` for large lists -- Implement proper image caching with `flutter_cache_manager` -- Avoid unnecessary widget rebuilds -- Use `const` widgets where possible - -### Security -- Never hardcode sensitive information -- Use `flutter_secure_storage` for sensitive data -- Validate all user inputs -- Implement proper authentication flows - -### Testing -- Write unit tests for business logic -- Use `flutter_test` for widget testing -- Test error scenarios and edge cases -- Maintain good test coverage - -### Localization -- Use `flutter_localizations` for internationalization -- Store all strings in localization files -- Support RTL languages if needed -- Use `intl` package for date/number formatting - -## Development Workflow -1. Follow feature-based development -2. Use proper branching strategy -3. Write tests for new features -4. Update documentation when adding new packages -5. Run `flutter analyze` before committing -6. Use `build_runner` for code generation - -## Common Commands -- `flutter pub get` - Install dependencies -- `flutter packages pub run build_runner build` - Generate code -- `flutter analyze` - Run static analysis -- `flutter test` - Run tests -- `flutter build` - Build for target platform diff --git a/school_data_hub_flutter/.gitignore b/school_data_hub_flutter/.gitignore index ff0b127c..ca9d8a52 100644 --- a/school_data_hub_flutter/.gitignore +++ b/school_data_hub_flutter/.gitignore @@ -29,6 +29,8 @@ test_enc.dart decrypt_files.dart **/scripts/ cursor_docs/ +test_enc.dart +.cursorrules # Flutter/Dart/Pub related **/doc/api/ diff --git a/school_data_hub_flutter/analysis_options.yaml b/school_data_hub_flutter/analysis_options.yaml index 3109e191..29345300 100644 --- a/school_data_hub_flutter/analysis_options.yaml +++ b/school_data_hub_flutter/analysis_options.yaml @@ -3,7 +3,7 @@ include: package:flutter_lints/flutter.yaml linter: rules: avoid_print: false # Allow print statements, useful for debugging - unused_local_variable: ignore # Ignore warnings about unused local variables + # unused_local_variable: ignore # Ignore warnings about unused local variables always_declare_return_types: true # Ensure all methods have a return type prefer_const_constructors: true # Prefer const constructors for better performance avoid_unnecessary_containers: true # Avoid redundant containers in UI diff --git a/school_data_hub_flutter/lib/app_utils/shorebird_code_push.dart b/school_data_hub_flutter/lib/app_utils/shorebird_code_push_page.dart similarity index 92% rename from school_data_hub_flutter/lib/app_utils/shorebird_code_push.dart rename to school_data_hub_flutter/lib/app_utils/shorebird_code_push_page.dart index 5b6fdff8..d4a85754 100644 --- a/school_data_hub_flutter/lib/app_utils/shorebird_code_push.dart +++ b/school_data_hub_flutter/lib/app_utils/shorebird_code_push_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:logging/logging.dart'; +import 'package:school_data_hub_flutter/common/services/notification_service.dart'; import 'package:school_data_hub_flutter/common/widgets/generic_components/bottom_nav_bar_no_filter_button.dart'; import 'package:school_data_hub_flutter/common/widgets/generic_components/generic_app_bar.dart'; import 'package:school_data_hub_flutter/core/updater/shorebird_update_manager.dart'; @@ -9,14 +10,14 @@ import 'package:watch_it/watch_it.dart'; final _log = Logger('CheckForUpdatesPage'); -class CheckForUpdatesPage extends StatefulWidget { - const CheckForUpdatesPage({super.key}); +class ShorebirdCodePushPage extends WatchingStatefulWidget { + const ShorebirdCodePushPage({super.key}); @override - State createState() => _CheckForUpdatesPageState(); + State createState() => _ShorebirdCodePushPageState(); } -class _CheckForUpdatesPageState extends State { +class _ShorebirdCodePushPageState extends State { final _updater = di().shorebirdUpdater; late final bool _isUpdaterAvailable; var _currentTrack = UpdateTrack.stable; @@ -123,26 +124,15 @@ class _CheckForUpdatesPageState extends State { } void _showRestartBanner() { - ScaffoldMessenger.of(context) - ..hideCurrentMaterialBanner() - ..showMaterialBanner( - MaterialBanner( - content: const Text( - 'Ein neuer Patch ist verfügbar! Bitte starte die App neu.', - ), - actions: [ - TextButton( - onPressed: () { - ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); - }, - child: const Text('Verstanden'), - ), - ], - ), - ); + di().showInformationDialog( + 'Ein neuer Patch ist verfügbar! Bitte starte die App neu.', + ); } void _showErrorBanner(Object error) { + di().showInformationDialog( + 'Fehler beim Herunterladen des Updates: $error.', + ); ScaffoldMessenger.of(context) ..hideCurrentMaterialBanner() ..showMaterialBanner( diff --git a/school_data_hub_flutter/lib/core/updater/shorebird_update_manager.dart b/school_data_hub_flutter/lib/core/updater/shorebird_update_manager.dart index 4dc6dd99..5590cc7f 100644 --- a/school_data_hub_flutter/lib/core/updater/shorebird_update_manager.dart +++ b/school_data_hub_flutter/lib/core/updater/shorebird_update_manager.dart @@ -37,7 +37,7 @@ class ShorebirdUpdateManager extends ChangeNotifier { // Auto-update settings bool _autoCheckEnabled = true; - Duration _autoCheckInterval = const Duration(hours: 6); + Duration _autoCheckInterval = const Duration(hours: 2); Timer? _autoCheckTimer; // Getters @@ -137,8 +137,15 @@ class ShorebirdUpdateManager extends ChangeNotifier { _log.info('Update available'); _updateAvailable = true; _setStatus(UpdateManagerStatus.updateAvailable); + di().showSnackBar( + NotificationType.info, + 'Ein Update wird heruntergeladen...', + ); await _updater.update(track: _currentTrack); - + di().showInformationDialog( + 'Ein Update wurde installiert. Bitte starten Sie die App neu, um die neueste Version der App zu verwenden.', + ); + _setStatus(UpdateManagerStatus.restartRequired); return true; case UpdateStatus.restartRequired: diff --git a/school_data_hub_flutter/lib/features/_schoolday_events/data/schoolday_event_api_service.dart b/school_data_hub_flutter/lib/features/_schoolday_events/data/schoolday_event_api_service.dart index 8037a168..8eccc598 100644 --- a/school_data_hub_flutter/lib/features/_schoolday_events/data/schoolday_event_api_service.dart +++ b/school_data_hub_flutter/lib/features/_schoolday_events/data/schoolday_event_api_service.dart @@ -186,7 +186,7 @@ class SchooldayEventApiService { final fileStream = file.openRead(); final fileLength = await file.length(); - _log.info('File length: $fileLength'); + _notificationService.apiRunning(true); try { await uploader.upload(fileStream, fileLength); @@ -228,8 +228,7 @@ class SchooldayEventApiService { StackTrace.current, ); - _notificationService.showSnackBar( - NotificationType.error, + _notificationService.showInformationDialog( 'Das Dokument konnte nicht aktualisiert werden: ${e.toString()}', ); diff --git a/school_data_hub_flutter/lib/features/app_settings/settings_page/settings_page.dart b/school_data_hub_flutter/lib/features/app_settings/settings_page/settings_page.dart index 721fd356..2e8dd7ec 100644 --- a/school_data_hub_flutter/lib/features/app_settings/settings_page/settings_page.dart +++ b/school_data_hub_flutter/lib/features/app_settings/settings_page/settings_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:school_data_hub_flutter/app_utils/logger/presentation/logs_page/logs_page.dart'; -import 'package:school_data_hub_flutter/app_utils/shorebird_code_push.dart'; +import 'package:school_data_hub_flutter/app_utils/shorebird_code_push_page.dart'; import 'package:school_data_hub_flutter/common/theme/app_colors.dart'; import 'package:school_data_hub_flutter/common/theme/styles.dart'; import 'package:school_data_hub_flutter/core/env/env_manager.dart'; @@ -97,7 +97,7 @@ class SettingsPage extends StatelessWidget { onPressed: (context) { Navigator.of(context).push( MaterialPageRoute( - builder: (ctx) => const CheckForUpdatesPage(), + builder: (ctx) => const ShorebirdCodePushPage(), ), ); }, diff --git a/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/learning_list_card/learning_list_card.dart b/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/learning_list_card/learning_list_card.dart index ffb800b2..63c931c4 100644 --- a/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/learning_list_card/learning_list_card.dart +++ b/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/learning_list_card/learning_list_card.dart @@ -126,28 +126,29 @@ class LearningListCard extends WatchingWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.book, - size: 30, - color: AppColors.interactiveColor, - ), - const Gap(5), - const Text( - 'Gelesen: ', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + if (selectedContent == SelectedContent.books) + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.book, + size: 30, + color: AppColors.interactiveColor, ), - ), - ], - ), - ], - ), + const Gap(5), + const Text( + 'Gelesen: ', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), Column( children: [ if (selectedContent == diff --git a/school_data_hub_flutter/lib/features/pupil/domain/filters/pupils_filter_impl.dart b/school_data_hub_flutter/lib/features/pupil/domain/filters/pupils_filter_impl.dart index 4ee1e622..bb8471e2 100644 --- a/school_data_hub_flutter/lib/features/pupil/domain/filters/pupils_filter_impl.dart +++ b/school_data_hub_flutter/lib/features/pupil/domain/filters/pupils_filter_impl.dart @@ -420,8 +420,9 @@ class PupilsFilterImplementation with ChangeNotifier implements PupilsFilter { value: true, ); } - notifyListeners(); + _textFilter.setFilterText(text ?? ''); + notifyListeners(); if (refresh) { refreshs(); } diff --git a/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_identity_extensions.dart b/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_identity_extensions.dart index 4812d8c4..b09ae083 100644 --- a/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_identity_extensions.dart +++ b/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_identity_extensions.dart @@ -10,50 +10,48 @@ extension PupilIdentityExtension on PupilIdentity { final specialNeeds = this.specialNeeds ?? ''; return [ - this.id.toString(), - this.firstName, - this.lastName, - this.group, - this.groupTutor, - this.schoolGrade, + id.toString(), + firstName, + lastName, + group, + groupTutor, + schoolGrade, specialNeeds, '', // this is a placeholder for the second special needs field in the administrative data source - this.gender, - this.language, - this.family ?? '', - this.birthday.formatDateForJson(normalizeUtc: false), + gender, + language, + family ?? '', + birthday.formatDateForJson(normalizeUtc: false), migrationSupportEnds, - this.pupilSince.formatDateForJson(normalizeUtc: false), - this.afterSchoolCare ? 'OFFGANZ' : '', - this.religion ?? '', - this.religionLessonsSince?.formatDateForJson(normalizeUtc: false) ?? '', - this.religionLessonsCancelledAt?.formatDateForJson(normalizeUtc: false) ?? - '', - this.familyLanguageLessonsSince?.formatDateForJson(normalizeUtc: false) ?? - '', - this.leavingDate?.formatDateForJson(normalizeUtc: false) ?? '', + pupilSince.formatDateForJson(normalizeUtc: false), + afterSchoolCare ? 'OFFGANZ' : '', + religion ?? '', + religionLessonsSince?.formatDateForJson(normalizeUtc: false) ?? '', + religionLessonsCancelledAt?.formatDateForJson(normalizeUtc: false) ?? '', + familyLanguageLessonsSince?.formatDateForJson(normalizeUtc: false) ?? '', + leavingDate?.formatDateForJson(normalizeUtc: false) ?? '', ].join(','); } bool isEqual(PupilIdentity other) { - return this.id == other.id && - this.firstName == other.firstName && - this.lastName == other.lastName && - this.group == other.group && - this.groupTutor == other.groupTutor && - this.schoolGrade == other.schoolGrade && - this.specialNeeds == other.specialNeeds && - this.gender == other.gender && - this.language == other.language && - this.family == other.family && - this.birthday == other.birthday && - this.migrationSupportEnds == other.migrationSupportEnds && - this.pupilSince == other.pupilSince && - this.afterSchoolCare == other.afterSchoolCare && - this.religion == other.religion && - this.religionLessonsSince == other.religionLessonsSince && - this.religionLessonsCancelledAt == other.religionLessonsCancelledAt && - this.familyLanguageLessonsSince == other.familyLanguageLessonsSince && - this.leavingDate == other.leavingDate; + return id == other.id && + firstName == other.firstName && + lastName == other.lastName && + group == other.group && + groupTutor == other.groupTutor && + schoolGrade == other.schoolGrade && + specialNeeds == other.specialNeeds && + gender == other.gender && + language == other.language && + family == other.family && + birthday == other.birthday && + migrationSupportEnds == other.migrationSupportEnds && + pupilSince == other.pupilSince && + afterSchoolCare == other.afterSchoolCare && + religion == other.religion && + religionLessonsSince == other.religionLessonsSince && + religionLessonsCancelledAt == other.religionLessonsCancelledAt && + familyLanguageLessonsSince == other.familyLanguageLessonsSince && + leavingDate == other.leavingDate; } } diff --git a/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_proxy.dart b/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_proxy.dart index dd3cec01..aec1d843 100644 --- a/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_proxy.dart +++ b/school_data_hub_flutter/lib/features/pupil/domain/models/pupil_proxy.dart @@ -130,7 +130,12 @@ class PupilProxy with ChangeNotifier { String get gender => _pupilIdentity.gender; String get language => _pupilIdentity.language; String? get family => _pupilIdentity.family; - DateTime get birthday => _pupilIdentity.birthday.add(const Duration(days: 1)); + DateTime get birthday => _pupilIdentity.birthday; + + bool get isBirthdayToday { + final today = DateTime.now(); + return today.month == birthday.month && today.day == birthday.day; + } int get age { final today = DateTime.now(); @@ -145,17 +150,14 @@ class PupilProxy with ChangeNotifier { String? get groupTutor => _pupilIdentity.groupTutor; - DateTime? get migrationSupportEnds => - _pupilIdentity.migrationSupportEnds?.add(const Duration(days: 1)); - DateTime get pupilSince => - _pupilIdentity.pupilSince.add(const Duration(days: 1)); + DateTime? get migrationSupportEnds => _pupilIdentity.migrationSupportEnds; + DateTime get pupilSince => _pupilIdentity.pupilSince; DateTime? get familyLanguageLessonsSince => - _pupilIdentity.familyLanguageLessonsSince?.add(const Duration(days: 1)); - DateTime? get religionLessonsSince => - _pupilIdentity.religionLessonsSince?.add(const Duration(days: 1)); + _pupilIdentity.familyLanguageLessonsSince; + DateTime? get religionLessonsSince => _pupilIdentity.religionLessonsSince; DateTime? get religionLessonsCancelledAt => - _pupilIdentity.religionLessonsCancelledAt?.add(const Duration(days: 1)); + _pupilIdentity.religionLessonsCancelledAt; String? get religion => _pupilIdentity.religion; //- PUPIL DATA GETTERS diff --git a/school_data_hub_flutter/lib/features/pupil/domain/pupil_proxy_manager.dart b/school_data_hub_flutter/lib/features/pupil/domain/pupil_proxy_manager.dart index 65b887e7..d66a0119 100644 --- a/school_data_hub_flutter/lib/features/pupil/domain/pupil_proxy_manager.dart +++ b/school_data_hub_flutter/lib/features/pupil/domain/pupil_proxy_manager.dart @@ -138,40 +138,47 @@ class PupilProxyManager extends ChangeNotifier { return pupilSiblings; } + /// Returns the relevant birthday date for a pupil, considering year boundaries. + /// If the birthday this year hasn't occurred yet, returns last year's birthday. + /// Otherwise, returns this year's birthday. + DateTime getRelevantBirthdayDate(PupilProxy pupil) { + final now = DateTime.now(); + final birthdayToLocal = pupil.birthday.toLocal(); + final birthdayThisYear = DateTime( + now.year, + birthdayToLocal.month, + birthdayToLocal.day, + ); + final birthdayLastYear = DateTime( + now.year - 1, + birthdayToLocal.month, + birthdayToLocal.day, + ); + return birthdayThisYear.isAfter(now) ? birthdayLastYear : birthdayThisYear; + } + List getPupilsWithBirthdaySinceDate(DateTime date) { Map allPupils = Map.of(_pupilIdPupilsMap); final DateTime now = DateTime.now(); allPupils.removeWhere((key, pupil) { - final birthdayToLocal = pupil.birthday.toLocal(); - final birthdayThisYear = DateTime( - now.year, - birthdayToLocal.month, - birthdayToLocal.day, - ); + final DateTime relevantBirthday = getRelevantBirthdayDate(pupil); - // Ensure the birthday this year is not before the specified date and not after today. - return !(birthdayThisYear.isSameDate(date) || - (birthdayThisYear.isAfter(date) && birthdayThisYear.isBefore(now))); + // Check if the relevant birthday falls within the range [date, now] + return !(relevantBirthday.isSameDate(date) || + relevantBirthday.isSameDate(now) || + (relevantBirthday.isAfter(date) && relevantBirthday.isBefore(now))); }); final pupilsWithBirthdaySinceDate = allPupils.values.toList(); + // Sort by most recent birthday (descending) pupilsWithBirthdaySinceDate.sort((b, a) { - final birthdayA = DateTime( - DateTime.now().year, - a.birthday.month, - a.birthday.day, - ); - - final birthdayB = DateTime( - DateTime.now().year, - b.birthday.month, - b.birthday.day, - ); + final relevantBirthdayA = getRelevantBirthdayDate(a); + final relevantBirthdayB = getRelevantBirthdayDate(b); - return birthdayA.compareTo(birthdayB); + return relevantBirthdayA.compareTo(relevantBirthdayB); }); return pupilsWithBirthdaySinceDate; @@ -201,7 +208,7 @@ class PupilProxyManager extends ChangeNotifier { } Future updatePupilList(List pupils) async { - await fetchPupilsByInternalId(pupils.map((e) => e.pupilId).toList()); + await fetchPupilsByInternalId(pupils.map((e) => e.internalId).toList()); } Future updatePupilData(int pupilId) async { diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/birthdays_page.dart b/school_data_hub_flutter/lib/features/pupil/presentation/birthdays_page.dart index 29ac7129..06c27dc1 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/birthdays_page.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/birthdays_page.dart @@ -17,8 +17,10 @@ class BirthdaysView extends StatelessWidget { @override Widget build(BuildContext context) { final Set seenBirthdays = {}; - final List pupils = di() - .getPupilsWithBirthdaySinceDate(selectedDate); + final pupilManager = di(); + final List pupils = pupilManager.getPupilsWithBirthdaySinceDate( + selectedDate, + ); return Scaffold( backgroundColor: AppColors.canvasColor, @@ -58,22 +60,15 @@ class BirthdaysView extends StatelessWidget { itemCount: pupils.length, itemBuilder: (context, int index) { PupilProxy listedPupil = pupils[index]; + + // Get the relevant birthday from the manager + final DateTime relevantBirthday = pupilManager + .getRelevantBirthdayDate(listedPupil); + final bool isBirthdayPrinted = seenBirthdays - .contains( - DateTime( - DateTime.now().year, - listedPupil.birthday.month, - listedPupil.birthday.day, - ), - ); + .contains(relevantBirthday); if (!isBirthdayPrinted) { - seenBirthdays.add( - DateTime( - DateTime.now().year, - listedPupil.birthday.month, - listedPupil.birthday.day, - ), - ); + seenBirthdays.add(relevantBirthday); } return Column( children: [ @@ -86,7 +81,7 @@ class BirthdaysView extends StatelessWidget { children: [ const Gap(5), Text( - '${DateTime(DateTime.now().year, listedPupil.birthday.month, listedPupil.birthday.day).asWeekdayName(context)}, ${DateTime(DateTime.now().year, listedPupil.birthday.month, listedPupil.birthday.day).formatDateForUser()}', + '${relevantBirthday.asWeekdayName(context)}, ${relevantBirthday.formatDateForUser()}', style: TextStyle( fontWeight: FontWeight.bold, color: diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/widgets/avatar.dart b/school_data_hub_flutter/lib/features/pupil/presentation/widgets/avatar.dart index 6f4b1d2c..c2cf1d8a 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/widgets/avatar.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/widgets/avatar.dart @@ -175,6 +175,30 @@ class AvatarWithBadges extends WatchingWidget { ), ), ), + if (pupil.isBirthdayToday) + Positioned( + bottom: -_badgeOffset, + left: 0, + right: 0, + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: _badgeSize, + height: _badgeSize, + decoration: const BoxDecoration( + color: Color.fromARGB(255, 228, 76, 99), + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + Icons.cake_rounded, + color: Colors.white, + size: 20, + ), + ), + ), + ), + ), if (pupil.specialNeeds != null) Positioned( top: 0, @@ -227,10 +251,19 @@ class AvatarWithBadges extends WatchingWidget { pupil.specialInformation!, ); }, - child: const Icon( - Icons.info_rounded, - size: 25, - color: Color.fromARGB(255, 6, 92, 163), + child: Container( + width: _badgeSize - 3, + height: _badgeSize - 3, + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + border: Border.all(color: Colors.white, width: 1), + ), + child: const Icon( + Icons.info_rounded, + size: 25, + color: Color.fromARGB(255, 6, 92, 163), + ), ), ), ), diff --git a/school_data_hub_flutter/lib/features/statistics/chart_page/chart_page_controller.dart b/school_data_hub_flutter/lib/features/statistics/chart_page/chart_page_controller.dart index 56a5c1bd..220fc875 100644 --- a/school_data_hub_flutter/lib/features/statistics/chart_page/chart_page_controller.dart +++ b/school_data_hub_flutter/lib/features/statistics/chart_page/chart_page_controller.dart @@ -170,7 +170,7 @@ class _ChartPageControllerState extends State { DateTime? supportCreated; int? supportLevel; - if (pupil.latestSupportLevel != null) { + if (pupil.latestSupportLevel != null && pupil.specialNeeds == null) { supportLevel = pupil.latestSupportLevel!.level; final d = pupil.latestSupportLevel!.createdAt.toLocal(); supportCreated = DateTime(d.year, d.month, d.day); @@ -242,41 +242,34 @@ class _ChartPageControllerState extends State { // Enrolled check if (p.sinceDate.isAfter(dayDate)) continue; - // New Pupils + // New Pupils (this is separate - an overlay metric) if ((p.sinceDate.isBefore(dayDate) || p.sinceDate == dayDate) && (p.sinceDate.isAfter(semesterStartDate) || p.sinceDate == semesterStartDate)) { newPupils++; } - // Special Needs + // Priority hierarchy for main categorization + // Each pupil is counted in exactly one category to avoid double-counting + + // 1. Special Needs (highest priority) if (p.hasSpecialNeeds) { specialNeeds++; } - - // Migration Support - bool isMigration = false; - if (p.migrationEnd != null) { - if (dayDate.isBefore(p.migrationEnd!) || dayDate == p.migrationEnd!) { - migrationSupport++; - isMigration = true; - } + // 2. Migration Support (only if not special needs) + else if (p.migrationEnd != null && + (dayDate.isBefore(p.migrationEnd!) || dayDate == p.migrationEnd!)) { + migrationSupport++; } - - // Support Level 3 - bool isLevel3 = false; - if (!p.hasSpecialNeeds && - p.supportLevel == 3 && - p.supportCreated != null) { - if (p.supportCreated!.isBefore(dayDate) || - p.supportCreated == dayDate) { - supportLevel3++; - isLevel3 = true; - } + // 3. Support Level 3 (only if not special needs or migration) + else if (p.supportLevel == 3 && + p.supportCreated != null && + (p.supportCreated!.isBefore(dayDate) || + p.supportCreated == dayDate)) { + supportLevel3++; } - - // Regular - if (!p.hasSpecialNeeds && !isMigration && !isLevel3) { + // 4. Regular pupils (everyone else) + else { regularPupils++; } } diff --git a/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/attendance_stats_view.dart b/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/attendance_stats_view.dart index 2df07032..c1ee3f8a 100644 --- a/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/attendance_stats_view.dart +++ b/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/attendance_stats_view.dart @@ -26,7 +26,12 @@ class AttendanceStatsView extends WatchingWidget { @override Widget build(BuildContext context) { - final _hiddenSeries = createOnce(() => ValueNotifier>(Set())); + final _hiddenSeries = createOnce(() => ValueNotifier>({})); + final hiddenSeriesSet = watchPropertyValue( + (ValueNotifier> p0) => p0.value, + target: _hiddenSeries, + ); + void _onSelectionChanged(charts.SelectionModel model) { final selectedDatum = model.selectedDatum; @@ -96,18 +101,17 @@ class AttendanceStatsView extends WatchingWidget { } void _toggleSeries(String seriesId) { - final modifiedHiddenSeries = _hiddenSeries.value; - if (_hiddenSeries.value.contains(seriesId)) { - modifiedHiddenSeries.remove(seriesId); - _hiddenSeries.value = modifiedHiddenSeries; + final currentSet = Set.from(_hiddenSeries.value); + if (currentSet.contains(seriesId)) { + currentSet.remove(seriesId); } else { - modifiedHiddenSeries.add(seriesId); - _hiddenSeries.value = modifiedHiddenSeries; + currentSet.add(seriesId); } + _hiddenSeries.value = currentSet; } Widget _buildLegendItem(String label, Color color, String seriesId) { - final isHidden = _hiddenSeries.value.contains(seriesId); + final isHidden = hiddenSeriesSet.contains(seriesId); return InkWell( onTap: () => _toggleSeries(seriesId), child: Opacity( @@ -145,7 +149,7 @@ class AttendanceStatsView extends WatchingWidget { List> _createAttendanceSeries() { final List> series = []; - if (!_hiddenSeries.value.contains('excused')) { + if (!hiddenSeriesSet.contains('excused')) { final excusedData = sortedSchooldays.map((schoolday) { final data = attendanceChartData[schoolday.schoolday]; final dateStr = _formatDateForChart(schoolday.schoolday); @@ -168,7 +172,7 @@ class AttendanceStatsView extends WatchingWidget { ); } - if (!_hiddenSeries.value.contains('unexcused')) { + if (!hiddenSeriesSet.contains('unexcused')) { final unexcusedData = sortedSchooldays.map((schoolday) { final data = attendanceChartData[schoolday.schoolday]; final dateStr = _formatDateForChart(schoolday.schoolday); @@ -191,7 +195,7 @@ class AttendanceStatsView extends WatchingWidget { ); } - if (!_hiddenSeries.value.contains('goneHome')) { + if (!hiddenSeriesSet.contains('goneHome')) { final goneHomeData = sortedSchooldays.map((schoolday) { final data = attendanceChartData[schoolday.schoolday]; final dateStr = _formatDateForChart(schoolday.schoolday); diff --git a/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/pupil_stats_view.dart b/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/pupil_stats_view.dart index 5109b19a..efeb71f1 100644 --- a/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/pupil_stats_view.dart +++ b/school_data_hub_flutter/lib/features/statistics/chart_page/widgets/pupil_stats_view.dart @@ -58,9 +58,12 @@ class _PupilStatsViewState extends State { final buffer = StringBuffer(); buffer.writeln('Datum: ${chartData.dateString}'); + buffer.writeln( + 'Gesamt: ${dateData.specialNeeds + dateData.migrationSupport + dateData.supportLevel3 + dateData.regularPupils}', + ); buffer.writeln(); buffer.writeln('Besonderer Förderbedarf: ${dateData.specialNeeds}'); - buffer.writeln('Migrationsunterstützung: ${dateData.migrationSupport}'); + buffer.writeln('Erstförderung Deutsch: ${dateData.migrationSupport}'); buffer.writeln('Förderstufe 3: ${dateData.supportLevel3}'); buffer.writeln('Reguläre Schüler: ${dateData.regularPupils}'); buffer.writeln('Neue Schüler: ${dateData.newPupils}'); diff --git a/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/enrollment_list_tiles.dart b/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/enrollment_list_tiles.dart index ddbb6453..c93e6de5 100644 --- a/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/enrollment_list_tiles.dart +++ b/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/enrollment_list_tiles.dart @@ -19,11 +19,11 @@ class EnrollmentListTiles extends WatchingWidget { DateTime(2025, 08, 01), )..sort((a, b) => b.pupilSince.compareTo(a.pupilSince)); //- TODO: Make the date dynamic based on the current school year - final pupilsEnrolledLastYearAfterRegulatDate = controller - .pupilsEnrolledBetweenDates( + final pupilsEnrolledLastYearAfterRegulatDate = + controller.pupilsEnrolledBetweenDates( DateFormat('yyy-MM-dd').parse('2024-08-02'), DateFormat('yyy-MM-dd').parse('2025-07-31'), - ); + )..sort((a, b) => b.pupilSince.compareTo(a.pupilSince)); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/pupil_enrollment_day_card.dart b/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/pupil_enrollment_day_card.dart index 4f3f8f17..5e6ddd99 100644 --- a/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/pupil_enrollment_day_card.dart +++ b/school_data_hub_flutter/lib/features/statistics/statistics_page/list_tiles/pupil_enrollment_day_card.dart @@ -46,7 +46,7 @@ class PupilEnrollmentDateCard extends StatelessWidget { overflow: TextOverflow.ellipsis, ), Text( - 'Aufnahmedatum: ${pupil.pupilSince.formatDateForUser()}', + 'Aufnahmedatum: ${pupil.pupilSince.toLocal().formatDateForUser()}', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, diff --git a/school_data_hub_flutter/pubspec.lock b/school_data_hub_flutter/pubspec.lock index 401c2f74..86eb9c51 100644 --- a/school_data_hub_flutter/pubspec.lock +++ b/school_data_hub_flutter/pubspec.lock @@ -1001,10 +1001,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1638,10 +1638,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" timing: dependency: transitive description: diff --git a/school_data_hub_flutter/test_enc.dart b/school_data_hub_flutter/test_enc.dart deleted file mode 100644 index 40a59308..00000000 --- a/school_data_hub_flutter/test_enc.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:encrypt/encrypt.dart' as enc; -// import 'dart:math' as math; - -void main() { - // String generateRandomUtf8StringOfLength(int length) { - // const chars = - // 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - // final random = math.Random.secure(); - // return List.generate(length, (index) => chars[random.nextInt(chars.length)]) - // .join(); - // } - - final key = - 'zRvyjwzJRg05dQEBMhCQxjGcGz3tMc0Y'; // generateRandomUtf8StringOfLength(32); - - final iv = 'd8wFL7Q2ppvQRGa2'; // generateRandomUtf8StringOfLength(16); - - print('Key: $key'); - print('iv: $iv'); - final encrypter = enc.Encrypter( - enc.AES(enc.Key.fromUtf8(key), mode: enc.AESMode.cbc), - ); - - final ivFromUtf8 = enc.IV.fromUtf8(iv); - - String encryptString(String nonEncryptedString) { - final encryptedString = encrypter - .encrypt(nonEncryptedString, iv: ivFromUtf8) - .base64; - return encryptedString; - } - - final encrypted = encryptString('68548123_s*HappyTree88\$'); - - print('Encrypted String: ${encrypted}'); -} diff --git a/school_data_hub_server/lib/src/_features/schoolday_events/helpers/schoolday_event_notification_helper.dart b/school_data_hub_server/lib/src/_features/schoolday_events/helpers/schoolday_event_notification_helper.dart index 259a2fb3..9f655b98 100644 --- a/school_data_hub_server/lib/src/_features/schoolday_events/helpers/schoolday_event_notification_helper.dart +++ b/school_data_hub_server/lib/src/_features/schoolday_events/helpers/schoolday_event_notification_helper.dart @@ -70,29 +70,28 @@ class SchooldayEventNotificationHelper { } String _getEventTypeText(SchooldayEventType type) => switch (type) { - SchooldayEventType.admonition => 'Rote Karte 🚫', - SchooldayEventType.admonitionAndBanned => 'Rote Karte und Abholen 🚫🏠️', - SchooldayEventType.afternoonCareAdmonition => 'Rote Karte OGS ⚠️🍽️', - SchooldayEventType.parentsMeeting => 'Elterngespräch 👪💬', - SchooldayEventType.otherEvent => 'Sonstiges 🗒️', + SchooldayEventType.admonition => '🟥 Rote Karte', + SchooldayEventType.admonitionAndBanned => '🟥🏠️ Rote Karte und Abholen', + SchooldayEventType.afternoonCareAdmonition => '🟥🍽️ Rote Karte OGS', + SchooldayEventType.parentsMeeting => '👪💬 Elterngespräch', + SchooldayEventType.otherEvent => '🗒️ Sonstiges', // TODO: Handle this case. - SchooldayEventType.notSet => '❓️', + SchooldayEventType.notSet => '❓️ Unbekannt', }; String _getEventReasonText(String reason) => reason - .replaceAll('gm*', '🤜🤕*') - .replaceAll('gl*', '🤜🎓️*') - .replaceAll('gs*', '🤜🏫*') - .replaceAll('ab*', '🤬💔*') - .replaceAll('gv*', '🚨😱*') - .replaceAll('äa*', '😈😖*') - .replaceAll('il*', '🎓️🙉*') - .replaceAll('us*', '🛑🎓️*') - .replaceAll('ss*', '📝*') - .replaceAll('le*', '💡🧠*') - .replaceAll('fi*', '🛟🧠*') - .replaceAll('ki*', '⚠️ℹ️*') - .replaceAll('üb*', '🧠🗺️*'); + .replaceAll('gm*', '(🤜🤕) ') + .replaceAll('gl*', '(🤜🎓️) ') + .replaceAll('gs*', '(🤜🏫) ') + .replaceAll('ab*', '(🤬💔) ') + .replaceAll('gv*', '(🚨😱) ') + .replaceAll('äa*', '(😈😖) ') + .replaceAll('il*', '(🎓️🙉) ') + .replaceAll('us*', '(🛑🎓️) ') + .replaceAll('ss*', '(📝) ') + .replaceAll('le*', '(💡🧠) ') + .replaceAll('fi*', '(🛟🧠) ') + .replaceAll('ki*', '(⚠️ℹ️) '); String _getSchooldayEventNotificationText( {required String eventcreator, @@ -138,11 +137,11 @@ String _getSchooldayEventNotificationHtml({ } return ''' -

${processedStatusChange == true ? '👀 ' : ''}${escapeHtml(eventType)}

für

+

${processedStatusChange == true ? '👀 bearbeitet
' : ''} ${escapeHtml(eventType)}

für

${escapeHtml(pupilName)}

Grund:

$eventReason

- ${processedStatusChange != null ? schooldayEvent.processed == true ? '

Status: Bearbeitet von ${escapeHtml(eventcreator)} am ${escapeHtml(dateTimeAsString)}' : '

Status: Nicht bearbeitet' : '

Eingetragen von ${escapeHtml(eventcreator)} am ${escapeHtml(dateTimeAsString)}

'} -${numberOfEvents != null ? '

Das ist das $numberOfEvents. Schulereignis dieser Art für ${escapeHtml(pupilName)}.

' : ''} +${processedStatusChange != null ? schooldayEvent.processed == true ? '

Status: Bearbeitet von ${escapeHtml(eventcreator)} am ${escapeHtml(dateTimeAsString)}

' : '

Status: Nicht bearbeitet

' : '

Eingetragen von ${escapeHtml(eventcreator)} am ${escapeHtml(dateTimeAsString)}

'} +${numberOfEvents != null && (processedStatusChange == false || processedStatusChange == null) ? '

Das ist das $numberOfEvents. Schulereignis dieser Art für ${escapeHtml(pupilName)}.

' : ''} '''; }