diff --git a/lib/src/api/lending_api.dart b/lib/src/api/lending_api.dart index 3fe086a..b1a8871 100644 --- a/lib/src/api/lending_api.dart +++ b/lib/src/api/lending_api.dart @@ -22,8 +22,7 @@ class LendingApi { 'borrowerId': data.borrowerId, 'thingIds': data.thingIds, 'checkedOutDate': data.checkedOutDate, - 'dueBackDate': data.dueBackDate, - 'notes': 'This loan was created by the Librarian app!' + 'dueBackDate': data.dueBackDate }); } diff --git a/lib/src/features/dashboard/pages/dashboard_page.dart b/lib/src/features/dashboard/pages/dashboard_page.dart index 9c65260..d42ed95 100644 --- a/lib/src/features/dashboard/pages/dashboard_page.dart +++ b/lib/src/features/dashboard/pages/dashboard_page.dart @@ -44,7 +44,7 @@ class _DashboardPageState extends ConsumerState { Navigator.push( context, MaterialPageRoute( - builder: (context) => LoanDetailsPage(loan), + builder: (context) => const LoanDetailsPage(), ), ); }, diff --git a/lib/src/features/loans/data/loans_repository.dart b/lib/src/features/loans/data/loans_repository.dart index 761cd2d..1a4a3d5 100644 --- a/lib/src/features/loans/data/loans_repository.dart +++ b/lib/src/features/loans/data/loans_repository.dart @@ -1,19 +1,21 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import 'package:librarian_app/src/api/lending_api.dart'; -import 'package:librarian_app/src/features/loans/models/loan_model.dart'; + +import '../models/loan_details_model.dart'; +import '../models/loan_model.dart'; class LoansRepository extends Notifier>> { @override Future> build() async => await getLoans(); - Future getLoan({ + Future getLoan({ required String id, required String thingId, }) async { try { final response = await LendingApi.fetchLoan(id: id, thingId: thingId); - return LoanModel.fromJson(response.data as Map); + return LoanDetailsModel.fromJson(response.data as Map); } catch (error) { return null; } diff --git a/lib/src/features/loans/models/loan_details_model.dart b/lib/src/features/loans/models/loan_details_model.dart new file mode 100644 index 0000000..c6756ab --- /dev/null +++ b/lib/src/features/loans/models/loan_details_model.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:librarian_app/src/features/borrowers/models/borrower_model.dart'; + +import 'thing_summary_model.dart'; + +class LoanDetailsModel { + final String id; + final int number; + final ThingSummaryModel thing; + final BorrowerModel borrower; + final DateTime checkedOutDate; + final String? notes; + final int remindersSent; + DateTime dueDate; + DateTime? checkedInDate; + + bool get isOverdue { + final now = DateTime.now(); + return DateUtils.dateOnly(dueDate).isBefore(DateUtils.dateOnly(now)); + } + + bool get isDueToday => DateUtils.isSameDay(DateTime.now(), dueDate); + + LoanDetailsModel({ + required this.id, + required this.number, + required this.thing, + required this.borrower, + required this.checkedOutDate, + required this.dueDate, + required this.remindersSent, + this.checkedInDate, + this.notes, + }); + + factory LoanDetailsModel.fromJson(Map json) { + return LoanDetailsModel( + id: json['id'] as String? ?? '?', + number: json['number'] as int, + thing: ThingSummaryModel.fromJson(json['thing'] as Map), + borrower: BorrowerModel( + id: json['borrower']?['id'] as String? ?? '?', + name: json['borrower']?['name'] as String? ?? '???', + email: json['borrower']?['contact']['email'] as String?, + phone: json['borrower']?['contact']['phone'] as String?, + issues: [], + ), + notes: json['notes'] as String?, + checkedOutDate: json['checkedOutDate'] != null + ? DateTime.parse(json['checkedOutDate'] as String) + : DateTime.now(), + checkedInDate: json['checkedInDate'] != null + ? DateTime.parse(json['checkedInDate'] as String) + : null, + dueDate: json['dueBackDate'] != null + ? DateTime.parse(json['dueBackDate']) + : DateTime.now(), + remindersSent: json['remindersSent'] as int, + ); + } +} diff --git a/lib/src/features/loans/models/loan_model.dart b/lib/src/features/loans/models/loan_model.dart index 9496e7f..3a9bd1b 100644 --- a/lib/src/features/loans/models/loan_model.dart +++ b/lib/src/features/loans/models/loan_model.dart @@ -8,8 +8,6 @@ class LoanModel { final ThingSummaryModel thing; final BorrowerModel borrower; final DateTime checkedOutDate; - final String? notes; - final int remindersSent; DateTime dueDate; DateTime? checkedInDate; @@ -27,9 +25,7 @@ class LoanModel { required this.borrower, required this.checkedOutDate, required this.dueDate, - required this.remindersSent, this.checkedInDate, - this.notes, }); factory LoanModel.fromJson(Map json) { @@ -40,11 +36,10 @@ class LoanModel { borrower: BorrowerModel( id: json['borrower']?['id'] as String? ?? '?', name: json['borrower']?['name'] as String? ?? '???', - email: json['borrower']?['contact']['email'] as String?, - phone: json['borrower']?['contact']['phone'] as String?, + email: null, + phone: null, issues: [], ), - notes: json['notes'] as String?, checkedOutDate: json['checkedOutDate'] != null ? DateTime.parse(json['checkedOutDate'] as String) : DateTime.now(), @@ -54,7 +49,6 @@ class LoanModel { dueDate: json['dueBackDate'] != null ? DateTime.parse(json['dueBackDate']) : DateTime.now(), - remindersSent: json['remindersSent'] as int, ); } } diff --git a/lib/src/features/loans/models/thing_summary_model.dart b/lib/src/features/loans/models/thing_summary_model.dart index 8cb01c8..7f6919b 100644 --- a/lib/src/features/loans/models/thing_summary_model.dart +++ b/lib/src/features/loans/models/thing_summary_model.dart @@ -2,11 +2,13 @@ class ThingSummaryModel { final String id; final String name; final int number; + final List images; const ThingSummaryModel({ required this.id, required this.name, required this.number, + required this.images, }); factory ThingSummaryModel.fromJson(Map json) { @@ -14,6 +16,9 @@ class ThingSummaryModel { id: json['id'] as String, name: json['name'] as String, number: json['number'] as int, + images: json['images'] != null + ? List.from(json['images'] as List) + : [], ); } } diff --git a/lib/src/features/loans/pages/loan_details_page.dart b/lib/src/features/loans/pages/loan_details_page.dart index df181e0..1b3913c 100644 --- a/lib/src/features/loans/pages/loan_details_page.dart +++ b/lib/src/features/loans/pages/loan_details_page.dart @@ -2,83 +2,24 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:librarian_app/src/features/inventory/pages/inventory_details_page.dart'; +import 'package:librarian_app/src/features/loans/providers/loan_details_provider.dart'; import 'package:librarian_app/src/features/loans/providers/loans_repository_provider.dart'; import 'package:librarian_app/src/features/loans/widgets/checkin/checkin_dialog.dart'; import 'package:librarian_app/src/features/loans/widgets/email/send_email_dialog.dart'; import 'package:librarian_app/src/features/loans/widgets/loan_details/loan_details.dart'; import 'package:librarian_app/src/features/loans/widgets/loan_details/loan_details_controller.dart'; -import '../models/loan_model.dart'; import '../widgets/edit/edit_loan_dialog.dart'; -class LoanDetailsPage extends ConsumerStatefulWidget { - const LoanDetailsPage(this.loan, {super.key}); - - final LoanModel loan; +class LoanDetailsPage extends ConsumerWidget { + const LoanDetailsPage({super.key}); @override - ConsumerState createState() => _LoanDetailsPageState(); -} - -class _LoanDetailsPageState extends ConsumerState { - Future _updateLoan( - String loanId, String thingId, DateTime newDueDate, String? notes) async { - final loans = ref.read(loansRepositoryProvider.notifier); - try { - await loans.updateLoan( - loanId: loanId, - thingId: thingId, - dueBackDate: newDueDate, - notes: notes); - - setState(() { - _loanFuture = loans.getLoan(id: loanId, thingId: thingId); - }); - } catch (error) { - if (kDebugMode) { - print(error); - } - } - } - - void _checkIn() async { - showDialog( - context: context, - builder: (context) { - final thing = widget.loan.thing; - return CheckinDialog( - thingNumber: thing.number, - onCheckin: () async { - final loans = ref.read(loansRepositoryProvider.notifier); - await loans.closeLoan( - loanId: widget.loan.id, - thingId: widget.loan.thing.id, - ); - }, - ); - }, - ).then((result) { - if (result ?? false) { - Navigator.of(context).pop(); - } - }); - } - - late Future _loanFuture; + Widget build(BuildContext context, WidgetRef ref) { + final loanDetailsFuture = ref.watch(loanDetailsProvider); - @override - void initState() { - super.initState(); - final loan = widget.loan; - _loanFuture = ref - .read(loansRepositoryProvider.notifier) - .getLoan(id: loan.id, thingId: loan.thing.id); - } - - @override - Widget build(BuildContext context) { return FutureBuilder( - future: _loanFuture, + future: loanDetailsFuture, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return loadingScaffold; @@ -90,6 +31,42 @@ class _LoanDetailsPageState extends ConsumerState { final loan = snapshot.data!; + Future updateLoan(String loanId, String thingId, + DateTime newDueDate, String? notes) async { + final loans = ref.read(loansRepositoryProvider.notifier); + try { + await loans.updateLoan( + loanId: loanId, + thingId: thingId, + dueBackDate: newDueDate, + notes: notes); + } catch (error) { + if (kDebugMode) { + print(error); + } + } + } + + void checkIn() async { + showDialog( + context: context, + builder: (context) { + return CheckinDialog( + thingNumber: loan.thing.number, + onCheckin: () async { + final loans = ref.read(loansRepositoryProvider.notifier); + await loans.closeLoan( + loanId: loan.id, thingId: loan.thing.id); + }, + ); + }, + ).then((result) { + if (result ?? false) { + Navigator.of(context).pop(); + } + }); + } + return Scaffold( appBar: AppBar( title: Text('#${loan.thing.number}'), @@ -104,7 +81,7 @@ class _LoanDetailsPageState extends ConsumerState { dueDate: loan.dueDate, notes: loan.notes, onSavePressed: (newDueDate, notes) async { - await _updateLoan( + await updateLoan( loan.id, loan.thing.id, newDueDate, notes); }, ); @@ -157,7 +134,7 @@ class _LoanDetailsPageState extends ConsumerState { ), ), floatingActionButton: FloatingActionButton( - onPressed: _checkIn, + onPressed: checkIn, tooltip: 'Check in', child: const Icon(Icons.check_rounded), ), diff --git a/lib/src/features/loans/pages/open_loan_page.dart b/lib/src/features/loans/pages/open_loan_page.dart index 90cf43c..0c1e458 100644 --- a/lib/src/features/loans/pages/open_loan_page.dart +++ b/lib/src/features/loans/pages/open_loan_page.dart @@ -59,6 +59,7 @@ class _OpenLoanPageState extends ConsumerState { id: t.id, name: t.name, number: t.number, + images: [], )) .toList(), checkedOutDate: DateTime.now(), diff --git a/lib/src/features/loans/providers/loan_details_provider.dart b/lib/src/features/loans/providers/loan_details_provider.dart index e9d3fad..2ee2a4b 100644 --- a/lib/src/features/loans/providers/loan_details_provider.dart +++ b/lib/src/features/loans/providers/loan_details_provider.dart @@ -1,17 +1,17 @@ -import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:librarian_app/src/features/loans/providers/loans_repository_provider.dart'; import 'package:librarian_app/src/features/loans/providers/selected_loan_provider.dart'; -import '../models/loan_model.dart'; +import '../models/loan_details_model.dart'; -final loanDetailsProvider = Provider>((ref) async { +final loanDetailsProvider = Provider>((ref) async { + ref.watch(loansRepositoryProvider); final selectedLoan = ref.watch(selectedLoanProvider); if (selectedLoan == null) { return null; } - final loans = await ref.watch(loansRepositoryProvider); - return loans.firstWhereOrNull((loan) => - loan.id == selectedLoan.id && loan.thing.id == selectedLoan.thing.id); + return await ref + .read(loansRepositoryProvider.notifier) + .getLoan(id: selectedLoan.id, thingId: selectedLoan.thing.id); }); diff --git a/lib/src/features/loans/widgets/checkout/checkout_stepper.dart b/lib/src/features/loans/widgets/checkout/checkout_stepper.dart index 3af4e9a..a34e681 100644 --- a/lib/src/features/loans/widgets/checkout/checkout_stepper.dart +++ b/lib/src/features/loans/widgets/checkout/checkout_stepper.dart @@ -6,7 +6,6 @@ import 'package:librarian_app/src/features/borrowers/widgets/borrower_details/bo import 'package:librarian_app/src/features/borrowers/widgets/borrower_search_delegate.dart'; import 'package:librarian_app/src/features/loans/pages/loan_details_page.dart'; import 'package:librarian_app/src/features/loans/providers/loans_controller_provider.dart'; -import 'package:librarian_app/src/features/loans/providers/selected_loan_provider.dart'; import 'package:librarian_app/src/utils/media_query.dart'; import 'package:librarian_app/src/widgets/filled_progress_button.dart'; import 'package:librarian_app/src/features/inventory/models/item_model.dart'; @@ -68,9 +67,8 @@ class _CheckoutStepperState extends ConsumerState { ); if (isMobile(context)) { - final loan = ref.read(selectedLoanProvider)!; Navigator.of(context).push(MaterialPageRoute(builder: (context) { - return LoanDetailsPage(loan); + return const LoanDetailsPage(); })); } }); @@ -216,6 +214,7 @@ class _CheckoutStepperState extends ConsumerState { id: t.id, name: t.name, number: t.number, + images: [], )) .toList(), dueDate: _dueDate, diff --git a/lib/src/features/loans/widgets/layouts/loans_desktop_layout.dart b/lib/src/features/loans/widgets/layouts/loans_desktop_layout.dart index 04cbd2c..7a8b7ba 100644 --- a/lib/src/features/loans/widgets/layouts/loans_desktop_layout.dart +++ b/lib/src/features/loans/widgets/layouts/loans_desktop_layout.dart @@ -3,9 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:librarian_app/src/widgets/fields/search_field.dart'; import 'package:librarian_app/src/features/dashboard/widgets/panes/list_pane.widget.dart'; import 'package:librarian_app/src/features/dashboard/widgets/panes/pane_header.widget.dart'; -import 'package:librarian_app/src/features/loans/providers/loan_details_provider.dart'; import 'package:librarian_app/src/features/loans/providers/loans_filter_provider.dart'; -import 'package:librarian_app/src/features/loans/providers/loans_repository_provider.dart'; import 'package:librarian_app/src/features/loans/providers/selected_loan_provider.dart'; import 'package:librarian_app/src/features/loans/widgets/loan_details/loan_details_pane.dart'; import 'package:librarian_app/src/features/loans/widgets/loans_list/loans_list_view.dart'; @@ -33,35 +31,8 @@ class LoansDesktopLayout extends ConsumerWidget { ), child: const LoansListView(), ), - Expanded( - child: FutureBuilder( - future: ref.watch(loanDetailsProvider), - builder: (context, snapshot) { - return LoanDetailsPane( - loan: snapshot.data, - onSave: (newDueDate, notes) { - final selectedLoan = ref.read(selectedLoanProvider)!; - ref.read(loansRepositoryProvider.notifier).updateLoan( - loanId: selectedLoan.id, - thingId: selectedLoan.thing.id, - dueBackDate: newDueDate, - notes: notes); - }, - onCheckIn: () { - final selectedLoan = ref.read(selectedLoanProvider)!; - ref - .read(loansRepositoryProvider.notifier) - .closeLoan( - loanId: selectedLoan.id, - thingId: selectedLoan.thing.id, - ) - .then((_) { - ref.read(selectedLoanProvider.notifier).state = null; - }); - }, - ); - }, - ), + const Expanded( + child: LoanDetailsPane(), ), ], ); diff --git a/lib/src/features/loans/widgets/loan_details/loan_details.dart b/lib/src/features/loans/widgets/loan_details/loan_details.dart index a956f56..dd60ae2 100644 --- a/lib/src/features/loans/widgets/loan_details/loan_details.dart +++ b/lib/src/features/loans/widgets/loan_details/loan_details.dart @@ -60,18 +60,23 @@ class LoanDetails extends StatelessWidget { final thingsCard = Card( elevation: cardElevation, child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Detail( useListTile: true, label: 'Thing', prefixIcon: Icon(Icons.build_rounded), ), - ...things.map((thing) { - return Detail( - useListTile: true, - value: '#${thing.number} ${thing.name}', - ); - }) + Container( + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8)), + color: Colors.black45, + shape: BoxShape.rectangle, + ), + height: 240, + child: _ThingImage(urls: things[0].images), + ), ], ), ); @@ -163,3 +168,39 @@ class LoanDetails extends StatelessWidget { ); } } + +class _ThingImage extends StatelessWidget { + const _ThingImage({required this.urls}); + + final List urls; + + @override + Widget build(BuildContext context) { + if (urls.isEmpty) { + return const Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [Icon(Icons.image), Text('No image')], + )); + } + return Image.network( + urls[0], + fit: BoxFit.contain, + height: 240, + loadingBuilder: (context, child, event) { + if (event == null) { + return child; + } + + final progress = + event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1); + + return Center( + child: CircularProgressIndicator(value: progress), + ); + }, + ); + } +} diff --git a/lib/src/features/loans/widgets/loan_details/loan_details_header.dart b/lib/src/features/loans/widgets/loan_details/loan_details_header.dart index c616a53..d50378e 100644 --- a/lib/src/features/loans/widgets/loan_details/loan_details_header.dart +++ b/lib/src/features/loans/widgets/loan_details/loan_details_header.dart @@ -4,7 +4,7 @@ import 'package:librarian_app/src/features/dashboard/widgets/panes/pane_header.w import 'package:librarian_app/src/features/loans/widgets/email/send_email_dialog.dart'; import 'package:librarian_app/src/features/loans/widgets/loan_details/loan_details_controller.dart'; -import '../../models/loan_model.dart'; +import '../../models/loan_details_model.dart'; import '../checkin/checkin_dialog.dart'; import '../edit/edit_loan_dialog.dart'; import 'thing_number.dart'; @@ -17,7 +17,7 @@ class LoanDetailsHeader extends ConsumerWidget { required this.onCheckIn, }); - final LoanModel loan; + final LoanDetailsModel loan; final void Function(DateTime dueDate, String? notes) onSave; final void Function() onCheckIn; diff --git a/lib/src/features/loans/widgets/loan_details/loan_details_pane.dart b/lib/src/features/loans/widgets/loan_details/loan_details_pane.dart index 92a5761..f752392 100644 --- a/lib/src/features/loans/widgets/loan_details/loan_details_pane.dart +++ b/lib/src/features/loans/widgets/loan_details/loan_details_pane.dart @@ -1,50 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:librarian_app/src/features/loans/providers/loan_details_provider.dart'; +import 'package:librarian_app/src/features/loans/providers/selected_loan_provider.dart'; import 'package:librarian_app/src/features/loans/widgets/loan_details/loan_details_header.dart'; -import '../../models/loan_model.dart'; +import '../../providers/loans_repository_provider.dart'; import 'loan_details.dart'; -class LoanDetailsPane extends StatelessWidget { - final LoanModel? loan; - final void Function(DateTime dueDate, String? notes) onSave; - final void Function() onCheckIn; - - const LoanDetailsPane({ - super.key, - required this.loan, - required this.onSave, - required this.onCheckIn, - }); +class LoanDetailsPane extends ConsumerWidget { + const LoanDetailsPane({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final selectedLoan = ref.read(selectedLoanProvider); + final loanDetailsFuture = ref.watch(loanDetailsProvider); + return Card( clipBehavior: Clip.antiAlias, - child: loan == null + child: selectedLoan == null ? const Center(child: Text('Loan Details')) - : Column( - children: [ - LoanDetailsHeader( - loan: loan!, - onSave: onSave, - onCheckIn: onCheckIn, - ), - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16), - child: LoanDetails( - borrower: loan!.borrower, - things: [loan!.thing], - notes: loan!.notes, - checkedOutDate: loan!.checkedOutDate, - dueDate: loan!.dueDate, - isOverdue: loan!.isOverdue, + : FutureBuilder( + future: loanDetailsFuture, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Center(child: Text(snapshot.error.toString())); + } + + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + + final loanDetails = snapshot.data!; + + return Column( + children: [ + LoanDetailsHeader( + loan: loanDetails, + onSave: (dueDate, notes) { + ref.read(loansRepositoryProvider.notifier).updateLoan( + loanId: selectedLoan.id, + thingId: selectedLoan.thing.id, + dueBackDate: dueDate, + notes: notes); + }, + onCheckIn: () { + ref + .read(loansRepositoryProvider.notifier) + .closeLoan( + loanId: selectedLoan.id, + thingId: selectedLoan.thing.id, + ) + .then((_) { + ref.read(selectedLoanProvider.notifier).state = null; + }); + }, + ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: LoanDetails( + borrower: loanDetails.borrower, + things: [loanDetails.thing], + notes: loanDetails.notes, + checkedOutDate: loanDetails.checkedOutDate, + dueDate: loanDetails.dueDate, + isOverdue: loanDetails.isOverdue, + ), + ), ), ), - ), - ), - ], + ], + ); + }, ), ); } diff --git a/lib/src/features/loans/widgets/loans_list/loans_list_view.dart b/lib/src/features/loans/widgets/loans_list/loans_list_view.dart index 1520dcc..be041bd 100644 --- a/lib/src/features/loans/widgets/loans_list/loans_list_view.dart +++ b/lib/src/features/loans/widgets/loans_list/loans_list_view.dart @@ -21,14 +21,14 @@ class LoansListView extends ConsumerWidget { return FutureBuilder( future: filteredLoans, builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } - if (snapshot.hasError) { return Center(child: Text(snapshot.error.toString())); } + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasData && snapshot.data!.isEmpty) { return const Center(child: Text('No results found')); }