feat: risk profile view

This commit is contained in:
Prajna Prayoga 2024-02-10 16:29:00 +07:00
parent 7a1cddee03
commit 6977c8166d
21 changed files with 615 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/icons/icon-coins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
assets/images/img-cat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
assets/images/img-deer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

BIN
assets/images/img-lion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -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';
}

View File

@ -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)
),
);

View File

@ -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<LoginView> {
currentPage++;
pageController.jumpToPage(1);
}else{
routePush(context, page: BottomNavigationView());
routePush(context, page: RiskProfileView());
}
}
}

View File

@ -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<QuestionView> createState() => _QuestionViewState();
}
class _QuestionViewState extends State<QuestionView> {
int currentPage = 0;
PageController pageController = PageController();
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => RiskProfileViewModel(),
child: Consumer<RiskProfileViewModel>(
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(),
)
],
),
);
}
}

View File

@ -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,
)
],
),
),
);
}
}

View File

@ -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,
)
],
)
],
),
),
);
}
}

View File

@ -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<Answer> answers;
RiskProfileQuestion(this.img, this.question, this.selectedScore, this.answers);
}
class RiskProfileResult {
String type;
String img;
String desc;
Color color;
List<dynamic> 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<int> listScore = [0,0,0,0,0];
RiskProfileResult typeResult = RiskProfileResult('', '', Colors.transparent, '', []);
List<RiskProfileQuestion> 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<RiskProfileResult> 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();
}
}

View File

@ -395,6 +395,7 @@ class _HomeViewState extends State<HomeView> {
width: SizeConfig.width,
marginVertical: 0,
heightWrapContent: true,
contentPadding: EdgeInsets.all(12),
onPressed: () {
routePush(context, page: InitialRegistrationStep());
},