@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,193 +1,104 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:intl/intl.dart';
3
- import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
4
- import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
5
3
  import 'package:provider/provider.dart';
6
4
  import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
5
+ import 'package:personal_finance_frontend_core_ui/utils/app_dialogs.dart';
6
+ import 'package:personal_finance_frontend_core_ui/widgets/rrsp_contribution_form.dart';
7
+ import '../viewmodels/rrsp_sun_life_viewmodel.dart';
7
8
 
8
- class RrspSunLifeScreen extends StatefulWidget {
9
+ class RrspSunLifeScreen extends StatelessWidget {
9
10
  const RrspSunLifeScreen({Key? key}) : super(key: key);
10
11
 
11
- @override
12
- _RrspSunLifeScreenState createState() => _RrspSunLifeScreenState();
13
- }
14
-
15
- class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
16
- final TransactionService _transactionService = TransactionService();
17
- List<Map<String, dynamic>> _contributions = [];
18
- List<Map<String, dynamic>> _sinteticoSummary = [];
19
- double _rrspTotalValue = 0.0;
20
- double _rrspCashBalance = 0.0;
21
- double _totalPortfolioBookCost = 0.0;
22
- bool _isLoading = true;
23
- String? _token;
24
-
25
- @override
26
- void initState() {
27
- super.initState();
28
- WidgetsBinding.instance.addPostFrameCallback((_) {
29
- final authProvider = Provider.of<AuthProvider>(context, listen: false);
30
- setState(() {
31
- _token = authProvider.token;
32
- });
33
- _fetchData();
34
- });
35
- }
36
-
37
- String currencySymbol = r'$'; // ou poderia vir de configuração, API, etc.
38
-
39
- String _formatCurrency(double value) {
40
- return '$currencySymbol${value.toStringAsFixed(2)}';
41
- }
42
-
43
- Future<void> _fetchData() async {
44
- if (_token == null) return;
45
- if (mounted) {
46
- setState(() {
47
- _isLoading = true;
48
- });
49
- }
50
-
51
- try {
52
- final contributions = await _transactionService.getRrspContributions(
53
- investmentAccount: 'RRSP Sun Life', token: _token);
54
- final balance =
55
- await _transactionService.getAccountBalance('RRSP Sun Life', token: _token);
56
- final totalPortfolioBookCost =
57
- await _transactionService.getTotalPortfolioBookCost(token: _token);
58
-
59
- final sinteticoSummaryData =
60
- _calculateSinteticoSummary(contributions, totalPortfolioBookCost);
61
-
62
- if (!mounted) return;
63
- setState(() {
64
- _contributions = contributions;
65
- _sinteticoSummary = sinteticoSummaryData['summary'];
66
- _rrspCashBalance = balance;
67
- _rrspTotalValue = sinteticoSummaryData['total_value'];
68
- _totalPortfolioBookCost = totalPortfolioBookCost;
69
- });
70
- } catch (e) {
71
- if (!mounted) return;
72
- ScaffoldMessenger.of(context).showSnackBar(
73
- SnackBar(content: Text('Error fetching RRSP data: $e')),
74
- );
75
- } finally {
76
- if (mounted) {
77
- setState(() {
78
- _isLoading = false;
79
- });
80
- }
81
- }
82
- }
83
-
84
- Map<String, dynamic> _calculateSinteticoSummary(
85
- List<Map<String, dynamic>> contributions, double totalPortfolioBookCost) {
86
- double totalUserContribution = 0;
87
- double totalCompanyContribution = 0;
88
- double totalUnrealizedPL = 0;
89
-
90
- for (var c in contributions) {
91
- totalUserContribution += double.parse(c['rrsp_amount'].toString());
92
- totalCompanyContribution += double.parse(c['dpsp_amount'].toString());
93
- totalUnrealizedPL += double.parse(c['return_amount']?.toString() ?? '0.0');
94
- }
95
-
96
- final totalContributed = totalUserContribution + totalCompanyContribution;
97
- final marketValue = totalContributed + totalUnrealizedPL;
98
-
99
- final summaryData = {
100
- 'user_contribution': totalUserContribution,
101
- 'company_contribution': totalCompanyContribution,
102
- 'total_contributed': totalContributed,
103
- 'unrealized_pl': totalUnrealizedPL,
104
- 'percent_return':
105
- (totalContributed > 0) ? (totalUnrealizedPL / totalContributed) * 100 : 0.0,
106
- 'market_value': marketValue,
107
- 'portfolio_allocation_percent': (totalPortfolioBookCost > 0)
108
- ? (marketValue / totalPortfolioBookCost) * 100
109
- : 0.0,
110
- };
111
-
112
- return {
113
- 'summary': [summaryData],
114
- 'total_value': marketValue
115
- };
116
- }
117
-
118
12
  @override
119
13
  Widget build(BuildContext context) {
120
- return Scaffold(
121
- appBar: AppBar(
122
- title: const Text('RRSP Sun Life Portfolio'),
123
- actions: [
124
- Center(
125
- child: Padding(
126
- padding: const EdgeInsets.only(right: 16.0),
127
- child: Column(
128
- mainAxisAlignment: MainAxisAlignment.center,
129
- crossAxisAlignment: CrossAxisAlignment.end,
130
- children: [
131
- Text(
132
- 'Total Portfolio: ${_formatCurrency(_rrspTotalValue)}',
133
- style: const TextStyle(
134
- fontSize: 16, fontWeight: FontWeight.bold),
14
+ final authProvider = Provider.of<AuthProvider>(context, listen: false);
15
+
16
+ return ChangeNotifierProvider(
17
+ create: (_) => RrspSunLifeViewModel(token: authProvider.token),
18
+ child: Consumer<RrspSunLifeViewModel>(
19
+ builder: (context, viewModel, child) {
20
+ return Scaffold(
21
+ appBar: AppBar(
22
+ title: const Text('RRSP Sun Life Portfolio'),
23
+ actions: [
24
+ Center(
25
+ child: Padding(
26
+ padding: const EdgeInsets.only(right: 16.0),
27
+ child: Column(
28
+ mainAxisAlignment: MainAxisAlignment.center,
29
+ crossAxisAlignment: CrossAxisAlignment.end,
30
+ children: [
31
+ Text(
32
+ 'Total Portfolio: ${_formatCurrency(viewModel.rrspTotalValue)}',
33
+ style: const TextStyle(
34
+ fontSize: 16, fontWeight: FontWeight.bold),
35
+ ),
36
+ Text(
37
+ 'Account Balance: ${_formatCurrency(viewModel.rrspCashBalance)}',
38
+ style: const TextStyle(fontSize: 12),
39
+ ),
40
+ ],
41
+ ),
135
42
  ),
136
- Text(
137
- 'Account Balance: ${_formatCurrency(_rrspCashBalance)}',
138
- style: const TextStyle(fontSize: 12),
139
- ),
140
- ],
141
- ),
43
+ ),
44
+ IconButton(
45
+ icon: const Icon(Icons.refresh),
46
+ onPressed: viewModel.fetchData,
47
+ tooltip: 'Refresh Data',
48
+ ),
49
+ ],
142
50
  ),
143
- ),
144
- IconButton(
145
- icon: const Icon(Icons.refresh),
146
- onPressed: _fetchData,
147
- tooltip: 'Refresh Data',
148
- ),
149
- ],
150
- ),
151
- body: _isLoading
152
- ? const Center(child: CircularProgressIndicator())
153
- : SingleChildScrollView(
154
- padding: const EdgeInsets.all(16.0),
155
- child: Column(
156
- crossAxisAlignment: CrossAxisAlignment.stretch,
157
- children: [
158
- Text('Summary',
159
- style: Theme.of(context).textTheme.titleLarge),
160
- const SizedBox(height: 8),
161
- _buildSinteticoTable(),
162
- const SizedBox(height: 24),
163
- RrspContributionForm(
164
- investmentAccount: 'RRSP Sun Life',
165
- onContributionLogged: () => _fetchData(),
166
- token: _token,
167
- ),
168
- const SizedBox(height: 24),
169
- Text(
170
- 'Contribution History',
171
- style: Theme.of(context).textTheme.titleLarge,
51
+ body: viewModel.isLoading
52
+ ? const Center(child: CircularProgressIndicator())
53
+ : RefreshIndicator(
54
+ onRefresh: viewModel.fetchData,
55
+ child: SingleChildScrollView(
56
+ padding: const EdgeInsets.all(16.0),
57
+ child: Column(
58
+ crossAxisAlignment: CrossAxisAlignment.stretch,
59
+ children: [
60
+ Text('Summary',
61
+ style: Theme.of(context).textTheme.titleLarge),
62
+ const SizedBox(height: 8),
63
+ _buildSinteticoTable(context, viewModel),
64
+ const SizedBox(height: 24),
65
+ RrspContributionForm(
66
+ investmentAccount: 'RRSP Sun Life',
67
+ onContributionLogged: viewModel.fetchData,
68
+ token: viewModel.token,
69
+ ),
70
+ const SizedBox(height: 24),
71
+ Text(
72
+ 'Contribution History',
73
+ style: Theme.of(context).textTheme.titleLarge,
74
+ ),
75
+ const SizedBox(height: 8),
76
+ _buildContributionTable(context, viewModel),
77
+ ],
78
+ ),
79
+ ),
172
80
  ),
173
- const SizedBox(height: 8),
174
- _buildContributionTable(),
175
- ],
176
- ),
177
- ),
81
+ );
82
+ },
83
+ ),
178
84
  );
179
85
  }
180
86
 
181
- Widget _buildSinteticoTable() {
182
- if (_sinteticoSummary.isEmpty) {
87
+ String _formatCurrency(double value) {
88
+ String currencySymbol = r'$';
89
+ return '$currencySymbol${value.toStringAsFixed(2)}';
90
+ }
91
+
92
+ Widget _buildSinteticoTable(
93
+ BuildContext context, RrspSunLifeViewModel viewModel) {
94
+ if (viewModel.sinteticoSummary.isEmpty) {
183
95
  return const SizedBox.shrink();
184
96
  }
185
97
 
186
- final summary = _sinteticoSummary.first;
98
+ final summary = viewModel.sinteticoSummary.first;
187
99
  final double unrealizedPL = summary['unrealized_pl'];
188
100
  final plColor = (unrealizedPL >= 0) ? Colors.green : Colors.red;
189
101
 
190
-
191
102
  return DataTable(
192
103
  columns: const [
193
104
  DataColumn(label: Text('User Contr.')),
@@ -201,24 +112,33 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
201
112
  rows: [
202
113
  DataRow(
203
114
  cells: [
204
- DataCell(Text(_formatCurrency(summary['user_contribution'] as double))),
205
- DataCell(Text(_formatCurrency(summary['company_contribution'] as double))),
206
115
  DataCell(
207
- Text(_formatCurrency(summary['total_contributed'] as double))),
208
- DataCell(Text(_formatCurrency(unrealizedPL), style: TextStyle(color: plColor))),
209
- DataCell(Text(
210
- '${(summary['percent_return'] as double).toStringAsFixed(2)}%', style: TextStyle(color: plColor))),
211
- DataCell(Text(_formatCurrency(summary['market_value'] as double))),
116
+ Text(_formatCurrency(summary['user_contribution'] as double))),
212
117
  DataCell(Text(
213
- '${(summary['portfolio_allocation_percent'] as double).toStringAsFixed(2)}%')),
118
+ _formatCurrency(summary['company_contribution'] as double))),
119
+ DataCell(
120
+ Text(_formatCurrency(summary['total_contributed'] as double))),
121
+ DataCell(Text(_formatCurrency(unrealizedPL),
122
+ style: TextStyle(color: plColor))),
123
+ DataCell(
124
+ Text('${(summary['percent_return'] as double).toStringAsFixed(2)}%',
125
+ style: TextStyle(color: plColor)),
126
+ ),
127
+ DataCell(
128
+ Text(_formatCurrency(summary['market_value'] as double))),
129
+ DataCell(
130
+ Text(
131
+ '${(summary['portfolio_allocation_percent'] as double).toStringAsFixed(2)}%'),
132
+ ),
214
133
  ],
215
134
  ),
216
135
  ],
217
136
  );
218
137
  }
219
138
 
220
- Widget _buildContributionTable() {
221
- if (_contributions.isEmpty) {
139
+ Widget _buildContributionTable(
140
+ BuildContext context, RrspSunLifeViewModel viewModel) {
141
+ if (viewModel.contributions.isEmpty) {
222
142
  return const Center(
223
143
  child: Padding(
224
144
  padding: EdgeInsets.all(16.0),
@@ -239,7 +159,7 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
239
159
  DataColumn(label: Text('Portfolio %')),
240
160
  DataColumn(label: Text('Actions')),
241
161
  ],
242
- rows: _contributions.map((c) {
162
+ rows: viewModel.contributions.map((c) {
243
163
  final contributionId = c['id'] as int;
244
164
  final rrspAmount = double.parse(c['rrsp_amount'].toString());
245
165
  final dpspAmount = double.parse(c['dpsp_amount'].toString());
@@ -247,13 +167,14 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
247
167
 
248
168
  final returnAmount =
249
169
  double.parse(c['return_amount']?.toString() ?? '0.0');
250
- final percentReturn =
251
- totalContributed > 0 ? (returnAmount / totalContributed) * 100 : 0.0;
170
+ final percentReturn = totalContributed > 0
171
+ ? (returnAmount / totalContributed) * 100
172
+ : 0.0;
252
173
 
253
174
  final cumulativeValue = totalContributed + returnAmount;
254
175
 
255
- final portfolioAllocation = _totalPortfolioBookCost > 0
256
- ? (cumulativeValue / _totalPortfolioBookCost) * 100
176
+ final portfolioAllocation = viewModel.totalPortfolioBookCost > 0
177
+ ? (cumulativeValue / viewModel.totalPortfolioBookCost) * 100
257
178
  : 0.0;
258
179
 
259
180
  String dateStr;
@@ -279,7 +200,8 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
279
200
  DataCell(
280
201
  IconButton(
281
202
  icon: const Icon(Icons.delete, color: Colors.red),
282
- onPressed: () => _confirmAndDeleteContribution(contributionId),
203
+ onPressed: () => _confirmAndDeleteContribution(
204
+ context, viewModel, contributionId),
283
205
  tooltip: 'Delete Contribution',
284
206
  ),
285
207
  ),
@@ -289,49 +211,22 @@ class _RrspSunLifeScreenState extends State<RrspSunLifeScreen> {
289
211
  );
290
212
  }
291
213
 
292
- Future<void> _confirmAndDeleteContribution(int contributionId) async {
293
- final bool? confirm = await showDialog<bool>(
294
- context: context,
295
- builder: (BuildContext context) {
296
- return AlertDialog(
297
- title: const Text('Confirm Deletion'),
298
- content: const Text('Are you sure you want to delete this contribution? This action cannot be undone.'),
299
- actions: <Widget>[
300
- TextButton(
301
- onPressed: () => Navigator.of(context).pop(false),
302
- child: const Text('Cancel'),
303
- ),
304
- TextButton(
305
- onPressed: () => Navigator.of(context).pop(true),
306
- child: const Text('Delete'),
307
- ),
308
- ],
309
- );
310
- },
214
+ Future<void> _confirmAndDeleteContribution(BuildContext context,
215
+ RrspSunLifeViewModel viewModel, int contributionId) async {
216
+ final bool? confirm = await AppDialogs.showDeleteConfirmationDialog(
217
+ context,
218
+ 'this contribution',
311
219
  );
312
220
 
313
221
  if (confirm == true) {
314
- if (_token == null) {
222
+ final success = await viewModel.deleteContribution(contributionId);
223
+ if (ScaffoldMessenger.of(context).mounted) {
315
224
  ScaffoldMessenger.of(context).showSnackBar(
316
- const SnackBar(content: Text('Authentication token not available.')),
317
- );
318
- return;
319
- }
320
- try {
321
- final success = await _transactionService.deleteRrspContribution(contributionId, token: _token);
322
- if (success) {
323
- ScaffoldMessenger.of(context).showSnackBar(
324
- const SnackBar(content: Text('Contribution deleted successfully.')),
325
- );
326
- _fetchData(); // Refresh data after deletion
327
- } else {
328
- ScaffoldMessenger.of(context).showSnackBar(
329
- const SnackBar(content: Text('Failed to delete contribution.')),
330
- );
331
- }
332
- } catch (e) {
333
- ScaffoldMessenger.of(context).showSnackBar(
334
- SnackBar(content: Text('Error deleting contribution: $e')),
225
+ SnackBar(
226
+ content: Text(success
227
+ ? 'Contribution deleted successfully.'
228
+ : 'Failed to delete contribution.'),
229
+ ),
335
230
  );
336
231
  }
337
232
  }
@@ -0,0 +1,180 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:personal_finance_frontend_core_services/services/crypto_service.dart';
3
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
4
+
5
+ class CryptoAccountViewModel extends ChangeNotifier {
6
+ final TransactionService _transactionService = TransactionService();
7
+ final CryptoService _cryptoService = CryptoService();
8
+
9
+ final String accountName;
10
+ String? _token;
11
+
12
+ List<Map<String, dynamic>> _trades = [];
13
+ List<Map<String, dynamic>> _assets = [];
14
+ List<Map<String, dynamic>> _portfolioSummary = [];
15
+ Map<String, double> _livePrices = {};
16
+ double _cashBalance = 0.0;
17
+ double _accountTotalValue = 0.0;
18
+ bool _isLoading = true;
19
+ bool _isFetchingPrices = false;
20
+
21
+ // Getters
22
+ List<Map<String, dynamic>> get trades => _trades;
23
+ List<Map<String, dynamic>> get assets => _assets;
24
+ List<Map<String, dynamic>> get portfolioSummary => _portfolioSummary;
25
+ Map<String, double> get livePrices => _livePrices;
26
+ double get cashBalance => _cashBalance;
27
+ double get accountTotalValue => _accountTotalValue;
28
+ bool get isLoading => _isLoading;
29
+ bool get isFetchingPrices => _isFetchingPrices;
30
+ String? get token => _token;
31
+
32
+ CryptoAccountViewModel({required this.accountName, required String? token}) {
33
+ _token = token;
34
+ fetchData();
35
+ }
36
+
37
+ void setTokenAndFetch(String? token) {
38
+ _token = token;
39
+ fetchData();
40
+ }
41
+
42
+ Future<void> fetchData() async {
43
+ if (_token == null) return;
44
+ _isLoading = true;
45
+ notifyListeners();
46
+
47
+ try {
48
+ final futures = <Future>[
49
+ _transactionService.getTrades(investmentAccount: accountName, token: _token),
50
+ _transactionService.getAccountBalance(accountName, token: _token),
51
+ _transactionService.getAssets(investmentAccount: accountName, token: _token),
52
+ _transactionService.getTotalPortfolioBookCost(token: _token),
53
+ ];
54
+ final results = await Future.wait(futures);
55
+
56
+ _trades = results[0] as List<Map<String, dynamic>>;
57
+ _cashBalance = results[1] as double;
58
+ _assets = results[2] as List<Map<String, dynamic>>;
59
+ final totalPortfolioBookCost = results[3] as double;
60
+
61
+ _calculateAndApplyPortfolioSummary(totalPortfolioBookCost);
62
+
63
+ await _fetchLivePrices();
64
+
65
+ } catch (e) {
66
+ debugPrint('Error fetching data: $e');
67
+ } finally {
68
+ _isLoading = false;
69
+ notifyListeners();
70
+ }
71
+ }
72
+
73
+ Future<void> _fetchLivePrices() async {
74
+ if (_portfolioSummary.isEmpty) return;
75
+ _isFetchingPrices = true;
76
+ notifyListeners();
77
+
78
+ final idsToFetch = _portfolioSummary
79
+ .map((p) => p['id_crypto'] as String? ?? p['symbol'] as String)
80
+ .where((s) => s.isNotEmpty)
81
+ .toList();
82
+
83
+ if (idsToFetch.isEmpty) {
84
+ _isFetchingPrices = false;
85
+ notifyListeners();
86
+ return;
87
+ }
88
+
89
+ final livePrices = await _cryptoService.getLiveCryptoPrices(idsToFetch);
90
+ _livePrices = livePrices;
91
+
92
+ _recalculateMarketValue();
93
+
94
+ _isFetchingPrices = false;
95
+ notifyListeners();
96
+ }
97
+
98
+ void _calculateAndApplyPortfolioSummary(double totalPortfolioBookCost) {
99
+ Map<String, dynamic> summary = {};
100
+ double accountPortfolioBookCost = 0;
101
+
102
+ for (var trade in _trades) {
103
+ final asset = trade['asset'];
104
+ if (asset == null || asset['symbol'] == null) continue;
105
+ String symbol = asset['symbol'];
106
+ String idCrypto = asset['id_crypto'] ?? symbol;
107
+ double shares = double.parse(trade['shares'].toString());
108
+ double price = double.parse(trade['price'].toString());
109
+ String tradeType = trade['trade_type'];
110
+
111
+ if (!summary.containsKey(symbol)) {
112
+ summary[symbol] = {
113
+ 'symbol': symbol,
114
+ 'id_crypto': idCrypto,
115
+ 'name': asset['name'] ?? symbol,
116
+ 'shares': 0.0,
117
+ 'total_cost': 0.0,
118
+ };
119
+ }
120
+
121
+ if (tradeType == 'buy') {
122
+ summary[symbol]['shares'] += shares;
123
+ summary[symbol]['total_cost'] += shares * price;
124
+ } else if (tradeType == 'sell') {
125
+ double originalShares = summary[symbol]['shares'];
126
+ if (originalShares > 0) {
127
+ double avgPrice = summary[symbol]['total_cost'] / originalShares;
128
+ summary[symbol]['total_cost'] -= shares * avgPrice;
129
+ }
130
+ summary[symbol]['shares'] -= shares;
131
+ }
132
+ }
133
+
134
+ summary.removeWhere((key, value) => value['shares'] < 0.01);
135
+ accountPortfolioBookCost = summary.values.fold(0.0, (sum, item) => sum + item['total_cost']);
136
+
137
+ List<Map<String, dynamic>> result = [];
138
+ summary.forEach((symbol, data) {
139
+ double shares = data['shares'];
140
+ double totalCost = data['total_cost'];
141
+ data['avg_price'] = (shares > 0) ? totalCost / shares : 0.0;
142
+ data['account_allocation'] = (accountPortfolioBookCost > 0) ? (totalCost / accountPortfolioBookCost) * 100 : 0.0;
143
+ data['stocks_allocation'] = (totalPortfolioBookCost > 0) ? (totalCost / totalPortfolioBookCost) * 100 : 0.0;
144
+ result.add(data);
145
+ });
146
+
147
+ _portfolioSummary = result;
148
+ _recalculateMarketValue();
149
+ }
150
+
151
+ void _recalculateMarketValue() {
152
+ double newTotalValue = _cashBalance;
153
+ for (final position in _portfolioSummary) {
154
+ final shares = double.parse(position['shares'].toString());
155
+ final idForLookup = (position['id_crypto'] as String? ?? position['symbol'] as String).toLowerCase();
156
+ final livePrice = _livePrices[idForLookup];
157
+ if (livePrice != null) {
158
+ newTotalValue += shares * livePrice;
159
+ } else {
160
+ newTotalValue += (position['total_cost'] as num?)?.toDouble() ?? 0.0;
161
+ }
162
+ }
163
+ _accountTotalValue = newTotalValue;
164
+ }
165
+
166
+ Future<bool> deleteTrade(int tradeId) async {
167
+ if (_token == null) return false;
168
+
169
+ try {
170
+ final success = await _transactionService.deleteTrade(tradeId, token: _token);
171
+ if (success) {
172
+ fetchData();
173
+ }
174
+ return success;
175
+ } catch (e) {
176
+ debugPrint("Error deleting trade: $e");
177
+ return false;
178
+ }
179
+ }
180
+ }