health stream, pie chart, transaction history and more
This commit is contained in:
parent
1ccff543a1
commit
ccefd5c2e4
|
@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
|
||||||
import 'providers/auth_provider.dart';
|
import 'providers/auth_provider.dart';
|
||||||
import 'providers/user_provider.dart';
|
import 'providers/user_provider.dart';
|
||||||
import 'providers/wallet_provider.dart';
|
import 'providers/wallet_provider.dart';
|
||||||
|
import 'providers/transaction_provider.dart';
|
||||||
import 'package:robo_advisory/config/Routes.dart';
|
import 'package:robo_advisory/config/Routes.dart';
|
||||||
import 'package:robo_advisory/injection/dependency_injection.dart';
|
import 'package:robo_advisory/injection/dependency_injection.dart';
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ class MyApp extends StatelessWidget {
|
||||||
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => UserProvider()),
|
ChangeNotifierProvider(create: (_) => UserProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => WalletProvider()),
|
ChangeNotifierProvider(create: (_) => WalletProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => TransactionProvider()),
|
||||||
],
|
],
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
initialRoute: Routes.login,
|
initialRoute: Routes.login,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class HealthSteam {
|
class HealthSteam {
|
||||||
String type;
|
String type;
|
||||||
String valueInPercentage;
|
int valueInPercentage;
|
||||||
String goldInGram;
|
String goldInGram;
|
||||||
String goldPriceInTurkishLira;
|
String goldPriceInTurkishLira;
|
||||||
String oneGramGoldPriceInLira;
|
String oneGramGoldPriceInLira;
|
||||||
|
@ -12,4 +12,11 @@ class HealthSteam {
|
||||||
this.goldPriceInTurkishLira,
|
this.goldPriceInTurkishLira,
|
||||||
this.oneGramGoldPriceInLira,
|
this.oneGramGoldPriceInLira,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
HealthSteam.fromJson(Map<dynamic, dynamic> map)
|
||||||
|
: type = map["type"],
|
||||||
|
valueInPercentage = map["value_in_percentage"],
|
||||||
|
goldInGram = map["gold_in_gram"],
|
||||||
|
goldPriceInTurkishLira = map["gold_price_in_turkish_lira"],
|
||||||
|
oneGramGoldPriceInLira = map["one_gram_gold_price_in_turkish_lira"];
|
||||||
}
|
}
|
||||||
|
|
27
lib/models/transaction.dart
Normal file
27
lib/models/transaction.dart
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
class Category {
|
||||||
|
String name;
|
||||||
|
String color;
|
||||||
|
Category({this.name, this.color});
|
||||||
|
|
||||||
|
Category.formJson(Map<String, dynamic> map)
|
||||||
|
: name = map["name"],
|
||||||
|
color = map["color"];
|
||||||
|
}
|
||||||
|
|
||||||
|
class Transaction {
|
||||||
|
String amount;
|
||||||
|
String description;
|
||||||
|
String date;
|
||||||
|
String name;
|
||||||
|
Category category;
|
||||||
|
|
||||||
|
Transaction(
|
||||||
|
{this.amount, this.description, this.date, this.name, this.category});
|
||||||
|
|
||||||
|
Transaction.fromJson(Map<String, dynamic> map)
|
||||||
|
: category = Category.formJson(map["category"]),
|
||||||
|
amount = map["amount"],
|
||||||
|
description = map["description"],
|
||||||
|
name = map["name"],
|
||||||
|
date = map["date"];
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:core';
|
||||||
import 'package:robo_advisory/models/healthSteam.dart';
|
import 'package:robo_advisory/models/healthSteam.dart';
|
||||||
import 'package:robo_advisory/models/expenses.dart';
|
import 'package:robo_advisory/models/expenses.dart';
|
||||||
|
|
||||||
|
@ -7,7 +8,7 @@ class Wallet {
|
||||||
String totalGoldInGram;
|
String totalGoldInGram;
|
||||||
String totalGoldInTurkishLira;
|
String totalGoldInTurkishLira;
|
||||||
String healthSteamDay;
|
String healthSteamDay;
|
||||||
List<HealthSteam> healthStream;
|
List<HealthSteam> healthStreams;
|
||||||
List<Expenses> expensesChart;
|
List<Expenses> expensesChart;
|
||||||
|
|
||||||
Wallet({
|
Wallet({
|
||||||
|
@ -16,7 +17,7 @@ class Wallet {
|
||||||
this.totalGoldInGram,
|
this.totalGoldInGram,
|
||||||
this.totalGoldInTurkishLira,
|
this.totalGoldInTurkishLira,
|
||||||
this.healthSteamDay,
|
this.healthSteamDay,
|
||||||
this.healthStream,
|
this.healthStreams,
|
||||||
this.expensesChart,
|
this.expensesChart,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -25,5 +26,8 @@ class Wallet {
|
||||||
totalTurkishLiraPool = map["total_turkish_lira"],
|
totalTurkishLiraPool = map["total_turkish_lira"],
|
||||||
totalGoldInGram = map["total_gold_in_gram"],
|
totalGoldInGram = map["total_gold_in_gram"],
|
||||||
totalGoldInTurkishLira = map["total_gold_in_turkish_lira"],
|
totalGoldInTurkishLira = map["total_gold_in_turkish_lira"],
|
||||||
healthSteamDay = map["health_stream_day"];
|
healthSteamDay = map["health_stream_day"],
|
||||||
|
healthStreams = (map["health_stream"] as List)
|
||||||
|
.map((i) => HealthSteam.fromJson(i))
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
40
lib/providers/transaction_provider.dart
Normal file
40
lib/providers/transaction_provider.dart
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:robo_advisory/models/transaction.dart';
|
||||||
|
import 'package:robo_advisory/repository/transaction.repository.dart';
|
||||||
|
|
||||||
|
class TransactionProvider with ChangeNotifier {
|
||||||
|
String _selectedCategory = "All";
|
||||||
|
List<Transaction> _transactionsData;
|
||||||
|
List<Transaction> _transactionsList;
|
||||||
|
|
||||||
|
List<Transaction> get transactionList {
|
||||||
|
if (_selectedCategory != 'All') {
|
||||||
|
return _transactionsList = _transactionsData
|
||||||
|
.where((transaction) =>
|
||||||
|
transaction.category.name == _selectedCategory.toLowerCase())
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
return _transactionsList = _transactionsData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTransactionsList(List<Transaction> transactions) {
|
||||||
|
_transactionsData = transactions;
|
||||||
|
_transactionsList = transactions;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
String get selectedCategory => _selectedCategory;
|
||||||
|
|
||||||
|
void setSelectedCategory(String category) {
|
||||||
|
_selectedCategory = category;
|
||||||
|
print(_selectedCategory);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Transaction>> loadTransactions() async {
|
||||||
|
return (await TransactionRepository.loadTransactions() as List)
|
||||||
|
.map((i) => Transaction.fromJson(i))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,9 +3,21 @@ import 'package:robo_advisory/models/user.dart';
|
||||||
|
|
||||||
class UserProvider with ChangeNotifier {
|
class UserProvider with ChangeNotifier {
|
||||||
User _user = new User();
|
User _user = new User();
|
||||||
|
int _selectedTabIndex = 0;
|
||||||
|
|
||||||
|
List<String> _categories = ["All", "Workplace", "Equipment", "Home", "Other"];
|
||||||
|
|
||||||
|
List<String> get categories => _categories;
|
||||||
|
|
||||||
User get user => _user;
|
User get user => _user;
|
||||||
|
|
||||||
|
int get selectedTabIndex => _selectedTabIndex;
|
||||||
|
|
||||||
|
void setSelectedTabIndex(int index) {
|
||||||
|
_selectedTabIndex = index;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void setUser(User user) {
|
void setUser(User user) {
|
||||||
_user = user;
|
_user = user;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
46
lib/repository/transaction.repository.dart
Normal file
46
lib/repository/transaction.repository.dart
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import 'package:robo_advisory/injection/dependency_injection.dart';
|
||||||
|
|
||||||
|
class TransactionRepository {
|
||||||
|
static loadTransactions() async {
|
||||||
|
switch (Injector.checkInjector()) {
|
||||||
|
case Flavor.MOCK:
|
||||||
|
return transactionJSON["dataKey"];
|
||||||
|
default:
|
||||||
|
// TODO: check how it will work with the API
|
||||||
|
return transactionJSON["dataKey"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final transactionJSON = {
|
||||||
|
"dataKey": [
|
||||||
|
{
|
||||||
|
"date": "2020-12-09",
|
||||||
|
"name": "Transaction 1",
|
||||||
|
"amount": "20",
|
||||||
|
"description": "lorem ipsum dolor sit",
|
||||||
|
"category": {"name": "workplace", "color": "#0293ee"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-12-01",
|
||||||
|
"name": "Transaction 2",
|
||||||
|
"amount": "23",
|
||||||
|
"description": "lorem ipsum dolor sit",
|
||||||
|
"category": {"name": "equipment", "color": "#f8b250"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-12-01",
|
||||||
|
"name": "Transaction 3",
|
||||||
|
"amount": "23",
|
||||||
|
"description": "lorem ipsum dolor sit",
|
||||||
|
"category": {"name": "home", "color": "#13d38e"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2020-12-01",
|
||||||
|
"name": "Transaction 4",
|
||||||
|
"amount": "23",
|
||||||
|
"description": "lorem ipsum dolor sit",
|
||||||
|
"category": {"name": "other", "color": "#845bef"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
|
@ -18,5 +18,28 @@ final wallerJSON = {
|
||||||
'total_turkish_lira': '5000.00 TL',
|
'total_turkish_lira': '5000.00 TL',
|
||||||
'total_gold_in_gram': '90.00g XAU',
|
'total_gold_in_gram': '90.00g XAU',
|
||||||
'total_gold_in_turkish_lira': '45,000.00 TL',
|
'total_gold_in_turkish_lira': '45,000.00 TL',
|
||||||
'health_stream_day': 'Day 01'
|
'health_stream_day': 'Day 01',
|
||||||
|
'health_stream': [
|
||||||
|
{
|
||||||
|
"type": "Loss",
|
||||||
|
"value_in_percentage": 30,
|
||||||
|
"gold_in_gram": "90.00 gXAU",
|
||||||
|
"gold_price_in_turkish_lira": "50,000.00 TL",
|
||||||
|
"one_gram_gold_price_in_turkish_lira": "555.55 TL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Equal",
|
||||||
|
"value_in_percentage": 50,
|
||||||
|
"gold_in_gram": "90.00 gXAU",
|
||||||
|
"gold_price_in_turkish_lira": "50,000.00 TL",
|
||||||
|
"one_gram_gold_price_in_turkish_lira": "555.55 TL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Gain",
|
||||||
|
"value_in_percentage": 20,
|
||||||
|
"gold_in_gram": "90.00 gXAU",
|
||||||
|
"gold_price_in_turkish_lira": "50,000.00 TL",
|
||||||
|
"one_gram_gold_price_in_turkish_lira": "555.55 TL"
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:robo_advisory/models/wallet.dart';
|
import 'package:robo_advisory/providers/user_provider.dart';
|
||||||
import 'package:robo_advisory/screens/dashboard/local_widgets/bottom_navigation.dart';
|
import 'package:robo_advisory/screens/dashboard/local_widgets/bottom_navigation.dart';
|
||||||
import 'package:robo_advisory/screens/home/home.dart';
|
import 'package:robo_advisory/screens/home/home.dart';
|
||||||
import 'package:robo_advisory/screens/fund_transfer/fund_transfer.dart';
|
import 'package:robo_advisory/screens/fund_transfer/fund_transfer.dart';
|
||||||
|
@ -7,11 +7,6 @@ import 'package:robo_advisory/screens/settlement/settlement.dart';
|
||||||
import 'package:robo_advisory/screens/history/history.dart';
|
import 'package:robo_advisory/screens/history/history.dart';
|
||||||
import 'package:robo_advisory/widgets/drawer.dart';
|
import 'package:robo_advisory/widgets/drawer.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:robo_advisory/providers/wallet_provider.dart';
|
|
||||||
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
|
||||||
|
|
||||||
// TODO: temp
|
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
class DashboardScreen extends StatefulWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
|
@ -25,39 +20,19 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||||
_scaffoldKey.currentState.openEndDrawer();
|
_scaffoldKey.currentState.openEndDrawer();
|
||||||
}
|
}
|
||||||
|
|
||||||
int _selectedIndex = 0;
|
|
||||||
bool loadingData = true;
|
|
||||||
Wallet walletData;
|
|
||||||
|
|
||||||
void _onItemTapped(int index) {
|
|
||||||
setState(() {
|
|
||||||
_selectedIndex = index;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void initState() {
|
|
||||||
WalletProvider walletProvider =
|
|
||||||
Provider.of<WalletProvider>(context, listen: false);
|
|
||||||
walletProvider.loadWallet().then((value) {
|
|
||||||
if (value.totalAssets != null) {
|
|
||||||
Timer(Duration(seconds: 3), () {
|
|
||||||
setState(() {
|
|
||||||
loadingData = false;
|
|
||||||
walletData = value;
|
|
||||||
});
|
|
||||||
print(value.totalAssets);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
int _selectedIndex =
|
||||||
|
Provider.of<UserProvider>(context, listen: true).selectedTabIndex;
|
||||||
|
|
||||||
|
void _onItemTapped(int index) {
|
||||||
|
Provider.of<UserProvider>(context, listen: false)
|
||||||
|
.setSelectedTabIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
var currentTab = [
|
var currentTab = [
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
toggleDrawer: _openEndDrawer,
|
toggleDrawer: _openEndDrawer,
|
||||||
wallet: walletData,
|
|
||||||
),
|
),
|
||||||
FundTransferScreen(),
|
FundTransferScreen(),
|
||||||
SettlementScreen(),
|
SettlementScreen(),
|
||||||
|
@ -66,16 +41,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
key: _scaffoldKey,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
body: loadingData
|
body: currentTab[_selectedIndex],
|
||||||
? Container(
|
|
||||||
child: Center(
|
|
||||||
child: SpinKitFadingFour(
|
|
||||||
color: Colors.black,
|
|
||||||
size: 100.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: currentTab[_selectedIndex],
|
|
||||||
endDrawer: AppDrawer(),
|
endDrawer: AppDrawer(),
|
||||||
bottomNavigationBar: BottomNavigation(
|
bottomNavigationBar: BottomNavigation(
|
||||||
selectedIndex: _selectedIndex,
|
selectedIndex: _selectedIndex,
|
||||||
|
|
|
@ -1,11 +1,109 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:robo_advisory/providers/user_provider.dart';
|
||||||
|
import 'package:robo_advisory/theme/theme.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:robo_advisory/models/transaction.dart';
|
||||||
|
import 'package:robo_advisory/providers/transaction_provider.dart';
|
||||||
|
import 'package:robo_advisory/screens/history/local_widgets/transaction_list_tile.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
|
||||||
|
// TODO: temp
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
class HistoryScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_HistoryScreenState createState() => _HistoryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistoryScreenState extends State<HistoryScreen> {
|
||||||
|
bool loadingData = true;
|
||||||
|
List<Transaction> transactionData;
|
||||||
|
|
||||||
|
void initState() {
|
||||||
|
TransactionProvider transactionProvider =
|
||||||
|
Provider.of<TransactionProvider>(context, listen: false);
|
||||||
|
transactionProvider.loadTransactions().then((value) {
|
||||||
|
if (value != null) {
|
||||||
|
Timer(Duration(seconds: 3), () {
|
||||||
|
setState(() {
|
||||||
|
loadingData = false;
|
||||||
|
transactionData = value;
|
||||||
|
Provider.of<TransactionProvider>(context, listen: false)
|
||||||
|
.setTransactionsList(transactionData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
print(value);
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
class HistoryScreen extends StatelessWidget {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
List<Transaction> transactionList =
|
||||||
|
Provider.of<TransactionProvider>(context, listen: true).transactionList;
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
child: loadingData
|
||||||
|
? Container(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text('History Screen'),
|
child: SpinKitFadingFour(
|
||||||
|
color: Colors.black,
|
||||||
|
size: 100.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: AppTheme.padding,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
items: Provider.of<UserProvider>(context, listen: true)
|
||||||
|
.categories
|
||||||
|
.map((String value) {
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(value),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
hint: new Text("Select Category"),
|
||||||
|
value: Provider.of<TransactionProvider>(context,
|
||||||
|
listen: true)
|
||||||
|
.selectedCategory,
|
||||||
|
onChanged: (String newValue) {
|
||||||
|
Provider.of<TransactionProvider>(context,
|
||||||
|
listen: false)
|
||||||
|
.setSelectedCategory(newValue);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 20.0,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: transactionList.length != 0
|
||||||
|
? SingleChildScrollView(
|
||||||
|
child: ListView.builder(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: transactionList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return TransactionListTile(
|
||||||
|
transactionObject: transactionList[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Center(
|
||||||
|
child: Text('No Record Found'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
52
lib/screens/history/local_widgets/transaction_list_tile.dart
Normal file
52
lib/screens/history/local_widgets/transaction_list_tile.dart
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:robo_advisory/models/transaction.dart';
|
||||||
|
import 'package:robo_advisory/utils/helpers.dart';
|
||||||
|
|
||||||
|
class TransactionListTile extends StatelessWidget {
|
||||||
|
final Transaction transactionObject;
|
||||||
|
TransactionListTile({this.transactionObject});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
margin: EdgeInsets.all(0),
|
||||||
|
color: Color(0xFFE5E5E5),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(5.0),
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
leading: SizedBox(
|
||||||
|
height: double.infinity,
|
||||||
|
width: 3.0,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: HexColor(transactionObject.category.color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Transform.translate(
|
||||||
|
offset: Offset(-40, 0),
|
||||||
|
child: Text(transactionObject.name),
|
||||||
|
),
|
||||||
|
subtitle: Transform.translate(
|
||||||
|
offset: Offset(-40, 0),
|
||||||
|
child: Text(transactionObject.description),
|
||||||
|
),
|
||||||
|
trailing: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text("${transactionObject.amount} USD"),
|
||||||
|
Text(transactionObject.date)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 20.0,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,18 +2,54 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:robo_advisory/models/wallet.dart';
|
import 'package:robo_advisory/models/wallet.dart';
|
||||||
import 'package:robo_advisory/theme/theme.dart';
|
import 'package:robo_advisory/theme/theme.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:robo_advisory/providers/wallet_provider.dart';
|
||||||
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
||||||
|
import 'package:robo_advisory/screens/home/local_widgets/financial_health_stream.dart';
|
||||||
|
import 'package:robo_advisory/screens/home/local_widgets/expenses_chart.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
// TODO: temp
|
||||||
HomeScreen({@required this.toggleDrawer, @required this.wallet});
|
import 'dart:async';
|
||||||
|
|
||||||
|
class HomeScreen extends StatefulWidget {
|
||||||
|
HomeScreen({@required this.toggleDrawer});
|
||||||
final Function toggleDrawer;
|
final Function toggleDrawer;
|
||||||
final Wallet wallet;
|
@override
|
||||||
double calculatePercentage(double fullWidth, int percentValue) {
|
_HomeScreenState createState() => _HomeScreenState();
|
||||||
return (percentValue / 100) * fullWidth;
|
}
|
||||||
|
|
||||||
|
class _HomeScreenState extends State<HomeScreen> {
|
||||||
|
bool loadingData = true;
|
||||||
|
Wallet walletData;
|
||||||
|
|
||||||
|
void initState() {
|
||||||
|
WalletProvider walletProvider =
|
||||||
|
Provider.of<WalletProvider>(context, listen: false);
|
||||||
|
walletProvider.loadWallet().then((value) {
|
||||||
|
if (value.totalAssets != null) {
|
||||||
|
Timer(Duration(seconds: 3), () {
|
||||||
|
setState(() {
|
||||||
|
loadingData = false;
|
||||||
|
walletData = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return loadingData
|
||||||
|
? Container(
|
||||||
|
child: Center(
|
||||||
|
child: SpinKitFadingFour(
|
||||||
|
color: Colors.black,
|
||||||
|
size: 100.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -34,7 +70,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: toggleDrawer,
|
onTap: widget.toggleDrawer,
|
||||||
child: Icon(Icons.more_vert),
|
child: Icon(Icons.more_vert),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -64,7 +100,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
wallet.totalAssets,
|
walletData.totalAssets,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
@ -94,7 +130,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
wallet.totalTurkishLiraPool,
|
walletData.totalTurkishLiraPool,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
|
@ -124,7 +160,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
wallet.totalGoldInGram,
|
walletData.totalGoldInGram,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
|
@ -141,64 +177,8 @@ class HomeScreen extends StatelessWidget {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30.0,
|
height: 30.0,
|
||||||
),
|
),
|
||||||
Container(
|
FinancialHealthStream(
|
||||||
width: double.infinity,
|
healthSteams: walletData.healthStreams),
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: 20.0, right: 20.0, top: 7.0, bottom: 20.0),
|
|
||||||
color: Color(0xFFE5E5E5),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Financial Health Stream',
|
|
||||||
style: GoogleFonts.inter(
|
|
||||||
textStyle: TextStyle(
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
height: 10.0,
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: calculatePercentage(
|
|
||||||
MediaQuery.of(context).size.width - 100, 30),
|
|
||||||
height: 5.0,
|
|
||||||
child: const DecoratedBox(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF892626),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: calculatePercentage(
|
|
||||||
MediaQuery.of(context).size.width - 100, 50),
|
|
||||||
height: 5.0,
|
|
||||||
child: const DecoratedBox(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF896126),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
|
||||||
width: calculatePercentage(
|
|
||||||
MediaQuery.of(context).size.width - 100, 20),
|
|
||||||
height: 5.0,
|
|
||||||
child: const DecoratedBox(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF348926),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 30.0,
|
height: 30.0,
|
||||||
),
|
),
|
||||||
|
@ -222,7 +202,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
wallet.totalGoldInTurkishLira,
|
walletData.totalGoldInTurkishLira,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
|
@ -252,7 +232,7 @@ class HomeScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
wallet.totalGoldInGram,
|
walletData.totalGoldInGram,
|
||||||
style: GoogleFonts.inter(
|
style: GoogleFonts.inter(
|
||||||
textStyle: TextStyle(
|
textStyle: TextStyle(
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
|
@ -264,6 +244,10 @@ class HomeScreen extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 20.0,
|
||||||
|
),
|
||||||
|
ExpensesChart()
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
107
lib/screens/home/local_widgets/expenses_chart.dart
Normal file
107
lib/screens/home/local_widgets/expenses_chart.dart
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import 'package:fl_chart/fl_chart.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:robo_advisory/providers/transaction_provider.dart';
|
||||||
|
import 'package:robo_advisory/providers/user_provider.dart';
|
||||||
|
|
||||||
|
class ExpensesChart extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => ExpensesChartState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExpensesChartState extends State {
|
||||||
|
int touchedIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<String> categories =
|
||||||
|
Provider.of<UserProvider>(context, listen: true).categories;
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: PieChart(
|
||||||
|
PieChartData(
|
||||||
|
pieTouchData: PieTouchData(touchCallback: (pieTouchResponse) {
|
||||||
|
setState(() {
|
||||||
|
if (pieTouchResponse.touchInput is FlLongPressEnd ||
|
||||||
|
pieTouchResponse.touchInput is FlPanEnd) {
|
||||||
|
touchedIndex = -1;
|
||||||
|
} else {
|
||||||
|
touchedIndex = pieTouchResponse.touchedSectionIndex;
|
||||||
|
if (touchedIndex != null) {
|
||||||
|
Provider.of<UserProvider>(context, listen: false)
|
||||||
|
.setSelectedTabIndex(3);
|
||||||
|
Provider.of<TransactionProvider>(context, listen: false)
|
||||||
|
.setSelectedCategory(categories[touchedIndex + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
borderData: FlBorderData(
|
||||||
|
show: false,
|
||||||
|
),
|
||||||
|
sectionsSpace: 1,
|
||||||
|
centerSpaceRadius: 70,
|
||||||
|
sections: showingSections()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PieChartSectionData> showingSections() {
|
||||||
|
return List.generate(4, (i) {
|
||||||
|
final isTouched = i == touchedIndex;
|
||||||
|
final double fontSize = isTouched ? 25 : 16;
|
||||||
|
final double radius = isTouched ? 60 : 50;
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
return PieChartSectionData(
|
||||||
|
color: const Color(0xff0293ee),
|
||||||
|
value: 50,
|
||||||
|
title: '50%',
|
||||||
|
radius: radius,
|
||||||
|
titleStyle: TextStyle(
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: const Color(0xffffffff)),
|
||||||
|
);
|
||||||
|
case 1:
|
||||||
|
return PieChartSectionData(
|
||||||
|
color: const Color(0xfff8b250),
|
||||||
|
value: 12,
|
||||||
|
title: '12%',
|
||||||
|
radius: radius,
|
||||||
|
titleStyle: TextStyle(
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: const Color(0xffffffff)),
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return PieChartSectionData(
|
||||||
|
color: const Color(0xff13d38e),
|
||||||
|
value: 13,
|
||||||
|
title: '13%',
|
||||||
|
radius: radius,
|
||||||
|
titleStyle: TextStyle(
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: const Color(0xffffffff)),
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return PieChartSectionData(
|
||||||
|
color: const Color(0xff845bef),
|
||||||
|
value: 25,
|
||||||
|
title: '25%',
|
||||||
|
radius: radius,
|
||||||
|
titleStyle: TextStyle(
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: const Color(0xffffffff)),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
137
lib/screens/home/local_widgets/financial_health_stream.dart
Normal file
137
lib/screens/home/local_widgets/financial_health_stream.dart
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:robo_advisory/models/healthSteam.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:flutter_custom_dialog/flutter_custom_dialog.dart';
|
||||||
|
|
||||||
|
class FinancialHealthStream extends StatelessWidget {
|
||||||
|
FinancialHealthStream({@required this.healthSteams});
|
||||||
|
final List<HealthSteam> healthSteams;
|
||||||
|
|
||||||
|
double calculatePercentage(double fullWidth, int percentValue) {
|
||||||
|
return (percentValue / 100) * fullWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
YYDialog.init(context);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: EdgeInsets.only(left: 20.0, right: 20.0, top: 7.0, bottom: 10.0),
|
||||||
|
color: Color(0xFFE5E5E5),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Financial Health Stream',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 3.0,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Day 01',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 10.0,
|
||||||
|
),
|
||||||
|
new LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return Row(
|
||||||
|
children: healthSteams
|
||||||
|
.asMap()
|
||||||
|
.map((index, healthStream) => MapEntry(
|
||||||
|
index,
|
||||||
|
healthStreamBar(context, healthStream, index,
|
||||||
|
constraints.maxWidth)))
|
||||||
|
.values
|
||||||
|
.toList()
|
||||||
|
//
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
GestureDetector healthStreamBar(BuildContext context, HealthSteam healthSteam,
|
||||||
|
int index, double widthOFRow) {
|
||||||
|
final List<Color> healthSteamColors = <Color>[
|
||||||
|
Color(0xFF892626),
|
||||||
|
Color(0xFF896126),
|
||||||
|
Color(0xFF348926)
|
||||||
|
];
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
YYDialog().build(context)
|
||||||
|
..backgroundColor = healthSteamColors[index]
|
||||||
|
..width = 140
|
||||||
|
..borderRadius = 4.0
|
||||||
|
..text(
|
||||||
|
padding: EdgeInsets.only(top: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
text: healthSteam.goldInGram,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
)
|
||||||
|
..text(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
text: healthSteam.goldPriceInTurkishLira,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
)
|
||||||
|
..text(
|
||||||
|
padding: EdgeInsets.only(bottom: 10.0),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
text: "1.00g = " + healthSteam.oneGramGoldPriceInLira,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
)
|
||||||
|
..show();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width:
|
||||||
|
calculatePercentage(widthOFRow, healthSteam.valueInPercentage),
|
||||||
|
height: 5.0,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: healthSteamColors[index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 3.0,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
healthSteam.type,
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
13
lib/utils/helpers.dart
Normal file
13
lib/utils/helpers.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class HexColor extends Color {
|
||||||
|
static int _getColorFromHex(String hexColor) {
|
||||||
|
hexColor = hexColor.toUpperCase().replaceAll("#", "");
|
||||||
|
if (hexColor.length == 6) {
|
||||||
|
hexColor = "FF" + hexColor;
|
||||||
|
}
|
||||||
|
return int.parse(hexColor, radix: 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
|
||||||
|
}
|
21
pubspec.lock
21
pubspec.lock
|
@ -64,6 +64,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.5"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -85,11 +92,25 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.2.1"
|
version: "5.2.1"
|
||||||
|
fl_chart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fl_chart
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_custom_dialog:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_custom_dialog
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.20"
|
||||||
flutter_spinkit:
|
flutter_spinkit:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -35,6 +35,8 @@ dependencies:
|
||||||
pin_code_fields: ^6.0.1
|
pin_code_fields: ^6.0.1
|
||||||
flutter_svg: ^0.19.1
|
flutter_svg: ^0.19.1
|
||||||
google_fonts: ^1.1.1
|
google_fonts: ^1.1.1
|
||||||
|
fl_chart: ^0.12.0
|
||||||
|
flutter_custom_dialog: ^1.0.20
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
Loading…
Reference in a new issue