diff --git a/lib/data/repositories/bal_repository.dart b/lib/data/repositories/bal_repository.dart index 983fe55..4d04374 100644 --- a/lib/data/repositories/bal_repository.dart +++ b/lib/data/repositories/bal_repository.dart @@ -22,6 +22,17 @@ class BalRepository { } } + Future>> _getBalsNoCache() async { + final result = await _apiClient.getBals(); + switch (result) { + case Ok(): + _bals = result.value; + return Result.ok(result.value); + case Error(): + return result; + } + } + Future> balById(int id) async { if (_bals == null) { await getBals(); @@ -39,13 +50,20 @@ class BalRepository { } } - Future> addBal(String name) async { - final result = await _apiClient.addBal(name); - switch (result) { - case Ok(): - return result; - case Error(): - return result; - } + Future> editBal( + int id, + String name, + DateTime start, + DateTime end, + ) async { + final result = await _apiClient.editBal(id, name, start, end); + _getBalsNoCache(); + return result; + } + + Future> addBal(String name, DateTime start, DateTime end) async { + final result = await _apiClient.addBal(name, start, end); + _getBalsNoCache(); + return result; } } diff --git a/lib/data/services/api_client.dart b/lib/data/services/api_client.dart index 00270d3..b2c1230 100644 --- a/lib/data/services/api_client.dart +++ b/lib/data/services/api_client.dart @@ -42,6 +42,38 @@ class ApiClient { * ================= */ + Future> editBal( + int id, + String name, + DateTime start, + DateTime end, + ) async { + final client = Client(); + try { + final headers = await _getHeaders({"Content-Type": "application/json"}); + final body = { + "name": name, + "start_timestamp": (start.millisecondsSinceEpoch / 1000).round(), + "end_timestamp": (end.millisecondsSinceEpoch / 1000).round(), + }; + final response = await client.patch( + Uri.parse("https://$apiBasePath/bal/${id.toString()}"), + headers: headers, + body: jsonEncode(body), + ); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + return Result.ok(Bal.fromJSON(json)); + } else { + throw Exception("Something went wrong"); + } + } catch (e) { + return Result.error(Exception(e)); + } finally { + client.close(); + } + } + Future> getBalById(int id) async { final client = Client(); try { @@ -54,7 +86,7 @@ class ApiClient { final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); } else if (response.statusCode == 403) { - return Result.error(Exception("You don't own the specified bal")); + throw Exception("You don't own the specified bal"); } else { return Result.error( Exception("No bal wirth this id exists the database"), @@ -67,11 +99,15 @@ class ApiClient { } } - Future> addBal(String name) async { + Future> addBal(String name, DateTime start, DateTime end) async { final client = Client(); try { final headers = await _getHeaders({"Content-Type": "application/json"}); - final body = {"name": name}; + final body = { + "name": name, + "start_timestamp": (start.millisecondsSinceEpoch / 1000).round(), + "end_timestamp": (end.millisecondsSinceEpoch / 1000).round(), + }; final response = await client.post( Uri.parse("https://$apiBasePath/bal"), headers: headers, @@ -81,10 +117,9 @@ class ApiClient { final json = jsonDecode(response.body); return Result.ok(Bal.fromJSON(json)); } else { - return Result.error(Exception("Something went wrong")); + throw Exception("Something went wrong"); } } catch (e) { - debugPrint("\n\n\n\n${e.toString()}\n\n\n\n"); return Result.error(Exception(e)); } finally { client.close(); @@ -101,12 +136,17 @@ class ApiClient { ); if (response.statusCode == 200) { final json = jsonDecode(response.body) as List; - debugPrint("\n\n\n\nRECEIVED $json\n\n\n\n"); + debugPrint("\n\n\n\nRECEIVED : $json\n\n\n\n"); + debugPrint( + "\n\n\n\nFORMATTED : ${json.map((element) => Bal.fromJSON(element)).toList()}\n\n\n\n", + ); + return Result.ok(json.map((element) => Bal.fromJSON(element)).toList()); } else { - return Result.error(Exception("Something wrong happened")); + throw Exception("Something wrong happened"); } } catch (e) { + debugPrint("ERROR: ${e.toString()}"); return Result.error(Exception(e)); } finally { client.close(); @@ -127,7 +167,7 @@ class ApiClient { } else if (response.statusCode == 404) { return Result.ok(null); } else { - return Result.error(Exception("Something went wrong")); + throw Exception("Something went wrong"); } } catch (e) { return Result.error(Exception(e)); @@ -154,7 +194,7 @@ class ApiClient { final json = jsonDecode(response.body); return Result.ok(Book.fromJSON(json)); } else { - return Result.error(Exception("The book was not found")); + throw Exception("The book was not found"); } } catch (e) { return Result.error(Exception("API $e")); @@ -193,9 +233,9 @@ class ApiClient { final json = jsonDecode(response.body); return Result.ok(BookInstance.fromJSON(json)); } else if (response.statusCode == 403) { - return Result.error(Exception("You don't own that book instance")); + throw Exception("You don't own that book instance"); } else { - return Result.error(Exception("Something wrong happened")); + throw Exception("Something wrong happened"); } } catch (e) { return Result.error(Exception(e)); @@ -225,7 +265,7 @@ class ApiClient { json.map((element) => Owner.fromJSON(element)).toList(), ); } else { - return Result.error(Exception("Invalid request")); + throw Exception("Invalid request"); } } on Exception catch (error) { return Result.error(error); @@ -257,7 +297,7 @@ class ApiClient { final json = jsonDecode(response.body); return Result.ok(Owner.fromJSON(json)); } else { - return Result.error(Exception("Invalid request")); + throw Exception("Invalid request"); } } on Exception catch (error) { return Result.error(error); diff --git a/lib/main.dart b/lib/main.dart index fa5226c..40d2d0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:seshat/config/dependencies.dart'; import 'package:seshat/routing/router.dart'; -void main() { +void main() async { Logger.root.level = Level.ALL; WidgetsFlutterBinding.ensureInitialized(); @@ -19,6 +20,12 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp.router( + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: [const Locale("fr")], routerConfig: router(context.read()), theme: ThemeData.dark(), ); diff --git a/lib/ui/bal_page/view_model/bal_view_model.dart b/lib/ui/bal_page/view_model/bal_view_model.dart index 9339a4b..1e0f675 100644 --- a/lib/ui/bal_page/view_model/bal_view_model.dart +++ b/lib/ui/bal_page/view_model/bal_view_model.dart @@ -16,6 +16,26 @@ class BalViewModel extends ChangeNotifier { int? id; Bal? get bal => _bal; + Future> editBal( + int id, + String name, + DateTime start, + DateTime end, + ) async { + final result = await _balRepository.editBal(id, name, start, end); + switch (result) { + case Ok(): + debugPrint("\n\n\n\nDID EDIT\n\n\n\n"); + _bal = result.value; + notifyListeners(); + break; + case Error(): + debugPrint("\n\n\n\nERROR: ${result.error}"); + break; + } + return result; + } + /* * ================================= * =====[ COMMAND AND LOADING ]===== diff --git a/lib/ui/bal_page/widget/bal_page.dart b/lib/ui/bal_page/widget/bal_page.dart index 8c7fd9d..763f2ee 100644 --- a/lib/ui/bal_page/widget/bal_page.dart +++ b/lib/ui/bal_page/widget/bal_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:seshat/domain/models/bal.dart'; import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart'; +import 'package:seshat/ui/bal_page/widget/pending/bal_pending_screen.dart'; import 'package:seshat/ui/core/ui/navigation_bar.dart'; import 'package:seshat/ui/core/ui/await_loading.dart'; @@ -16,26 +17,29 @@ class BalPage extends StatefulWidget { class _BalPageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - bottomNavigationBar: AppNavigationBar(startIndex: 0), - body: ListenableBuilder( - listenable: widget.viewModel, - builder: (context, child) { - return switch (widget.viewModel.isLoaded) { - false => AwaitLoading(), - true => switch (widget.viewModel.bal == null) { - true => Center( + return ListenableBuilder( + listenable: widget.viewModel, + builder: (context, child) { + return switch (widget.viewModel.isLoaded) { + false => Scaffold( + bottomNavigationBar: AppNavigationBar(startIndex: 0), + body: AwaitLoading(), + ), + true => switch (widget.viewModel.bal == null) { + true => Scaffold( + bottomNavigationBar: AppNavigationBar(startIndex: 0), + body: Center( child: Text("La BAL référencée n'est pas accessible"), ), - false => switch (widget.viewModel.bal!.state) { - BalState.pending => Center(child: Text("Pending")), - BalState.ongoing => Center(child: Text("Ongoing")), - BalState.ended => Center(child: Text("Ending")), - }, + ), + false => switch (widget.viewModel.bal!.state) { + BalState.pending => BalPendingScreen(viewModel: widget.viewModel), + BalState.ongoing => Center(child: Text("Ongoing")), + BalState.ended => Center(child: Text("Ending")), }, - }; - }, - ), + }, + }; + }, ); } } diff --git a/lib/ui/bal_page/widget/pending/bal_pending_screen.dart b/lib/ui/bal_page/widget/pending/bal_pending_screen.dart index b65257a..09683e9 100644 --- a/lib/ui/bal_page/widget/pending/bal_pending_screen.dart +++ b/lib/ui/bal_page/widget/pending/bal_pending_screen.dart @@ -1,11 +1,191 @@ import 'package:flutter/material.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; +import 'package:seshat/domain/models/bal.dart'; +import 'package:seshat/ui/bal_page/view_model/bal_view_model.dart'; +import 'package:seshat/ui/core/ui/navigation_bar.dart'; class BalPendingScreen extends StatelessWidget { - const BalPendingScreen({super.key}); + const BalPendingScreen({super.key, required this.viewModel}); + + final BalViewModel viewModel; @override Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); + return Scaffold( + bottomNavigationBar: AppNavigationBar(startIndex: 0), + appBar: AppBar( + title: Text(viewModel.bal!.name), + actions: [ + IconButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => EditPopup(viewModel: viewModel), + ); + }, + icon: Icon(Icons.edit), + ), + ], + ), + body: Center( + child: ElevatedButton( + onPressed: () {}, + child: Text("Démarrer cette BAL"), + ), + ), + ); + } +} + +class EditPopup extends StatefulWidget { + const EditPopup({super.key, required this.viewModel}); + + final BalViewModel viewModel; + + @override + State createState() => _EditPopup(); +} + +class _EditPopup extends State { + final GlobalKey _formKey = GlobalKey(); + String? name; + DateTime? start; + Future _selectStart() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: start ?? widget.viewModel.bal!.startTime, + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 2), + locale: Locale("fr", "FR"), + ); + + setState(() { + start = pickedDate; + }); + } + + DateTime? end; + Future _selectEnd() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: end ?? widget.viewModel.bal!.endTime, + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 2), + locale: Locale("fr", "FR"), + ); + + setState(() { + end = pickedDate; + }); + } + + @override + Widget build(BuildContext context) { + initializeDateFormatting(); + var format = DateFormat("dd MMM yyyy", "fr"); + return AlertDialog( + title: Text("Créer une BAL"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: "Nom de la BAL", + border: OutlineInputBorder(), + ), + initialValue: widget.viewModel.bal!.name, + validator: (value) { + if (value == null || value.isEmpty) { + return "Veuillez entrer un nom"; + } + return null; + }, + onSaved: (newValue) { + name = newValue; + }, + ), + Row( + children: [ + Text("Date de début : "), + TextButton( + onPressed: () { + _selectStart(); + }, + child: Text( + format.format(start ?? widget.viewModel.bal!.startTime), + locale: Locale("fr"), + ), + ), + ], + ), + Row( + children: [ + Text("Date de fin : "), + TextButton( + onPressed: () { + _selectEnd(); + }, + child: Text( + format.format(end ?? widget.viewModel.bal!.endTime), + ), + ), + ], + ), + Text("Note: Les dates sont à titre purement indicatif."), + ], + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Annuler"), + ), + TextButton( + onPressed: () async { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + + final Bal bal = widget.viewModel.bal!; + + final result = await widget.viewModel.editBal( + bal.id, + name ?? bal.name, + start ?? bal.startTime, + end ?? bal.endTime, + ); + if (result is Error && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Une erreur est survenue")), + ); + } + + if (context.mounted) { + Navigator.of(context).pop(); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Veuillez indiquer une date de début et de fin.", + ), + ), + ); + } + }, + child: Text("Valider"), + ), + ], + ); } } diff --git a/lib/ui/core/ui/await_loading.dart b/lib/ui/core/ui/await_loading.dart index 7342527..cfc4729 100644 --- a/lib/ui/core/ui/await_loading.dart +++ b/lib/ui/core/ui/await_loading.dart @@ -23,10 +23,12 @@ class _AwaitLoadingState extends State { @override Widget build(BuildContext context) { t = Timer(Duration(seconds: 8), () { - setState(() { - text = - "Il semblerait qu'il y ait un problème. Vérifiez que vous êtes connecté·e à internet."; - }); + if (context.mounted) { + setState(() { + text = + "Il semblerait qu'il y ait un problème. Vérifiez que vous êtes connecté·e à internet."; + }); + } }); return Column( diff --git a/lib/ui/home_page/view_model/home_view_model.dart b/lib/ui/home_page/view_model/home_view_model.dart index a35c33c..29ffeda 100644 --- a/lib/ui/home_page/view_model/home_view_model.dart +++ b/lib/ui/home_page/view_model/home_view_model.dart @@ -24,8 +24,12 @@ class HomeViewModel extends ChangeNotifier { Bal? _currentBal; Bal? get currentBal => _currentBal; - Future> createBal(String name) async { - final result = await _balRepository.addBal(name); + Future> createBal( + String name, + DateTime start, + DateTime end, + ) async { + final result = await _balRepository.addBal(name, start, end); switch (result) { case Ok(): final result2 = await _balRepository.getBals(); @@ -34,7 +38,6 @@ class HomeViewModel extends ChangeNotifier { _bals = result2.value..sort((a, b) => a.compareTo(b)); break; case Error(): - debugPrint("\n\n\n\n${result2.error.toString()}\n\n\n\n"); return result2; } break; diff --git a/lib/ui/home_page/widgets/create_confirmation_popup.dart b/lib/ui/home_page/widgets/create_confirmation_popup.dart index 211f2ae..a7ed3a9 100644 --- a/lib/ui/home_page/widgets/create_confirmation_popup.dart +++ b/lib/ui/home_page/widgets/create_confirmation_popup.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; import 'package:seshat/ui/home_page/view_model/home_view_model.dart'; class CreateConfirmationPopup extends StatefulWidget { @@ -14,8 +16,40 @@ class CreateConfirmationPopup extends StatefulWidget { class _CreateConfirmationPopupState extends State { final GlobalKey _formKey = GlobalKey(); String? name; + DateTime? start; + Future _selectStart() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: start ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 2), + locale: Locale("fr"), + ); + + setState(() { + start = pickedDate; + }); + } + + DateTime? end; + Future _selectEnd() async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: end ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(DateTime.now().year + 2), + locale: Locale("fr"), + ); + + setState(() { + end = pickedDate; + }); + } + @override Widget build(BuildContext context) { + initializeDateFormatting(); + var format = DateFormat("dd MMM yyyy", "fr"); return AlertDialog( title: Text("Créer une BAL"), content: Column( @@ -41,6 +75,29 @@ class _CreateConfirmationPopupState extends State { name = newValue; }, ), + Row( + children: [ + Text("Date de début : "), + TextButton( + onPressed: () { + _selectStart(); + }, + child: Text(format.format(start ?? DateTime.now())), + ), + ], + ), + Row( + children: [ + Text("Date de fin : "), + TextButton( + onPressed: () { + _selectEnd(); + }, + child: Text(format.format(end ?? DateTime.now())), + ), + ], + ), + Text("Note: Les dates sont à titre purement indicatif."), ], ), ), @@ -55,12 +112,23 @@ class _CreateConfirmationPopupState extends State { ), TextButton( onPressed: () async { - if (_formKey.currentState!.validate()) { + if (_formKey.currentState!.validate() && + start != null && + end != null) { _formKey.currentState!.save(); - await widget.viewModel.createBal(name!); - } - if (context.mounted) { - Navigator.of(context).pop(); + await widget.viewModel.createBal(name!, start!, end!); + + if (context.mounted) { + Navigator.of(context).pop(); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + "Veuillez indiquer une date de début et de fin.", + ), + ), + ); } }, child: Text("Valider"), diff --git a/lib/ui/home_page/widgets/home_page.dart b/lib/ui/home_page/widgets/home_page.dart index 9db731f..f365a74 100644 --- a/lib/ui/home_page/widgets/home_page.dart +++ b/lib/ui/home_page/widgets/home_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:intl/intl.dart'; import 'package:seshat/domain/models/bal.dart'; import 'package:seshat/ui/core/ui/navigation_bar.dart'; import 'package:seshat/ui/home_page/view_model/home_view_model.dart'; @@ -18,6 +20,8 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { @override Widget build(BuildContext context) { + initializeDateFormatting(); + var format = DateFormat("dd MMM yyyy", "fr"); return Scaffold( bottomNavigationBar: AppNavigationBar(startIndex: 0), appBar: AppBar( @@ -55,30 +59,24 @@ class _HomePageState extends State { title: Text(bal.name), subtitle: switch (bal.state) { BalState.pending => Text( - "À venir · Débute le ${bal.startTime.toString()}", + "À venir · Débute le ${format.format(bal.startTime)}", ), BalState.ongoing => Text("En cours"), BalState.ended => Text("Terminée"), }, trailing: switch (bal.state) { - BalState.pending => IconButton( - onPressed: () { - _moveToBal(context, bal.id); - }, - icon: Icon(Icons.edit), - ), - BalState.ongoing => IconButton( - onPressed: () { - _moveToBal(context, bal.id); - }, - icon: Icon(Icons.arrow_forward), - ), BalState.ended => IconButton( onPressed: () { _moveToBal(context, bal.id); }, icon: Icon(Icons.analytics), ), + _ => IconButton( + onPressed: () { + _moveToBal(context, bal.id); + }, + icon: Icon(Icons.arrow_forward), + ), }, ), ), @@ -113,6 +111,7 @@ class _HomePageState extends State { onPressed: () { showDialog( context: context, + barrierDismissible: false, builder: (context) { return CreateConfirmationPopup( viewModel: widget.viewModel, diff --git a/pubspec.lock b/pubspec.lock index 3f86d44..5485707 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -94,6 +94,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_secure_storage: dependency: "direct main" description: @@ -184,6 +189,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" js: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index adee4a1..951b35d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,9 @@ environment: dependencies: flutter: sdk: flutter + + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -44,6 +47,7 @@ dependencies: nested: ^1.0.0 flutter_secure_storage: ^9.2.4 rxdart: ^0.28.0 + intl: ^0.20.2 dev_dependencies: flutter_test: