From 6f24fa33d1c1648c0b28bef78eef8fad97e0196c Mon Sep 17 00:00:00 2001 From: MarkG Date: Sun, 16 May 2021 09:02:01 +0700 Subject: [PATCH] feat: integrate survey questions screen (#64) --- lib/components/common/progress_hud.dart | 15 ++- lib/configs/factories.dart | 2 +- lib/configs/routes.dart | 2 + lib/gen/configs.gen.dart | 4 +- ....dart => survey_question_answer_info.dart} | 7 +- lib/models/survey_question_info.dart | 6 +- lib/models/survey_submit_answer_info.dart | 14 +++ lib/models/survey_submit_question_info.dart | 18 ++++ .../survey_completed_interactor.dart | 8 ++ .../survey_completed_module.dart | 31 +++++++ .../survey_completed_presenter.dart | 7 ++ .../survey_completed_router.dart | 5 + .../survey_completed_view.dart | 23 +++++ .../survey_detail/survey_detail_module.dart | 1 + .../survey_detail/survey_detail_router.dart | 7 +- .../components/answers/nps_answer.dart | 6 +- .../components/answers/select_answer.dart | 4 +- .../survey_questions/components/content.dart | 61 ++++++++----- .../survey_questions/components/slide.dart | 91 +++++++++++++------ .../survey_questions_interactor.dart | 25 ++++- .../survey_questions_module.dart | 10 +- .../survey_questions_presenter.dart | 55 +++++++++++ .../survey_questions_router.dart | 22 ++++- .../survey_questions_view.dart | 23 +++-- lib/repositories/survey_repository.dart | 17 ++++ lib/services/api/api_exception.dart | 4 +- lib/services/api/api_service.dart | 22 +++-- .../survey/params/survey_submit_params.dart | 19 ++++ .../api/survey/survey_api_service.dart | 29 ++++-- 29 files changed, 442 insertions(+), 96 deletions(-) rename lib/models/{survey_answer_info.dart => survey_question_answer_info.dart} (66%) create mode 100644 lib/models/survey_submit_answer_info.dart create mode 100644 lib/models/survey_submit_question_info.dart create mode 100644 lib/modules/survey_completed/survey_completed_interactor.dart create mode 100644 lib/modules/survey_completed/survey_completed_module.dart create mode 100644 lib/modules/survey_completed/survey_completed_presenter.dart create mode 100644 lib/modules/survey_completed/survey_completed_router.dart create mode 100644 lib/modules/survey_completed/survey_completed_view.dart create mode 100644 lib/services/api/survey/params/survey_submit_params.dart diff --git a/lib/components/common/progress_hud.dart b/lib/components/common/progress_hud.dart index 4716a19..9ef1cd1 100644 --- a/lib/components/common/progress_hud.dart +++ b/lib/components/common/progress_hud.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; -import 'package:modal_progress_hud_nsn/modal_progress_hud_nsn.dart'; import 'package:streams_provider/streams_provider.dart'; import 'package:survey/core/viper/module.dart'; @@ -37,10 +36,16 @@ class ProgressHUD extends StatelessWidget { ); } - return ModalProgressHUD( - inAsyncCall: isShow, - progressIndicator: progressIndicator, - child: child, + return Stack( + children: [ + child, + if (isShow) + const Opacity( + opacity: 0.3, + child: ModalBarrier(dismissible: false, color: Colors.grey), + ), + if (isShow) Center(child: progressIndicator), + ], ); } } diff --git a/lib/configs/factories.dart b/lib/configs/factories.dart index dc1cf9c..72c7e68 100644 --- a/lib/configs/factories.dart +++ b/lib/configs/factories.dart @@ -10,5 +10,5 @@ final Map _factories = { SurveyQuestionInfo: () => SurveyQuestionInfo(), SurveyQuestionDisplayType: (v) => SurveyQuestionDisplayType(v as String), SurveyQuestionPickType: (v) => SurveyQuestionPickType(v as String), - SurveyAnswerInfo: () => SurveyAnswerInfo(), + SurveyQuestionAnswerInfo: () => SurveyQuestionAnswerInfo(), }; diff --git a/lib/configs/routes.dart b/lib/configs/routes.dart index 7396c0d..068aabd 100644 --- a/lib/configs/routes.dart +++ b/lib/configs/routes.dart @@ -7,4 +7,6 @@ final Map _routes = { ForgotPasswordModule.routePath: (_) => ForgotPasswordModule(), SideMenuModule.routePath: (_) => SideMenuModule(), SurveyDetailModule.routePath: (_) => SurveyDetailModule(), + SurveyQuestionsModule.routePath: (_) => SurveyQuestionsModule(), + SurveyCompletedModule.routePath: (_) => SurveyCompletedModule(), }; diff --git a/lib/gen/configs.gen.dart b/lib/gen/configs.gen.dart index e3f4ce2..f535fe1 100644 --- a/lib/gen/configs.gen.dart +++ b/lib/gen/configs.gen.dart @@ -2,7 +2,7 @@ import 'package:survey/gen/flavors.gen.dart'; import 'package:flutter/widgets.dart'; import 'package:survey/models/auth_token_info.dart'; import 'package:survey/models/detailed_survey_info.dart'; -import 'package:survey/models/survey_answer_info.dart'; +import 'package:survey/models/survey_question_answer_info.dart'; import 'package:survey/models/survey_info.dart'; import 'package:survey/models/survey_question_info.dart'; import 'package:survey/models/user_info.dart'; @@ -11,7 +11,9 @@ import 'package:survey/modules/home/home_module.dart'; import 'package:survey/modules/landing/landing_module.dart'; import 'package:survey/modules/login/login_module.dart'; import 'package:survey/modules/side_menu/side_menu_module.dart'; +import 'package:survey/modules/survey_completed/survey_completed_module.dart'; import 'package:survey/modules/survey_detail/survey_detail_module.dart'; +import 'package:survey/modules/survey_questions/survey_questions_module.dart'; import 'package:survey/services/api/api_service.dart'; part 'package:survey/configs/app.dart'; diff --git a/lib/models/survey_answer_info.dart b/lib/models/survey_question_answer_info.dart similarity index 66% rename from lib/models/survey_answer_info.dart rename to lib/models/survey_question_answer_info.dart index 9f27ec2..0b05036 100644 --- a/lib/models/survey_answer_info.dart +++ b/lib/models/survey_question_answer_info.dart @@ -1,6 +1,7 @@ import 'package:object_mapper/object_mapper.dart'; +import 'package:survey/models/survey_submit_answer_info.dart'; -class SurveyAnswerInfo with Mappable { +class SurveyQuestionAnswerInfo with Mappable { String? id; String? content; int? displayOrder; @@ -23,4 +24,8 @@ class SurveyAnswerInfo with Mappable { (v) => displayOrder = v as int, ); } + + SurveySubmitAnswerInfo toAnswer([String? answer]) { + return SurveySubmitAnswerInfo(id: id!, answer: answer); + } } diff --git a/lib/models/survey_question_info.dart b/lib/models/survey_question_info.dart index 378d9d1..2ec3d36 100644 --- a/lib/models/survey_question_info.dart +++ b/lib/models/survey_question_info.dart @@ -1,5 +1,5 @@ import 'package:object_mapper/object_mapper.dart'; -import 'package:survey/models/survey_answer_info.dart'; +import 'package:survey/models/survey_question_answer_info.dart'; class SurveyQuestionInfo with Mappable { String? id; @@ -10,8 +10,8 @@ class SurveyQuestionInfo with Mappable { bool? isMandatory; String? coverImageUrl; double? coverImageOpacity; - List answers = []; - List get orderedAnswers => answers.toList() + List answers = []; + List get orderedAnswers => answers.toList() ..sort((a, b) => a.displayOrder!.compareTo(b.displayOrder!)); @override diff --git a/lib/models/survey_submit_answer_info.dart b/lib/models/survey_submit_answer_info.dart new file mode 100644 index 0000000..70480a2 --- /dev/null +++ b/lib/models/survey_submit_answer_info.dart @@ -0,0 +1,14 @@ +import 'package:object_mapper/object_mapper.dart'; + +class SurveySubmitAnswerInfo with Mappable { + SurveySubmitAnswerInfo({required this.id, this.answer}); + + String id; + String? answer; + + @override + void mapping(Mapper map) { + map("id", id, (v) => id = v as String); + map("answer", answer, (v) => answer = v as String?); + } +} diff --git a/lib/models/survey_submit_question_info.dart b/lib/models/survey_submit_question_info.dart new file mode 100644 index 0000000..2a4eca0 --- /dev/null +++ b/lib/models/survey_submit_question_info.dart @@ -0,0 +1,18 @@ +import 'package:object_mapper/object_mapper.dart'; +import 'package:survey/models/survey_submit_answer_info.dart'; + +class SurveySubmitQuestionInfo with Mappable { + const SurveySubmitQuestionInfo({ + required this.questionId, + required this.answers, + }); + + final String questionId; + final List answers; + + @override + void mapping(Mapper map) { + map("id", questionId, (v) {}); + map("answers", answers, (v) {}); + } +} diff --git a/lib/modules/survey_completed/survey_completed_interactor.dart b/lib/modules/survey_completed/survey_completed_interactor.dart new file mode 100644 index 0000000..36b0f8f --- /dev/null +++ b/lib/modules/survey_completed/survey_completed_interactor.dart @@ -0,0 +1,8 @@ +part of 'survey_completed_module.dart'; + +abstract class SurveyCompletedInteractorDelegate {} + +abstract class SurveyCompletedInteractor extends ArgumentsInteractor< + SurveyCompletedInteractorDelegate, SurveyCompletedArguments> {} + +class SurveyCompletedInteractorImpl extends SurveyCompletedInteractor {} diff --git a/lib/modules/survey_completed/survey_completed_module.dart b/lib/modules/survey_completed/survey_completed_module.dart new file mode 100644 index 0000000..0d0dff5 --- /dev/null +++ b/lib/modules/survey_completed/survey_completed_module.dart @@ -0,0 +1,31 @@ +import 'package:flutter/widgets.dart' hide Router; +import 'package:survey/core/viper/module.dart'; +import 'package:survey/models/survey_question_info.dart'; + +part 'survey_completed_view.dart'; + +part 'survey_completed_interactor.dart'; + +part 'survey_completed_presenter.dart'; + +part 'survey_completed_router.dart'; + +class SurveyCompletedModule extends ArgumentsModule< + SurveyCompletedView, + SurveyCompletedInteractor, + SurveyCompletedPresenter, + SurveyCompletedRouter, + SurveyCompletedArguments> { + static const routePath = "survey/completed"; + + @override + Widget build(BuildContext context) { + return const SurveyCompletedViewImpl(); + } +} + +class SurveyCompletedArguments extends ModuleArguments { + SurveyCompletedArguments({required this.outro}); + + final SurveyQuestionInfo outro; +} diff --git a/lib/modules/survey_completed/survey_completed_presenter.dart b/lib/modules/survey_completed/survey_completed_presenter.dart new file mode 100644 index 0000000..bd9270a --- /dev/null +++ b/lib/modules/survey_completed/survey_completed_presenter.dart @@ -0,0 +1,7 @@ +part of 'survey_completed_module.dart'; + +abstract class SurveyCompletedPresenter extends Presenter {} + +class SurveyCompletedPresenterImpl extends SurveyCompletedPresenter + implements SurveyCompletedViewDelegate, SurveyCompletedInteractorDelegate {} diff --git a/lib/modules/survey_completed/survey_completed_router.dart b/lib/modules/survey_completed/survey_completed_router.dart new file mode 100644 index 0000000..a958b34 --- /dev/null +++ b/lib/modules/survey_completed/survey_completed_router.dart @@ -0,0 +1,5 @@ +part of 'survey_completed_module.dart'; + +abstract class SurveyCompletedRouter extends Router {} + +class SurveyCompletedRouterImpl extends SurveyCompletedRouter {} diff --git a/lib/modules/survey_completed/survey_completed_view.dart b/lib/modules/survey_completed/survey_completed_view.dart new file mode 100644 index 0000000..d2f4686 --- /dev/null +++ b/lib/modules/survey_completed/survey_completed_view.dart @@ -0,0 +1,23 @@ +part of 'survey_completed_module.dart'; + +abstract class SurveyCompletedViewDelegate {} + +abstract class SurveyCompletedView extends View {} + +class SurveyCompletedViewImpl extends StatefulWidget { + const SurveyCompletedViewImpl({Key? key}) : super(key: key); + + @override + _SurveyCompletedViewImplState createState() => + _SurveyCompletedViewImplState(); +} + +class _SurveyCompletedViewImplState extends ViewState< + SurveyCompletedViewImpl, + SurveyCompletedModule, + SurveyCompletedViewDelegate> implements SurveyCompletedView { + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/lib/modules/survey_detail/survey_detail_module.dart b/lib/modules/survey_detail/survey_detail_module.dart index 201efe2..c20ada7 100644 --- a/lib/modules/survey_detail/survey_detail_module.dart +++ b/lib/modules/survey_detail/survey_detail_module.dart @@ -10,6 +10,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:survey/models/detailed_survey_info.dart'; import 'package:survey/models/survey_info.dart'; import 'package:survey/modules/screen.dart'; +import 'package:survey/modules/survey_questions/survey_questions_module.dart'; import 'package:survey/repositories/survey_repository.dart'; import 'package:survey/services/locator/locator_service.dart'; diff --git a/lib/modules/survey_detail/survey_detail_router.dart b/lib/modules/survey_detail/survey_detail_router.dart index 67b8b08..d183b98 100644 --- a/lib/modules/survey_detail/survey_detail_router.dart +++ b/lib/modules/survey_detail/survey_detail_router.dart @@ -8,5 +8,10 @@ abstract class SurveyDetailRouter extends Router { class SurveyDetailRouterImpl extends SurveyDetailRouter { @override void pushToSurveyQuestionsScreen(BuildContext context, - {required DetailedSurveyInfo survey}) {} + {required DetailedSurveyInfo survey}) { + context.navigator.pushReplacementNamed( + SurveyQuestionsModule.routePath, + arguments: SurveyQuestionsArguments(survey: survey), + ); + } } diff --git a/lib/modules/survey_questions/components/answers/nps_answer.dart b/lib/modules/survey_questions/components/answers/nps_answer.dart index 4792d69..b8e4caf 100644 --- a/lib/modules/survey_questions/components/answers/nps_answer.dart +++ b/lib/modules/survey_questions/components/answers/nps_answer.dart @@ -9,8 +9,8 @@ class NPSAnswer extends StatefulWidget { }) : super(key: key); final int? score; - final List items; - final ValueChanged? onSelect; + final List items; + final ValueChanged? onSelect; @override _NPSAnswerState createState() => _NPSAnswerState(); @@ -86,7 +86,7 @@ class _NPSAnswerState extends State { return GestureDetector( onTap: () { selected.add(i); - if (widget.onSelect != null) widget.onSelect!(i + 1); + if (widget.onSelect != null) widget.onSelect!(widget.items[i]); }, child: SizedBox( width: 35, diff --git a/lib/modules/survey_questions/components/answers/select_answer.dart b/lib/modules/survey_questions/components/answers/select_answer.dart index ea14e14..fed8af4 100644 --- a/lib/modules/survey_questions/components/answers/select_answer.dart +++ b/lib/modules/survey_questions/components/answers/select_answer.dart @@ -13,8 +13,8 @@ class SelectAnswer extends StatefulWidget { final bool isMultiSelection; final List selectedIndexes; - final List options; - final ValueChanged>? onSelect; + final List options; + final ValueChanged>? onSelect; @override _SelectAnswerState createState() => _SelectAnswerState(); diff --git a/lib/modules/survey_questions/components/content.dart b/lib/modules/survey_questions/components/content.dart index 76d54e8..6f1caac 100644 --- a/lib/modules/survey_questions/components/content.dart +++ b/lib/modules/survey_questions/components/content.dart @@ -8,34 +8,45 @@ class Content extends StatelessWidget { final state = context.findRootAncestorStateOfType<_SurveyQuestionsViewImplState>()!; - return Screen( - body: Stack( - children: [ - StreamsSelector0>.value( - stream: state._questions, - builder: (_, questions, __) => CarouselSlider.builder( - itemCount: questions.length, - itemBuilder: (_, i, __) => Slide(questions: questions, index: i), - options: CarouselOptions( - height: double.infinity, - viewportFraction: 1, - enableInfiniteScroll: false, + return StreamsSelector0.value( + stream: state.isProgressHUDShown, + builder: (_, isShown, child) => ProgressHUD( + isShow: isShown, + child: child!, + ), + child: Screen( + body: Stack( + children: [ + StreamsSelector0>.value( + stream: state._questions, + builder: (_, questions, __) => CarouselSlider.builder( + itemCount: questions.length, + itemBuilder: (_, i, __) => + Slide(questions: questions, index: i), + carouselController: state._carouselController, + options: CarouselOptions( + height: double.infinity, + scrollPhysics: const NeverScrollableScrollPhysics(), + viewportFraction: 1, + enableInfiniteScroll: false, + ), ), ), - ), - SafeArea( - child: NavigationBar( - key: state._navigationBarKey, - isBackButtonHidden: true, - leftChildren: [ - PlatformButton( - onPressed: () => state.delegate?.closeButtonDidTap.add(null), - child: Assets.images.navCloseButton.svg(), - ) - ], + SafeArea( + child: NavigationBar( + key: state._navigationBarKey, + isBackButtonHidden: true, + leftChildren: [ + PlatformButton( + onPressed: () => + state.delegate?.closeButtonDidTap.add(null), + child: Assets.images.navCloseButton.svg(), + ) + ], + ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/modules/survey_questions/components/slide.dart b/lib/modules/survey_questions/components/slide.dart index 428661d..a6dcefe 100644 --- a/lib/modules/survey_questions/components/slide.dart +++ b/lib/modules/survey_questions/components/slide.dart @@ -1,7 +1,7 @@ part of '../survey_questions_module.dart'; class Slide extends StatelessWidget { - const Slide({ + Slide({ Key? key, required this.questions, required this.index, @@ -11,6 +11,7 @@ class Slide extends StatelessWidget { final int index; bool get _isLast => index == questions.length - 1; SurveyQuestionInfo get question => questions[index]; + final _answers = BehaviorSubject>.seeded([]); @override Widget build(BuildContext context) { @@ -57,28 +58,36 @@ class Slide extends StatelessWidget { child: _makeAnswer(), ), ), - Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - if (_isLast) - Button( - title: AppLocalizations.of(context)! - .surveyQuestionsScreenSubmitButtonTitle, - ) - else - PlatformButton( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(28), - color: question.isMandatory ?? false - ? Colors.grey - : Colors.white, + StreamsSelector0.value( + stream: _answers.map((event) => + event.isNotEmpty || !(question.isMandatory ?? true)), + builder: (_, isEnabled, __) => + Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + if (_isLast) + Button( + onPressed: () => state.delegate?.submitButtonDidTap + .add(_answers.value), + title: AppLocalizations.of(context)! + .surveyQuestionsScreenSubmitButtonTitle, + isEnabled: isEnabled, + ) + else + PlatformButton( + onPressed: () => state.delegate?.submitButtonDidTap + .add(_answers.value), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + color: isEnabled ? Colors.white : Colors.grey, + ), + width: 56, + height: 56, + child: Assets.images.arrowRightIcon + .svg(fit: BoxFit.none), ), - width: 56, - height: 56, - child: - Assets.images.arrowRightIcon.svg(fit: BoxFit.none), ), - ), - ]), + ]), + ), ], ), ), @@ -93,25 +102,55 @@ class Slide extends StatelessWidget { return SelectAnswer( options: question.orderedAnswers, isMultiSelection: question.pickType == SurveyQuestionPickType.any, + onSelect: _onSelectSelect, ); case SurveyQuestionDisplayType.heart: - return const RatingAnswer(symbol: "❤️"); + return RatingAnswer( + symbol: "❤️", + onSelect: _onRatingSelect, + ); case SurveyQuestionDisplayType.star: - return const RatingAnswer(symbol: "⭐️"); + return RatingAnswer( + symbol: "⭐️", + onSelect: _onRatingSelect, + ); case SurveyQuestionDisplayType.smiley: - return const RatingAnswer(symbol: "😃"); + return RatingAnswer( + symbol: "😃", + onSelect: _onRatingSelect, + ); case SurveyQuestionDisplayType.nps: return NPSAnswer( items: question.orderedAnswers.sublist(1), + onSelect: _onNPSSelect, ); case SurveyQuestionDisplayType.textarea: - return const TextFieldAnswer( + return TextFieldAnswer( isMultiLines: true, + onTextChange: _onTextChange, ); case SurveyQuestionDisplayType.textField: - return const TextFieldAnswer(); + return TextFieldAnswer( + onTextChange: _onTextChange, + ); default: return const SizedBox.shrink(); } } + + void _onSelectSelect(List questionAnswer) { + _answers.add(questionAnswer.map((e) => e.toAnswer()).toList()); + } + + void _onRatingSelect(int? i) { + _answers.add([if (i != null) question.orderedAnswers[i - 1].toAnswer()]); + } + + void _onNPSSelect(SurveyQuestionAnswerInfo? questionAnswer) { + _answers.add([if (questionAnswer != null) questionAnswer.toAnswer()]); + } + + void _onTextChange(String text) { + _answers.add([question.orderedAnswers.first.toAnswer(text)]); + } } diff --git a/lib/modules/survey_questions/survey_questions_interactor.dart b/lib/modules/survey_questions/survey_questions_interactor.dart index 60f4fb1..0e38a5a 100644 --- a/lib/modules/survey_questions/survey_questions_interactor.dart +++ b/lib/modules/survey_questions/survey_questions_interactor.dart @@ -1,10 +1,20 @@ part of 'survey_questions_module.dart'; -abstract class SurveyQuestionsInteractorDelegate {} +abstract class SurveyQuestionsInteractorDelegate { + BehaviorSubject get submitDidSuccess; + + BehaviorSubject get submitDidFail; +} abstract class SurveyQuestionsInteractor extends ArgumentsInteractor< SurveyQuestionsInteractorDelegate, SurveyQuestionsArguments> { + final SurveyRepository _surveyRepository = locator.get(); + List get questions; + + SurveyQuestionInfo get outro; + + void submit(List questions); } class SurveyQuestionsInteractorImpl extends SurveyQuestionsInteractor { @@ -14,4 +24,17 @@ class SurveyQuestionsInteractorImpl extends SurveyQuestionsInteractor { element.displayType != SurveyQuestionDisplayType.intro && element.displayType != SurveyQuestionDisplayType.outro) .toList(); + + @override + SurveyQuestionInfo get outro => arguments!.survey.questions.firstWhere( + (element) => element.displayType == SurveyQuestionDisplayType.outro); + + @override + void submit(List questions) { + _surveyRepository + .submit(surveyId: arguments!.survey.id!, questions: questions) + .then((value) => delegate?.submitDidSuccess.add(null)) + .onError((Exception exception, stackTrace) => + delegate?.submitDidFail.add(exception)); + } } diff --git a/lib/modules/survey_questions/survey_questions_module.dart b/lib/modules/survey_questions/survey_questions_module.dart index 4ef7744..f0c6d9c 100644 --- a/lib/modules/survey_questions/survey_questions_module.dart +++ b/lib/modules/survey_questions/survey_questions_module.dart @@ -6,15 +6,21 @@ import 'package:scroll_snap_list/scroll_snap_list.dart'; import 'package:streams_provider/streams_provider.dart'; import 'package:survey/components/alert/alert.dart'; import 'package:survey/components/button/button.dart'; +import 'package:survey/components/common/progress_hud.dart'; import 'package:survey/components/confirm/confirm.dart'; import 'package:survey/components/navigation_bar/navigation_bar.dart'; import 'package:survey/core/viper/module.dart'; import 'package:survey/gen/assets.gen.dart'; import 'package:survey/models/detailed_survey_info.dart'; -import 'package:survey/models/survey_answer_info.dart'; +import 'package:survey/models/survey_submit_answer_info.dart'; +import 'package:survey/models/survey_question_answer_info.dart'; import 'package:survey/models/survey_question_info.dart'; +import 'package:survey/models/survey_submit_question_info.dart'; import 'package:survey/modules/screen.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:survey/modules/survey_completed/survey_completed_module.dart'; +import 'package:survey/repositories/survey_repository.dart'; +import 'package:survey/services/locator/locator_service.dart'; part 'survey_questions_view.dart'; @@ -42,7 +48,7 @@ class SurveyQuestionsModule extends ArgumentsModule< SurveyQuestionsPresenter, SurveyQuestionsRouter, SurveyQuestionsArguments> { - static const routePath = ""; + static const routePath = "survey/questions"; @override Widget build(BuildContext context) { diff --git a/lib/modules/survey_questions/survey_questions_presenter.dart b/lib/modules/survey_questions/survey_questions_presenter.dart index c59c340..d1cff9a 100644 --- a/lib/modules/survey_questions/survey_questions_presenter.dart +++ b/lib/modules/survey_questions/survey_questions_presenter.dart @@ -8,8 +8,20 @@ class SurveyQuestionsPresenterImpl extends SurveyQuestionsPresenter SurveyQuestionsPresenterImpl() { stateDidInit.voidListen(_stateDidInit).addTo(disposeBag); closeButtonDidTap.voidListen(_closeButtonDidTap).addTo(disposeBag); + submitButtonDidTap.listen(_submitButtonDidTap).addTo(disposeBag); + closeConfirmDialogDidClose + .listen(_closeConfirmDialogDidClose) + .addTo(disposeBag); + + submitDidFail.listen(_submitDidFail).addTo(disposeBag); + submitDidSuccess.voidListen(_submitDidSuccess).addTo(disposeBag); } + int _currentPage = 0; + SurveyQuestionInfo get _currentQuestion => interactor.questions[_currentPage]; + + final _submitQuestions = List.empty(growable: true); + @override final stateDidInit = BehaviorSubject(); @@ -19,6 +31,18 @@ class SurveyQuestionsPresenterImpl extends SurveyQuestionsPresenter @override final closeButtonDidTap = BehaviorSubject(); + @override + final submitButtonDidTap = BehaviorSubject>(); + + @override + final alertDialogDidClose = BehaviorSubject(); + + @override + final submitDidFail = BehaviorSubject(); + + @override + final submitDidSuccess = BehaviorSubject(); + void _stateDidInit() { view.setQuestions(interactor.questions); } @@ -26,4 +50,35 @@ class SurveyQuestionsPresenterImpl extends SurveyQuestionsPresenter void _closeButtonDidTap() { view.showCloseConfirmDialog(); } + + void _submitButtonDidTap(List answers) { + _submitQuestions.add( + SurveySubmitQuestionInfo( + questionId: _currentQuestion.id!, answers: answers), + ); + + // Last question + if (_currentPage >= interactor.questions.length - 1) { + view.showProgressHUD(); + interactor.submit(_submitQuestions); + return; + } + _currentPage++; + view.moveTo(_currentPage); + } + + void _closeConfirmDialogDidClose(OkCancelResult result) { + if (result != OkCancelResult.ok) return; + router.popBack(view.context); + } + + void _submitDidSuccess() { + view.dismissProgressHUD(); + router.pushToSurveyCompletedScreen(view.context, outro: interactor.outro); + } + + void _submitDidFail(Exception exception) { + view.dismissProgressHUD(); + view.alert(exception); + } } diff --git a/lib/modules/survey_questions/survey_questions_router.dart b/lib/modules/survey_questions/survey_questions_router.dart index 73903aa..1eb9ae8 100644 --- a/lib/modules/survey_questions/survey_questions_router.dart +++ b/lib/modules/survey_questions/survey_questions_router.dart @@ -1,5 +1,23 @@ part of 'survey_questions_module.dart'; -abstract class SurveyQuestionsRouter extends Router {} +abstract class SurveyQuestionsRouter extends Router { + void pushToSurveyCompletedScreen(BuildContext context, + {required SurveyQuestionInfo outro}); + void popBack(BuildContext context); +} -class SurveyQuestionsRouterImpl extends SurveyQuestionsRouter {} +class SurveyQuestionsRouterImpl extends SurveyQuestionsRouter { + @override + void pushToSurveyCompletedScreen(BuildContext context, + {required SurveyQuestionInfo outro}) { + context.navigator.pushReplacementNamed( + SurveyCompletedModule.routePath, + arguments: SurveyCompletedArguments(outro: outro), + ); + } + + @override + void popBack(BuildContext context) { + context.navigator.pop(); + } +} diff --git a/lib/modules/survey_questions/survey_questions_view.dart b/lib/modules/survey_questions/survey_questions_view.dart index d9ccd45..a163720 100644 --- a/lib/modules/survey_questions/survey_questions_view.dart +++ b/lib/modules/survey_questions/survey_questions_view.dart @@ -1,17 +1,22 @@ part of 'survey_questions_module.dart'; -abstract class SurveyQuestionsViewDelegate { +abstract class SurveyQuestionsViewDelegate implements AlertViewMixinDelegate { BehaviorSubject get stateDidInit; BehaviorSubject get closeButtonDidTap; BehaviorSubject get closeConfirmDialogDidClose; + + BehaviorSubject> get submitButtonDidTap; } -abstract class SurveyQuestionsView extends View { +abstract class SurveyQuestionsView extends View + with AlertViewMixin, ProgressHUDViewMixin { void setQuestions(List questions); void showCloseConfirmDialog(); + + void moveTo(int i); } class SurveyQuestionsViewImpl extends StatefulWidget { @@ -22,12 +27,13 @@ class SurveyQuestionsViewImpl extends StatefulWidget { _SurveyQuestionsViewImplState(); } -class _SurveyQuestionsViewImplState extends ViewState< - SurveyQuestionsViewImpl, - SurveyQuestionsModule, - SurveyQuestionsViewDelegate> implements SurveyQuestionsView { +class _SurveyQuestionsViewImplState extends ViewState + with AlertViewMixin, ProgressHUDViewMixin + implements SurveyQuestionsView { final _questions = BehaviorSubject>(); final _navigationBarKey = GlobalKey(); + final _carouselController = CarouselController(); @override void initState() { @@ -57,4 +63,9 @@ class _SurveyQuestionsViewImplState extends ViewState< .surveyQuestionsScreenCloseConfirmDialogOkLabel, ).then((value) => delegate?.closeConfirmDialogDidClose.add(value)); } + + @override + void moveTo(int i) { + _carouselController.animateToPage(i); + } } diff --git a/lib/repositories/survey_repository.dart b/lib/repositories/survey_repository.dart index e4d4847..b11ce83 100644 --- a/lib/repositories/survey_repository.dart +++ b/lib/repositories/survey_repository.dart @@ -1,5 +1,7 @@ import 'package:survey/models/detailed_survey_info.dart'; import 'package:survey/models/survey_info.dart'; +import 'package:survey/models/survey_submit_question_info.dart'; +import 'package:survey/services/api/survey/params/survey_submit_params.dart'; import 'package:survey/services/api/survey/survey_api_service.dart'; import 'package:survey/services/local_storage/local_storage_service.dart'; import 'package:survey/services/locator/locator_service.dart'; @@ -10,7 +12,13 @@ abstract class SurveyRepository { Future get isSurveysCached; Future> fetchSurveys({bool force}); + Future fetchDetailedSurvey(String id); + + Future submit({ + required String surveyId, + required List questions, + }); } class SurveyRepositoryImpl implements SurveyRepository { @@ -52,4 +60,13 @@ class SurveyRepositoryImpl implements SurveyRepository { final params = SurveyInfoParams(id: id); return _surveyApiService.info(params: params); } + + @override + Future submit({ + required String surveyId, + required List questions, + }) { + final params = SurveySubmitParams(surveyId: surveyId, questions: questions); + return _surveyApiService.submit(params: params); + } } diff --git a/lib/services/api/api_exception.dart b/lib/services/api/api_exception.dart index 9a6fdbc..349814f 100644 --- a/lib/services/api/api_exception.dart +++ b/lib/services/api/api_exception.dart @@ -16,7 +16,7 @@ class ApiException implements LocalizedException { final json = exception.response!.data as Map; final source = json["errors"][0]["source"] as String?; final message = json["errors"][0]["detail"] as String; - final code = json["errors"][0]["code"] as String; + final code = json["errors"][0]["code"] as String?; return ApiException( source: source, @@ -33,5 +33,5 @@ class ApiException implements LocalizedException { final String? source; @override final String message; - final String code; + final String? code; } diff --git a/lib/services/api/api_service.dart b/lib/services/api/api_service.dart index 6bf618d..a29b2e2 100644 --- a/lib/services/api/api_service.dart +++ b/lib/services/api/api_service.dart @@ -113,7 +113,7 @@ class ApiServiceImpl implements ApiService { } } - Future> _request({ + Future?> _request({ required HttpMethod method, String? baseUrl, required String endpoint, @@ -136,23 +136,29 @@ class ApiServiceImpl implements ApiService { final url = finalBaseUrl! + endpoint; try { - return await _httpService.request( + final response = await _httpService.request( method: method, data: params?.toJson(), url: url, headers: headers, - ) as Map; + ); + + if (response is! Map) return null; + + return response; } on HttpException catch (e) { throw ApiException.fromHttpException(e) ?? e; } } - T _convertResponseToObject(Map response) { + T _convertResponseToObject(Map? response) { + if (response == null && T.toString() == "void") return null as T; + if (T == ApiRawResponse) { - return Mapper.fromJson(response).toObject()!; + return Mapper.fromJson(response!).toObject()!; } - if (response["data"] == null && T.toString() == "void") { + if (response!["data"] == null && T.toString() == "void") { return null as T; } @@ -165,7 +171,9 @@ class ApiServiceImpl implements ApiService { } ApiListObject _convertResponseToListObject( - Map response) { + Map? response) { + if (response == null) return const ApiListObject(items: []); + if (response["data"] is! List) { throw ApiException.wrongResponseStructure; } diff --git a/lib/services/api/survey/params/survey_submit_params.dart b/lib/services/api/survey/params/survey_submit_params.dart new file mode 100644 index 0000000..5f6ae77 --- /dev/null +++ b/lib/services/api/survey/params/survey_submit_params.dart @@ -0,0 +1,19 @@ +import 'package:object_mapper/object_mapper.dart'; +import 'package:survey/models/survey_submit_question_info.dart'; +import 'package:survey/services/api/api_service.dart'; + +class SurveySubmitParams extends ApiParams { + SurveySubmitParams({ + required this.surveyId, + required this.questions, + }); + + final String surveyId; + final List questions; + + @override + void mapping(Mapper map) { + map("survey_id", surveyId, (v) {}); + map("questions", questions, (v) {}); + } +} diff --git a/lib/services/api/survey/survey_api_service.dart b/lib/services/api/survey/survey_api_service.dart index 4f51f85..1d87104 100644 --- a/lib/services/api/survey/survey_api_service.dart +++ b/lib/services/api/survey/survey_api_service.dart @@ -1,9 +1,10 @@ import 'package:object_mapper/object_mapper.dart'; import 'package:survey/models/detailed_survey_info.dart'; -import 'package:survey/models/survey_answer_info.dart'; +import 'package:survey/models/survey_question_answer_info.dart'; import 'package:survey/models/survey_info.dart'; import 'package:survey/models/survey_question_info.dart'; import 'package:survey/services/api/api_service.dart'; +import 'package:survey/services/api/survey/params/survey_submit_params.dart'; import 'package:survey/services/http/http_service.dart'; import 'package:survey/services/locator/locator_service.dart'; @@ -17,6 +18,8 @@ abstract class SurveyApiService { Future info({required SurveyInfoParams params}); Future> list({required SurveyListParams params}); + + Future submit({required SurveySubmitParams params}); } class SurveyApiServiceImpl implements SurveyApiService { @@ -37,10 +40,10 @@ class SurveyApiServiceImpl implements SurveyApiService { // Get all answers final rawAnswers = (rawResponse.included ?? []) .where((element) => element.type == "answer"); - final allAnswers = List.empty(growable: true); + final allAnswers = List.empty(growable: true); for (final rawObject in rawAnswers) { - final answer = - Mapper.fromJson(rawObject.toJson()).toObject()!; + final answer = Mapper.fromJson(rawObject.toJson()) + .toObject()!; allAnswers.add(answer); } @@ -49,14 +52,15 @@ class SurveyApiServiceImpl implements SurveyApiService { .where((element) => element.type == "question"); for (final ApiRawObject rawObject in rawQuestions) { // Find all related answers for this question - final answers = List.empty(growable: true); + final answers = List.empty(growable: true); if (rawObject.relationships?["answers"] != null && rawObject.relationships?["answers"]["data"] != null && rawObject.relationships?["answers"]["data"] is List) { for (final item in rawObject.relationships?["answers"]["data"]) { - final answer = allAnswers.cast().firstWhere( - (element) => element!.id == item["id"], - orElse: () => null); + final answer = allAnswers + .cast() + .firstWhere((element) => element!.id == item["id"], + orElse: () => null); if (answer == null) continue; answers.add(answer); } @@ -80,4 +84,13 @@ class SurveyApiServiceImpl implements SurveyApiService { params: params, ); } + + @override + Future submit({required SurveySubmitParams params}) { + return _apiService.callForList( + method: HttpMethod.post, + endpoint: "/responses", + params: params, + ); + } }