diff --git a/app/lib/app/app.dart b/app/lib/app/app.dart index 37e6c46d..ec03a429 100644 --- a/app/lib/app/app.dart +++ b/app/lib/app/app.dart @@ -3,6 +3,9 @@ import 'package:app/core/infrastructure/message_bus.dart'; import 'package:app/core/infrastructure/tracker.dart'; import 'package:app/features/accueil/presentation/cubit/home_disclaimer_cubit.dart'; import 'package:app/features/actions/detail/infrastructure/action_repository.dart'; +import 'package:app/features/actions/home/infrastructure/home_actions_repository.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_bloc.dart'; +import 'package:app/features/actions/list/domain/actions_port.dart'; import 'package:app/features/actions/list/infrastructure/actions_adapter.dart'; import 'package:app/features/articles/domain/articles_port.dart'; import 'package:app/features/articles/infrastructure/articles_api_adapter.dart'; @@ -147,7 +150,7 @@ class _AppState extends State { create: (final context) => ArticlesApiAdapter(client: widget.dioHttpClient), ), - RepositoryProvider( + RepositoryProvider( create: (final context) => ActionsAdapter(client: widget.dioHttpClient), ), @@ -187,6 +190,13 @@ class _AppState extends State { MissionHomeRepository(client: widget.dioHttpClient), ), ), + BlocProvider( + create: (final context) => HomeActionsBloc( + repository: HomeActionsRepository( + client: widget.dioHttpClient, + ), + ), + ), BlocProvider(create: (final context) => AideBloc()), BlocProvider( create: (final context) => diff --git a/app/lib/features/accueil/presentation/pages/home_page.dart b/app/lib/features/accueil/presentation/pages/home_page.dart index f4f51911..b15696f4 100644 --- a/app/lib/features/accueil/presentation/pages/home_page.dart +++ b/app/lib/features/accueil/presentation/pages/home_page.dart @@ -2,6 +2,7 @@ import 'package:app/core/presentation/widgets/fondamentaux/rounded_rectangle_bor import 'package:app/core/presentation/widgets/fondamentaux/text_styles.dart'; import 'package:app/features/accueil/presentation/cubit/home_disclaimer_cubit.dart'; import 'package:app/features/accueil/presentation/cubit/home_disclaimer_state.dart'; +import 'package:app/features/actions/home/presentation/widgets/actions_section.dart'; import 'package:app/features/assistances/core/presentation/widgets/assitances_section.dart'; import 'package:app/features/environmental_performance/home/presentation/widgets/environmental_performance_section.dart'; import 'package:app/features/environmental_performance/summary/presentation/bloc/environmental_performance_bloc.dart'; @@ -10,6 +11,7 @@ import 'package:app/features/first_name/presentation/pages/first_name_page.dart' import 'package:app/features/menu/presentation/pages/root_page.dart'; import 'package:app/features/mission/home/presentation/widgets/mission_section.dart'; import 'package:app/features/survey/survey_section.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:app/features/theme/presentation/pages/theme_page.dart'; import 'package:app/features/utilisateur/presentation/bloc/utilisateur_bloc.dart'; import 'package:app/features/utilisateur/presentation/bloc/utilisateur_event.dart'; @@ -74,10 +76,10 @@ class _TabPart extends StatelessWidget { physics: NeverScrollableScrollPhysics(), children: [ _Home(), - ThemePage(type: 'alimentation'), - ThemePage(type: 'logement'), - ThemePage(type: 'transport'), - ThemePage(type: 'consommation'), + ThemePage(themeType: ThemeType.alimentation), + ThemePage(themeType: ThemeType.logement), + ThemePage(themeType: ThemeType.transport), + ThemePage(themeType: ThemeType.consommation), ], ), ), @@ -158,6 +160,11 @@ class _HomeState extends State<_Home> { child: AssitancesSection(), ), SizedBox(height: DsfrSpacings.s4w), + Padding( + padding: EdgeInsets.symmetric(horizontal: paddingVerticalPage), + child: ActionsSection(), + ), + SizedBox(height: DsfrSpacings.s4w), SurveySection(), ], ), diff --git a/app/lib/features/actions/home/infrastructure/home_actions_repository.dart b/app/lib/features/actions/home/infrastructure/home_actions_repository.dart new file mode 100644 index 00000000..22bdcbf1 --- /dev/null +++ b/app/lib/features/actions/home/infrastructure/home_actions_repository.dart @@ -0,0 +1,41 @@ +import 'package:app/core/infrastructure/endpoints.dart'; +import 'package:app/core/infrastructure/http_client_helpers.dart'; +import 'package:app/features/actions/list/domain/action_item.dart'; +import 'package:app/features/actions/list/infrastructure/action_item_mapper.dart'; +import 'package:app/features/authentification/core/infrastructure/dio_http_client.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; +import 'package:fpdart/fpdart.dart'; + +class HomeActionsRepository { + const HomeActionsRepository({required final DioHttpClient client}) + : _client = client; + + final DioHttpClient _client; + + Future>> fetch({ + required final ThemeType? themeType, + }) async { + final queryParameters = {'status': 'en_cours'}; + if (themeType != null) { + queryParameters.putIfAbsent('thematique', () => themeType.name); + } + final response = await _client.get( + Uri(path: Endpoints.actions, queryParameters: queryParameters).toString(), + ); + + if (isResponseUnsuccessful(response.statusCode)) { + return Left(Exception('Erreur lors de la récupération des actions')); + } + + final json = response.data! as List; + + return Right( + json + .take(5) + .map( + (final e) => ActionItemMapper.fromJson(e as Map), + ) + .toList(), + ); + } +} diff --git a/app/lib/features/actions/home/presentation/bloc/home_actions_bloc.dart b/app/lib/features/actions/home/presentation/bloc/home_actions_bloc.dart new file mode 100644 index 00000000..cf9fedf6 --- /dev/null +++ b/app/lib/features/actions/home/presentation/bloc/home_actions_bloc.dart @@ -0,0 +1,17 @@ +import 'package:app/features/actions/home/infrastructure/home_actions_repository.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_event.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_state.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class HomeActionsBloc extends Bloc { + HomeActionsBloc({required final HomeActionsRepository repository}) + : super(const HomeActionsInitial()) { + on((final event, final emit) async { + final result = await repository.fetch(themeType: event.themeType); + result.fold( + (final l) => emit(const HomeActionsLoadSuccess(actions: [])), + (final r) => emit(HomeActionsLoadSuccess(actions: r)), + ); + }); + } +} diff --git a/app/lib/features/actions/home/presentation/bloc/home_actions_event.dart b/app/lib/features/actions/home/presentation/bloc/home_actions_event.dart new file mode 100644 index 00000000..62cf9810 --- /dev/null +++ b/app/lib/features/actions/home/presentation/bloc/home_actions_event.dart @@ -0,0 +1,21 @@ +import 'package:app/features/theme/core/domain/theme_type.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +sealed class HomeActionsEvent extends Equatable { + const HomeActionsEvent(); + + @override + List get props => []; +} + +@immutable +final class HomeActionsLoadRequested extends HomeActionsEvent { + const HomeActionsLoadRequested(this.themeType); + + final ThemeType? themeType; + + @override + List get props => [themeType]; +} diff --git a/app/lib/features/actions/home/presentation/bloc/home_actions_state.dart b/app/lib/features/actions/home/presentation/bloc/home_actions_state.dart new file mode 100644 index 00000000..3139b03c --- /dev/null +++ b/app/lib/features/actions/home/presentation/bloc/home_actions_state.dart @@ -0,0 +1,26 @@ +import 'package:app/features/actions/list/domain/action_item.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; + +@immutable +sealed class HomeActionsState extends Equatable { + const HomeActionsState(); + + @override + List get props => []; +} + +@immutable +final class HomeActionsInitial extends HomeActionsState { + const HomeActionsInitial(); +} + +@immutable +final class HomeActionsLoadSuccess extends HomeActionsState { + const HomeActionsLoadSuccess({required this.actions}); + + final List actions; + + @override + List get props => [actions]; +} diff --git a/app/lib/features/actions/home/presentation/widgets/actions_section.dart b/app/lib/features/actions/home/presentation/widgets/actions_section.dart new file mode 100644 index 00000000..1d9165a7 --- /dev/null +++ b/app/lib/features/actions/home/presentation/widgets/actions_section.dart @@ -0,0 +1,137 @@ +import 'package:app/core/presentation/widgets/fondamentaux/shadows.dart'; +import 'package:app/features/accueil/presentation/widgets/title_section.dart'; +import 'package:app/features/actions/detail/presentation/pages/action_detail_page.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_bloc.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_event.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_state.dart'; +import 'package:app/features/actions/list/domain/action_item.dart'; +import 'package:app/features/actions/list/presentation/pages/action_list_page.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; +import 'package:app/features/theme/presentation/widgets/theme_type_tag.dart'; +import 'package:app/l10n/l10n.dart'; +import 'package:dsfr/dsfr.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class ActionsSection extends StatelessWidget { + const ActionsSection({super.key, this.themeType}); + + final ThemeType? themeType; + + @override + Widget build(final BuildContext context) { + context.read().add(HomeActionsLoadRequested(themeType)); + + return BlocBuilder( + builder: (final context, final state) => switch (state) { + HomeActionsInitial() => const SizedBox.shrink(), + HomeActionsLoadSuccess() => _Section(state), + }, + ); + } +} + +class _Section extends StatelessWidget { + const _Section(this.state); + + final HomeActionsLoadSuccess state; + + @override + Widget build(final context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const TitleSection( + title: Localisation.homeActionsTitle, + subTitle: Localisation.homeActionsSubTitle, + ), + const SizedBox(height: DsfrSpacings.s2w), + _Actions(actions: state.actions), + const SizedBox(height: DsfrSpacings.s2w), + Align( + alignment: Alignment.centerLeft, + child: DsfrLink.md( + label: Localisation.homeActionsLink, + onTap: () async => + GoRouter.of(context).pushNamed(ActionListPage.name), + ), + ), + ], + ); +} + +class _Actions extends StatelessWidget { + const _Actions({required this.actions}); + + final List actions; + + @override + Widget build(final BuildContext context) => actions.isEmpty + ? const Text( + Localisation.homeActionsListEmpty, + style: DsfrTextStyle.bodySm(), + ) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + clipBehavior: Clip.none, + child: IntrinsicHeight( + child: Row( + children: actions + .map(_Action.new) + .separator(const SizedBox(width: DsfrSpacings.s2w)) + .toList(), + ), + ), + ); +} + +class _Action extends StatelessWidget { + const _Action(this.item); + + final ActionItem item; + + @override + Widget build(final context) { + const width = 250.0; + + return GestureDetector( + onTap: () async { + final result = await GoRouter.of(context).pushNamed( + ActionDetailPage.name, + pathParameters: {'id': item.id.value}, + ); + + if (result != true || !context.mounted) {} + // if (context.mounted) { + // context.read().add(const MissionHomeFetch()); + // } + }, + child: DecoratedBox( + decoration: const ShapeDecoration( + color: Colors.white, + shadows: recommandationOmbre, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DsfrSpacings.s1w)), + ), + ), + child: SizedBox( + width: width, + child: Padding( + padding: const EdgeInsets.all(DsfrSpacings.s2w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ThemeTypeTag(themeType: item.themeType), + const SizedBox(height: DsfrSpacings.s1w), + Expanded( + child: Text(item.titre, style: const DsfrTextStyle.bodyLg()), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/app/lib/features/actions/list/application/fetch_actions.dart b/app/lib/features/actions/list/application/fetch_actions.dart index ecaa97f8..4412bcca 100644 --- a/app/lib/features/actions/list/application/fetch_actions.dart +++ b/app/lib/features/actions/list/application/fetch_actions.dart @@ -13,9 +13,7 @@ class FetchActions { final result = await _port.fetchActions(); return result.map( - (final actions) => actions - .where((final action) => action.status != ActionStatus.toDo) - .sorted((final a, final b) { + (final actions) => actions.sorted((final a, final b) { final order = [ ActionStatus.inProgress, ActionStatus.done, diff --git a/app/lib/features/actions/list/domain/action_item.dart b/app/lib/features/actions/list/domain/action_item.dart index 736fa57f..36674d84 100644 --- a/app/lib/features/actions/list/domain/action_item.dart +++ b/app/lib/features/actions/list/domain/action_item.dart @@ -1,18 +1,21 @@ import 'package:app/features/actions/core/domain/action_id.dart'; import 'package:app/features/actions/core/domain/action_status.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:equatable/equatable.dart'; final class ActionItem extends Equatable { const ActionItem({ required this.id, + required this.themeType, required this.titre, required this.status, }); final ActionId id; + final ThemeType themeType; final String titre; final ActionStatus status; @override - List get props => [id, titre, status]; + List get props => [id, themeType, titre, status]; } diff --git a/app/lib/features/actions/list/infrastructure/action_item_mapper.dart b/app/lib/features/actions/list/infrastructure/action_item_mapper.dart index f6a012bc..12c4a644 100644 --- a/app/lib/features/actions/list/infrastructure/action_item_mapper.dart +++ b/app/lib/features/actions/list/infrastructure/action_item_mapper.dart @@ -1,12 +1,14 @@ import 'package:app/features/actions/core/domain/action_id.dart'; import 'package:app/features/actions/core/domain/action_status.dart'; import 'package:app/features/actions/list/domain/action_item.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; abstract final class ActionItemMapper { const ActionItemMapper._(); static ActionItem fromJson(final Map json) => ActionItem( id: ActionId(json['id'] as String), + themeType: _mapThemeType(json['thematique'] as String), titre: json['titre'] as String, status: _actionStatusfromJson(json['status'] as String), ); @@ -22,4 +24,12 @@ abstract final class ActionItemMapper { // ignore: no-equal-switch-expression-cases _ => ActionStatus.toDo, }; + + static ThemeType _mapThemeType(final String? type) => switch (type) { + 'alimentation' => ThemeType.alimentation, + 'transport' => ThemeType.transport, + 'consommation' => ThemeType.consommation, + 'logement' => ThemeType.logement, + _ => ThemeType.decouverte, + }; } diff --git a/app/lib/features/actions/list/infrastructure/actions_adapter.dart b/app/lib/features/actions/list/infrastructure/actions_adapter.dart index 3acf4a92..e0ebe7c8 100644 --- a/app/lib/features/actions/list/infrastructure/actions_adapter.dart +++ b/app/lib/features/actions/list/infrastructure/actions_adapter.dart @@ -16,7 +16,14 @@ class ActionsAdapter implements ActionsPort { @override Future>> fetchActions() async { - final response = await _client.get(Endpoints.actions); + final string = Uri( + path: Endpoints.actions, + queryParameters: { + 'status': ['en_cours', 'pas_envie', 'abondon', 'fait'], + }, + ).toString(); + + final response = await _client.get(string); if (isResponseUnsuccessful(response.statusCode)) { return Left(Exception('Erreur lors de la récupération des actions')); diff --git a/app/lib/features/mission/home/presentation/widgets/mission_section.dart b/app/lib/features/mission/home/presentation/widgets/mission_section.dart index 5bd3546d..d6b4401b 100644 --- a/app/lib/features/mission/home/presentation/widgets/mission_section.dart +++ b/app/lib/features/mission/home/presentation/widgets/mission_section.dart @@ -20,14 +20,11 @@ class MissionSection extends StatelessWidget { Widget build(final context) { context.read().add(const MissionHomeFetch()); - return Builder( - builder: (final context) => - BlocBuilder( - builder: (final context, final state) => switch (state) { - MissionHomeInitial() => const SizedBox.shrink(), - MissionHomeLoadSuccess() => _Section(state), - }, - ), + return BlocBuilder( + builder: (final context, final state) => switch (state) { + MissionHomeInitial() => const SizedBox.shrink(), + MissionHomeLoadSuccess() => _Section(state), + }, ); } } diff --git a/app/lib/features/theme/core/domain/theme_port.dart b/app/lib/features/theme/core/domain/theme_port.dart index 3578f116..966e5b50 100644 --- a/app/lib/features/theme/core/domain/theme_port.dart +++ b/app/lib/features/theme/core/domain/theme_port.dart @@ -1,13 +1,14 @@ import 'package:app/features/theme/core/domain/mission_liste.dart'; import 'package:app/features/theme/core/domain/service_item.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:fpdart/fpdart.dart'; abstract interface class ThemePort { Future>> recupererMissions( - final String themeType, + final ThemeType themeType, ); Future>> getServices( - final String themeType, + final ThemeType themeType, ); } diff --git a/app/lib/features/theme/core/infrastructure/theme_api_adapter.dart b/app/lib/features/theme/core/infrastructure/theme_api_adapter.dart index 6c0e9940..7a1decd6 100644 --- a/app/lib/features/theme/core/infrastructure/theme_api_adapter.dart +++ b/app/lib/features/theme/core/infrastructure/theme_api_adapter.dart @@ -4,6 +4,7 @@ import 'package:app/features/authentification/core/infrastructure/dio_http_clien import 'package:app/features/theme/core/domain/mission_liste.dart'; import 'package:app/features/theme/core/domain/service_item.dart'; import 'package:app/features/theme/core/domain/theme_port.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:app/features/theme/core/infrastructure/mission_liste_mapper.dart'; import 'package:app/features/theme/core/infrastructure/service_item_mapper.dart'; import 'package:fpdart/fpdart.dart'; @@ -16,10 +17,10 @@ class ThemeApiAdapter implements ThemePort { @override Future>> recupererMissions( - final String themeType, + final ThemeType themeType, ) async { final response = await _client.get( - Endpoints.missionsRecommandeesParThematique(themeType), + Endpoints.missionsRecommandeesParThematique(themeType.name), ); if (isResponseUnsuccessful(response.statusCode)) { @@ -38,10 +39,10 @@ class ThemeApiAdapter implements ThemePort { @override Future>> getServices( - final String themeType, + final ThemeType themeType, ) async { final response = await _client.get( - Endpoints.servicesParThematique(themeType), + Endpoints.servicesParThematique(themeType.name), ); return isResponseSuccessful(response.statusCode) diff --git a/app/lib/features/theme/presentation/bloc/theme_bloc.dart b/app/lib/features/theme/presentation/bloc/theme_bloc.dart index 0014d493..083e6af4 100644 --- a/app/lib/features/theme/presentation/bloc/theme_bloc.dart +++ b/app/lib/features/theme/presentation/bloc/theme_bloc.dart @@ -14,22 +14,16 @@ class ThemeBloc extends Bloc { ), ) { on((final event, final emit) async { - final type = event.themeType; - final missionsResult = await themePort.recupererMissions(type); - final servicesResult = await themePort.getServices(type); + final themeType = event.themeType; + final missionsResult = await themePort.recupererMissions(themeType); + final servicesResult = await themePort.getServices(themeType); missionsResult.fold( (final l) {}, (final missions) => servicesResult.fold( (final l) {}, (final services) => emit( state.copyWith( - themeType: switch (type) { - 'alimentation' => ThemeType.alimentation, - 'transport' => ThemeType.transport, - 'consommation' => ThemeType.consommation, - 'logement' => ThemeType.logement, - _ => ThemeType.decouverte, - }, + themeType: themeType, missions: missions, services: services, ), diff --git a/app/lib/features/theme/presentation/bloc/theme_event.dart b/app/lib/features/theme/presentation/bloc/theme_event.dart index f3c65ade..51476291 100644 --- a/app/lib/features/theme/presentation/bloc/theme_event.dart +++ b/app/lib/features/theme/presentation/bloc/theme_event.dart @@ -1,3 +1,4 @@ +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; @@ -13,7 +14,7 @@ sealed class ThemeEvent extends Equatable { final class ThemeRecuperationDemandee extends ThemeEvent { const ThemeRecuperationDemandee(this.themeType); - final String themeType; + final ThemeType themeType; @override List get props => [themeType]; diff --git a/app/lib/features/theme/presentation/pages/theme_page.dart b/app/lib/features/theme/presentation/pages/theme_page.dart index d226e7f8..bd6ccc32 100644 --- a/app/lib/features/theme/presentation/pages/theme_page.dart +++ b/app/lib/features/theme/presentation/pages/theme_page.dart @@ -4,6 +4,7 @@ import 'package:app/core/presentation/widgets/composants/badge.dart'; import 'package:app/core/presentation/widgets/composants/image.dart'; import 'package:app/core/presentation/widgets/fondamentaux/colors.dart'; import 'package:app/core/presentation/widgets/fondamentaux/rounded_rectangle_border.dart'; +import 'package:app/features/actions/home/presentation/widgets/actions_section.dart'; import 'package:app/features/mission/mission/presentation/pages/mission_page.dart'; import 'package:app/features/recommandations/presentation/widgets/mes_recommandations.dart'; import 'package:app/features/theme/core/domain/mission_liste.dart'; @@ -22,21 +23,21 @@ import 'package:go_router/go_router.dart'; final themeRouteObserver = RouteObserver>(); class ThemePage extends StatelessWidget { - const ThemePage({super.key, required this.type}); + const ThemePage({super.key, required this.themeType}); - final String type; + final ThemeType themeType; @override Widget build(final context) => BlocProvider( create: (final context) => ThemeBloc(themePort: context.read()), - child: _Page(type), + child: _Page(themeType), ); } class _Page extends StatefulWidget { - const _Page(this.type); + const _Page(this.themeType); - final String type; + final ThemeType themeType; @override State<_Page> createState() => _PageState(); @@ -45,7 +46,9 @@ class _Page extends StatefulWidget { class _PageState extends State<_Page> with RouteAware { void _handleMission() { if (mounted) { - context.read().add(ThemeRecuperationDemandee(widget.type)); + context + .read() + .add(ThemeRecuperationDemandee(widget.themeType)); } } @@ -68,23 +71,27 @@ class _PageState extends State<_Page> with RouteAware { } @override - Widget build(final context) => const _View(); + Widget build(final context) => _View(widget.themeType); } class _View extends StatelessWidget { - const _View(); + const _View(this.themeType); + + final ThemeType themeType; @override Widget build(final context) => ListView( padding: const EdgeInsets.all(paddingVerticalPage), - children: const [ - _ImageEtTitre(), - SizedBox(height: DsfrSpacings.s5w), - _Missions(), - SizedBox(height: DsfrSpacings.s5w), - _Services(), - SizedBox(height: DsfrSpacings.s5w), - SafeArea(child: _Recommandations()), + children: [ + const _ImageEtTitre(), + const SizedBox(height: DsfrSpacings.s5w), + const _Missions(), + const SizedBox(height: DsfrSpacings.s5w), + ActionsSection(themeType: themeType), + const SizedBox(height: DsfrSpacings.s5w), + const _Services(), + const SizedBox(height: DsfrSpacings.s5w), + const SafeArea(child: _Recommandations()), ], ); } diff --git a/app/lib/l10n/l10n.dart b/app/lib/l10n/l10n.dart index ecffa89d..e3ae0a8c 100644 --- a/app/lib/l10n/l10n.dart +++ b/app/lib/l10n/l10n.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid-duplicate-constant-values + import 'package:flutter/widgets.dart'; abstract final class Localisation { @@ -141,9 +143,14 @@ abstract final class Localisation { 'Pour réduire votre impact, voici une liste d’actions recommandés **pour vous !**'; static const missionActionsTitle = 'Choisir au moins **une action** que vous pourriez réaliser'; + static const missionTitle = 'Recommandés **pour vous**'; static const missionSubTitle = 'Des solutions **adaptées à votre situation** et les clés pour comprendre'; - static const missionTitle = 'Recommandés **pour vous**'; + static const homeActionsTitle = 'Mes actions'; + static const homeActionsSubTitle = + 'Gagner des feuilles chaque semaine avec de nouvelles actions'; + static const homeActionsListEmpty = + 'Vous n’avez aucune action à réaliser en ce moment. Débloquez-en de nouvelles dans les missions.'; static const modifier = 'Modifier'; static const moinsDe35m2 = 'Moins de 35 m²'; static const monIdentite = 'Mon identité'; @@ -242,6 +249,7 @@ Si vous ne disposez pas de votre dernier avis d’impôt, renseignez la somme de static const uneMaison = 'Une maison'; static const valider = 'Valider'; static const voirLesDemarches = 'Voir les démarches'; + static const homeActionsLink = 'Voir toutes mes actions'; static const vousAvezDejaUnCompte = 'Vous avez déjà un compte ?'; static const vousEtesProprietaireDeVotreLogement = 'Vous êtes propriétaire de votre logement ?'; diff --git a/app/test/actions/action_list_page_test.dart b/app/test/actions/action_list_page_test.dart index 7ebd6bd3..1979bbfa 100644 --- a/app/test/actions/action_list_page_test.dart +++ b/app/test/actions/action_list_page_test.dart @@ -1,5 +1,3 @@ -import 'package:app/core/infrastructure/endpoints.dart'; -import 'package:app/features/actions/core/domain/action_status.dart'; import 'package:app/features/actions/detail/presentation/pages/action_detail_page.dart'; import 'package:app/features/actions/list/domain/actions_port.dart'; import 'package:app/features/actions/list/infrastructure/action_item_mapper.dart'; @@ -59,8 +57,13 @@ void main() { authenticationService: authenticationService, ), ); - actions = List.generate(4, (final _) => actionItemFaker()); - dio.getM(Endpoints.actions, responseData: actions); + actions = List.generate(4, (final _) => actionItemFaker()) + .where((final e) => e['status'] != 'todo') + .toList(); + dio.getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours&status=pas_envie&status=abondon&status=fait', + responseData: actions, + ); }); group('La liste des actions devrait ', () { @@ -73,12 +76,7 @@ void main() { for (final action in actions) { final expected = ActionItemMapper.fromJson(action); - expect( - find.text(expected.titre), - expected.status == ActionStatus.toDo - ? findsNothing - : findsOneWidget, - ); + expect(find.text(expected.titre), findsOneWidget); } }, ); diff --git a/app/test/environmental_performance/home/environmental_performance_home_page_test.dart b/app/test/environmental_performance/home/environmental_performance_home_page_test.dart index 39a3c5f4..898fc042 100644 --- a/app/test/environmental_performance/home/environmental_performance_home_page_test.dart +++ b/app/test/environmental_performance/home/environmental_performance_home_page_test.dart @@ -1,6 +1,8 @@ import 'package:app/core/infrastructure/endpoints.dart'; import 'package:app/features/accueil/presentation/cubit/home_disclaimer_cubit.dart'; import 'package:app/features/accueil/presentation/pages/home_page.dart'; +import 'package:app/features/actions/home/infrastructure/home_actions_repository.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_bloc.dart'; import 'package:app/features/assistances/core/presentation/bloc/aides_accueil_bloc.dart'; import 'package:app/features/authentification/core/infrastructure/dio_http_client.dart'; import 'package:app/features/environmental_performance/questions/infrastructure/environment_performance_question_repository.dart'; @@ -32,11 +34,17 @@ import '../../old/mocks/recommandations_port_mock.dart'; import '../summary/environmental_performance_data.dart'; Future pumpHomePage(final WidgetTester tester, final DioMock dio) async { - dio.getM(Endpoints.missionsRecommandees, responseData: missionThematiques); + dio + ..getM(Endpoints.missionsRecommandees, responseData: missionThematiques) + ..getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours', + responseData: [], + ); final client = DioHttpClient( dio: dio, authenticationService: authenticationService, ); + await pumpPage( tester: tester, repositoryProviders: [ @@ -61,6 +69,11 @@ Future pumpHomePage(final WidgetTester tester, final DioMock dio) async { BlocProvider( create: (final context) => GamificationBlocFake(), ), + BlocProvider( + create: (final context) => HomeActionsBloc( + repository: HomeActionsRepository(client: client), + ), + ), BlocProvider( create: (final context) => UtilisateurBloc( authentificationPort: AuthentificationPortMock( diff --git a/app/test/features/step/initialize_context.dart b/app/test/features/step/initialize_context.dart index 998dd523..be90af53 100644 --- a/app/test/features/step/initialize_context.dart +++ b/app/test/features/step/initialize_context.dart @@ -25,6 +25,7 @@ Future initializeContext(final WidgetTester tester) async { setMissionRecommanded(); setAssistances(); setPoints(); + setActions(); } void setCommunes() => FeatureContext.instance.dioMock.getM( @@ -138,3 +139,9 @@ void setDeleteAccount() => void setForgotPassword() => FeatureContext.instance.dioMock ..postM(Endpoints.oubliMotDePasse) ..postM(Endpoints.modifierMotDePasse); + +void setActions() => FeatureContext.instance.dioMock + ..getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours', + responseData: [], + ); diff --git a/app/test/helpers/faker.dart b/app/test/helpers/faker.dart index 68cd289d..4aadab02 100644 --- a/app/test/helpers/faker.dart +++ b/app/test/helpers/faker.dart @@ -31,15 +31,10 @@ Map actionItemFaker() { return { 'id': faker.guid.guid(), - 'status': [ - 'todo', - 'en_cours', - 'pas_envie', - 'deja_fait', - 'abondon', - 'fait', - ].elementAt(faker.randomGenerator.integer(5)), + 'thematique': generateThematique, 'titre': _fakerSentenceBetter(), + 'status': faker.randomGenerator + .element(['en_cours', 'pas_envie', 'deja_fait', 'abondon', 'fait']), }; } diff --git a/app/test/mission/mission_test.dart b/app/test/mission/mission_test.dart index af8876e0..768609fd 100644 --- a/app/test/mission/mission_test.dart +++ b/app/test/mission/mission_test.dart @@ -2,6 +2,8 @@ import 'package:app/core/infrastructure/endpoints.dart'; import 'package:app/core/infrastructure/message_bus.dart'; import 'package:app/features/accueil/presentation/cubit/home_disclaimer_cubit.dart'; import 'package:app/features/accueil/presentation/pages/home_page.dart'; +import 'package:app/features/actions/home/infrastructure/home_actions_repository.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_bloc.dart'; import 'package:app/features/assistances/core/presentation/bloc/aides_accueil_bloc.dart'; import 'package:app/features/authentification/core/infrastructure/dio_http_client.dart'; import 'package:app/features/environmental_performance/questions/infrastructure/environment_performance_question_repository.dart'; @@ -43,7 +45,11 @@ Future pumpForMissionPage( Endpoints.questions('ENCHAINEMENT_KYC_mini_bilan_carbone'), responseData: miniBilan, ) - ..getM(Endpoints.missionsRecommandees, responseData: missionThematiques); + ..getM(Endpoints.missionsRecommandees, responseData: missionThematiques) + ..getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours', + responseData: [], + ); final client = DioHttpClient( dio: dio, @@ -111,6 +117,10 @@ Future pumpForMissionPage( create: (final context) => MissionHomeBloc(repository: MissionHomeRepository(client: client)), ), + BlocProvider( + create: (final context) => + HomeActionsBloc(repository: HomeActionsRepository(client: client)), + ), ], router: GoRouter( routes: [ diff --git a/app/test/old/api/theme_api_adapter_test.dart b/app/test/old/api/theme_api_adapter_test.dart index 3eb22935..23b49e1e 100644 --- a/app/test/old/api/theme_api_adapter_test.dart +++ b/app/test/old/api/theme_api_adapter_test.dart @@ -41,7 +41,7 @@ void main() { ); final adapter = initializeAdapter(dio); - final result = await adapter.recupererMissions('alimentation'); + final result = await adapter.recupererMissions(ThemeType.alimentation); expect( result.getRight().getOrElse(() => throw Exception()), diff --git a/app/test/old/mocks/theme_port_mock.dart b/app/test/old/mocks/theme_port_mock.dart index 8f012c62..67a01d33 100644 --- a/app/test/old/mocks/theme_port_mock.dart +++ b/app/test/old/mocks/theme_port_mock.dart @@ -1,6 +1,7 @@ import 'package:app/features/theme/core/domain/mission_liste.dart'; import 'package:app/features/theme/core/domain/service_item.dart'; import 'package:app/features/theme/core/domain/theme_port.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:fpdart/src/either.dart'; class ThemePortMock implements ThemePort { @@ -10,13 +11,13 @@ class ThemePortMock implements ThemePort { @override Future>> recupererMissions( - final String themeType, + final ThemeType themeType, ) async => Right(List.of(missionListe)); @override Future>> getServices( - final String themeType, + final ThemeType themeType, ) async => const Right([ ServiceItem( diff --git a/app/test/old/steps/iel_lance_lapplication.dart b/app/test/old/steps/iel_lance_lapplication.dart index 37a8904f..946ff511 100644 --- a/app/test/old/steps/iel_lance_lapplication.dart +++ b/app/test/old/steps/iel_lance_lapplication.dart @@ -88,6 +88,14 @@ Future ielLanceLapplication(final WidgetTester tester) async { Endpoints.bilan, responseData: environmentalPerformancePartialData, ) + ..getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours', + responseData: [], + ) + ..getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours&thematique=alimentation', + responseData: [], + ) ..getM(Endpoints.missionsRecommandees, responseData: missionThematiques); await mockNetworkImages(() async { diff --git a/app/test/services/services_adapter_test.dart b/app/test/services/services_adapter_test.dart index 91f8af9f..94882110 100644 --- a/app/test/services/services_adapter_test.dart +++ b/app/test/services/services_adapter_test.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:app/core/infrastructure/endpoints.dart'; import 'package:app/features/authentification/core/infrastructure/dio_http_client.dart'; import 'package:app/features/theme/core/domain/service_item.dart'; +import 'package:app/features/theme/core/domain/theme_type.dart'; import 'package:app/features/theme/core/infrastructure/theme_api_adapter.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fpdart/fpdart.dart'; @@ -12,10 +13,10 @@ import '../old/mocks/authentication_service_fake.dart'; void main() { test('getServices', () async { - const themeType = 'alimentation'; + const themeType = ThemeType.alimentation; final dio = DioMock() ..getM( - Endpoints.servicesParThematique(themeType), + Endpoints.servicesParThematique(themeType.name), responseData: jsonDecode( ''' [ diff --git a/app/test/services/services_section_test.dart b/app/test/services/services_section_test.dart index 306622ef..3c2d7d75 100644 --- a/app/test/services/services_section_test.dart +++ b/app/test/services/services_section_test.dart @@ -1,3 +1,6 @@ +import 'package:app/features/actions/home/infrastructure/home_actions_repository.dart'; +import 'package:app/features/actions/home/presentation/bloc/home_actions_bloc.dart'; +import 'package:app/features/authentification/core/infrastructure/dio_http_client.dart'; import 'package:app/features/gamification/domain/gamification_port.dart'; import 'package:app/features/gamification/presentation/bloc/gamification_bloc.dart'; import 'package:app/features/recommandations/domain/recommandation.dart'; @@ -15,6 +18,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:mocktail_image_network/mocktail_image_network.dart'; import '../helpers/authentication_service_setup.dart'; +import '../helpers/dio_mock.dart'; import '../helpers/faker.dart'; import '../helpers/pump_page.dart'; @@ -69,18 +73,38 @@ Future _pumpThemePage( recommandationsPort: recommandationsPort, ), ), + BlocProvider( + create: (final context) { + final dioMock = DioMock() + ..getM( + '/utilisateurs/%7BuserId%7D/defis_v2?status=en_cours&thematique=alimentation', + responseData: [], + ); + + return HomeActionsBloc( + repository: HomeActionsRepository( + client: DioHttpClient( + dio: dioMock, + authenticationService: authenticationService, + ), + ), + ); + }, + ), ], page: GoRoute( path: 'path', name: ' name', builder: (final context, final state) => const ThemePage( - type: 'alimentation', + themeType: ThemeType.alimentation, ), ), ); } void main() { + registerFallbackValue(ThemeType.alimentation); + group('Services devrait ', () { testWidgets( 'afficher la liste des services de la thématique', diff --git a/packages/dsfr.dart/lib/src/composants/links.dart b/packages/dsfr.dart/lib/src/composants/links.dart index eaf80e01..ea95cae0 100644 --- a/packages/dsfr.dart/lib/src/composants/links.dart +++ b/packages/dsfr.dart/lib/src/composants/links.dart @@ -118,34 +118,37 @@ class _DsfrLinkState extends State with MaterialStateMixin { final link = Semantics( enabled: widget.onTap != null, link: true, - child: InkWell( - onTap: widget.onTap, - onHighlightChanged: updateMaterialState(WidgetState.pressed), - onHover: updateMaterialState(WidgetState.hovered), - highlightColor: const Color(0x21000000), - splashFactory: NoSplash.splashFactory, - canRequestFocus: widget.onTap != null, - onFocusChange: updateMaterialState(WidgetState.focused), - child: DecoratedBox( - decoration: BoxDecoration( - border: !isFocused && !isDisabled - ? Border( - bottom: BorderSide( - color: resolveForegroundColor, - width: isPressed || isHovered - ? widget.underlineThickness - : 1, - ), - ) - : null, - ), - child: Text.rich( - TextSpan( - children: widget.iconPosition == DsfrLinkIconPosition.start - ? list - : list.reversed.toList(), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onTap, + onHighlightChanged: updateMaterialState(WidgetState.pressed), + onHover: updateMaterialState(WidgetState.hovered), + highlightColor: const Color(0x21000000), + splashFactory: NoSplash.splashFactory, + canRequestFocus: widget.onTap != null, + onFocusChange: updateMaterialState(WidgetState.focused), + child: DecoratedBox( + decoration: BoxDecoration( + border: !isFocused && !isDisabled + ? Border( + bottom: BorderSide( + color: resolveForegroundColor, + width: isPressed || isHovered + ? widget.underlineThickness + : 1, + ), + ) + : null, + ), + child: Text.rich( + TextSpan( + children: widget.iconPosition == DsfrLinkIconPosition.start + ? list + : list.reversed.toList(), + ), + style: widget.textStyle.copyWith(color: resolveForegroundColor), ), - style: widget.textStyle.copyWith(color: resolveForegroundColor), ), ), ),