diff --git a/school_data_hub_client/lib/src/protocol/client.dart b/school_data_hub_client/lib/src/protocol/client.dart index a37ef36d..236f187a 100644 --- a/school_data_hub_client/lib/src/protocol/client.dart +++ b/school_data_hub_client/lib/src/protocol/client.dart @@ -669,11 +669,12 @@ class EndpointPupilBookLending extends _i1.EndpointRef { {}, ); - _i2.Future<_i22.PupilBookLending?> fetchPupilBookLendingById(int id) => + _i2.Future<_i22.PupilBookLending?> fetchPupilBookLendingByLendingId( + String lendingId) => caller.callServerEndpoint<_i22.PupilBookLending?>( 'pupilBookLending', - 'fetchPupilBookLendingById', - {'id': id}, + 'fetchPupilBookLendingByLendingId', + {'lendingId': lendingId}, ); _i2.Future<_i5.PupilData> updatePupilBookLending( @@ -684,11 +685,11 @@ class EndpointPupilBookLending extends _i1.EndpointRef { {'pupilBookLending': pupilBookLending}, ); - _i2.Future<_i5.PupilData> deletePupilBookLending(int id) => + _i2.Future<_i5.PupilData> deletePupilBookLending(String lendingId) => caller.callServerEndpoint<_i5.PupilData>( 'pupilBookLending', 'deletePupilBookLending', - {'id': id}, + {'lendingId': lendingId}, ); } diff --git a/school_data_hub_flutter/lib/features/books/data/pupil_book_api_service.dart b/school_data_hub_flutter/lib/features/books/data/pupil_book_api_service.dart index aff970d4..04ca1052 100644 --- a/school_data_hub_flutter/lib/features/books/data/pupil_book_api_service.dart +++ b/school_data_hub_flutter/lib/features/books/data/pupil_book_api_service.dart @@ -7,13 +7,18 @@ final _client = di(); class PupilBookApiService { //- create pupil book lending - Future postPupilBookLending( - {required int pupilId, - required String libraryId, - required String lentBy}) async { + Future postPupilBookLending({ + required int pupilId, + required String libraryId, + required String lentBy, + }) async { final pupil = await ClientHelper.apiCall( - call: () => _client.pupilBookLending - .postPupilBookLending(pupilId, libraryId, lentBy), + call: + () => _client.pupilBookLending.postPupilBookLending( + pupilId, + libraryId, + lentBy, + ), errorMessage: 'Fehler beim Erstellen des Leihvorgangs', ); return pupil; @@ -33,7 +38,7 @@ class PupilBookApiService { //- delete pupil book - Future deletePupilBook(int lendingId) async { + Future deletePupilBook(String lendingId) async { final pupil = await ClientHelper.apiCall( call: () => _client.pupilBookLending.deletePupilBookLending(lendingId), errorMessage: 'Fehler beim Löschen des Leihvorgangs', diff --git a/school_data_hub_flutter/lib/features/books/domain/book_helper.dart b/school_data_hub_flutter/lib/features/books/domain/book_helper.dart index 3868c1bc..476500b5 100644 --- a/school_data_hub_flutter/lib/features/books/domain/book_helper.dart +++ b/school_data_hub_flutter/lib/features/books/domain/book_helper.dart @@ -5,18 +5,28 @@ import 'package:watch_it/watch_it.dart'; enum BookBorrowStatus { since2Weeks, since3Weeks, since5weeks } class BookHelpers { - static List pupilBooksLinkedToBook( - {required String libraryId}) { - // returned starting with the most recent - final pupilBooks = di() - .allPupils - .map((pupil) => pupil.pupilBooks) - .expand((element) => element as Iterable) - .where((pupilBook) => pupilBook.libraryBook?.libraryId == libraryId) - .toList(); + static List pupilBookLendingsLinkedToLibraryBook({ + required int libraryBookId, + }) { + // Get all pupil book lendings + final allPupilBookLendings = + di().allPupils + .map((pupil) => pupil.pupilBookLendings ?? []) + .expand((element) => element) + .toList(); - pupilBooks.sort((a, b) => b.lentAt.compareTo(a.lentAt)); - return pupilBooks; + // Filter by libraryId + final pupilBookLendingsLinkedToLibraryBook = + allPupilBookLendings.where((pupilBook) { + final match = pupilBook.libraryBookId == libraryBookId; + + return match; + }).toList(); + + pupilBookLendingsLinkedToLibraryBook.sort( + (a, b) => b.lentAt.compareTo(a.lentAt), + ); + return pupilBookLendingsLinkedToLibraryBook; } static BookBorrowStatus getBorrowedStatus(PupilBookLending book) { diff --git a/school_data_hub_flutter/lib/features/books/domain/book_manager.dart b/school_data_hub_flutter/lib/features/books/domain/book_manager.dart index 500d1693..2564a9ea 100644 --- a/school_data_hub_flutter/lib/features/books/domain/book_manager.dart +++ b/school_data_hub_flutter/lib/features/books/domain/book_manager.dart @@ -15,8 +15,9 @@ class BookManager { ValueListenable> get libraryBookProxies => _libraryBookProxies; - final _isbnLibraryBooksMap = - ValueNotifier>>({}); + final _isbnLibraryBooksMap = ValueNotifier>>( + {}, + ); ValueListenable>> get isbnLibraryBooksMap => _isbnLibraryBooksMap; @@ -29,9 +30,7 @@ class BookManager { List selectedTags = []; // Liste für ausgewählte Buch-Tags final _lastSelectedLocation = ValueNotifier( - LibraryBookLocation( - location: 'Bitte auswählen', - ), + LibraryBookLocation(location: 'Bitte auswählen'), ); ValueListenable get lastLocationValue => _lastSelectedLocation; @@ -41,7 +40,7 @@ class BookManager { BookManager(); -// final session = di().credentials.value; + // final session = di().credentials.value; int _currentPage = 1; final int _perPage = 30; @@ -72,13 +71,12 @@ class BookManager { // - manage collections - void _refreshLibraryBookProxyCollections( - List libraryBooks, - ) { + void _refreshLibraryBookProxyCollections(List libraryBooks) { final List libraryBookProxies = []; for (LibraryBook libraryBook in libraryBooks) { - LibraryBookProxy libraryBookProxy = - LibraryBookProxy(librarybook: libraryBook); + LibraryBookProxy libraryBookProxy = LibraryBookProxy( + librarybook: libraryBook, + ); // 1. Add the libraryBookProxy to the collection libraryBookProxies.add(libraryBookProxy); @@ -90,12 +88,14 @@ class BookManager { .where((p) => p.libraryId == libraryBookProxy.libraryId); if (existingLibraryBookProxy.isNotEmpty) { // If it exists, remove it from the list - _isbnLibraryBooksMap.value[libraryBook.book!.isbn]! - .removeWhere((p) => p.libraryId == libraryBookProxy.libraryId); + _isbnLibraryBooksMap.value[libraryBook.book!.isbn]!.removeWhere( + (p) => p.libraryId == libraryBookProxy.libraryId, + ); } // Add the new libraryBookProxy to the list - _isbnLibraryBooksMap.value[libraryBook.book!.isbn]! - .add(libraryBookProxy); + _isbnLibraryBooksMap.value[libraryBook.book!.isbn]!.add( + libraryBookProxy, + ); } else { _isbnLibraryBooksMap.value[libraryBook.book!.isbn] = [libraryBookProxy]; } @@ -107,8 +107,9 @@ class BookManager { } void _addLibraryBookProxyToCollections(LibraryBook libraryBook) { - final LibraryBookProxy libraryBookProxy = - LibraryBookProxy(librarybook: libraryBook); + final LibraryBookProxy libraryBookProxy = LibraryBookProxy( + librarybook: libraryBook, + ); final List libraryBookProxies = _libraryBookProxies.value.toList(); libraryBookProxies.add(libraryBookProxy); @@ -116,11 +117,10 @@ class BookManager { _isbnLibraryBooksMap.value[libraryBook.book!.isbn] = libraryBookProxies; } - void _updateLibraryBookProxyInCollections( - LibraryBook libraryBook, - ) { - final LibraryBookProxy libraryBookProxy = - LibraryBookProxy(librarybook: libraryBook); + void _updateLibraryBookProxyInCollections(LibraryBook libraryBook) { + final LibraryBookProxy libraryBookProxy = LibraryBookProxy( + librarybook: libraryBook, + ); final List libraryBookProxies = _libraryBookProxies.value.toList(); int index = libraryBookProxies.indexWhere( @@ -139,11 +139,13 @@ class BookManager { } void _removeLibraryBookProxyFromCollection( - LibraryBookProxy libraryBookProxy) { + LibraryBookProxy libraryBookProxy, + ) { final List libraryBookProxies = _libraryBookProxies.value.toList(); - libraryBookProxies - .removeWhere((p) => p.libraryId == libraryBookProxy.libraryId); + libraryBookProxies.removeWhere( + (p) => p.libraryId == libraryBookProxy.libraryId, + ); _libraryBookProxies.value = libraryBookProxies; // Remove from the isbnLibraryBooksMap final isbnLibraryBookProxyList = @@ -163,16 +165,23 @@ class BookManager { //- get functions - LibraryBookProxy? getLibraryBookByLibraryBookId(int? libraryBookId) { + LibraryBookProxy? getLibraryBookById(int? libraryBookId) { if (libraryBookId == null) return null; - return _libraryBookProxies.value - .firstWhereOrNull((element) => element.libraryBookId == libraryBookId); + return _libraryBookProxies.value.firstWhereOrNull( + (element) => element.id == libraryBookId, + ); } List getLibraryBooksByIsbn(int isbn) { return _isbnLibraryBooksMap.value[isbn] ?? []; } + LibraryBookProxy? getLibraryBookByLibraryId(int id) { + return _libraryBookProxies.value.firstWhereOrNull( + (element) => element.id == id, + ); + } + //- Repository calls //- BOOK TAGS @@ -213,11 +222,9 @@ class BookManager { } Future postLocation(String locationName) async { - final newLocation = LibraryBookLocation( - location: locationName, - ); - final LibraryBookLocation? responseLocation = - await _bookApiService.postBookLocation(newLocation); + final newLocation = LibraryBookLocation(location: locationName); + final LibraryBookLocation? responseLocation = await _bookApiService + .postBookLocation(newLocation); if (responseLocation == null) { return; } @@ -230,9 +237,10 @@ class BookManager { return; } - _locations.value = _locations.value - .where((loc) => loc.location != location.location) - .toList(); + _locations.value = + _locations.value + .where((loc) => loc.location != location.location) + .toList(); } void setLastLocationValue(LibraryBookLocation location) { @@ -250,7 +258,9 @@ class BookManager { _refreshLibraryBookProxyCollections(responseBooks); _notificationService.showSnackBar( - NotificationType.success, 'Bücher erfolgreich geladen'); + NotificationType.success, + 'Bücher erfolgreich geladen', + ); } Future postLibraryBook({ @@ -269,7 +279,9 @@ class BookManager { _addLibraryBookProxyToCollections(responseBook); _notificationService.showSnackBar( - NotificationType.success, 'Arbeitsheft erfolgreich erstellt'); + NotificationType.success, + 'Arbeitsheft erfolgreich erstellt', + ); } Future updateBookProperty({ @@ -280,27 +292,30 @@ class BookManager { String? description, String? readingLevel, }) async { - final LibraryBook? updatedbook = - await _bookApiService.updateLibraryBookOrBook( - isbn: isbn, - libraryId: libraryId, - title: title, - author: author, - description: description, - readingLevel: readingLevel, - ); + final LibraryBook? updatedbook = await _bookApiService + .updateLibraryBookOrBook( + isbn: isbn, + libraryId: libraryId, + title: title, + author: author, + description: description, + readingLevel: readingLevel, + ); if (updatedbook == null) { return; } _updateLibraryBookProxyInCollections(updatedbook); _notificationService.showSnackBar( - NotificationType.success, 'Arbeitsheft erfolgreich aktualisiert'); + NotificationType.success, + 'Arbeitsheft erfolgreich aktualisiert', + ); } Future deleteLibraryBook(LibraryBookProxy libraryBookProxy) async { - final bool? success = - await _bookApiService.deleteLibraryBook(libraryBookProxy.libraryBookId); + final bool? success = await _bookApiService.deleteLibraryBook( + libraryBookProxy.id, + ); if (success == null) { return; } @@ -334,17 +349,27 @@ class BookManager { } final searchResults = []; for (final result in results) { - final LibraryBookProxy libraryBookProxy = - LibraryBookProxy(librarybook: result); - searchResults.add(libraryBookProxy); + final LibraryBookProxy libraryBookProxy = LibraryBookProxy( + librarybook: result, + ); + // Check if this libraryId already exists to prevent duplicates + if (!searchResults.any( + (existing) => existing.libraryId == libraryBookProxy.libraryId, + )) { + searchResults.add(libraryBookProxy); + } } _searchResults.value = searchResults; _notificationService.showSnackBar( - NotificationType.success, 'Suchergebnisse aktualisiert'); + NotificationType.success, + 'Suchergebnisse aktualisiert', + ); } catch (e) { _notificationService.showSnackBar( - NotificationType.error, 'Fehler bei der Suche: $e'); + NotificationType.error, + 'Fehler bei der Suche: $e', + ); } } @@ -380,13 +405,19 @@ class BookManager { _hasMorePages = false; } else { final List searchResultsToUpdate = - searchResults.value; + _searchResults.value.toList(); // Create a copy for (final result in newPageResults) { - final LibraryBookProxy libraryBookProxy = - LibraryBookProxy(librarybook: result); - searchResultsToUpdate.add(libraryBookProxy); + final LibraryBookProxy libraryBookProxy = LibraryBookProxy( + librarybook: result, + ); + // Check if this libraryId already exists to prevent duplicates + if (!searchResultsToUpdate.any( + (existing) => existing.libraryId == libraryBookProxy.libraryId, + )) { + searchResultsToUpdate.add(libraryBookProxy); + } } - _searchResults.value = [...searchResultsToUpdate]; + _searchResults.value = searchResultsToUpdate; if (newPageResults.length < _perPage) { _hasMorePages = false; } diff --git a/school_data_hub_flutter/lib/features/books/domain/models/library_book_proxy.dart b/school_data_hub_flutter/lib/features/books/domain/models/library_book_proxy.dart index a060bdb1..d0999ac5 100644 --- a/school_data_hub_flutter/lib/features/books/domain/models/library_book_proxy.dart +++ b/school_data_hub_flutter/lib/features/books/domain/models/library_book_proxy.dart @@ -5,7 +5,7 @@ class LibraryBookProxy with ChangeNotifier { LibraryBook _librarybook; LibraryBookProxy({required LibraryBook librarybook}) - : _librarybook = librarybook; + : _librarybook = librarybook; void updateLibraryBook(LibraryBook librarybook) { _librarybook = librarybook; @@ -20,7 +20,7 @@ class LibraryBookProxy with ChangeNotifier { String get libraryId => _librarybook.libraryId; - int get libraryBookId => _librarybook.id!; + int get id => _librarybook.id!; LibraryBookLocation get location => _librarybook.location!; diff --git a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_card.dart b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_card.dart index 4893c072..b6216077 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_card.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_card.dart @@ -24,8 +24,10 @@ class BookCard extends WatchingWidget { const BookCard({required this.isbn, super.key}); final int isbn; - List libraryBookPupilBooks(String libraryId) { - return BookHelpers.pupilBooksLinkedToBook(libraryId: libraryId); + List libraryBookPupilBookLendings(int libraryBookId) { + return BookHelpers.pupilBookLendingsLinkedToLibraryBook( + libraryBookId: libraryBookId, + ); } @override @@ -258,7 +260,7 @@ class BookCard extends WatchingWidget { Column( children: bookProxies.map((book) { - return LibraryBookCard(bookProxy: book); + return LibraryBookCard(libraryBookProxy: book); }).toList(), ), const Gap(10), diff --git a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_pupil_card.dart b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_pupil_card.dart index 26c4ae44..81f4f4e6 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_pupil_card.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/book_pupil_card.dart @@ -12,16 +12,18 @@ 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'; -class BookPupilCard extends WatchingWidget { +class BookLendingPupilCard extends WatchingWidget { final PupilBookLending passedPupilBook; - const BookPupilCard({required this.passedPupilBook, super.key}); + const BookLendingPupilCard({required this.passedPupilBook, super.key}); @override Widget build(BuildContext context) { final pupil = watch( - di().getPupilByPupilId(passedPupilBook.pupilId)!); - final watchedPupilBook = pupil.pupilBooks?.firstWhere( - (element) => element.lendingId == passedPupilBook.lendingId); + di().getPupilByPupilId(passedPupilBook.pupilId)!, + ); + final watchedPupilBook = pupil.pupilBookLendings?.firstWhere( + (element) => element.lendingId == passedPupilBook.lendingId, + ); void updatepupilBookRating(int rating) { // TODO: Uncomment this when the API is ready // di() @@ -33,8 +35,12 @@ class BookPupilCard 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: Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -59,13 +65,16 @@ class BookPupilCard extends WatchingWidget { scrollDirection: Axis.horizontal, child: InkWell( onTap: () { - di() - .setPupilProfileNavPage(9); - Navigator.of(context).push(MaterialPageRoute( - builder: (ctx) => PupilProfilePage( - pupil: pupil, + di().setPupilProfileNavPage( + 9, + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: + (ctx) => + PupilProfilePage(pupil: pupil), ), - )); + ); }, child: Row( children: [ @@ -104,9 +113,7 @@ class BookPupilCard extends WatchingWidget { children: [ Text( watchedPupilBook!.lentBy, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontWeight: FontWeight.bold), ), const Gap(2), const Icon( @@ -116,9 +123,7 @@ class BookPupilCard extends WatchingWidget { const Gap(2), Text( watchedPupilBook.lentAt.formatForUser(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), @@ -153,23 +158,27 @@ class BookPupilCard extends WatchingWidget { children: [ const Gap(20), GrowthDropdown( - dropdownValue: watchedPupilBook.score, - onChangedFunction: updatepupilBookRating), + dropdownValue: watchedPupilBook.score, + onChangedFunction: updatepupilBookRating, + ), ], ), const Gap(5), ], ), - const Text('Status:', - style: TextStyle(fontWeight: FontWeight.bold)), + const Text( + 'Status:', + style: TextStyle(fontWeight: FontWeight.bold), + ), const Gap(2), InkWell( onLongPress: () async { final status = await longTextFieldDialog( - title: 'Status', - labelText: 'Status', - initialValue: watchedPupilBook.status ?? '', - parentContext: context); + title: 'Status', + labelText: 'Status', + initialValue: watchedPupilBook.status ?? '', + parentContext: context, + ); if (status == null) return; // TODO: Uncomment this when the API is ready // await di().patchPupilBook( @@ -177,9 +186,7 @@ class BookPupilCard extends WatchingWidget { }, child: Text( watchedPupilBook.status ?? 'Keine Einträge', - style: const TextStyle( - fontSize: 16, - ), + style: const TextStyle(fontSize: 16), ), ), const Gap(10), diff --git a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/library_book_card.dart b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/library_book_card.dart index c48a1b72..fd222373 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/library_book_card.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/library_book_card.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:school_data_hub_client/school_data_hub_client.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/common/widgets/custom_expansion_tile/custom_expansion_tile_switch.dart'; @@ -9,27 +8,27 @@ import 'package:school_data_hub_flutter/features/books/domain/models/library_boo import 'package:school_data_hub_flutter/features/books/presentation/book_list_page/widgets/book_pupil_card.dart'; class LibraryBookCard extends StatelessWidget { - final LibraryBookProxy bookProxy; - const LibraryBookCard({required this.bookProxy, super.key}); - List libraryBookPupilBooks(int bookId) { - return BookHelpers.pupilBooksLinkedToBook(libraryId: bookProxy.libraryId); - } + final LibraryBookProxy libraryBookProxy; + const LibraryBookCard({required this.libraryBookProxy, super.key}); @override Widget build(BuildContext context) { final tileController = CustomExpansionTileController(); - final bookPupilBooks = - BookHelpers.pupilBooksLinkedToBook(libraryId: bookProxy.libraryId); - BookBorrowStatus? bookBorrowStatus = bookPupilBooks.isEmpty - ? null - : BookHelpers.getBorrowedStatus(bookPupilBooks.first); - final Color borrowedColor = bookProxy.available - ? Colors.green - : bookBorrowStatus == BookBorrowStatus.since2Weeks + final bookPupilLendings = BookHelpers.pupilBookLendingsLinkedToLibraryBook( + libraryBookId: libraryBookProxy.id, + ); + BookBorrowStatus? bookBorrowStatus = + bookPupilLendings.isEmpty + ? null + : BookHelpers.getBorrowedStatus(bookPupilLendings.first); + final Color borrowedColor = + libraryBookProxy.available + ? Colors.green + : bookBorrowStatus == BookBorrowStatus.since2Weeks ? Colors.yellow : bookBorrowStatus == BookBorrowStatus.since3Weeks - ? Colors.orange - : Colors.red; + ? Colors.orange + : Colors.red; return Column( children: [ Row( @@ -38,7 +37,7 @@ class LibraryBookCard extends StatelessWidget { const Text('Buch-ID:'), const Gap(10), Text( - bookProxy.libraryId, + libraryBookProxy.libraryId, overflow: TextOverflow.fade, style: const TextStyle( fontSize: 16, @@ -50,7 +49,7 @@ class LibraryBookCard extends StatelessWidget { const Text('Ablageort:'), const Gap(10), Text( - bookProxy.location.location, + libraryBookProxy.location.location, overflow: TextOverflow.fade, style: const TextStyle( fontSize: 16, @@ -75,9 +74,10 @@ class LibraryBookCard extends StatelessWidget { CustomExpansionTileContent( title: null, tileController: tileController, - widgetList: bookPupilBooks.isEmpty - ? [ - const Padding( + widgetList: + bookPupilLendings.isEmpty + ? [ + const Padding( padding: EdgeInsets.all(15.0), child: Text( 'Keine Ausleihen', @@ -86,11 +86,12 @@ class LibraryBookCard extends StatelessWidget { fontWeight: FontWeight.bold, color: Colors.black, ), - )) - ] - : bookPupilBooks.map((pupilBook) { - return BookPupilCard(passedPupilBook: pupilBook); - }).toList(), + ), + ), + ] + : bookPupilLendings.map((pupilBook) { + return BookLendingPupilCard(passedPupilBook: pupilBook); + }).toList(), ), const Gap(5), ], diff --git a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/pupil_book_card.dart b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/pupil_book_card.dart index 41e20a48..30e834c9 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/pupil_book_card.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/book_list_page/widgets/pupil_book_card.dart @@ -16,183 +16,192 @@ import 'package:watch_it/watch_it.dart'; final _hubSessionManager = di(); class PupilBookCard extends WatchingWidget { - const PupilBookCard( - {required this.pupilBook, required this.pupilId, super.key}); + const PupilBookCard({ + required this.pupilBook, + required this.pupilId, + super.key, + }); final PupilBookLending pupilBook; final int pupilId; @override Widget build(BuildContext context) { - final LibraryBookProxy bookProxy = di() - .getLibraryBookByLibraryBookId(pupilBook.libraryBookId)!; + final LibraryBookProxy bookProxy = + di().getLibraryBookById(pupilBook.libraryBookId)!; return ClipRRect( borderRadius: BorderRadius.circular(20), child: Card( - child: InkWell( - // onTap: () { - // Navigator.of(context).push(MaterialPageRoute( - // builder: (ctx) => SchoolListPupils( - // workbook, - // ), - // )); - // }, - onLongPress: () async { - if (pupilBook.lentBy != _hubSessionManager.userName || - !_hubSessionManager.isAdmin) { - informationDialog(context, 'Keine Berechtigung', - 'Arbeitshefte können nur von der eintragenden Person bearbeitet werden!'); - return; - } - final bool? result = await confirmationDialog( + child: InkWell( + // onTap: () { + // Navigator.of(context).push(MaterialPageRoute( + // builder: (ctx) => SchoolListPupils( + // workbook, + // ), + // )); + // }, + onLongPress: () async { + if (pupilBook.lentBy != _hubSessionManager.userName || + !_hubSessionManager.isAdmin) { + informationDialog( + context, + 'Keine Berechtigung', + 'Arbeitshefte können nur von der eintragenden Person bearbeitet werden!', + ); + return; + } + final bool? result = await confirmationDialog( context: context, title: 'Ausleihe löschen', message: - 'Ausleihe des Buches "${bookProxy.title}" wirklich löschen?'); - if (result == true) { - // TODO: Uncomment this when the API is ready - // di().deletePupilBook( - // lendingId: pupilBook.lendingId); - } - }, - child: Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Gap(5), - Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - InkWell( - onTap: () async { - final File? file = await createImageFile(context); - if (file == null) return; - // TODO: Uncomment this when the API is ready ? - // await di() - // .postWorkbookFile(file, book.isbn); - }, - // Todo: Uncomment this when the API is ready - // onLongPress: (bookProxy.book.imagePath == null) - // ? () {} - // : () async { - // if (bookProxy.imageId == null) { - // return; - // } - // final bool? result = await confirmationDialog( - // context: context, - // title: 'Bild löschen', - // message: 'Bild löschen?'); - // if (result != true) return; - // // await di() - // // .deleteAuthorizationFile( - // // pupil.internalId, - // // authorizationId, - // // pupilAuthorization.fileId!, - // // ); - // }, - child: Container(), - // Provider.value( - // updateShouldNotify: (oldValue, newValue) => - // oldValue.documentUrl != newValue.documentUrl, - // value: DocumentImageData( - // documentTag: book.imageId, - // documentUrl: - // '${di().env!.serverUrl}${WorkbookApiService().getWorkbookImage(book.isbn)}', - // size: 100), - // child: const DocumentImage(), - // ), - ), - const Gap(10), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10, bottom: 8), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - bookProxy.title, - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), + 'Ausleihe des Buches "${bookProxy.title}" wirklich löschen?', + ); + if (result == true) { + // TODO: Uncomment this when the API is ready + // di().deletePupilBook( + // lendingId: pupilBook.lendingId); + } + }, + child: Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Gap(5), + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + InkWell( + onTap: () async { + final File? file = await createImageFile(context); + if (file == null) return; + // TODO: Uncomment this when the API is ready ? + // await di() + // .postWorkbookFile(file, book.isbn); + }, + // Todo: Uncomment this when the API is ready + // onLongPress: (bookProxy.book.imagePath == null) + // ? () {} + // : () async { + // if (bookProxy.imageId == null) { + // return; + // } + // final bool? result = await confirmationDialog( + // context: context, + // title: 'Bild löschen', + // message: 'Bild löschen?'); + // if (result != true) return; + // // await di() + // // .deleteAuthorizationFile( + // // pupil.internalId, + // // authorizationId, + // // pupilAuthorization.fileId!, + // // ); + // }, + child: Container(), + // Provider.value( + // updateShouldNotify: (oldValue, newValue) => + // oldValue.documentUrl != newValue.documentUrl, + // value: DocumentImageData( + // documentTag: book.imageId, + // documentUrl: + // '${di().env!.serverUrl}${WorkbookApiService().getWorkbookImage(book.isbn)}', + // size: 100), + // child: const DocumentImage(), + // ), + ), + const Gap(10), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 10, bottom: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + bookProxy.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - const Gap(10), - ], - ), - const Gap(5), - // Row( - // children: [ - // const Text('ISBN:'), - // const Gap(10), - // Text( - // workbook.isbn.toString(), - // style: const TextStyle( - // fontSize: 16, - // fontWeight: FontWeight.bold, - // color: Colors.black, - // ), - // ), - // ], - // ), - // const Gap(5), - Row( - children: [ - Text(bookProxy.author, + const Gap(10), + ], + ), + const Gap(5), + // Row( + // children: [ + // const Text('ISBN:'), + // const Gap(10), + // Text( + // workbook.isbn.toString(), + // style: const TextStyle( + // fontSize: 16, + // fontWeight: FontWeight.bold, + // color: Colors.black, + // ), + // ), + // ], + // ), + // const Gap(5), + Row( + children: [ + Text( + bookProxy.author, overflow: TextOverflow.fade, - style: const TextStyle( - fontSize: 14, - )), - const Spacer(), - Text( - bookProxy.readingLevel ?? ReadingLevel.notSet.value, - maxLines: 2, - overflow: TextOverflow.fade, - style: const TextStyle( - fontSize: 14, + style: const TextStyle(fontSize: 14), ), - ), - const Gap(10), - ], - ), - const Gap(5), - Row( - children: [ - const Text('Ausgeliehen von:'), - const Gap(5), - Text( - pupilBook.lentBy, - style: const TextStyle( - fontWeight: FontWeight.bold, + const Spacer(), + Text( + bookProxy.readingLevel ?? + ReadingLevel.notSet.value, + maxLines: 2, + overflow: TextOverflow.fade, + style: const TextStyle(fontSize: 14), ), - ), - const Gap(5), - const Text('am'), - const Gap(5), - Text( - pupilBook.lentAt.formatForUser(), - style: const TextStyle( - fontWeight: FontWeight.bold, + const Gap(10), + ], + ), + const Gap(5), + Row( + children: [ + const Text('Ausgeliehen von:'), + const Gap(5), + Text( + pupilBook.lentBy, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const Gap(5), + const Text('am'), + const Gap(5), + Text( + pupilBook.lentAt.formatForUser(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), ), - ) - ], - ), - const Gap(10), - ], + ], + ), + const Gap(10), + ], + ), ), ), - ), - ], + ], + ), ), ), - )), + ), ); } } diff --git a/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_result_card.dart b/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_result_card.dart index be198f88..67669e29 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_result_card.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_result_card.dart @@ -4,6 +4,7 @@ import 'package:school_data_hub_flutter/app_utils/extensions.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/common/widgets/dialogs/long_textfield_dialog.dart'; +import 'package:school_data_hub_flutter/common/widgets/unencrypted_image_in_card.dart'; import 'package:school_data_hub_flutter/features/books/domain/book_manager.dart'; import 'package:school_data_hub_flutter/features/books/domain/models/enums.dart'; import 'package:school_data_hub_flutter/features/books/domain/models/library_book_proxy.dart'; @@ -74,30 +75,16 @@ class SearchResultBookCard extends WatchingWidget { Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Column( - children: [SizedBox(height: 10), const Gap(10)], - - // TODOD: Repair when image loading is implemented - // InkWell( - // onTap: () async { - // final File? file = await uploadImageFile(context); - // if (file == null) return; - // await locator() - // .patchBookImage(file, book.isbn); - // }, - // child: UnencryptedImageInCard( - // documentImageData: DocumentImageData( - // documentTag: book.imageId, - // documentUrl: - // '${locator().env!.serverUrl}${BookApiService.getBookImageUrl(book.isbn)}', - // size: 140, - // ), - // ), - // ), + // Book image on the left side + UnencryptedImageInCard( + cacheKey: bookProxy.isbn.toString(), + path: bookProxy.imagePath, + size: 100, ), + const Gap(15), Expanded( child: Padding( - padding: const EdgeInsets.only(left: 15, bottom: 8), + padding: const EdgeInsets.only(bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, @@ -195,9 +182,24 @@ class SearchResultBookCard extends WatchingWidget { ), Column( children: - books.map((book) { - return LibraryBookCard(bookProxy: book); - }).toList(), + books + .fold>([], ( + uniqueBooks, + book, + ) { + // Only add if libraryId is not already in the list + if (!uniqueBooks.any( + (existing) => + existing.libraryId == book.libraryId, + )) { + uniqueBooks.add(book); + } + return uniqueBooks; + }) + .map((book) { + return LibraryBookCard(libraryBookProxy: book); + }) + .toList(), ), const Gap(10), ], diff --git a/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_results_page.dart b/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_results_page.dart index b2963d18..bace8829 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_results_page.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/book_search_page/book_search_results_page.dart @@ -43,9 +43,10 @@ class BookSearchResultsPage extends WatchingWidget { keywords: keywords, location: location, readingLevel: readingLevel, - available: borrowStatus == BorrowedStatus.available - ? true - : borrowStatus == BorrowedStatus.borrowed + available: + borrowStatus == BorrowedStatus.available + ? true + : borrowStatus == BorrowedStatus.borrowed ? false : null, ); @@ -53,8 +54,10 @@ class BookSearchResultsPage extends WatchingWidget { }); return Scaffold( - appBar: - const GenericAppBar(iconData: Icons.search, title: 'Suchergebnisse'), + appBar: const GenericAppBar( + iconData: Icons.search, + title: 'Suchergebnisse', + ), body: ValueListenableBuilder>( valueListenable: bookManager.searchResults, builder: (context, searchResults, _) { @@ -62,8 +65,10 @@ class BookSearchResultsPage extends WatchingWidget { return const Center(child: Text("Keine Ergebnisse")); } - final groupedMap = - groupBy(searchResults, (LibraryBookProxy book) => book.isbn); + final groupedMap = groupBy( + searchResults, + (LibraryBookProxy book) => book.isbn, + ); final groups = groupedMap.values.toList(); return Center( diff --git a/school_data_hub_flutter/lib/features/books/presentation/widgets/pupil_book_card.dart b/school_data_hub_flutter/lib/features/books/presentation/widgets/pupil_book_card.dart index a9357c2d..cf3543ad 100644 --- a/school_data_hub_flutter/lib/features/books/presentation/widgets/pupil_book_card.dart +++ b/school_data_hub_flutter/lib/features/books/presentation/widgets/pupil_book_card.dart @@ -8,224 +8,261 @@ import 'package:school_data_hub_flutter/common/widgets/dialogs/confirmation_dial import 'package:school_data_hub_flutter/common/widgets/dialogs/information_dialog.dart'; import 'package:school_data_hub_flutter/common/widgets/dialogs/long_textfield_dialog.dart'; import 'package:school_data_hub_flutter/common/widgets/growth_dropdown.dart'; +import 'package:school_data_hub_flutter/common/widgets/unencrypted_image_in_card.dart'; import 'package:school_data_hub_flutter/core/session/hub_session_manager.dart'; import 'package:school_data_hub_flutter/features/books/domain/book_manager.dart'; import 'package:school_data_hub_flutter/features/books/domain/models/library_book_proxy.dart'; import 'package:school_data_hub_flutter/features/pupil/domain/pupil_manager.dart'; import 'package:watch_it/watch_it.dart'; -class PupilBookCard extends StatelessWidget { - const PupilBookCard( - {required this.pupilBookLending, required this.pupilId, super.key}); +class PupilBookLendingCard extends StatelessWidget { + const PupilBookLendingCard({ + required this.pupilBookLending, + required this.pupilId, + super.key, + }); final PupilBookLending pupilBookLending; final int pupilId; @override Widget build(BuildContext context) { final LibraryBookProxy book = - di().getLibraryBookByLibraryBookId(pupilBookLending.id!)!; + di().getLibraryBookById(pupilBookLending.libraryBookId)!; void updatepupilBookRating(int rating) { - di().updatePupilBook( - pupilBookLending: pupilBookLending, score: (value: rating)); + di().updatePupilBookLending( + pupilBookLending: pupilBookLending, + score: (value: rating), + ); } return ClipRRect( borderRadius: BorderRadius.circular(20), child: Card( - child: InkWell( - onLongPress: () async { - if (pupilBookLending.lentBy != di().userName || - !di().isAdmin) { - informationDialog(context, 'Keine Berechtigung', - 'Ausleihen können nur von der eintragenden Person bearbeitet werden!'); - return; - } - final bool? result = await confirmationDialog( + child: InkWell( + onLongPress: () async { + if (pupilBookLending.lentBy != di().userName || + !di().isAdmin) { + informationDialog( + context, + 'Keine Berechtigung', + 'Ausleihen können nur von der eintragenden Person bearbeitet werden!', + ); + return; + } + final bool? result = await confirmationDialog( context: context, title: 'Ausleihe löschen', - message: 'Ausleihe von "${book.title}" wirklich löschen?'); - if (result == true) { - di().deletePupilBook(lendingId: pupilBookLending.id!); - } - }, - child: Padding( - padding: - const EdgeInsets.only(top: 8.0, bottom: 5, left: 10, right: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Text( - book.title, - style: const TextStyle( - fontSize: 20, fontWeight: FontWeight.bold), + message: 'Ausleihe von "${book.title}" wirklich löschen?', + ); + if (result == true) { + di().deletePupilBook( + lendingId: pupilBookLending.lendingId, + ); + } + }, + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 5, + left: 10, + right: 10, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + book.title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), ), ), - ), - const Gap(10), - ], - ), - const Gap(10), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Gap(5), - const Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // TODO: Uncomment this when image loading is implemented - const SizedBox( - height: 140, - ), - // UnencryptedImageInCard( - // documentImageData: DocumentImageData( - // documentTag: book.imageId, - // documentUrl: - // '${di().env!.serverUrl}${BookApiService.getBookImageUrl(book.isbn)}', - // size: 140), - // ), - ], - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 10, bottom: 8), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Gap(5), - Row( - children: [ - Text( - pupilBookLending.lentBy, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - const Gap(2), - const Icon( - Icons.arrow_circle_right_rounded, - color: Colors.orange, - ), - const Gap(2), - Text( - pupilBookLending.lentAt - .toLocal() - .formatForUser(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - if (pupilBookLending.returnedAt != null) + const Gap(10), + ], + ), + const Gap(10), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Gap(5), + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + UnencryptedImageInCard( + cacheKey: book.isbn.toString(), + path: book.imagePath, + size: 80, + ), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 10, bottom: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Gap(5), + Text('Bucherei-Id: ${book.libraryId}'), + const Gap(5), Row( children: [ Text( - pupilBookLending.receivedBy!, + pupilBookLending.lentBy, style: const TextStyle( fontWeight: FontWeight.bold, ), ), const Gap(2), const Icon( - Icons.arrow_circle_left_rounded, - color: Colors.green, + Icons.arrow_circle_right_rounded, + color: Colors.orange, ), const Gap(2), Text( - pupilBookLending.returnedAt!.formatForUser(), + pupilBookLending.lentAt + .toLocal() + .formatForUser(), style: const TextStyle( fontWeight: FontWeight.bold, ), ), ], ), - const Gap(10), - ], + if (pupilBookLending.returnedAt != null) + Row( + children: [ + Text( + pupilBookLending.receivedBy!, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + const Gap(2), + const Icon( + Icons.arrow_circle_left_rounded, + color: Colors.green, + ), + const Gap(2), + Text( + pupilBookLending.returnedAt! + .formatForUser(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Gap(10), + ], + ), ), ), - ), - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GrowthDropdown( + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GrowthDropdown( dropdownValue: pupilBookLending.score, - onChangedFunction: updatepupilBookRating), - ], - ), - ], - ), - const Text( - 'Beobachtungen:', - style: TextStyle( - fontWeight: FontWeight.bold, + onChangedFunction: updatepupilBookRating, + ), + ], + ), + ], + ), + const Text( + 'Beobachtungen:', + style: TextStyle(fontWeight: FontWeight.bold), ), - ), - InkWell( - onLongPress: () async { - final status = await longTextFieldDialog( + InkWell( + onTap: () async { + final status = await longTextFieldDialog( title: 'Status', labelText: 'Status', initialValue: pupilBookLending.status ?? '', - parentContext: context); - if (status == null) return; - await di().updatePupilBook( + parentContext: context, + ); + if (status == null) return; + await di().updatePupilBookLending( pupilBookLending: pupilBookLending, - status: (value: status)); - }, - child: Text( - (pupilBookLending.status == null || - pupilBookLending.status == '') - ? 'Kein Eintrag' - : pupilBookLending.status!, - style: const TextStyle( - fontSize: 16, - color: AppColors.interactiveColor, + status: (value: status), + ); + }, + borderRadius: BorderRadius.circular(8), + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.interactiveColor.withValues( + alpha: 0.3, + ), + ), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + (pupilBookLending.status == null || + pupilBookLending.status == '') + ? 'Kein Eintrag - Tippen zum Bearbeiten' + : pupilBookLending.status!, + style: const TextStyle( + fontSize: 16, + color: AppColors.interactiveColor, + ), + ), ), ), - ), - const Gap(10), - if (pupilBookLending.returnedAt == null) ...[ - const Gap(5), - Row( - children: [ - Expanded( - child: Padding( - padding: - const EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: ElevatedButton( - onPressed: () async { - final result = await confirmationDialog( + const Gap(10), + if (pupilBookLending.returnedAt == null) ...[ + const Gap(5), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 8, + right: 8, + bottom: 8, + ), + child: ElevatedButton( + onPressed: () async { + final result = await confirmationDialog( context: context, title: 'Buch zurückgeben', message: - 'Buch "${book.title}" wirklich zurückgeben?'); - if (result!) { - di().returnLibraryBook( - pupilBookLending: pupilBookLending, + 'Buch "${book.title}" wirklich zurückgeben?', ); - } - }, - style: AppStyles.successButtonStyle, - child: const Text( - 'BUCH ZURÜCKGEBEN', - style: AppStyles.buttonTextStyle, + if (result!) { + di().returnLibraryBook( + pupilBookLending: pupilBookLending, + ); + } + }, + style: AppStyles.successButtonStyle, + child: const Text( + 'BUCH ZURÜCKGEBEN', + style: AppStyles.buttonTextStyle, + ), ), ), ), - ), - ], - ) - ] - ], + ], + ), + ], + ], + ), ), ), - )), + ), ); } } 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 d49c52ae..6402854d 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 @@ -28,21 +28,34 @@ class LearningListCard extends WatchingWidget { watch(pupil); final expansionTileController = createOnce( - () => CustomExpansionTileController()); + () => CustomExpansionTileController(), + ); final selectedContentNotifier = SelectedLearningContentNotifier(); - final selectedContent = watchPropertyValue((m) => m.selectedContent, - target: selectedContentNotifier); + final selectedContent = watchPropertyValue( + (m) => m.selectedContent, + target: selectedContentNotifier, + ); final competenceCheckstats = CompetenceHelper.competenceChecksStats(pupil); final totalCompetencesToReport = competenceCheckstats.total; final totalCompetencesChecked = competenceCheckstats.checked; + // Calculate book lending statistics for this pupil + final pupilBookLendings = pupil.pupilBookLendings ?? []; + final totalLendings = pupilBookLendings.length; + final notReturnedLendings = + pupilBookLendings.where((lending) => lending.returnedAt == null).length; + return Card( color: Colors.white, 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: Column( children: [ Row( @@ -65,12 +78,14 @@ class LearningListCard extends WatchingWidget { child: InkWell( onTap: () { di().setPupilProfileNavPage( - ProfileNavigationState.learning.value); - Navigator.of(context).push(MaterialPageRoute( - builder: (ctx) => PupilProfilePage( - pupil: pupil, + ProfileNavigationState.learning.value, + ); + Navigator.of(context).push( + MaterialPageRoute( + builder: + (ctx) => PupilProfilePage(pupil: pupil), ), - )); + ); }, child: Row( children: [ @@ -111,8 +126,9 @@ class LearningListCard extends WatchingWidget { includeSwitch: true, switchColor: AppColors.interactiveColor, customExpansionTileController: expansionTileController, - expansionSwitchWidget: - CompetenceChecksBadges(pupil: pupil), + expansionSwitchWidget: CompetenceChecksBadges( + pupil: pupil, + ), ), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -134,14 +150,16 @@ class LearningListCard extends WatchingWidget { CustomExpansionTileSwitch( customExpansionTileController: expansionTileController, expansionSwitchWidget: const Text( - 'Lernziele sind noch nicht implementiert'), + 'Lernziele sind noch nicht implementiert', + ), ), ], if (selectedContent == SelectedContent.workbooks) ...[ CustomExpansionTileSwitch( customExpansionTileController: expansionTileController, - expansionSwitchWidget: - WorkbooksInfoSwitch(pupil: pupil), + expansionSwitchWidget: WorkbooksInfoSwitch( + pupil: pupil, + ), includeSwitch: true, switchColor: AppColors.interactiveColor, ), @@ -149,7 +167,46 @@ class LearningListCard extends WatchingWidget { if (selectedContent == SelectedContent.books) ...[ CustomExpansionTileSwitch( customExpansionTileController: expansionTileController, - expansionSwitchWidget: const Text('Bücher'), + expansionSwitchWidget: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Gap(10), + Column( + children: [ + Text( + totalLendings.toString(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + const Text( + 'gesamt', + style: TextStyle(fontSize: 10), + ), + ], + ), + const Gap(10), + Column( + children: [ + Text( + notReturnedLendings.toString(), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + const Text( + 'aktiv', + style: TextStyle(fontSize: 10), + ), + ], + ), + const Gap(10), + ], + ), ), ], ], @@ -167,19 +224,20 @@ class LearningListCard extends WatchingWidget { Padding( padding: const EdgeInsets.only(top: 5, left: 5, right: 5), child: CustomExpansionTileContent( - title: null, - tileController: expansionTileController, - widgetList: [ - if (selectedContent == SelectedContent.competenceStatuses) - PupilLearningContentCompetenceStatuses(pupil: pupil), - if (selectedContent == SelectedContent.competenceGoals) - PupilLearningContentCompetenceGoals(pupil: pupil), - if (selectedContent == SelectedContent.workbooks) - PupilLearningContentWorkbooks(pupil: pupil), - if (selectedContent == SelectedContent.books) - PupilLearningContentBooks(pupil: pupil) - ]), - ) + title: null, + tileController: expansionTileController, + widgetList: [ + if (selectedContent == SelectedContent.competenceStatuses) + PupilLearningContentCompetenceStatuses(pupil: pupil), + if (selectedContent == SelectedContent.competenceGoals) + PupilLearningContentCompetenceGoals(pupil: pupil), + if (selectedContent == SelectedContent.workbooks) + PupilLearningContentWorkbooks(pupil: pupil), + if (selectedContent == SelectedContent.books) + PupilLearningContentBooks(pupil: pupil), + ], + ), + ), ], ), ); diff --git a/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/pupil_learning_content/pupil_learning_content_books.dart b/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/pupil_learning_content/pupil_learning_content_books.dart index 2f571216..b358a75e 100644 --- a/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/pupil_learning_content/pupil_learning_content_books.dart +++ b/school_data_hub_flutter/lib/features/learning/presentation/pupil_competence_list_page/widgets/pupil_learning_content/pupil_learning_content_books.dart @@ -7,7 +7,7 @@ import 'package:school_data_hub_flutter/app_utils/scanner.dart'; import 'package:school_data_hub_flutter/common/services/notification_service.dart'; import 'package:school_data_hub_flutter/common/theme/styles.dart'; import 'package:school_data_hub_flutter/common/widgets/dialogs/short_textfield_dialog.dart'; -import 'package:school_data_hub_flutter/features/books/presentation/book_list_page/widgets/pupil_book_card.dart'; +import 'package:school_data_hub_flutter/features/books/presentation/widgets/pupil_book_card.dart'; import 'package:school_data_hub_flutter/features/pupil/domain/models/pupil_proxy.dart'; import 'package:school_data_hub_flutter/features/pupil/domain/pupil_manager.dart'; import 'package:watch_it/watch_it.dart'; @@ -35,50 +35,56 @@ class PupilLearningContentBooks extends StatelessWidget { String? bookId; if (Platform.isIOS || Platform.isAndroid) { final scannedBookId = await qrScanner( - context: context, overlayText: 'Buch-ID scannen'); + context: context, + overlayText: 'Buch-ID scannen', + ); if (!(scannedBookId != null)) { - di().showSnackBar(NotificationType.error, - 'Buch-ID konnte nicht gescannt werden'); + di().showSnackBar( + NotificationType.error, + 'Buch-ID konnte nicht gescannt werden', + ); return; } bookId = scannedBookId.replaceFirst('Buch ID: ', ''); } else { bookId = await shortTextfieldDialog( - context: context, - title: 'Buch-Id', - labelText: 'Buch-Id eingeben', - hintText: 'Buch-Id', - obscureText: false); + context: context, + title: 'Bibliotheks-Id', + labelText: 'Buch-Id eingeben', + hintText: 'Buch-Id', + obscureText: false, + ); } if (bookId != null) { di().postPupilBookLending( - pupilId: pupil.pupilId, libraryId: bookId); + pupilId: pupil.pupilId, + libraryId: bookId, + ); return; } }, - child: const Text( - "BUCH AUSLEIHEN", - style: AppStyles.buttonTextStyle, - ), + child: const Text("BUCH AUSLEIHEN", style: AppStyles.buttonTextStyle), ), const Gap(5), - if (pupil.pupilBooks!.isNotEmpty) ...[ + if (pupil.pupilBookLendings!.isNotEmpty) ...[ const Gap(10), ListView.builder( padding: const EdgeInsets.all(0), shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: pupil.pupilBooks!.length, + itemCount: pupil.pupilBookLendings!.length, itemBuilder: (context, int index) { - List pupilBooks = pupil.pupilBooks!; + List pupilBookLendings = + pupil.pupilBookLendings!; return ClipRRect( borderRadius: BorderRadius.circular(25.0), child: Column( children: [ - PupilBookCard( - pupilBook: pupilBooks[index], - pupilId: pupil.internalId), + PupilBookLendingCard( + pupilBookLending: pupilBookLendings[index], + pupilId: pupil.pupilId, + ), ], ), ); 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 7cba461a..125b48e5 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 @@ -193,7 +193,7 @@ class PupilProxy with ChangeNotifier { List? get pupilWorkbooks => _pupilData.pupilWorkbooks; - List? get pupilBooks => _pupilData.pupilBookLendings; + List? get pupilBookLendings => _pupilData.pupilBookLendings; // learning support related diff --git a/school_data_hub_flutter/lib/features/pupil/domain/pupil_manager.dart b/school_data_hub_flutter/lib/features/pupil/domain/pupil_manager.dart index 68f2f53c..4b91dbbc 100644 --- a/school_data_hub_flutter/lib/features/pupil/domain/pupil_manager.dart +++ b/school_data_hub_flutter/lib/features/pupil/domain/pupil_manager.dart @@ -318,7 +318,7 @@ class PupilManager extends ChangeNotifier { return; } - Future deletePupilBook({required int lendingId}) async { + Future deletePupilBook({required String lendingId}) async { final pupil = await _pupilBookApiService.deletePupilBook(lendingId); if (pupil == null) { return; @@ -347,7 +347,7 @@ class PupilManager extends ChangeNotifier { return; } - Future updatePupilBook({ + Future updatePupilBookLending({ required PupilBookLending pupilBookLending, DateTime? lentAt, String? lentBy, diff --git a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/pupil_profile_page_content.dart b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/pupil_profile_page_content.dart index c765cae6..4097490e 100644 --- a/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/pupil_profile_page_content.dart +++ b/school_data_hub_flutter/lib/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/pupil_profile_page_content.dart @@ -8,6 +8,8 @@ import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profil import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/communication_content/pupil_profile_communication_content.dart'; import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/credit/pupil_profile_credit_content.dart'; import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/infos_content/pupil_profile_infos_content.dart'; +import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/learning_content/pupil_profile_learning_content.dart'; +import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/learning_support_content/pupil_profile_learning_support_content.dart'; import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/school_list_content/pupil_school_lists_content_card.dart'; import 'package:school_data_hub_flutter/features/pupil/presentation/pupil_profile_page/widgets/pupil_profile_page_content/schoolday_events_content/pupil_profile_schoolday_events_content.dart'; import 'package:school_data_hub_flutter/features/pupil/presentation/widgets/pupil_profile_attendance_content.dart'; @@ -102,10 +104,10 @@ class PupilProfilePageContent extends WatchingWidget { return PupilSchoolListsContentCard(pupil: pupil); } else if (navState == ProfileNavigationState.authorization.value) { return PupilProfileAuthorizationContent(pupil: pupil); - // } else if (navState == ProfileNavigationState.learningSupport.value) { - // return PupilProfileLearningSupportContent(pupil: pupil); - // } else if (navState == ProfileNavigationState.learning.value) { - // return PupilLearningContent(pupil: pupil); + } else if (navState == ProfileNavigationState.learningSupport.value) { + return PupilProfileLearningSupportContent(pupil: pupil); + } else if (navState == ProfileNavigationState.learning.value) { + return PupilLearningContent(pupil: pupil); } else { return PupilProfileInfosContent(pupil: pupil); } diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_classroom_page/new_classroom_page.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_classroom_page/new_classroom_page.dart index cba39ca4..9a17481b 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_classroom_page/new_classroom_page.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_classroom_page/new_classroom_page.dart @@ -119,6 +119,8 @@ class NewClassroomPage extends WatchingWidget { backgroundColor: Colors.green, ), ); + + Navigator.of(context).pop(); } else { // Create new classroom final newClassroom = Classroom( @@ -134,9 +136,9 @@ class NewClassroomPage extends WatchingWidget { backgroundColor: Colors.green, ), ); - } - Navigator.of(context).pop(); + Navigator.of(context).pop(newClassroom); + } }, onCancel: () => Navigator.of(context).pop(), onDelete: diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_lesson_group_page/new_lesson_group_page.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_lesson_group_page/new_lesson_group_page.dart index e7831e80..b0fea3ec 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_lesson_group_page/new_lesson_group_page.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_lesson_group_page/new_lesson_group_page.dart @@ -85,225 +85,236 @@ class NewLessonGroupPage extends WatchingWidget { constraints: const BoxConstraints(maxWidth: 800), child: Form( key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Name field - NameField(controller: nameController), - const Gap(20), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name field + NameField(controller: nameController), + const Gap(20), - // Color picker field - ValueListenableBuilder( - valueListenable: selectedColor, - builder: (context, color, child) { - return ColorPickerField( - selectedColor: color, - onColorChanged: (newColor) { - selectedColor.value = newColor; - }, - ); - }, - ), - const Gap(20), + // Color picker field + ValueListenableBuilder( + valueListenable: selectedColor, + builder: (context, color, child) { + return ColorPickerField( + selectedColor: color, + onColorChanged: (newColor) { + selectedColor.value = newColor; + }, + ); + }, + ), + const Gap(20), - // Pupil Management Section - ValueListenableBuilder>( - valueListenable: selectedPupilIds, - builder: (context, pupilIds, child) { - return PupilManagementSection( - timetableManager: timetableManager, - lessonGroupId: lessonGroup?.id, - selectedPupilIds: pupilIds, - onPupilIdsChanged: (newPupilIds) { - selectedPupilIds.value = newPupilIds; - }, - ); - }, - ), - const Gap(32), + // Pupil Management Section + ValueListenableBuilder>( + valueListenable: selectedPupilIds, + builder: (context, pupilIds, child) { + return PupilManagementSection( + timetableManager: timetableManager, + lessonGroupId: lessonGroup?.id, + selectedPupilIds: pupilIds, + onPupilIdsChanged: (newPupilIds) { + selectedPupilIds.value = newPupilIds; + }, + ); + }, + ), + const Gap(32), - // Action buttons - ActionButtons( - isEditing: _isEditing, - onSave: () { - if (!formKey.currentState!.validate()) { - return; - } + // Action buttons + ActionButtons( + isEditing: _isEditing, + onSave: () { + if (!formKey.currentState!.validate()) { + return; + } - final name = nameController.text.trim(); - final color = selectedColor.value; + final name = nameController.text.trim(); + final color = selectedColor.value; - if (name.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Bitte füllen Sie alle Pflichtfelder aus', + if (name.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Bitte füllen Sie alle Pflichtfelder aus', + ), + backgroundColor: Colors.red, ), - backgroundColor: Colors.red, - ), - ); - return; - } + ); + return; + } - final currentTimetable = timetableManager.timetable.value; - if (currentTimetable?.id == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Fehler: Kein Stundenplan ausgewählt', + final currentTimetable = + timetableManager.timetable.value; + if (currentTimetable?.id == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Fehler: Kein Stundenplan ausgewählt', + ), + backgroundColor: Colors.red, ), - backgroundColor: Colors.red, - ), + ); + return; + } + + final now = DateTime.now().toUtcForServer(); + final lessonGroupData = LessonGroup( + id: lessonGroup?.id, + publicId: + lessonGroup?.publicId ?? + 'GROUP_${now.millisecondsSinceEpoch}', + name: name, + color: color, + timetableId: currentTimetable!.id!, + createdBy: lessonGroup?.createdBy ?? 'user', + createdAt: lessonGroup?.createdAt ?? now, + modifiedBy: 'user', + modifiedAt: now, ); - return; - } - final now = DateTime.now().toUtcForServer(); - final lessonGroupData = LessonGroup( - id: lessonGroup?.id, - publicId: - lessonGroup?.publicId ?? - 'GROUP_${now.millisecondsSinceEpoch}', - name: name, - color: color, - timetableId: currentTimetable!.id!, - createdBy: lessonGroup?.createdBy ?? 'user', - createdAt: lessonGroup?.createdAt ?? now, - modifiedBy: 'user', - modifiedAt: now, - ); + if (_isEditing) { + // Update existing lesson group + timetableManager.updateLessonGroup(lessonGroupData); - if (_isEditing) { - // Update existing lesson group - timetableManager.updateLessonGroup(lessonGroupData); + // Update pupil memberships for existing lesson group + if (lessonGroup?.id != null) { + timetableManager + .updatePupilMembershipsForLessonGroup( + lessonGroup!.id!, + selectedPupilIds.value, + ); + } - // Update pupil memberships for existing lesson group - if (lessonGroup?.id != null) { - timetableManager.updatePupilMembershipsForLessonGroup( - lessonGroup!.id!, - selectedPupilIds.value, + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Klasse erfolgreich aktualisiert'), + backgroundColor: Colors.green, + ), ); - } - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Klasse erfolgreich aktualisiert'), - backgroundColor: Colors.green, - ), - ); - } else { - // Create new lesson group - timetableManager.addLessonGroup(lessonGroupData); + Navigator.of(context).pop(); + } else { + // Create new lesson group + timetableManager.addLessonGroup(lessonGroupData); - // Add pupil memberships for new lesson group - if (lessonGroupData.id != null && - selectedPupilIds.value.isNotEmpty) { - timetableManager.updatePupilMembershipsForLessonGroup( - lessonGroupData.id!, - selectedPupilIds.value, + // Add pupil memberships for new lesson group + if (lessonGroupData.id != null && + selectedPupilIds.value.isNotEmpty) { + timetableManager + .updatePupilMembershipsForLessonGroup( + lessonGroupData.id!, + selectedPupilIds.value, + ); + } + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Klasse erfolgreich erstellt'), + backgroundColor: Colors.green, + ), ); - } - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Klasse erfolgreich erstellt'), - backgroundColor: Colors.green, - ), - ); - } + Navigator.of(context).pop(lessonGroupData); + } + }, + onCancel: () => Navigator.of(context).pop(), + onDelete: + _isEditing + ? () { + if (lessonGroup?.id == null) return; - Navigator.of(context).pop(); - }, - onCancel: () => Navigator.of(context).pop(), - onDelete: - _isEditing - ? () { - if (lessonGroup?.id == null) return; + // Check if the lesson group is used in any scheduled lessons + final scheduledLessons = + timetableManager.scheduledLessons.value + .where( + (lesson) => + lesson.lessonGroupId == + lessonGroup!.id, + ) + .toList(); - // Check if the lesson group is used in any scheduled lessons - final scheduledLessons = - timetableManager.scheduledLessons.value - .where( - (lesson) => - lesson.lessonGroupId == - lessonGroup!.id, - ) - .toList(); + if (scheduledLessons.isNotEmpty) { + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text( + 'Klasse kann nicht gelöscht werden', + ), + content: Text( + 'Diese Klasse wird in ${scheduledLessons.length} geplanten Stunden verwendet und kann nicht gelöscht werden.', + ), + actions: [ + TextButton( + onPressed: + () => + Navigator.of( + context, + ).pop(), + child: const Text('OK'), + ), + ], + ), + ); + return; + } - if (scheduledLessons.isNotEmpty) { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text( - 'Klasse kann nicht gelöscht werden', - ), + title: const Text('Klasse löschen'), content: Text( - 'Diese Klasse wird in ${scheduledLessons.length} geplanten Stunden verwendet und kann nicht gelöscht werden.', + 'Sind Sie sicher, dass Sie die Klasse "${lessonGroup!.name}" löschen möchten?\n\n' + 'Diese Aktion kann nicht rückgängig gemacht werden.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text('OK'), + child: const Text('Abbrechen'), + ), + TextButton( + onPressed: () { + timetableManager + .removeLessonGroup( + lessonGroup!.id!, + ); + Navigator.of( + context, + ).pop(); // Close dialog + Navigator.of( + context, + ).pop(); // Close page + + ScaffoldMessenger.of( + context, + ).showSnackBar( + SnackBar( + content: Text( + 'Klasse "${lessonGroup!.name}" wurde gelöscht', + ), + backgroundColor: Colors.red, + ), + ); + }, + style: TextButton.styleFrom( + foregroundColor: Colors.red, + ), + child: const Text('Löschen'), ), ], ), ); - return; } - - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('Klasse löschen'), - content: Text( - 'Sind Sie sicher, dass Sie die Klasse "${lessonGroup!.name}" löschen möchten?\n\n' - 'Diese Aktion kann nicht rückgängig gemacht werden.', - ), - actions: [ - TextButton( - onPressed: - () => Navigator.of(context).pop(), - child: const Text('Abbrechen'), - ), - TextButton( - onPressed: () { - timetableManager.removeLessonGroup( - lessonGroup!.id!, - ); - Navigator.of( - context, - ).pop(); // Close dialog - Navigator.of( - context, - ).pop(); // Close page - - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - 'Klasse "${lessonGroup!.name}" wurde gelöscht', - ), - backgroundColor: Colors.red, - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - child: const Text('Löschen'), - ), - ], - ), - ); - } - : null, - ), - ], + : null, + ), + ], + ), ), ), ), diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/new_scheduled_lesson_page.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/new_scheduled_lesson_page.dart index a01f9c93..28890e10 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/new_scheduled_lesson_page.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/new_scheduled_lesson_page.dart @@ -159,255 +159,267 @@ class NewScheduledLessonPage extends WatchingWidget { constraints: const BoxConstraints(maxWidth: 800), child: Form( key: formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Subject selection - SubjectDropdown( - selectedSubject: selectedSubjectValue, - onSubjectChanged: (subject) { - selectedSubject.value = subject; - }, - ), - const Gap(20), - - // Time slot selection - TimeSlotDropdown( - selectedSlot: selectedSlotValue, - onSlotChanged: (slot) { - selectedSlot.value = slot; - - // Reset lesson group if it conflicts with the new time slot - if (selectedLessonGroupValue != null && - _hasLessonGroupConflict( - selectedLessonGroupValue, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Subject selection + SubjectDropdown( + selectedSubject: selectedSubjectValue, + onSubjectChanged: (subject) { + selectedSubject.value = subject; + }, + ), + const Gap(20), + + // Time slot selection + TimeSlotDropdown( + selectedSlot: selectedSlotValue, + onSlotChanged: (slot) { + selectedSlot.value = slot; + + // Reset lesson group if it conflicts with the new time slot + if (selectedLessonGroupValue != null && + _hasLessonGroupConflict( + selectedLessonGroupValue, + selectedSlotValue, + )) { + selectedLessonGroup.value = null; + } + + // Reset classroom if it conflicts with the new time slot + if (selectedClassroomValue != null && + _hasClassroomConflict( + selectedClassroomValue, + selectedSlotValue, + )) { + selectedClassroom.value = null; + } + }, + hasLessonGroupConflict: + (group) => + _hasLessonGroupConflict(group, selectedSlotValue), + hasClassroomConflict: + (classroom) => _hasClassroomConflict( + classroom, selectedSlotValue, - )) { - selectedLessonGroup.value = null; - } - - // Reset classroom if it conflicts with the new time slot - if (selectedClassroomValue != null && - _hasClassroomConflict( - selectedClassroomValue, + ), + ), + const Gap(20), + + // Classroom selection + ClassroomDropdown( + selectedClassroom: selectedClassroomValue, + onClassroomChanged: (classroom) { + selectedClassroom.value = classroom; + }, + hasClassroomConflict: + (classroom) => _hasClassroomConflict( + classroom, selectedSlotValue, - )) { - selectedClassroom.value = null; - } - }, - hasLessonGroupConflict: - (group) => - _hasLessonGroupConflict(group, selectedSlotValue), - hasClassroomConflict: - (classroom) => - _hasClassroomConflict(classroom, selectedSlotValue), - ), - const Gap(20), - - // Classroom selection - ClassroomDropdown( - selectedClassroom: selectedClassroomValue, - onClassroomChanged: (classroom) { - selectedClassroom.value = classroom; - }, - hasClassroomConflict: - (classroom) => - _hasClassroomConflict(classroom, selectedSlotValue), - ), - const Gap(20), - - // Lesson group selection - LessonGroupDropdown( - selectedLessonGroup: selectedLessonGroupValue, - onLessonGroupChanged: (group) { - selectedLessonGroup.value = group; - }, - hasLessonGroupConflict: - (group) => - _hasLessonGroupConflict(group, selectedSlotValue), - ), - const Gap(20), - - // Teacher selection - TeacherSelection( - selectedTeachers: selectedTeachersValue, - onTeachersChanged: (teachers) { - selectedTeachers.value = teachers; - dropdownKey.value++; // Force dropdown rebuild - }, - dropdownKey: dropdownKeyValue, - ), - const Gap(20), - - // Lesson ID field - LessonIdField(controller: lessonIdController), - const Gap(32), - - // Action buttons - ActionButtons( - isEditing: _isEditing, - onSave: () { - if (!formKey.currentState!.validate()) { - return; - } - - if (selectedSubjectValue == null || - selectedSlotValue == null || - selectedClassroomValue == null || - selectedLessonGroupValue == null || - selectedTeachersValue.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Bitte füllen Sie alle Pflichtfelder aus', - ), - backgroundColor: Colors.red, ), - ); - return; - } + ), + const Gap(20), + + // Lesson group selection + LessonGroupDropdown( + selectedLessonGroup: selectedLessonGroupValue, + onLessonGroupChanged: (group) { + selectedLessonGroup.value = group; + }, + hasLessonGroupConflict: + (group) => + _hasLessonGroupConflict(group, selectedSlotValue), + ), + const Gap(20), + + // Teacher selection + TeacherSelection( + selectedTeachers: selectedTeachersValue, + onTeachersChanged: (teachers) { + selectedTeachers.value = teachers; + dropdownKey.value++; // Force dropdown rebuild + }, + dropdownKey: dropdownKeyValue, + ), + const Gap(20), + + // Lesson ID field + LessonIdField(controller: lessonIdController), + const Gap(32), + + // Action buttons + ActionButtons( + isEditing: _isEditing, + onSave: () { + if (!formKey.currentState!.validate()) { + return; + } - final now = DateTime.now().toUtcForServer(); + if (selectedSubjectValue == null || + selectedSlotValue == null || + selectedClassroomValue == null || + selectedLessonGroupValue == null || + selectedTeachersValue.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Bitte füllen Sie alle Pflichtfelder aus', + ), + backgroundColor: Colors.red, + ), + ); + return; + } - if (_isEditing) { - final editingLesson = - timetableManager.scheduledLessons.value - .where((lesson) => lesson.id == editingLessonId) - .firstOrNull; + final now = DateTime.now().toUtcForServer(); + + if (_isEditing) { + final editingLesson = + timetableManager.scheduledLessons.value + .where( + (lesson) => lesson.id == editingLessonId, + ) + .firstOrNull; + + if (editingLesson != null) { + // Update existing lesson + final updatedLesson = editingLesson.copyWith( + subjectId: selectedSubjectValue.id!, + subject: selectedSubjectValue, + scheduledAtId: selectedSlotValue.id!, + scheduledAt: selectedSlotValue, + lessonId: lessonIdController.text.trim(), + roomId: selectedClassroomValue.id!, + room: selectedClassroomValue, + lessonGroupId: selectedLessonGroupValue.id!, + lessonGroup: selectedLessonGroupValue, + mainTeacherId: selectedTeachersValue.first.id!, + modifiedBy: 'user', // TODO: Get actual user + modifiedAt: now, + ); + + timetableManager.updateScheduledLesson( + updatedLesson, + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Stunde erfolgreich aktualisiert mit ${selectedTeachersValue.length} Lehrer(n)', + ), + backgroundColor: Colors.green, + ), + ); + } + } else { + // Create new lesson + final nextAvailableOrder = timetableManager + .getNextAvailableOrderForSlot( + selectedSlotValue.id!, + ); + + final newLesson = ScheduledLesson( + active: true, - if (editingLesson != null) { - // Update existing lesson - final updatedLesson = editingLesson.copyWith( subjectId: selectedSubjectValue.id!, subject: selectedSubjectValue, scheduledAtId: selectedSlotValue.id!, scheduledAt: selectedSlotValue, + timetableId: + timetableManager.timetable.value?.id ?? + 1, // Get from current timetable lessonId: lessonIdController.text.trim(), roomId: selectedClassroomValue.id!, room: selectedClassroomValue, lessonGroupId: selectedLessonGroupValue.id!, lessonGroup: selectedLessonGroupValue, + timetableSlotOrder: nextAvailableOrder, mainTeacherId: selectedTeachersValue.first.id!, - modifiedBy: 'user', // TODO: Get actual user - modifiedAt: now, + createdBy: + di() + .userName!, // TODO: Get actual user + createdAt: now, ); - timetableManager.updateScheduledLesson(updatedLesson); + timetableManager.addScheduledLesson(newLesson); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Stunde erfolgreich aktualisiert mit ${selectedTeachersValue.length} Lehrer(n)', + 'Stunde erfolgreich erstellt mit ${selectedTeachersValue.length} Lehrer(n)', ), backgroundColor: Colors.green, ), ); } - } else { - // Create new lesson - final nextAvailableOrder = timetableManager - .getNextAvailableOrderForSlot( - selectedSlotValue.id!, - ); - final newLesson = ScheduledLesson( - active: true, - - subjectId: selectedSubjectValue.id!, - subject: selectedSubjectValue, - scheduledAtId: selectedSlotValue.id!, - scheduledAt: selectedSlotValue, - timetableId: - timetableManager.timetable.value?.id ?? - 1, // Get from current timetable - lessonId: lessonIdController.text.trim(), - roomId: selectedClassroomValue.id!, - room: selectedClassroomValue, - lessonGroupId: selectedLessonGroupValue.id!, - lessonGroup: selectedLessonGroupValue, - timetableSlotOrder: nextAvailableOrder, - mainTeacherId: selectedTeachersValue.first.id!, - createdBy: - di() - .userName!, // TODO: Get actual user - createdAt: now, - ); - - timetableManager.addScheduledLesson(newLesson); - - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Stunde erfolgreich erstellt mit ${selectedTeachersValue.length} Lehrer(n)', - ), - backgroundColor: Colors.green, - ), - ); - } - - Navigator.of(context).pop(); - }, - onCancel: () => Navigator.of(context).pop(), - onDelete: - _isEditing - ? () { - final editingLesson = - timetableManager.scheduledLessons.value - .where( - (lesson) => - lesson.id == editingLessonId, - ) - .firstOrNull; - - if (editingLesson?.id == null) return; - - showDialog( - context: context, - builder: - (context) => AlertDialog( - title: const Text('Stunde löschen'), - content: const Text( - 'Sind Sie sicher, dass Sie diese Stunde löschen möchten?', - ), - actions: [ - TextButton( - onPressed: - () => Navigator.of(context).pop(), - child: const Text('Abbrechen'), + Navigator.of(context).pop(); + }, + onCancel: () => Navigator.of(context).pop(), + onDelete: + _isEditing + ? () { + final editingLesson = + timetableManager.scheduledLessons.value + .where( + (lesson) => + lesson.id == editingLessonId, + ) + .firstOrNull; + + if (editingLesson?.id == null) return; + + showDialog( + context: context, + builder: + (context) => AlertDialog( + title: const Text('Stunde löschen'), + content: const Text( + 'Sind Sie sicher, dass Sie diese Stunde löschen möchten?', ), - TextButton( - onPressed: () { - timetableManager - .removeScheduledLesson( - editingLesson!.id!, - ); - Navigator.of( - context, - ).pop(); // Close dialog - Navigator.of( - context, - ).pop(); // Close page - - ScaffoldMessenger.of( - context, - ).showSnackBar( - const SnackBar( - content: Text( - 'Stunde erfolgreich gelöscht', + actions: [ + TextButton( + onPressed: + () => + Navigator.of(context).pop(), + child: const Text('Abbrechen'), + ), + TextButton( + onPressed: () { + timetableManager + .removeScheduledLesson( + editingLesson!.id!, + ); + Navigator.of( + context, + ).pop(); // Close dialog + Navigator.of( + context, + ).pop(); // Close page + + ScaffoldMessenger.of( + context, + ).showSnackBar( + const SnackBar( + content: Text( + 'Stunde erfolgreich gelöscht', + ), + backgroundColor: + Colors.orange, ), - backgroundColor: Colors.orange, - ), - ); - }, - child: const Text('Löschen'), - ), - ], - ), - ); - } - : null, - ), - ], + ); + }, + child: const Text('Löschen'), + ), + ], + ), + ); + } + : null, + ), + ], + ), ), ), ), diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/classroom_dropdown.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/classroom_dropdown.dart index faeb0c18..9dfc95b6 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/classroom_dropdown.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/classroom_dropdown.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:school_data_hub_client/school_data_hub_client.dart'; import 'package:school_data_hub_flutter/features/timetable/domain/timetable_manager.dart'; +import 'package:school_data_hub_flutter/features/timetable/presentation/new_classroom_page/new_classroom_page.dart'; import 'package:school_data_hub_flutter/features/timetable/presentation/widgets/timetable_utils.dart'; import 'package:watch_it/watch_it.dart'; @@ -37,36 +38,55 @@ class ClassroomDropdown extends WatchingWidget { ? selectedClassroom : null; - return DropdownButtonFormField( - value: validInitialValue, - decoration: InputDecoration( - labelText: 'Raum *', - border: const OutlineInputBorder(), - helperText: - selectedSlot != null - ? 'Nur verfügbare Räume für ${TimetableUtils.getWeekdayName(selectedSlot)}' - : 'Wählen Sie zuerst einen Zeitslot aus', - ), - items: - availableClassrooms.map((classroom) { - return DropdownMenuItem( - value: classroom, - child: Text('${classroom.roomCode} - ${classroom.roomName}'), - ); - }).toList(), - onChanged: onClassroomChanged, - validator: (value) { - if (value == null) { - return 'Bitte wählen Sie einen Raum aus'; - } + return Row( + children: [ + Expanded( + child: DropdownButtonFormField( + initialValue: validInitialValue, + decoration: InputDecoration( + labelText: 'Raum *', + border: const OutlineInputBorder(), + helperText: + 'Nur verfügbare Räume für ${TimetableUtils.getWeekdayName(selectedSlot)}', + ), + items: + availableClassrooms.map((classroom) { + return DropdownMenuItem( + value: classroom, + child: Text( + '${classroom.roomCode} - ${classroom.roomName}', + ), + ); + }).toList(), + onChanged: onClassroomChanged, + validator: (value) { + if (value == null) { + return 'Bitte wählen Sie einen Raum aus'; + } + + // Additional validation: check for conflicts + if (hasClassroomConflict(value)) { + return 'Dieser Raum ist bereits zu dieser Zeit belegt'; + } - // Additional validation: check for conflicts - if (hasClassroomConflict(value)) { - return 'Dieser Raum ist bereits zu dieser Zeit belegt'; - } + return null; + }, + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const NewClassroomPage()), + ); - return null; - }, + if (result != null && context.mounted) { + onClassroomChanged(result); + } + }, + child: const Icon(Icons.add, color: Colors.blue), + ), + ], ); } } diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/lesson_group_dropdown.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/lesson_group_dropdown.dart index 115e8214..302fc2a6 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/lesson_group_dropdown.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/lesson_group_dropdown.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:school_data_hub_client/school_data_hub_client.dart'; import 'package:school_data_hub_flutter/features/timetable/domain/timetable_manager.dart'; +import 'package:school_data_hub_flutter/features/timetable/presentation/new_lesson_group_page/new_lesson_group_page.dart'; import 'package:school_data_hub_flutter/features/timetable/presentation/widgets/timetable_utils.dart'; import 'package:watch_it/watch_it.dart'; @@ -37,36 +38,55 @@ class LessonGroupDropdown extends WatchingWidget { ? selectedLessonGroup : null; - return DropdownButtonFormField( - value: validInitialValue, - decoration: InputDecoration( - labelText: 'Klasse *', - border: const OutlineInputBorder(), - helperText: - selectedSlot != null - ? 'Nur verfügbare Klassen für ${TimetableUtils.getWeekdayName(selectedSlot)}' - : 'Wählen Sie zuerst einen Zeitslot aus', - ), - items: - availableLessonGroups.map((group) { - return DropdownMenuItem( - value: group, - child: Text(group.name), - ); - }).toList(), - onChanged: onLessonGroupChanged, - validator: (value) { - if (value == null) { - return 'Bitte wählen Sie eine Klasse aus'; - } + return Row( + children: [ + Expanded( + child: DropdownButtonFormField( + initialValue: validInitialValue, + decoration: InputDecoration( + labelText: 'Klasse *', + border: const OutlineInputBorder(), + helperText: + 'Nur verfügbare Klassen für ${TimetableUtils.getWeekdayName(selectedSlot)}', + ), + items: + availableLessonGroups.map((group) { + return DropdownMenuItem( + value: group, + child: Text(group.name), + ); + }).toList(), + onChanged: onLessonGroupChanged, + validator: (value) { + if (value == null) { + return 'Bitte wählen Sie eine Klasse aus'; + } + + // Additional validation: check for conflicts + if (hasLessonGroupConflict(value)) { + return 'Diese Klasse hat bereits eine Stunde zu dieser Zeit'; + } - // Additional validation: check for conflicts - if (hasLessonGroupConflict(value)) { - return 'Diese Klasse hat bereits eine Stunde zu dieser Zeit'; - } + return null; + }, + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const NewLessonGroupPage(), + ), + ); - return null; - }, + if (result != null && context.mounted) { + onLessonGroupChanged(result); + } + }, + child: const Icon(Icons.add, color: Colors.blue), + ), + ], ); } } diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/subject_dropdown.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/subject_dropdown.dart index 3de221a1..6cb0302e 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/subject_dropdown.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_scheduled_lesson_page/widgets/subject_dropdown.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:school_data_hub_client/school_data_hub_client.dart'; import 'package:school_data_hub_flutter/features/timetable/domain/timetable_manager.dart'; +import 'package:school_data_hub_flutter/features/timetable/presentation/new_subject_page/new_subject_page.dart'; import 'package:school_data_hub_flutter/features/timetable/presentation/widgets/timetable_utils.dart'; import 'package:watch_it/watch_it.dart'; @@ -19,39 +20,67 @@ class SubjectDropdown extends WatchingWidget { Widget build(BuildContext context) { final subjects = watchValue((TimetableManager m) => m.subjects); - return DropdownButtonFormField( - value: selectedSubject, - decoration: const InputDecoration( - labelText: 'Fach *', - border: OutlineInputBorder(), - ), - items: - subjects.map((subject) { - return DropdownMenuItem( - value: subject, - child: Row( - children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: TimetableUtils.parseColor(subject.color ?? ''), - shape: BoxShape.circle, + // Ensure the selectedSubject is in the subjects list + final validSelectedSubject = + selectedSubject != null && + subjects.any((subject) => subject.id == selectedSubject!.id) + ? selectedSubject + : null; + + return Row( + children: [ + Expanded( + child: DropdownButtonFormField( + initialValue: validSelectedSubject, + decoration: const InputDecoration( + labelText: 'Fach *', + border: OutlineInputBorder(), + ), + items: + subjects.map((subject) { + return DropdownMenuItem( + value: subject, + child: Row( + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: TimetableUtils.parseColor( + subject.color ?? '', + ), + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text(subject.name), + ], ), - ), - const SizedBox(width: 8), - Text(subject.name), - ], - ), + ); + }).toList(), + onChanged: onSubjectChanged, + validator: (value) { + if (value == null) { + return 'Bitte wählen Sie ein Fach aus'; + } + return null; + }, + ), + ), + const SizedBox(width: 10), + InkWell( + onTap: () async { + final result = await Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const NewSubjectPage()), ); - }).toList(), - onChanged: onSubjectChanged, - validator: (value) { - if (value == null) { - return 'Bitte wählen Sie ein Fach aus'; - } - return null; - }, + + if (result != null && context.mounted) { + onSubjectChanged(result); + } + }, + child: const Icon(Icons.add, color: Colors.blue), + ), + ], ); } } diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/new_subject_page/new_subject_page.dart b/school_data_hub_flutter/lib/features/timetable/presentation/new_subject_page/new_subject_page.dart index 2956aaa4..fd0e4435 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/new_subject_page/new_subject_page.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/new_subject_page/new_subject_page.dart @@ -97,12 +97,14 @@ class NewSubjectPage extends WatchingWidget { if (_isEditing) { await timetableManager.updateSubject(newSubject); + if (context.mounted) { + Navigator.pop(context); + } } else { await timetableManager.addSubject(newSubject); - } - - if (context.mounted) { - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context, newSubject); + } } }, onCancel: () { diff --git a/school_data_hub_flutter/lib/features/timetable/presentation/widgets/timetable_grid.dart b/school_data_hub_flutter/lib/features/timetable/presentation/widgets/timetable_grid.dart index 8d362c74..616d9d82 100644 --- a/school_data_hub_flutter/lib/features/timetable/presentation/widgets/timetable_grid.dart +++ b/school_data_hub_flutter/lib/features/timetable/presentation/widgets/timetable_grid.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:school_data_hub_client/school_data_hub_client.dart'; import 'package:school_data_hub_flutter/features/timetable/domain/timetable_manager.dart'; import 'package:school_data_hub_flutter/features/timetable/presentation/classroom_list_page/classroom_list_page.dart'; -import 'package:school_data_hub_flutter/features/timetable/presentation/new_lesson_group_page/new_lesson_group_page.dart'; import 'package:school_data_hub_flutter/features/timetable/presentation/widgets/lesson_cell/lesson_cell.dart'; import 'package:school_data_hub_flutter/features/user/domain/user_manager.dart'; import 'package:watch_it/watch_it.dart'; @@ -104,41 +103,41 @@ class TimetableGrid extends WatchingWidget { ); } - if (allLessonGroupsForWeekday.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.group, size: 64, color: Colors.grey), - const SizedBox(height: 16), - const Text( - 'Keine Klassen verfügbar', - style: TextStyle(fontSize: 18, color: Colors.grey), - ), - const SizedBox(height: 8), - const Text( - 'Erstellen Sie Klassen um Stunden zu planen', - style: TextStyle(fontSize: 14, color: Colors.grey), - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const NewLessonGroupPage(), - ), - ); - // Refresh data when returning from NewLessonGroupPage - // Note: This will trigger a rebuild of the TimetableGrid - // since it's watching the TimetableManager state - }, - child: const Text('Klasse erstellen'), - ), - ], - ), - ); - } + // if (allLessonGroupsForWeekday.isEmpty) { + // return Center( + // child: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // const Icon(Icons.group, size: 64, color: Colors.grey), + // const SizedBox(height: 16), + // const Text( + // 'Keine Klassen verfügbar', + // style: TextStyle(fontSize: 18, color: Colors.grey), + // ), + // const SizedBox(height: 8), + // const Text( + // 'Erstellen Sie Klassen um Stunden zu planen', + // style: TextStyle(fontSize: 14, color: Colors.grey), + // ), + // const SizedBox(height: 16), + // ElevatedButton( + // onPressed: () async { + // await Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => const NewLessonGroupPage(), + // ), + // ); + // // Refresh data when returning from NewLessonGroupPage + // // Note: This will trigger a rebuild of the TimetableGrid + // // since it's watching the TimetableManager state + // }, + // child: const Text('Klasse erstellen'), + // ), + // ], + // ), + // ); + // } return Scrollbar( controller: horizontalScrollController, diff --git a/school_data_hub_server/lib/server.dart b/school_data_hub_server/lib/server.dart index e890d32d..312d2c9d 100644 --- a/school_data_hub_server/lib/server.dart +++ b/school_data_hub_server/lib/server.dart @@ -21,22 +21,28 @@ import 'src/generated/protocol.dart'; // configuring Relic (Serverpod's web-server), or need custom setup work. void run(List args) async { - // Set the global logging level - Logger.root.level = Level.ALL; + // Enable hierarchical logging to allow setting levels on non-root loggers + Logger.root.level = Level.INFO; + hierarchicalLoggingEnabled = true; + + // Reduce noise from mailer package SMTP connections + Logger('Connection').level = Level.WARNING; // Add your custom colored console listener Logger.root.onRecord.listen((record) { final colorFormatter = ColorFormatter(); log(colorFormatter.format(record)); }); - + final _logger = Logger('ServerpodInit'); // Also add a simple console output for Docker environments - // TODO ADVICE: Is this print bad in production? - Logger.root.onRecord.listen((record) { - print( - '${record.time}: ${record.level.name}: ${record.loggerName}: ${record.message}'); - }); + + // Logger.root.onRecord.listen((record) { + // print( + // '${record.time}: ${record.level.name}: ${record.loggerName}: ${record.message}'); + // }); + // auth configuration + // TODO: configure this properly auth.AuthConfig.set(auth.AuthConfig( enableUserImages: false, userCanEditUserName: false, @@ -87,19 +93,6 @@ void run(List args) async { '/*', ); - String publicStoragePath; - if (Platform.isLinux && Directory('/app').existsSync()) { - publicStoragePath = '/app/storage/public'; - } else { - publicStoragePath = p.join(Directory.current.path, 'storage', 'public'); - } - - pod.webServer.addRoute( - RouteStaticDirectory( - serverDirectory: publicStoragePath, basePath: '/files/public'), - '/files/public/*', - ); - // Start the server. await pod.start(); @@ -110,17 +103,17 @@ void run(List args) async { // Check if there are any users in the database. If not, we need to populate the test environment. final userCount = await auth.UserInfo.db.count(session); - print('Current user count in database: $userCount'); + _logger.info('Current user count in database: $userCount'); final adminUser = await auth.UserInfo.db.findFirstRow( session, where: (t) => t.fullName.equals('Administrator'), ); if (adminUser == null) { - print('No users found, populating test environment...'); + _logger.warning('No users found, populating test environment...'); await populateTestEnvironment(session); } else { - print('Users already exist, skipping test environment population'); + _logger.info('Users already exist, skipping test environment population'); } // TODO: uncomment in production @@ -137,4 +130,24 @@ void run(List args) async { null, const Duration(seconds: 1), ); + + // Send startup notification email + // try { + // MailerService.instance.initializeFromSession(session); + // final success = await MailerService.instance.sendNotification( + // recipient: '', + // subject: 'Server Started', + // message: 'School Data Hub Server has started successfully.\n\n' + // 'Timestamp: ${DateTime.now().toIso8601String()}\n' + // 'User count: $userCount', + // ); + + // if (success) { + // _logger.info('Startup notification email sent successfully'); + // } else { + // _logger.severe('Failed to send startup notification email'); + // } + // } catch (e) { + // _logger.severe('Error sending startup notification email: $e'); + // } } 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 11e39174..5921c57a 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 @@ -17,17 +17,24 @@ class LibraryBooksEndpoint extends Endpoint { where: (t) => t.isbn.equals(isbn), transaction: transaction, ); + if (book == null) { + throw Exception('Book with ISBN $isbn not found.'); + } final libraryBookLocation = await LibraryBookLocation.db.findFirstRow( session, where: (t) => t.location.equals(location.location), transaction: transaction); + if (libraryBookLocation == null) { + throw Exception( + 'Library book location "${location.location}" not found.'); + } final libraryBook = LibraryBook( id: null, - bookId: book!.id!, + bookId: book.id!, libraryId: libraryId, available: true, book: book, - location: libraryBookLocation!, + location: libraryBookLocation, locationId: libraryBookLocation.id!, ); @@ -39,13 +46,19 @@ class LibraryBooksEndpoint extends Endpoint { session, libraryBookInDatabase, libraryBookLocation, transaction: transaction); + if (libraryBookInDatabase.id == null) { + throw Exception('Failed to create library book - no ID assigned.'); + } final attachedLibraryBookInDatabase = await LibraryBook.db.findById( session, libraryBookInDatabase.id!, include: LibraryBookSchemas.allInclude, transaction: transaction); + if (attachedLibraryBookInDatabase == null) { + throw Exception('Failed to retrieve created library book.'); + } return attachedLibraryBookInDatabase; }); - return libraryBookResponse!; + return libraryBookResponse; } //- read @@ -134,7 +147,7 @@ class LibraryBooksEndpoint extends Endpoint { final libraryBooks = await LibraryBook.db.find( session, - where: (_) => query!, + where: query != null ? (_) => query! : null, include: LibraryBookSchemas.allInclude, // limit: libraryBookQuery.perPage, // offset: libraryBookQuery.page * libraryBookQuery.perPage, @@ -256,7 +269,11 @@ class LibraryBooksEndpoint extends Endpoint { where: (t) => t.libraryId.equals(libraryId), include: LibraryBookSchemas.allInclude, ); - return updatedLibraryBook!; + if (updatedLibraryBook == null) { + throw Exception( + 'Library book with id $libraryId not found after update.'); + } + return updatedLibraryBook; } //- delete diff --git a/school_data_hub_server/lib/src/_features/books/endpoints/pupil_book_lending_endpoint.dart b/school_data_hub_server/lib/src/_features/books/endpoints/pupil_book_lending_endpoint.dart index d6f7ca70..a503a989 100644 --- a/school_data_hub_server/lib/src/_features/books/endpoints/pupil_book_lending_endpoint.dart +++ b/school_data_hub_server/lib/src/_features/books/endpoints/pupil_book_lending_endpoint.dart @@ -42,6 +42,9 @@ class PupilBookLendingEndpoint extends Endpoint { await PupilBookLending.db.attachRow.libraryBook( session, pupilBookLendingInDatabase, libraryBook, transaction: transaction); + libraryBook.available = false; + await LibraryBook.db + .updateRow(session, libraryBook, transaction: transaction); final updatedPupil = await PupilData.db.findFirstRow(session, where: (t) => t.id.equals(pupilBookLending.pupilId), @@ -62,13 +65,13 @@ class PupilBookLendingEndpoint extends Endpoint { return pupilBookLendings; } - Future fetchPupilBookLendingById( + Future fetchPupilBookLendingByLendingId( Session session, - int id, + String lendingId, ) async { final pupilBookLending = await PupilBookLending.db.findFirstRow( session, - where: (t) => t.id.equals(id), + where: (t) => t.lendingId.equals(lendingId), ); return pupilBookLending; } @@ -79,6 +82,14 @@ class PupilBookLendingEndpoint extends Endpoint { final updatedPupilBookLending = await PupilBookLending.db.updateRow(session, pupilBookLending); + // if the book was returned, set the library book available to true + if (pupilBookLending.returnedAt != null) { + final libraryBook = await LibraryBook.db.findFirstRow(session, + where: (t) => t.id.equals(updatedPupilBookLending.libraryBookId)); + libraryBook!.available = true; + await LibraryBook.db.updateRow(session, libraryBook); + } + final pupil = await PupilData.db.findFirstRow(session, where: (t) => t.id.equals(updatedPupilBookLending.pupilId), include: PupilSchemas.allInclude); @@ -86,14 +97,15 @@ class PupilBookLendingEndpoint extends Endpoint { } //- delete - Future deletePupilBookLending(Session session, int id) async { + Future deletePupilBookLending( + Session session, String lendingId) async { // Check if the pupil book lending exists final pupilBookLending = await PupilBookLending.db.findFirstRow( session, - where: (t) => t.id.equals(id), + where: (t) => t.lendingId.equals(lendingId), ); if (pupilBookLending == null) { - throw Exception('Pupil book lending with id $id does not exist.'); + throw Exception('Pupil book lending with id $lendingId does not exist.'); } await PupilBookLending.db.deleteRow(session, pupilBookLending); diff --git a/school_data_hub_server/lib/src/generated/endpoints.dart b/school_data_hub_server/lib/src/generated/endpoints.dart index d6647425..d0bc3d8d 100644 --- a/school_data_hub_server/lib/src/generated/endpoints.dart +++ b/school_data_hub_server/lib/src/generated/endpoints.dart @@ -1461,12 +1461,12 @@ class Endpoints extends _i1.EndpointDispatch { (endpoints['pupilBookLending'] as _i11.PupilBookLendingEndpoint) .fetchPupilBookLendings(session), ), - 'fetchPupilBookLendingById': _i1.MethodConnector( - name: 'fetchPupilBookLendingById', + 'fetchPupilBookLendingByLendingId': _i1.MethodConnector( + name: 'fetchPupilBookLendingByLendingId', params: { - 'id': _i1.ParameterDescription( - name: 'id', - type: _i1.getType(), + 'lendingId': _i1.ParameterDescription( + name: 'lendingId', + type: _i1.getType(), nullable: false, ) }, @@ -1475,9 +1475,9 @@ class Endpoints extends _i1.EndpointDispatch { Map params, ) async => (endpoints['pupilBookLending'] as _i11.PupilBookLendingEndpoint) - .fetchPupilBookLendingById( + .fetchPupilBookLendingByLendingId( session, - params['id'], + params['lendingId'], ), ), 'updatePupilBookLending': _i1.MethodConnector( @@ -1502,9 +1502,9 @@ class Endpoints extends _i1.EndpointDispatch { 'deletePupilBookLending': _i1.MethodConnector( name: 'deletePupilBookLending', params: { - 'id': _i1.ParameterDescription( - name: 'id', - type: _i1.getType(), + 'lendingId': _i1.ParameterDescription( + name: 'lendingId', + type: _i1.getType(), nullable: false, ) }, @@ -1515,7 +1515,7 @@ class Endpoints extends _i1.EndpointDispatch { (endpoints['pupilBookLending'] as _i11.PupilBookLendingEndpoint) .deletePupilBookLending( session, - params['id'], + params['lendingId'], ), ), }, diff --git a/school_data_hub_server/lib/src/generated/protocol.yaml b/school_data_hub_server/lib/src/generated/protocol.yaml index b647b913..49fa8fd6 100644 --- a/school_data_hub_server/lib/src/generated/protocol.yaml +++ b/school_data_hub_server/lib/src/generated/protocol.yaml @@ -57,7 +57,7 @@ libraryBooks: pupilBookLending: - postPupilBookLending: - fetchPupilBookLendings: - - fetchPupilBookLendingById: + - fetchPupilBookLendingByLendingId: - updatePupilBookLending: - deletePupilBookLending: competenceCheck: diff --git a/school_data_hub_server/lib/src/utils/MAILER_README.md b/school_data_hub_server/lib/src/utils/MAILER_README.md new file mode 100644 index 00000000..a06bdd50 --- /dev/null +++ b/school_data_hub_server/lib/src/utils/MAILER_README.md @@ -0,0 +1,160 @@ +# MailerService Singleton + +A singleton service for sending emails in your Serverpod application. + +## Features + +- **Singleton Pattern**: Single instance across your application +- **Serverpod Integration**: Works seamlessly with Serverpod sessions +- **Configuration Management**: Uses Serverpod's password system for secure credential storage +- **Multiple Email Types**: Support for plain text, HTML, attachments, CC, BCC +- **Convenience Methods**: Pre-built methods for common email scenarios +- **Error Handling**: Comprehensive error handling with logging + +## Setup + +### 1. Add Email Configuration to passwords.yaml + +Add your email configuration to `config/passwords.yaml`: + +```yaml +shared: + emailUsername: 'your-email@domain.com' + emailPassword: 'your-app-password' + emailSmtpHost: 'smtp-mail.outlook.com' + emailSmtpPort: '587' +``` + +### 2. Initialize the Service + +The service can be initialized in two ways: + +#### Option A: From Session (Recommended) +```dart +MailerService.instance.initializeFromSession(session); +``` + +#### Option B: Manual Configuration +```dart +MailerService.instance.initialize( + username: 'your-email@domain.com', + password: 'your-password', + smtpHost: 'smtp-mail.outlook.com', + smtpPort: 587, + fromName: 'Your App Name', + defaultRecipient: 'admin@yourdomain.com', +); +``` + +## Usage Examples + +### Basic Email +```dart +// In your endpoint +Future sendBasicEmail(Session session) async { + MailerService.instance.initializeFromSession(session); + + return await MailerService.instance.sendEmail( + subject: 'Test Email', + body: 'This is a test email.', + recipient: 'user@example.com', + ); +} +``` + +### Contact Form Email +```dart +Future handleContactForm(Session session, String message) async { + MailerService.instance.initializeFromSession(session); + + return await MailerService.instance.sendContactEmail( + subject: 'Contact Form Submission', + body: message, + senderEmail: 'user@example.com', + ); +} +``` + +### Notification Email +```dart +Future sendSystemAlert(Session session, String alert) async { + MailerService.instance.initializeFromSession(session); + + return await MailerService.instance.sendNotification( + subject: 'System Alert', + message: alert, + ); +} +``` + +### Rich Email with HTML and Attachments +```dart +Future sendRichEmail(Session session) async { + MailerService.instance.initializeFromSession(session); + + return await MailerService.instance.sendEmail( + subject: 'Rich Email', + body: 'Plain text version', + htmlBody: '

HTML Version

Rich content here

', + recipient: 'user@example.com', + ccRecipients: ['cc@example.com'], + bccRecipients: ['bcc@example.com'], + attachments: [FileAttachment(File('path/to/file.pdf'))], + ); +} +``` + +## API Reference + +### sendEmail() +Main method for sending emails with full customization options. + +**Parameters:** +- `subject` (required): Email subject +- `body` (required): Plain text email body +- `recipient` (optional): Recipient email (uses default if not provided) +- `htmlBody` (optional): HTML version of the email +- `ccRecipients` (optional): List of CC recipients +- `bccRecipients` (optional): List of BCC recipients +- `attachments` (optional): List of email attachments + +**Returns:** `Future` - true if email sent successfully, false otherwise + +### sendContactEmail() +Convenience method for contact form emails. + +**Parameters:** +- `subject` (required): Email subject +- `body` (required): Email body +- `senderEmail` (optional): Sender's email address + +### sendNotification() +Convenience method for system notifications. + +**Parameters:** +- `subject` (required): Notification subject +- `message` (required): Notification message + +## Error Handling + +The service includes comprehensive error handling: + +- **MailerException**: Caught and logged, returns false +- **General Exceptions**: Caught and logged, returns false +- **Configuration Errors**: Will throw if service not properly initialized + +## Security Notes + +- All email configuration (credentials, SMTP host, and port) are stored in `passwords.yaml` which should not be committed to version control +- The service uses secure SMTP connections (port 587 with TLS by default) +- Consider using app-specific passwords for email accounts with 2FA enabled +- SMTP host and port are now configurable per environment for maximum flexibility + +## Dependencies + +Make sure you have the `mailer` package in your `pubspec.yaml`: + +```yaml +dependencies: + mailer: ^6.0.1 +``` diff --git a/school_data_hub_server/lib/src/utils/local_storage.dart b/school_data_hub_server/lib/src/utils/local_storage.dart index 75194c37..c3f0a421 100644 --- a/school_data_hub_server/lib/src/utils/local_storage.dart +++ b/school_data_hub_server/lib/src/utils/local_storage.dart @@ -36,18 +36,6 @@ class LocalStorage extends DatabaseCloudStorage { // We sanitize the path for different platforms File _getFileByPath(String path) => File(p.join(pathPrefix, path)); - // File _getFileByPath(String filePath) { - // final fileName = p.basename(filePath); - // final parentDirectoryPath = p.dirname(filePath); - // final directoryPath = p.join(pathPrefix, parentDirectoryPath); - // // final directory = Directory(directoryPath); - // // if (!directory.existsSync()) { - // // directory.createSync(recursive: true); - // // } - // log('Getting file: ${p.join(directoryPath, fileName)}'); - // return File(p.join(directoryPath, fileName)); - // } - @override Future deleteFile({ required Session session, diff --git a/school_data_hub_server/lib/src/utils/mailer.dart b/school_data_hub_server/lib/src/utils/mailer.dart new file mode 100644 index 00000000..b318fab7 --- /dev/null +++ b/school_data_hub_server/lib/src/utils/mailer.dart @@ -0,0 +1,133 @@ +import 'package:logging/logging.dart'; +import 'package:mailer/mailer.dart'; +import 'package:mailer/smtp_server.dart'; +import 'package:serverpod/serverpod.dart'; + +/// Singleton service for sending emails +class MailerService { + static MailerService? _instance; + static MailerService get instance { + _instance ??= MailerService._internal(); + return _instance!; + } + + MailerService._internal(); + final _logger = Logger('MailerService'); + late final String _username; + late final String _password; + late final String _smtpHost; + late final int _smtpPort; + late final String _fromName; + late final String _defaultRecipient; + + /// Initialize the mailer service with configuration + void initialize({ + required String username, + required String password, + required String smtpHost, + required int smtpPort, + required String fromName, + required String defaultRecipient, + }) { + _username = username; + _password = password; + _smtpHost = smtpHost; + _smtpPort = smtpPort; + _fromName = fromName; + _defaultRecipient = defaultRecipient; + } + + /// Initialize from Serverpod session (recommended approach) + void initializeFromSession(Session session) { + final passwords = session.passwords; + + initialize( + username: passwords['emailUsername'] ?? '', + password: passwords['emailPassword'] ?? '', + smtpHost: passwords['emailSmtpHost'] ?? '', + smtpPort: int.tryParse(passwords['emailSmtpPort'] ?? '0') ?? 587, + fromName: 'Schuldaten Benachrichtigungen', + defaultRecipient: '', + ); + } + + /// Send an email with the specified parameters + Future sendEmail({ + required String subject, + required String body, + String? recipient, + String? htmlBody, + List? ccRecipients, + List? bccRecipients, + List? attachments, + }) async { + try { + final smtpServer = SmtpServer( + _smtpHost, + port: _smtpPort, + username: _username, + password: _password, + ); + + final message = Message() + ..from = Address(_username, _fromName) + ..recipients.add(recipient ?? _defaultRecipient) + ..subject = subject + ..text = body; + + if (htmlBody != null) { + message.html = htmlBody; + } + + if (ccRecipients != null && ccRecipients.isNotEmpty) { + message.ccRecipients.addAll(ccRecipients); + } + + if (bccRecipients != null && bccRecipients.isNotEmpty) { + message.bccRecipients.addAll(bccRecipients); + } + + if (attachments != null && attachments.isNotEmpty) { + message.attachments.addAll(attachments); + } + + final sendReport = await send(message, smtpServer); + _logger.info('Email sent successfully: ${sendReport.toString()}'); + return true; + } on MailerException catch (e) { + _logger.severe('Failed to send email: $e'); + return false; + } catch (e) { + _logger.severe('Unexpected error sending email: $e'); + return false; + } + } + + /// Send a contact form email (convenience method) + Future sendContactEmail({ + required String subject, + required String body, + String? senderEmail, + }) async { + final emailBody = + senderEmail != null ? 'From: $senderEmail\n\n$body' : body; + + return sendEmail( + subject: 'Contact Form: $subject', + body: emailBody, + ); + } + + /// Send a notification email (convenience method) + Future sendNotification({ + required String subject, + required String message, + required String recipient, + }) async { + return sendEmail( + subject: 'Notification: $subject', + body: message, + recipient: recipient, + ); + } +} diff --git a/school_data_hub_server/pubspec.lock b/school_data_hub_server/pubspec.lock index 17b96f98..96b6436c 100644 --- a/school_data_hub_server/pubspec.lock +++ b/school_data_hub_server/pubspec.lock @@ -265,6 +265,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + intl: + dependency: transitive + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" io: dependency: transitive description: @@ -305,6 +313,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + mailer: + dependency: "direct main" + description: + name: mailer + sha256: db61f51ea301e8dcbfe5894e037ccd30a6246eb0a7bcfa007aefa2fc11a8f96e + url: "https://pub.dev" + source: hosted + version: "6.5.0" matcher: dependency: transitive description: @@ -325,10 +341,10 @@ packages: dependency: transitive description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" mustache_template: dependency: transitive description: diff --git a/school_data_hub_server/pubspec.yaml b/school_data_hub_server/pubspec.yaml index 708441ee..0dc56e39 100644 --- a/school_data_hub_server/pubspec.yaml +++ b/school_data_hub_server/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: serverpod: 2.9.1 serverpod_auth_server: 2.9.1 uml_for_serverpod: 0.0.5 + mailer: ^6.5.0 dev_dependencies: lints: '>=3.0.0 <6.0.0' diff --git a/school_data_hub_server/test/integration/test_tools/serverpod_test_tools.dart b/school_data_hub_server/test/integration/test_tools/serverpod_test_tools.dart index 2440d756..3b22dda8 100644 --- a/school_data_hub_server/test/integration/test_tools/serverpod_test_tools.dart +++ b/school_data_hub_server/test/integration/test_tools/serverpod_test_tools.dart @@ -2035,22 +2035,22 @@ class _PupilBookLendingEndpoint { }); } - _i3.Future<_i23.PupilBookLending?> fetchPupilBookLendingById( + _i3.Future<_i23.PupilBookLending?> fetchPupilBookLendingByLendingId( _i1.TestSessionBuilder sessionBuilder, - int id, + String lendingId, ) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( endpoint: 'pupilBookLending', - method: 'fetchPupilBookLendingById', + method: 'fetchPupilBookLendingByLendingId', ); try { var _localCallContext = await _endpointDispatch.getMethodCallContext( createSessionCallback: (_) => _localUniqueSession, endpointPath: 'pupilBookLending', - methodName: 'fetchPupilBookLendingById', - parameters: _i1.testObjectToJson({'id': id}), + methodName: 'fetchPupilBookLendingByLendingId', + parameters: _i1.testObjectToJson({'lendingId': lendingId}), serializationManager: _serializationManager, ); var _localReturnValue = await (_localCallContext.method.call( @@ -2096,7 +2096,7 @@ class _PupilBookLendingEndpoint { _i3.Future<_i6.PupilData> deletePupilBookLending( _i1.TestSessionBuilder sessionBuilder, - int id, + String lendingId, ) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2109,7 +2109,7 @@ class _PupilBookLendingEndpoint { createSessionCallback: (_) => _localUniqueSession, endpointPath: 'pupilBookLending', methodName: 'deletePupilBookLending', - parameters: _i1.testObjectToJson({'id': id}), + parameters: _i1.testObjectToJson({'lendingId': lendingId}), serializationManager: _serializationManager, ); var _localReturnValue = await (_localCallContext.method.call(