diff --git a/assets/icons/icon-balance.png b/assets/icons/icon-balance.png new file mode 100644 index 0000000..fea0f54 Binary files /dev/null and b/assets/icons/icon-balance.png differ diff --git a/assets/icons/icon-coins.png b/assets/icons/icon-coins.png new file mode 100644 index 0000000..7eee7fc Binary files /dev/null and b/assets/icons/icon-coins.png differ diff --git a/assets/icons/icon-money-receive.png b/assets/icons/icon-money-receive.png new file mode 100644 index 0000000..7f8f94f Binary files /dev/null and b/assets/icons/icon-money-receive.png differ diff --git a/assets/icons/icon-strongbox.png b/assets/icons/icon-strongbox.png new file mode 100644 index 0000000..d7e94bc Binary files /dev/null and b/assets/icons/icon-strongbox.png differ diff --git a/assets/images/img-business-failure.png b/assets/images/img-business-failure.png new file mode 100644 index 0000000..9aa4de9 Binary files /dev/null and b/assets/images/img-business-failure.png differ diff --git a/assets/images/img-cat.png b/assets/images/img-cat.png new file mode 100644 index 0000000..3d02a68 Binary files /dev/null and b/assets/images/img-cat.png differ diff --git a/assets/images/img-data-analysis.png b/assets/images/img-data-analysis.png new file mode 100644 index 0000000..26c8473 Binary files /dev/null and b/assets/images/img-data-analysis.png differ diff --git a/assets/images/img-data-report.png b/assets/images/img-data-report.png new file mode 100644 index 0000000..0b07fa5 Binary files /dev/null and b/assets/images/img-data-report.png differ diff --git a/assets/images/img-deer.png b/assets/images/img-deer.png new file mode 100644 index 0000000..aceaae3 Binary files /dev/null and b/assets/images/img-deer.png differ diff --git a/assets/images/img-growing.png b/assets/images/img-growing.png new file mode 100644 index 0000000..3b4c28d Binary files /dev/null and b/assets/images/img-growing.png differ diff --git a/assets/images/img-leader.png b/assets/images/img-leader.png new file mode 100644 index 0000000..4697310 Binary files /dev/null and b/assets/images/img-leader.png differ diff --git a/assets/images/img-lion.png b/assets/images/img-lion.png new file mode 100644 index 0000000..8077b80 Binary files /dev/null and b/assets/images/img-lion.png differ diff --git a/assets/images/img-money-income.png b/assets/images/img-money-income.png new file mode 100644 index 0000000..df194c1 Binary files /dev/null and b/assets/images/img-money-income.png differ diff --git a/lib/application/assets/path_assets.dart b/lib/application/assets/path_assets.dart index e86ba76..a9de378 100644 --- a/lib/application/assets/path_assets.dart +++ b/lib/application/assets/path_assets.dart @@ -16,6 +16,10 @@ class PathAssets { static const String iconPortofolioMoneyMarket = 'assets/icons/icon-portofolio-moneymarket.png'; static const String iconShield = 'assets/icons/icon-shield.png'; static const String iconFlag = 'assets/icons/icon-flag.png'; + static const String iconStrongBox = 'assets/icons/icon-strongbox.png'; + static const String iconBalance = 'assets/icons/icon-balance.png'; + static const String iconMoneyReceive = 'assets/icons/icon-money-receive.png'; + static const String iconCoins = 'assets/icons/icon-coins.png'; /// IMAGE static const String imgSplashLogo = 'assets/images/splash-logo.png'; @@ -33,4 +37,13 @@ class PathAssets { static const String imgArticles = 'assets/images/img-articles.png'; static const String imgProduct = 'assets/images/img-product.png'; static const String imgSuccessSignup = 'assets/images/img-success-signup.png'; + static const String imgDataReport = 'assets/images/img-data-report.png'; + static const String imgDataAnalysis = 'assets/images/img-data-analysis.png'; + static const String imgBusinessFailure = 'assets/images/img-business-failure.png'; + static const String imgLeader = 'assets/images/img-leader.png'; + static const String imgMoneyIncome = 'assets/images/img-money-income.png'; + static const String imgGrowing = 'assets/images/img-growing.png'; + static const String imgCat = 'assets/images/img-cat.png'; + static const String imgDeer = 'assets/images/img-deer.png'; + static const String imgLion = 'assets/images/img-lion.png'; } diff --git a/lib/application/component/button/button_back.dart b/lib/application/component/button/button_back.dart index 9f2db7f..37e1a51 100644 --- a/lib/application/component/button/button_back.dart +++ b/lib/application/component/button/button_back.dart @@ -4,7 +4,8 @@ import 'package:flutter/material.dart'; class ButtonBack extends StatelessWidget { final EdgeInsets? margin; - const ButtonBack({super.key, this.margin}); + final void Function()? onPress; + const ButtonBack({super.key, this.margin, this.onPress}); @override Widget build(BuildContext context) { @@ -18,9 +19,7 @@ class ButtonBack extends StatelessWidget { side: BorderSide(color: ColorPalette.slate200) ) ), - onPressed: () { - Navigator.pop(context); - }, + onPressed: onPress ?? () => Navigator.pop(context), icon: const Icon(Icons.arrow_back) ), ); diff --git a/lib/features/auth/login/view/login_view.dart b/lib/features/auth/login/view/login_view.dart index d8aaf2a..35157ca 100644 --- a/lib/features/auth/login/view/login_view.dart +++ b/lib/features/auth/login/view/login_view.dart @@ -10,6 +10,7 @@ import 'package:cims_apps/core/utils/size_config.dart'; import 'package:cims_apps/features/auth/login/view/password_view.dart'; import 'package:cims_apps/features/auth/login/view/phone_number_view.dart'; import 'package:cims_apps/features/auth/login/view_model/login_view_model.dart'; +import 'package:cims_apps/features/auth/registration/view/risk_profile/risk_profile_view.dart'; import 'package:cims_apps/features/bottom_navigation_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -89,7 +90,7 @@ class _LoginViewState extends State { currentPage++; pageController.jumpToPage(1); }else{ - routePush(context, page: BottomNavigationView()); + routePush(context, page: RiskProfileView()); } } } diff --git a/lib/features/auth/registration/view/risk_profile/question_view.dart b/lib/features/auth/registration/view/risk_profile/question_view.dart new file mode 100644 index 0000000..c758c80 --- /dev/null +++ b/lib/features/auth/registration/view/risk_profile/question_view.dart @@ -0,0 +1,164 @@ +import 'package:cims_apps/application/assets/path_assets.dart'; +import 'package:cims_apps/application/component/button/button_back.dart'; +import 'package:cims_apps/application/component/button/button_view.dart'; +import 'package:cims_apps/application/component/image/image_view.dart'; +import 'package:cims_apps/application/theme/color_palette.dart'; +import 'package:cims_apps/core/route/route.dart'; +import 'package:cims_apps/core/utils/size_config.dart'; +import 'package:cims_apps/features/auth/registration/view/risk_profile/results_view.dart'; +import 'package:cims_apps/features/auth/registration/view/risk_profile/risk_profile_view.dart'; +import 'package:cims_apps/features/auth/registration/view/risk_profile/risk_profile_view_model/risk_profile_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class QuestionView extends StatefulWidget { + const QuestionView({super.key}); + + @override + State createState() => _QuestionViewState(); +} + +class _QuestionViewState extends State { + int currentPage = 0; + PageController pageController = PageController(); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => RiskProfileViewModel(), + child: Consumer( + builder: (context, provider, child) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 70, + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + automaticallyImplyLeading: false, + centerTitle: true, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ButtonBack(), + const Text('Risk Profile', textAlign: TextAlign.center), + SizedBox( + width: SizeConfig.width * 0.1 + ) + ], + ), + shape: const RoundedRectangleBorder( + side: BorderSide(color: ColorPalette.slate200) + ), + ), + body: PageView( + controller: pageController, + physics: NeverScrollableScrollPhysics(), + children: provider.listRiskProfileQuestion.asMap().entries.map((e) { + return containerQuestion( + e.value, + e.key, + (index, score) => provider.selectedAnswer(index, score) + ); + }).toList(), + ), + bottomNavigationBar: SizedBox( + height: 84, + child: ButtonView( + name: 'Next', + marginVertical: 16, + onPressed: () { + if(currentPage > 3){ + int totalScore = provider.listScore.reduce((value, element) => value + element); + provider.setTypeResult(totalScore); + routePush(context, page: ResultsView(totalScore: totalScore.toString(), typeResult: provider.typeResult)); + }else{ + setState(() { + currentPage += 1; + }); + pageController.nextPage(duration: Duration(milliseconds: 300), curve: Curves.ease); + } + }, + ), + ), + ); + } + ), + ); + } + + Widget containerQuestion(RiskProfileQuestion data, int index, void Function(int index, int score) onTapAnswers) { + return SizedBox( + height: SizeConfig.height, + width: SizeConfig.width, + child: ListView( + padding: EdgeInsets.all(24), + children: [ + ImageView( + image: data.img, + fit: BoxFit.fitWidth, + ), + SizedBox( + height: 16, + ), + Text( + '${index + 1} of 5 question', + style: TextStyle( + fontWeight: FontWeight.bold, + color: ColorPalette.primary + ), + ), + SizedBox( + height: 12, + ), + Text( + data.question, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + color: ColorPalette.slate800 + ), + ), + SizedBox( + height: 16, + ), + Column( + children: data.answers.asMap().entries.map((e) { + bool selected = data.selectedScore == e.value.score; + return GestureDetector( + onTap: () { + onTapAnswers(index, e.value.score); + }, + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + margin: EdgeInsets.only(top: 12), + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: selected ? ColorPalette.primary : ColorPalette.slate200), + borderRadius: BorderRadius.circular(8) + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + e.value.title, + style: TextStyle( + fontSize: 16, + color: selected ? ColorPalette.primary : ColorPalette.slate500, + fontWeight: FontWeight.w500 + ), + ), + ), + if(selected)...[ + Icon(Icons.check_circle_rounded, color: ColorPalette.primary) + ] + ], + ), + ), + ); + }).toList(), + ) + ], + ), + ); + } +} diff --git a/lib/features/auth/registration/view/risk_profile/results_view.dart b/lib/features/auth/registration/view/risk_profile/results_view.dart new file mode 100644 index 0000000..6faac0f --- /dev/null +++ b/lib/features/auth/registration/view/risk_profile/results_view.dart @@ -0,0 +1,187 @@ +import 'package:cims_apps/application/assets/path_assets.dart'; +import 'package:cims_apps/application/component/button/button_back.dart'; +import 'package:cims_apps/application/component/button/button_view.dart'; +import 'package:cims_apps/application/component/image/image_view.dart'; +import 'package:cims_apps/application/theme/color_palette.dart'; +import 'package:cims_apps/core/route/route.dart'; +import 'package:cims_apps/core/utils/size_config.dart'; +import 'package:cims_apps/features/auth/registration/view/registration_password_view.dart'; +import 'package:cims_apps/features/auth/registration/view/risk_profile/risk_profile_view_model/risk_profile_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ResultsView extends StatelessWidget { + final String totalScore; + final RiskProfileResult typeResult; + const ResultsView({super.key, required this.typeResult, required this.totalScore}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 70, + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + automaticallyImplyLeading: false, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ButtonBack(), + const Text('Risk Profile', textAlign: TextAlign.center), + SizedBox( + width: SizeConfig.width * 0.1 + ) + ], + ), + shape: const RoundedRectangleBorder( + side: BorderSide(color: ColorPalette.slate200) + ), + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + color: typeResult.color, + image: DecorationImage(image: AssetImage(typeResult.img), alignment: Alignment.centerRight) + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + typeResult.type, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24, + color: ColorPalette.white + ), + ), + SizedBox(height: 16,), + Text('Total Score :', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: ColorPalette.white + ), + ), + Text(totalScore, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 28, + color: ColorPalette.white + ), + ) + ], + ), + ), + ], + ), + ), + ), + SizedBox( + height: 24, + ), + Text( + typeResult.desc, + style: TextStyle( + color: ColorPalette.slate500, + fontSize: 16 + ) + ), + SizedBox( + height: 24, + ), + Text( + 'Suitable Product', + style: TextStyle( + color: ColorPalette.slate800, + fontWeight: FontWeight.bold, + fontSize: 16 + ), + ), + SizedBox( + height: 16, + ), + Wrap( + runSpacing: 16, + children: typeResult.suitableProduct.map((e) { + return Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(6), + border: Border.all(color: ColorPalette.slate200), + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(8), + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: typeResult.color.withOpacity(0.1) + ), + child: Image.asset(e['icon'], width: SizeConfig.width * 0.07, color: typeResult.color) + ), + SizedBox( + width: 12, + ), + Expanded( + child: Text(e['desc'], + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: ColorPalette.slate800 + ), + ), + ) + ], + ), + ); + }).toList(), + ), + SizedBox( + height: 32, + ), + ButtonView( + name: 'Re-test', + onPressed: () { + }, + marginVertical: 0, + backgroundColor: ColorPalette.white, + textColor: ColorPalette.primary, + borderColor: ColorPalette.primary, + isOutlined: true, + textSize: 16, + heightWrapContent: true, + contentPadding: EdgeInsets.all(16), + width: SizeConfig.width, + ), + SizedBox( + height: 16, + ), + ButtonView( + name: 'Confirm', + onPressed: () { + routePush(context, page: DialogSuccess()); + }, + marginVertical: 0, + textSize: 16, + heightWrapContent: true, + contentPadding: EdgeInsets.all(16), + width: SizeConfig.width, + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/auth/registration/view/risk_profile/risk_profile_view.dart b/lib/features/auth/registration/view/risk_profile/risk_profile_view.dart new file mode 100644 index 0000000..c274599 --- /dev/null +++ b/lib/features/auth/registration/view/risk_profile/risk_profile_view.dart @@ -0,0 +1,112 @@ +import 'package:cims_apps/application/assets/path_assets.dart'; +import 'package:cims_apps/application/component/button/button_back.dart'; +import 'package:cims_apps/application/component/button/button_view.dart'; +import 'package:cims_apps/application/component/image/image_view.dart'; +import 'package:cims_apps/application/theme/color_palette.dart'; +import 'package:cims_apps/core/route/route.dart'; +import 'package:cims_apps/core/utils/size_config.dart'; +import 'package:cims_apps/features/auth/registration/view/risk_profile/question_view.dart'; +import 'package:flutter/material.dart'; + +class RiskProfileView extends StatelessWidget { + const RiskProfileView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + toolbarHeight: 70, + backgroundColor: Colors.white, + surfaceTintColor: Colors.white, + automaticallyImplyLeading: false, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ButtonBack(), + const Text('Risk Profile', textAlign: TextAlign.center), + SizedBox( + width: SizeConfig.width * 0.1 + ) + ], + ), + shape: const RoundedRectangleBorder( + side: BorderSide(color: ColorPalette.slate200) + ), + ), + body: Container( + width: SizeConfig.width, + height: SizeConfig.height, + padding: EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + ImageView(image: PathAssets.imgDataReport), + SizedBox( + height: 24, + ), + Text( + 'Know Your Risk Profile', + textAlign: TextAlign.center, + style: TextStyle( + color: ColorPalette.slate800, + fontWeight: FontWeight.bold, + fontSize: 24 + ), + ), + SizedBox( + height: 12, + ), + Text( + 'We will provide recommendations that match your profile and risk tolerance level.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: ColorPalette.slate500 + ), + ), + ], + ), + Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ImageView( + image: PathAssets.iconShield, + width: 20, + height: 22, + ), + SizedBox( + width: 8, + ), + Text( + 'Your data is secure and encrypted', + style: TextStyle( + fontWeight: FontWeight.w600, + color: ColorPalette.primary, + fontSize: 16 + ), + ) + ], + ), + SizedBox( + height: 24, + ), + ButtonView( + name: "Let's Start", + onPressed: () { + routePush(context, page: QuestionView()); + }, + marginVertical: 0, + ) + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/auth/registration/view/risk_profile/risk_profile_view_model/risk_profile_view_model.dart b/lib/features/auth/registration/view/risk_profile/risk_profile_view_model/risk_profile_view_model.dart new file mode 100644 index 0000000..929b5cb --- /dev/null +++ b/lib/features/auth/registration/view/risk_profile/risk_profile_view_model/risk_profile_view_model.dart @@ -0,0 +1,133 @@ + +import 'package:cims_apps/application/assets/path_assets.dart'; +import 'package:cims_apps/application/theme/color_palette.dart'; +import 'package:flutter/material.dart'; + +class RiskProfileQuestion { + String img; + int selectedScore; + String question; + List answers; + + RiskProfileQuestion(this.img, this.question, this.selectedScore, this.answers); +} + +class RiskProfileResult { + String type; + String img; + String desc; + Color color; + List suitableProduct; + + RiskProfileResult(this.type, this.img, this.color, this.desc, this.suitableProduct); +} + +class Answer { + int score; + String title; + + Answer(this.score, this.title); +} + +class RiskProfileViewModel extends ChangeNotifier { + List listScore = [0,0,0,0,0]; + RiskProfileResult typeResult = RiskProfileResult('', '', Colors.transparent, '', []); + + List listRiskProfileQuestion = [ + RiskProfileQuestion( + PathAssets.imgGrowing, 'How long is your planned investment horizon?', 0, + [ + Answer(2, '<1 year'), + Answer(4, '1-3 year'), + Answer(6, '3-5 year'), + Answer(8, '>5 year') + ] + ), + RiskProfileQuestion( + PathAssets.imgLeader, 'Investment objectives of mutualfund investors?', 0, + [ + Answer(2, 'Security of Investment Funds'), + Answer(4, 'Investment Income and Security'), + Answer(6, 'Revenue and growth over the long term'), + Answer(8, 'Growth') + ] + ), + RiskProfileQuestion( + PathAssets.imgBusinessFailure, 'How much can I afford to lose on my investment (Risk level you can afford)?', 0, + [ + Answer(2, '0%'), + Answer(4, '<25%'), + Answer(6, '26-50%'), + Answer(8, '>50%') + ] + ), + RiskProfileQuestion( + PathAssets.imgMoneyIncome, 'I am willing to use .... % of my income for investment (financial circumstances of the financier)?', 0, + [ + Answer(2, '0%'), + Answer(4, '<25%'), + Answer(6, '26-50%'), + Answer(8, '>50%') + ] + ), + RiskProfileQuestion( + PathAssets.imgDataAnalysis, 'Mutual Fund Investment Knowledge Level?', 0, + [ + Answer(2, 'Low'), + Answer(4, 'Medium'), + Answer(6, 'High'), + ] + ) + ]; + + List listRiskProfileResult = [ + RiskProfileResult( + 'Conservative', + PathAssets.imgCat, + ColorPalette.green500, + 'Investors with a conservative risk profile are risk-averse or do not want to experience large losses. Therefore, mutual fund products that are suitable for conservative investors are products that have low risk and stable returns.', + [ + {'desc': 'Money Market Mutual Fund', 'icon': PathAssets.iconStrongBox}, + {'desc': 'Fixed Income Mutual Fund', 'icon': PathAssets.iconMoneyReceive}, + {'desc': 'Balanced Mutual Fund', 'icon': PathAssets.iconBalance}, + ] + ), + RiskProfileResult( + 'Moderate', + PathAssets.imgDeer, + ColorPalette.orange500, + 'Investors with a moderate risk profile are investors who are ready to accept moderate risk to get higher returns than conservative mutual fund products. Therefore, mutual fund products that are suitable for moderate investors are products that have moderate risk and higher returns than conservative mutual fund products.', + [ + {'desc': 'Fixed Income Mutual Fund', 'icon': PathAssets.iconMoneyReceive}, + {'desc': 'Balanced Mutual Fund', 'icon': PathAssets.iconBalance}, + ] + ), + RiskProfileResult( + 'Aggressive', + PathAssets.imgLion, + ColorPalette.purple500, + 'Investors with an aggressive risk profile are investors who are ready to accept high risks to get high returns. Therefore, mutual fund products that are suitable for aggressive investors are products that have high risk and high returns.', + [ + {'desc': 'Equity Mutual Fund', 'icon': PathAssets.iconCoins}, + {'desc': 'Aggressive Balanced Fund', 'icon': PathAssets.iconBalance}, + ] + ) + ]; + + void selectedAnswer(int index, int score) { + listScore[index] = score; + listRiskProfileQuestion[index].selectedScore = score; + notifyListeners(); + } + + void setTypeResult(int totalScore) { + if(totalScore <= 25){ + typeResult = listRiskProfileResult[0]; + }else if(totalScore <= 50){ + typeResult = listRiskProfileResult[1]; + }else{ + typeResult = listRiskProfileResult[2]; + } + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/features/dashboard/dashboard_account/view/homepage/homepage_view.dart b/lib/features/dashboard/dashboard_account/view/homepage/homepage_view.dart index e3262e3..0e81a6d 100644 --- a/lib/features/dashboard/dashboard_account/view/homepage/homepage_view.dart +++ b/lib/features/dashboard/dashboard_account/view/homepage/homepage_view.dart @@ -395,6 +395,7 @@ class _HomeViewState extends State { width: SizeConfig.width, marginVertical: 0, heightWrapContent: true, + contentPadding: EdgeInsets.all(12), onPressed: () { routePush(context, page: InitialRegistrationStep()); },