diff --git a/lib/recommendation/class/recommendation.dart b/lib/recommendation/class/recommendation.dart new file mode 100644 index 000000000..934366e69 --- /dev/null +++ b/lib/recommendation/class/recommendation.dart @@ -0,0 +1,59 @@ +class Recommendation { + final String? id; + final DateTime? creation; + final String title; + final String? code; + final String summary; + final String description; + + Recommendation({ + this.id, + this.creation, + required this.title, + required this.code, + required this.summary, + required this.description, + }); + + Recommendation.fromJson(Map json) + : id = json["id"], + creation = DateTime.parse(json["creation"]), + title = json["title"], + code = json["code"], + summary = json["summary"], + description = json["description"]; + + Map toJson() { + final data = {}; + data["title"] = title; + data["code"] = code; + data["summary"] = summary; + data["description"] = description; + return data; + } + + Recommendation copyWith({id, creation, title, code, summary, description}) { + return Recommendation( + id: id ?? this.id, + creation: creation ?? this.creation, + title: title ?? this.title, + code: code ?? this.code, + summary: summary ?? this.summary, + description: description ?? this.description, + ); + } + + static Recommendation empty() { + return Recommendation( + title: "", + code: null, + summary: "", + description: "", + ); + } + + @override + String toString() { + return 'Recommendation{id: $id, creation: $creation, title: $title, code: $code, summary: $summary, description: $description}'; + } +} diff --git a/lib/recommendation/providers/is_recommendation_admin_provider.dart b/lib/recommendation/providers/is_recommendation_admin_provider.dart new file mode 100644 index 000000000..64d2a9be1 --- /dev/null +++ b/lib/recommendation/providers/is_recommendation_admin_provider.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/user/providers/user_provider.dart'; + +final isRecommendationAdminProvider = StateProvider( + (ref) { + final me = ref.watch(userProvider); + return me.groups + .map((e) => e.id) + .contains("53a669d6-84b1-4352-8d7c-421c1fbd9c6a"); + }, +); diff --git a/lib/recommendation/providers/recommendation_list_provider.dart b/lib/recommendation/providers/recommendation_list_provider.dart new file mode 100644 index 000000000..13f772f68 --- /dev/null +++ b/lib/recommendation/providers/recommendation_list_provider.dart @@ -0,0 +1,57 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/recommendation/repositories/recommendation_repository.dart'; +import 'package:myecl/tools/providers/list_notifier.dart'; +import 'package:myecl/tools/token_expire_wrapper.dart'; + +class RecommendationListNotifier extends ListNotifier { + final RecommendationRepository recommendationRepository; + + RecommendationListNotifier({required this.recommendationRepository}) + : super(const AsyncValue.loading()); + + Future>> loadRecommendation() async { + return await loadList(recommendationRepository.getRecommendationList); + } + + Future addRecommendation(Recommendation recommendation) async { + return await add( + recommendationRepository.createRecommendation, recommendation); + } + + Future updateRecommendation(Recommendation recommendation) async { + return await update( + recommendationRepository.updateRecommendation, + (recommendations, recommendation) => recommendations + ..[recommendations.indexWhere((r) => r.id == recommendation.id)] = + recommendation, + recommendation, + ); + } + + Future deleteRecommendation(Recommendation recommendation) async { + return await delete( + recommendationRepository.deleteRecommendation, + (recommendations, recommendation) => + recommendations..removeWhere((r) => r.id == recommendation.id), + recommendation.id!, + recommendation, + ); + } +} + +final recommendationListProvider = StateNotifierProvider< + RecommendationListNotifier, AsyncValue>>( + (ref) { + final recommendatioRepository = ref.watch(recommendationRepositoryProvider); + final provider = RecommendationListNotifier( + recommendationRepository: recommendatioRepository); + tokenExpireWrapperAuth( + ref, + () async { + await provider.loadRecommendation(); + }, + ); + return provider; + }, +); diff --git a/lib/recommendation/providers/recommendation_logo_map_provider.dart b/lib/recommendation/providers/recommendation_logo_map_provider.dart new file mode 100644 index 000000000..168ae25d2 --- /dev/null +++ b/lib/recommendation/providers/recommendation_logo_map_provider.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/recommendation/providers/recommendation_list_provider.dart'; +import 'package:myecl/tools/providers/map_provider.dart'; +import 'package:myecl/tools/token_expire_wrapper.dart'; + +class RecommendationLogoMapNotifier extends MapNotifier { + RecommendationLogoMapNotifier() : super(); +} + +final recommendationLogoMapProvider = StateNotifierProvider< + RecommendationLogoMapNotifier, + AsyncValue>?>>>( + (ref) { + RecommendationLogoMapNotifier recommendationLogoMapNotifier = + RecommendationLogoMapNotifier(); + tokenExpireWrapperAuth( + ref, + () async { + ref.watch(recommendationListProvider).maybeWhen( + data: (data) { + recommendationLogoMapNotifier.loadTList(data); + return recommendationLogoMapNotifier; + }, + orElse: () { + recommendationLogoMapNotifier.loadTList([]); + return recommendationLogoMapNotifier; + }, + ); + }, + ); + return recommendationLogoMapNotifier; + }, +); diff --git a/lib/recommendation/providers/recommendation_logo_provider.dart b/lib/recommendation/providers/recommendation_logo_provider.dart new file mode 100644 index 000000000..9d4227b14 --- /dev/null +++ b/lib/recommendation/providers/recommendation_logo_provider.dart @@ -0,0 +1,30 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/recommendation/repositories/recommendation_logo_repository.dart'; +import 'package:myecl/tools/providers/single_notifier.dart'; + +class RecommendationLogoNotifier extends SingleNotifier { + final RecommendationLogoRepository recommendationLogoRepository; + RecommendationLogoNotifier({required this.recommendationLogoRepository}) + : super(const AsyncValue.loading()); + + Future getRecommendationLogo(String id) async { + return await recommendationLogoRepository.getRecommendationLogo(id); + } + + Future updateRecommendationLogo(String id, Uint8List bytes) async { + return await recommendationLogoRepository.addRecommendationLogo(bytes, id); + } +} + +final recommendationLogoProvider = + StateNotifierProvider>( + (ref) { + final recommendationLogoRepository = + ref.watch(recommendationLogoRepositoryProvider); + return RecommendationLogoNotifier( + recommendationLogoRepository: recommendationLogoRepository); + }, +); diff --git a/lib/recommendation/providers/recommendation_provider.dart b/lib/recommendation/providers/recommendation_provider.dart new file mode 100644 index 000000000..8ad401be2 --- /dev/null +++ b/lib/recommendation/providers/recommendation_provider.dart @@ -0,0 +1,17 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; + +class RecommendationNotifier extends StateNotifier { + RecommendationNotifier() : super(Recommendation.empty()); + + void setRecommendation(Recommendation r) { + state = r; + } +} + +final recommendationProvider = + StateNotifierProvider( + (ref) { + return RecommendationNotifier(); + }, +); diff --git a/lib/recommendation/repositories/recommendation_logo_repository.dart b/lib/recommendation/repositories/recommendation_logo_repository.dart new file mode 100644 index 000000000..eaabd4d79 --- /dev/null +++ b/lib/recommendation/repositories/recommendation_logo_repository.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/auth/providers/openid_provider.dart'; +import 'package:myecl/tools/repository/logo_repository.dart'; + +class RecommendationLogoRepository extends LogoRepository { + @override + // ignore: overridden_fields + final ext = "recommendation/recommendations"; + + Future getRecommendationLogo(String id) async { + final uint8List = await getLogo("", suffix: "/$id/picture"); + return Image.memory(uint8List); + } + + Future addRecommendationLogo(Uint8List bytes, String id) async { + final uint8List = await addLogo(bytes, "", suffix: "/$id/picture"); + return Image.memory(uint8List); + } +} + +final recommendationLogoRepositoryProvider = + Provider( + (ref) { + final token = ref.watch(tokenProvider); + return RecommendationLogoRepository()..setToken(token); + }, +); diff --git a/lib/recommendation/repositories/recommendation_repository.dart b/lib/recommendation/repositories/recommendation_repository.dart new file mode 100644 index 000000000..11df5a122 --- /dev/null +++ b/lib/recommendation/repositories/recommendation_repository.dart @@ -0,0 +1,35 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:myecl/auth/providers/openid_provider.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/tools/repository/repository.dart'; + +class RecommendationRepository extends Repository { + @override + // ignore: overridden_fields + final ext = 'recommendation/recommendations'; + + Future> getRecommendationList() async { + return List.from( + (await getList()).map((x) => Recommendation.fromJson(x))); + } + + Future createRecommendation( + Recommendation recommendation) async { + return Recommendation.fromJson(await create(recommendation.toJson())); + } + + Future updateRecommendation(Recommendation recommendation) async { + return await update(recommendation.toJson(), "/${recommendation.id}"); + } + + Future deleteRecommendation(String recommendationId) async { + return await delete("/$recommendationId"); + } +} + +final recommendationRepositoryProvider = Provider( + (ref) { + final token = ref.watch(tokenProvider); + return RecommendationRepository()..setToken(token); + }, +); diff --git a/lib/recommendation/router.dart b/lib/recommendation/router.dart new file mode 100644 index 000000000..09544ea9c --- /dev/null +++ b/lib/recommendation/router.dart @@ -0,0 +1,57 @@ +import 'package:either_dart/either.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:myecl/drawer/class/module.dart'; +import 'package:myecl/recommendation/providers/is_recommendation_admin_provider.dart'; +import 'package:myecl/recommendation/ui/pages/main_page.dart' + deferred as main_page; +import 'package:myecl/recommendation/ui/pages/information_page.dart' + deferred as information_page; +import 'package:myecl/recommendation/ui/pages/add_edit_page.dart' + deferred as add_edit_page; +import 'package:myecl/tools/middlewares/admin_middleware.dart'; +import 'package:myecl/tools/middlewares/authenticated_middleware.dart'; +import 'package:myecl/tools/middlewares/deferred_middleware.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class RecommendationRouter { + final ProviderRef ref; + + static const String root = '/recommendation'; + static const String information = '/information'; + static const String addEdit = '/add_edit'; + static final Module module = Module( + name: "Bons plans", + icon: const Left(HeroIcons.newspaper), + root: RecommendationRouter.root, + selected: false, + ); + + RecommendationRouter(this.ref); + + QRoute route() => QRoute( + path: RecommendationRouter.root, + builder: () => main_page.RecommendationMainPage(), + middleware: [ + AuthenticatedMiddleware(ref), + DeferredLoadingMiddleware(main_page.loadLibrary), + ], + children: [ + QRoute( + path: information, + builder: () => information_page.InformationRecommendationPage(), + middleware: [ + DeferredLoadingMiddleware(information_page.loadLibrary) + ], + ), + QRoute( + path: addEdit, + builder: () => add_edit_page.AddEditRecommendationPage(), + middleware: [ + AdminMiddleware(ref, isRecommendationAdminProvider), + DeferredLoadingMiddleware(add_edit_page.loadLibrary) + ], + ), + ], + ); +} diff --git a/lib/recommendation/tools/constants.dart b/lib/recommendation/tools/constants.dart new file mode 100644 index 000000000..90b326618 --- /dev/null +++ b/lib/recommendation/tools/constants.dart @@ -0,0 +1,25 @@ +class RecommendationTextConstants { + static const String recommendation = "Bons plans"; + static const String title = "Titre"; + static const String logo = "Logo"; + static const String code = "Code"; + static const String summary = "Court résumé"; + static const String description = "Description"; + static const String add = "Ajouter"; + static const String edit = "Modifier"; + static const String delete = "Supprimer"; + static const String addImage = "Veuillez ajouter une image"; + static const String addedRecommendation = "Bon plan ajouté"; + static const String editedRecommendation = "Bon plan modifié"; + static const String deleteRecommendationConfirmation = + "Êtes-vous sûr de vouloir supprimer ce bon plan ?"; + static const String deleteRecommendation = "Suppresion"; + static const String deletingRecommendationError = + "Erreur lors de la suppression"; + static const String deletedRecommendation = "Bon plan supprimé"; + static const String incorrectOrMissingFields = + 'Champs incorrects ou manquants'; + static const String editingError = "Échec de la modification"; + static const String addingError = "Échec de l'ajout"; + static const String copiedCode = "Code de réduction copié"; +} diff --git a/lib/recommendation/ui/pages/add_edit_page.dart b/lib/recommendation/ui/pages/add_edit_page.dart new file mode 100644 index 000000000..49924ec26 --- /dev/null +++ b/lib/recommendation/ui/pages/add_edit_page.dart @@ -0,0 +1,215 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/recommendation/providers/recommendation_list_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_logo_map_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_logo_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_provider.dart'; +import 'package:myecl/recommendation/tools/constants.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_template.dart'; +import 'package:myecl/tools/functions.dart'; +import 'package:myecl/tools/ui/builders/waiting_button.dart'; +import 'package:myecl/tools/ui/layouts/add_edit_button_layout.dart'; +import 'package:myecl/tools/ui/widgets/image_picker_on_tap.dart'; +import 'package:myecl/tools/ui/widgets/text_entry.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class AddEditRecommendationPage extends HookConsumerWidget { + const AddEditRecommendationPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = GlobalKey(); + final ImagePicker picker = ImagePicker(); + final recommendation = ref.watch(recommendationProvider); + final recommendationNotifier = ref.watch(recommendationProvider.notifier); + final recommendationList = ref.watch(recommendationListProvider); + final recommendationListNotifier = + ref.watch(recommendationListProvider.notifier); + final recommendationLogoNotifier = + ref.watch(recommendationLogoProvider.notifier); + final logoBytes = useState(null); + final logo = useState(null); + final isEdit = recommendation.id != Recommendation.empty().id; + + final title = useTextEditingController(text: recommendation.title); + final code = useTextEditingController(text: recommendation.code); + final summary = useTextEditingController(text: recommendation.summary); + final description = + useTextEditingController(text: recommendation.description); + + ref.watch(recommendationLogoMapProvider).whenData( + (value) { + if (value[recommendation] != null) { + value[recommendation]!.whenData( + (data) { + if (data.isNotEmpty) { + logo.value = data.first; + } + }, + ); + } + }, + ); + + void displayAdvertToastWithContext(TypeMsg type, String msg) { + displayToast(context, type, msg); + } + + return RecommendationTemplate( + child: Form( + key: formKey, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: SingleChildScrollView( + child: Column( + children: [ + TextEntry( + maxLines: 1, + label: RecommendationTextConstants.title, + controller: title, + ), + const SizedBox(height: 30), + FormField( + validator: (e) { + if (logoBytes.value == null && !isEdit) { + return RecommendationTextConstants.addImage; + } + return null; + }, + builder: (formFieldState) => Center( + child: ImagePickerOnTap( + imageBytesNotifier: logoBytes, + imageNotifier: logo, + displayToastWithContext: displayAdvertToastWithContext, + picker: picker, + child: logo.value != null + ? Container( + height: 100, + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50.0), + image: DecorationImage( + image: logoBytes.value != null + ? Image.memory(logoBytes.value!).image + : logo.value!.image, + ), + ), + ) + : const HeroIcon( + HeroIcons.photo, + size: 100, + color: Colors.grey, + ), + ), + ), + ), + TextEntry( + maxLines: 1, + label: RecommendationTextConstants.code, + controller: code, + canBeEmpty: true, + ), + const SizedBox(height: 30), + TextEntry( + minLines: 1, + maxLines: 2, + keyboardType: TextInputType.multiline, + label: RecommendationTextConstants.summary, + controller: summary, + ), + const SizedBox(height: 30), + TextEntry( + minLines: 5, + maxLines: 50, + keyboardType: TextInputType.multiline, + label: RecommendationTextConstants.description, + controller: description, + ), + const SizedBox(height: 50), + WaitingButton( + child: Text( + isEdit + ? RecommendationTextConstants.edit + : RecommendationTextConstants.add, + style: const TextStyle( + color: Colors.white, + fontSize: 25, + fontWeight: FontWeight.bold, + ), + ), + onTap: () async { + if (formKey.currentState!.validate()) { + Recommendation newRecommendation = Recommendation( + id: recommendation.id, + creation: recommendation.creation, + title: title.text, + code: code.text == "" ? null : code.text, + summary: summary.text, + description: description.text, + ); + final value = isEdit + ? await recommendationListNotifier + .updateRecommendation(newRecommendation) + : await recommendationListNotifier + .addRecommendation(newRecommendation); + if (value) { + if (isEdit) { + recommendationNotifier + .setRecommendation(newRecommendation); + displayAdvertToastWithContext(TypeMsg.msg, + RecommendationTextConstants.editedRecommendation); + recommendationList.maybeWhen( + data: (list) { + if (logoBytes.value != null) { + recommendationLogoNotifier + .updateRecommendationLogo( + recommendation.id!, logoBytes.value!); + } + }, + orElse: () {}, + ); + } else { + displayAdvertToastWithContext(TypeMsg.msg, + RecommendationTextConstants.addedRecommendation); + recommendationList.maybeWhen( + data: (list) { + final newRecommendation = list.last; + recommendationLogoNotifier + .updateRecommendationLogo( + newRecommendation.id!, logoBytes.value!); + }, + orElse: () {}, + ); + } + QR.back(); + } else { + displayAdvertToastWithContext( + TypeMsg.error, + isEdit + ? RecommendationTextConstants.editingError + : RecommendationTextConstants.addingError, + ); + } + } else { + displayToast(context, TypeMsg.error, + RecommendationTextConstants.incorrectOrMissingFields); + } + }, + builder: (child) => AddEditButtonLayout(child: child), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/recommendation/ui/pages/information_page.dart b/lib/recommendation/ui/pages/information_page.dart new file mode 100644 index 000000000..0e8b666a4 --- /dev/null +++ b/lib/recommendation/ui/pages/information_page.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:myecl/recommendation/providers/recommendation_provider.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_card.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_card_layout.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_template.dart'; + +class InformationRecommendationPage extends HookConsumerWidget { + const InformationRecommendationPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final recommendation = ref.watch(recommendationProvider); + + return RecommendationTemplate( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + children: [ + RecommendationCard( + recommendation: recommendation, + isMainPage: false, + ), + RecommendationCardLayout( + backgroundColor: Colors.grey.withOpacity(0.2), + child: Text( + recommendation.description, + textAlign: TextAlign.justify, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/recommendation/ui/pages/main_page.dart b/lib/recommendation/ui/pages/main_page.dart new file mode 100644 index 000000000..34ac68ad1 --- /dev/null +++ b/lib/recommendation/ui/pages/main_page.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/recommendation/providers/is_recommendation_admin_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_list_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_provider.dart'; +import 'package:myecl/recommendation/router.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_card.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_card_layout.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_template.dart'; +import 'package:myecl/tools/ui/builders/async_child.dart'; +import 'package:myecl/tools/ui/layouts/refresher.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class RecommendationMainPage extends HookConsumerWidget { + const RecommendationMainPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isRecommendationAdmin = ref.watch(isRecommendationAdminProvider); + final recommendationNotifier = ref.watch(recommendationProvider.notifier); + final recommendationList = ref.watch(recommendationListProvider); + final recommendationListNotifier = + ref.watch(recommendationListProvider.notifier); + + return RecommendationTemplate( + child: Refresher( + onRefresh: () async { + await recommendationListNotifier.loadRecommendation(); + }, + child: AsyncChild( + value: recommendationList, + builder: (context, data) => Column( + children: [ + const SizedBox(height: 30), + if (isRecommendationAdmin) + GestureDetector( + onTap: () { + recommendationNotifier + .setRecommendation(Recommendation.empty()); + QR.to(RecommendationRouter.root + + RecommendationRouter.addEdit); + }, + child: const RecommendationCardLayout( + child: Center( + child: HeroIcon( + HeroIcons.plus, + size: 50, + ), + ), + ), + ), + ...(data..sort((a, b) => b.creation!.compareTo(a.creation!))).map( + (e) => RecommendationCard( + recommendation: e, + isMainPage: true, + ), + ), + const SizedBox(height: 30), + ], + ), + ), + ), + ); + } +} diff --git a/lib/recommendation/ui/widgets/recommendation_card.dart b/lib/recommendation/ui/widgets/recommendation_card.dart new file mode 100644 index 000000000..6c5f80e74 --- /dev/null +++ b/lib/recommendation/ui/widgets/recommendation_card.dart @@ -0,0 +1,208 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/recommendation/providers/is_recommendation_admin_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_list_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_logo_map_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_logo_provider.dart'; +import 'package:myecl/recommendation/providers/recommendation_provider.dart'; +import 'package:myecl/recommendation/router.dart'; +import 'package:myecl/recommendation/tools/constants.dart'; +import 'package:myecl/recommendation/ui/widgets/recommendation_card_layout.dart'; +import 'package:myecl/tools/functions.dart'; +import 'package:myecl/tools/token_expire_wrapper.dart'; +import 'package:myecl/tools/ui/builders/auto_loader_child.dart'; +import 'package:myecl/tools/ui/layouts/card_button.dart'; +import 'package:myecl/tools/ui/widgets/dialog.dart'; +import 'package:qlevar_router/qlevar_router.dart'; + +class RecommendationCard extends HookConsumerWidget { + final Recommendation recommendation; + final bool isMainPage; + + const RecommendationCard({ + super.key, + required this.recommendation, + required this.isMainPage, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isRecommendationAdmin = ref.watch(isRecommendationAdminProvider); + final recommendationNotifier = ref.watch(recommendationProvider.notifier); + final recommendationListNotifier = + ref.watch(recommendationListProvider.notifier); + final recommendationLogoMap = ref.watch(recommendationLogoMapProvider); + final recommendationLogoMapNotifier = + ref.watch(recommendationLogoMapProvider.notifier); + final recommendationLogoNotifier = + ref.watch(recommendationLogoProvider.notifier); + + void displayToastWithContext(TypeMsg type, String message) { + displayToast(context, type, message); + } + + return AutoLoaderChild( + value: recommendationLogoMap, + notifier: recommendationLogoMapNotifier, + mapKey: recommendation, + loader: (ref) => + recommendationLogoNotifier.getRecommendationLogo(recommendation.id!), + loadingBuilder: (context) => const HeroIcon( + HeroIcons.photo, + ), + dataBuilder: (context, data) => RecommendationCardLayout( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(20.0), + child: Image( + width: 50, + image: data.first.image, + ), + ), + const SizedBox(width: 20), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + recommendation.title, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + if (recommendation.code != null) + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible( + child: Text( + recommendation.code!, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + ), + ), + ), + if (recommendation.code != null) + IconButton( + onPressed: () async { + await Clipboard.setData( + ClipboardData(text: recommendation.code!), + ); + displayToastWithContext( + TypeMsg.msg, + RecommendationTextConstants.copiedCode, + ); + }, + icon: const Icon(Icons.copy), + ), + ], + ), + Text( + recommendation.summary, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 15, + color: Colors.grey, + ), + ), + ], + ), + ), + const SizedBox(width: 20), + isMainPage + ? SizedBox( + width: 50, + child: IconButton( + onPressed: () { + recommendationNotifier + .setRecommendation(recommendation); + QR.to(RecommendationRouter.root + + RecommendationRouter.information); + }, + icon: const HeroIcon( + HeroIcons.informationCircle, + size: 40, + ), + ), + ) + : SizedBox( + width: 50, + child: isRecommendationAdmin + ? Column( + children: [ + GestureDetector( + onTap: () { + QR.to(RecommendationRouter.root + + RecommendationRouter.addEdit); + }, + child: const CardButton( + child: HeroIcon( + HeroIcons.pencil, + size: 25, + ), + ), + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () async { + await tokenExpireWrapper( + ref, + () async { + await showDialog( + context: context, + builder: (context) => CustomDialogBox( + descriptions: RecommendationTextConstants + .deleteRecommendationConfirmation, + onYes: () async { + final value = + await recommendationListNotifier + .deleteRecommendation( + recommendation); + if (value) { + displayToastWithContext( + TypeMsg.msg, + RecommendationTextConstants + .deletedRecommendation); + QR.back(); + } else { + displayToastWithContext( + TypeMsg.error, + RecommendationTextConstants + .deletingRecommendationError); + } + }, + title: RecommendationTextConstants + .deleteRecommendation, + ), + ); + }, + ); + }, + child: const CardButton( + color: Colors.black, + child: HeroIcon( + color: Colors.white, + HeroIcons.trash, + size: 25, + ), + ), + ), + ], + ) + : null, + ), + ], + ), + ), + ); + } +} diff --git a/lib/recommendation/ui/widgets/recommendation_card_layout.dart b/lib/recommendation/ui/widgets/recommendation_card_layout.dart new file mode 100644 index 000000000..0354eb73e --- /dev/null +++ b/lib/recommendation/ui/widgets/recommendation_card_layout.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:myecl/tools/ui/layouts/card_layout.dart'; + +class RecommendationCardLayout extends StatelessWidget { + final Widget child; + final Color backgroundColor; + + const RecommendationCardLayout({ + super.key, + required this.child, + this.backgroundColor = Colors.white, + }); + + @override + Widget build(BuildContext context) { + return CardLayout( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 30), + color: backgroundColor, + shadowColor: Colors.grey.withOpacity(0.2), + child: child, + ); + } +} diff --git a/lib/recommendation/ui/widgets/recommendation_template.dart b/lib/recommendation/ui/widgets/recommendation_template.dart new file mode 100644 index 000000000..d9ccab643 --- /dev/null +++ b/lib/recommendation/ui/widgets/recommendation_template.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:myecl/recommendation/router.dart'; +import 'package:myecl/recommendation/tools/constants.dart'; +import 'package:myecl/tools/ui/widgets/top_bar.dart'; + +class RecommendationTemplate extends StatelessWidget { + final Widget child; + const RecommendationTemplate({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const TopBar( + title: RecommendationTextConstants.recommendation, + root: RecommendationRouter.root, + ), + Expanded(child: child) + ], + ), + ); + } +} diff --git a/lib/router.dart b/lib/router.dart index a08978e72..25aea9e54 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -15,6 +15,7 @@ import 'package:myecl/others/ui/no_internet_page.dart' deferred as no_internet_page; import 'package:myecl/others/ui/no_module.dart' deferred as no_module_page; import 'package:myecl/others/ui/update_page.dart' deferred as update_page; +import 'package:myecl/recommendation/router.dart'; import 'package:myecl/settings/router.dart'; import 'package:myecl/raffle/router.dart'; import 'package:myecl/tools/middlewares/authenticated_middleware.dart'; @@ -80,6 +81,7 @@ class AppRouter { LoginRouter(ref).route(), LoginRouter(ref).passwordRoute(), RaffleRouter(ref).route(), + RecommendationRouter(ref).route(), SettingsRouter(ref).route(), VoteRouter(ref).route(), ]; diff --git a/lib/settings/providers/module_list_provider.dart b/lib/settings/providers/module_list_provider.dart index a26c5777b..1b0d289e4 100644 --- a/lib/settings/providers/module_list_provider.dart +++ b/lib/settings/providers/module_list_provider.dart @@ -12,6 +12,7 @@ import 'package:myecl/event/router.dart'; import 'package:myecl/home/router.dart'; import 'package:myecl/loan/router.dart'; import 'package:myecl/raffle/router.dart'; +import 'package:myecl/recommendation/router.dart'; import 'package:myecl/vote/router.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -40,6 +41,7 @@ class ModulesNotifier extends StateNotifier> { LoanRouter.module, EventRouter.module, RaffleRouter.module, + RecommendationRouter.module, ]; ModulesNotifier() : super([]); diff --git a/lib/tools/functions.dart b/lib/tools/functions.dart index b5c7d4e28..0fa7242c2 100644 --- a/lib/tools/functions.dart +++ b/lib/tools/functions.dart @@ -43,6 +43,7 @@ void displayToast(BuildContext context, TypeMsg type, String text) { return FlashBar( position: FlashPosition.top, controller: controller, + surfaceTintColor: Colors.transparent, backgroundColor: Colors.transparent, shadowColor: Colors.transparent, margin: const EdgeInsets.only(top: 30, left: 20, right: 20), diff --git a/lib/tools/ui/widgets/text_entry.dart b/lib/tools/ui/widgets/text_entry.dart index 70c7f3b88..b3d3a8854 100644 --- a/lib/tools/ui/widgets/text_entry.dart +++ b/lib/tools/ui/widgets/text_entry.dart @@ -44,11 +44,13 @@ class TextEntry extends StatelessWidget { keyboardType: keyboardType, cursorColor: color, onChanged: onChanged, - textInputAction: TextInputAction.next, + textInputAction: (keyboardType == TextInputType.multiline) + ? TextInputAction.newline + : TextInputAction.next, enabled: enabled, decoration: InputDecoration( label: Text( - label, + canBeEmpty ? '$label (optionnel)' : label, style: TextStyle(color: color, height: 0.5), ), suffix: suffixIcon == null && suffix.isEmpty diff --git a/test/recommendation/recommendation_list_provider_test.dart b/test/recommendation/recommendation_list_provider_test.dart new file mode 100644 index 000000000..efdb99643 --- /dev/null +++ b/test/recommendation/recommendation_list_provider_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:myecl/recommendation/class/recommendation.dart'; +import 'package:myecl/recommendation/providers/recommendation_list_provider.dart'; +import 'package:myecl/recommendation/repositories/recommendation_repository.dart'; + +class MockRecommendationRepository extends Mock + implements RecommendationRepository {} + +void main() { + group( + 'RoomListNotifier', + () { + test('Should load rooms', () async { + final mockRecommendationRepository = MockRecommendationRepository(); + final newRecommendation = Recommendation.empty().copyWith(id: "1"); + when(() => mockRecommendationRepository.getRecommendationList()) + .thenAnswer((_) async => [newRecommendation]); + final recommendationListProvider = RecommendationListNotifier( + recommendationRepository: mockRecommendationRepository, + ); + final recommendations = + await recommendationListProvider.loadRecommendation(); + expect(recommendations, isA>>()); + expect( + recommendations + .maybeWhen( + data: (data) => data, + orElse: () => [], + ) + .length, + 1); + }); + + test('Should add a recommendation', () async { + final mockRecommendationRepository = MockRecommendationRepository(); + final newRecommendation = Recommendation.empty().copyWith(id: "1"); + when(() => mockRecommendationRepository.getRecommendationList()) + .thenAnswer((_) async => [Recommendation.empty()]); + when(() => mockRecommendationRepository.createRecommendation( + newRecommendation)).thenAnswer((_) async => newRecommendation); + final recommendationListProvider = RecommendationListNotifier( + recommendationRepository: mockRecommendationRepository, + ); + await recommendationListProvider.loadRecommendation(); + final recommendation = await recommendationListProvider + .addRecommendation(newRecommendation); + expect(recommendation, true); + }); + + test('Should update a recommendation', () async { + final mockRecommendationRepository = MockRecommendationRepository(); + final newRecommendation = Recommendation.empty().copyWith(id: "1"); + when(() => mockRecommendationRepository.getRecommendationList()) + .thenAnswer( + (_) async => [Recommendation.empty(), newRecommendation]); + when(() => mockRecommendationRepository.updateRecommendation( + newRecommendation)).thenAnswer((_) async => true); + final recommendationListProvider = RecommendationListNotifier( + recommendationRepository: mockRecommendationRepository, + ); + await recommendationListProvider.loadRecommendation(); + final room = await recommendationListProvider + .updateRecommendation(newRecommendation); + expect(room, true); + }); + + test( + 'Should delete a recommendation', + () async { + final mockRecommendationRepository = MockRecommendationRepository(); + final newRecommendation = Recommendation.empty().copyWith(id: "1"); + when(() => mockRecommendationRepository.getRecommendationList()) + .thenAnswer( + (_) async => [Recommendation.empty(), newRecommendation]); + when(() => mockRecommendationRepository.deleteRecommendation( + newRecommendation.id!)).thenAnswer((_) async => true); + final roomListProvider = RecommendationListNotifier( + recommendationRepository: mockRecommendationRepository, + ); + await roomListProvider.loadRecommendation(); + final room = + await roomListProvider.deleteRecommendation(newRecommendation); + expect(room, true); + }, + ); + }, + ); +}