@marcos_feitoza/personal-finance-frontend-core-ui 1.0.0
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.
- package/.circleci/config.yml +23 -0
- package/.metadata +45 -0
- package/CHANGELOG.md +26 -0
- package/README.md +16 -0
- package/analysis_options.yaml +28 -0
- package/android/app/build.gradle.kts +44 -0
- package/android/app/src/debug/AndroidManifest.xml +7 -0
- package/android/app/src/main/AndroidManifest.xml +45 -0
- package/android/app/src/main/kotlin/com/example/personal_finance_frontend_core_ui/MainActivity.kt +5 -0
- package/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/values/styles.xml +18 -0
- package/android/app/src/main/res/values-night/styles.xml +18 -0
- package/android/app/src/profile/AndroidManifest.xml +7 -0
- package/android/build.gradle.kts +21 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/android/gradle.properties +3 -0
- package/android/settings.gradle.kts +25 -0
- package/ios/Flutter/AppFrameworkInfo.plist +26 -0
- package/ios/Flutter/Debug.xcconfig +1 -0
- package/ios/Flutter/Release.xcconfig +1 -0
- package/ios/Runner/AppDelegate.swift +13 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
- package/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
- package/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
- package/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
- package/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
- package/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
- package/ios/Runner/Base.lproj/Main.storyboard +26 -0
- package/ios/Runner/Info.plist +49 -0
- package/ios/Runner/Runner-Bridging-Header.h +1 -0
- package/ios/Runner.xcodeproj/project.pbxproj +619 -0
- package/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- package/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
- package/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- package/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
- package/ios/RunnerTests/RunnerTests.swift +12 -0
- package/lib/main.dart +122 -0
- package/lib/personal_finance_frontend_core_ui.dart +1 -0
- package/lib/utils/currency_input_formatter.dart +49 -0
- package/lib/utils/currency_utils.dart +14 -0
- package/lib/utils/theme_notifier.dart +33 -0
- package/lib/widgets/app_widgets.dart +405 -0
- package/lib/widgets/crypto_trade_form.dart +357 -0
- package/lib/widgets/dividend_log_form.dart +151 -0
- package/lib/widgets/edit_transaction_dialog.dart +112 -0
- package/lib/widgets/expense_form.dart +238 -0
- package/lib/widgets/investment_form.dart +223 -0
- package/lib/widgets/rrsp_contribution_form.dart +157 -0
- package/lib/widgets/salary_form.dart +152 -0
- package/lib/widgets/trade_form.dart +374 -0
- package/lib/widgets/user_profile_avatar.dart +60 -0
- package/linux/CMakeLists.txt +128 -0
- package/linux/flutter/CMakeLists.txt +88 -0
- package/linux/flutter/generated_plugin_registrant.cc +11 -0
- package/linux/flutter/generated_plugin_registrant.h +15 -0
- package/linux/flutter/generated_plugins.cmake +23 -0
- package/linux/runner/CMakeLists.txt +26 -0
- package/linux/runner/main.cc +6 -0
- package/linux/runner/my_application.cc +130 -0
- package/linux/runner/my_application.h +18 -0
- package/macos/Flutter/Flutter-Debug.xcconfig +1 -0
- package/macos/Flutter/Flutter-Release.xcconfig +1 -0
- package/macos/Flutter/GeneratedPluginRegistrant.swift +10 -0
- package/macos/Runner/AppDelegate.swift +13 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/macos/Runner/Base.lproj/MainMenu.xib +343 -0
- package/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/macos/Runner/Configs/Release.xcconfig +2 -0
- package/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/macos/Runner/DebugProfile.entitlements +12 -0
- package/macos/Runner/Info.plist +32 -0
- package/macos/Runner/MainFlutterWindow.swift +15 -0
- package/macos/Runner/Release.entitlements +8 -0
- package/macos/Runner.xcodeproj/project.pbxproj +705 -0
- package/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
- package/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/macos/RunnerTests/RunnerTests.swift +12 -0
- package/package.json +29 -0
- package/pubspec.yaml +94 -0
- package/test/widget_test.dart +30 -0
- package/web/favicon.png +0 -0
- package/web/icons/Icon-192.png +0 -0
- package/web/icons/Icon-512.png +0 -0
- package/web/icons/Icon-maskable-192.png +0 -0
- package/web/icons/Icon-maskable-512.png +0 -0
- package/web/index.html +38 -0
- package/web/manifest.json +35 -0
- package/windows/CMakeLists.txt +108 -0
- package/windows/flutter/CMakeLists.txt +109 -0
- package/windows/flutter/generated_plugin_registrant.cc +11 -0
- package/windows/flutter/generated_plugin_registrant.h +15 -0
- package/windows/flutter/generated_plugins.cmake +23 -0
- package/windows/runner/CMakeLists.txt +40 -0
- package/windows/runner/Runner.rc +121 -0
- package/windows/runner/flutter_window.cpp +71 -0
- package/windows/runner/flutter_window.h +33 -0
- package/windows/runner/main.cpp +43 -0
- package/windows/runner/resource.h +16 -0
- package/windows/runner/resources/app_icon.ico +0 -0
- package/windows/runner/runner.exe.manifest +14 -0
- package/windows/runner/utils.cpp +65 -0
- package/windows/runner/utils.h +19 -0
- package/windows/runner/win32_window.cpp +288 -0
- package/windows/runner/win32_window.h +102 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:intl/intl.dart';
|
|
3
|
+
import 'package:personal_finance_frontend_core_services/services/crypto_service.dart';
|
|
4
|
+
import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
|
|
5
|
+
import 'package:personal_finance_frontend_core_ui/utils/currency_input_formatter.dart';
|
|
6
|
+
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
7
|
+
|
|
8
|
+
class CryptoTradeForm extends StatefulWidget {
|
|
9
|
+
final String accountName;
|
|
10
|
+
final List<Map<String, dynamic>> portfolioSummary;
|
|
11
|
+
final Function(Map<String, dynamic> tradeData) onTradeCreated;
|
|
12
|
+
final String? token;
|
|
13
|
+
|
|
14
|
+
const CryptoTradeForm(
|
|
15
|
+
{Key? key,
|
|
16
|
+
required this.accountName,
|
|
17
|
+
required this.portfolioSummary,
|
|
18
|
+
required this.onTradeCreated,
|
|
19
|
+
this.token})
|
|
20
|
+
: super(key: key);
|
|
21
|
+
|
|
22
|
+
@override
|
|
23
|
+
_CryptoTradeFormState createState() => _CryptoTradeFormState();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class _CryptoTradeFormState extends State<CryptoTradeForm> {
|
|
27
|
+
final _formKey = GlobalKey<FormState>();
|
|
28
|
+
final TransactionService _transactionService = TransactionService();
|
|
29
|
+
final CryptoService _cryptoService = CryptoService();
|
|
30
|
+
|
|
31
|
+
String _tradeType = 'buy';
|
|
32
|
+
final _symbolController = TextEditingController();
|
|
33
|
+
final _quantityController = TextEditingController();
|
|
34
|
+
final _totalCostController = TextEditingController();
|
|
35
|
+
final _estimatedSellValueController = TextEditingController();
|
|
36
|
+
final _dateController = TextEditingController(
|
|
37
|
+
text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
|
|
38
|
+
DateTime _selectedDate = DateTime.now();
|
|
39
|
+
bool _isSubmitting = false;
|
|
40
|
+
double? _livePriceForSell;
|
|
41
|
+
bool _isFetchingPrice = false;
|
|
42
|
+
String? _selectedSellSymbol;
|
|
43
|
+
|
|
44
|
+
String currencySymbol = r'$';
|
|
45
|
+
|
|
46
|
+
String _formatCurrency(double value) {
|
|
47
|
+
return '$currencySymbol${value.toStringAsFixed(2)}';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void initState() {
|
|
52
|
+
super.initState();
|
|
53
|
+
_quantityController.addListener(_updateEstimatedSellValue);
|
|
54
|
+
_symbolController.addListener(_handleSymbolChange);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
void dispose() {
|
|
59
|
+
_symbolController.removeListener(_handleSymbolChange);
|
|
60
|
+
_quantityController.removeListener(_updateEstimatedSellValue);
|
|
61
|
+
_symbolController.dispose();
|
|
62
|
+
_quantityController.dispose();
|
|
63
|
+
_totalCostController.dispose();
|
|
64
|
+
_estimatedSellValueController.dispose();
|
|
65
|
+
_dateController.dispose();
|
|
66
|
+
super.dispose();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void _handleSymbolChange() {
|
|
70
|
+
if (_symbolController.text.isEmpty) {
|
|
71
|
+
setState(() {
|
|
72
|
+
_livePriceForSell = null;
|
|
73
|
+
_estimatedSellValueController.clear();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Future<void> _updateEstimatedSellValue() async {
|
|
79
|
+
if (_tradeType == 'sell') {
|
|
80
|
+
final symbol = _symbolController.text.toUpperCase();
|
|
81
|
+
if (symbol.isEmpty) {
|
|
82
|
+
setState(() {
|
|
83
|
+
_livePriceForSell = null;
|
|
84
|
+
_estimatedSellValueController.clear();
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
setState(() {
|
|
90
|
+
_isFetchingPrice = true;
|
|
91
|
+
_estimatedSellValueController.text = "Fetching price...";
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
final position = widget.portfolioSummary.firstWhere(
|
|
95
|
+
(p) => p['symbol'] == symbol, orElse: () => {});
|
|
96
|
+
if (position.isEmpty) {
|
|
97
|
+
setState(() {
|
|
98
|
+
_estimatedSellValueController.text = "Asset not in portfolio";
|
|
99
|
+
_isFetchingPrice = false;
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
final idForLookup = (position['id_crypto'] as String? ?? symbol).toLowerCase();
|
|
105
|
+
final livePrices = await _cryptoService.getLiveCryptoPrices([idForLookup]);
|
|
106
|
+
final livePrice = livePrices[idForLookup];
|
|
107
|
+
|
|
108
|
+
if (!mounted) return;
|
|
109
|
+
|
|
110
|
+
setState(() {
|
|
111
|
+
_livePriceForSell = livePrice;
|
|
112
|
+
if (livePrice != null) {
|
|
113
|
+
final shares = double.tryParse(_quantityController.text) ?? 0.0;
|
|
114
|
+
final estimatedValue = shares * livePrice;
|
|
115
|
+
_estimatedSellValueController.text = _formatCurrency(estimatedValue);
|
|
116
|
+
} else {
|
|
117
|
+
_estimatedSellValueController.text = "Price not available";
|
|
118
|
+
}
|
|
119
|
+
_isFetchingPrice = false;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
Future<void> _selectDate(BuildContext context) async {
|
|
125
|
+
final DateTime? picked = await showDatePicker(
|
|
126
|
+
context: context,
|
|
127
|
+
initialDate: _selectedDate,
|
|
128
|
+
firstDate: DateTime(2000),
|
|
129
|
+
lastDate: DateTime(2101),
|
|
130
|
+
);
|
|
131
|
+
if (picked != null && picked != _selectedDate) {
|
|
132
|
+
setState(() {
|
|
133
|
+
_selectedDate = picked;
|
|
134
|
+
_dateController.text = DateFormat('dd/MM/yyyy').format(_selectedDate);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
Future<void> _submitForm() async {
|
|
140
|
+
if (!_formKey.currentState!.validate()) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
final symbol = _symbolController.text;
|
|
145
|
+
final quantity = double.tryParse(_quantityController.text) ?? 0.0;
|
|
146
|
+
final totalCost = CurrencyInputFormatter.unformat(_totalCostController.text);
|
|
147
|
+
final selectedDate = _selectedDate;
|
|
148
|
+
|
|
149
|
+
setState(() {
|
|
150
|
+
_isSubmitting = true;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
Map<String, dynamic> tradeData;
|
|
154
|
+
|
|
155
|
+
if (_tradeType == 'buy') {
|
|
156
|
+
final validationResult = await _cryptoService.validateCryptoSymbol(symbol);
|
|
157
|
+
if (validationResult == null) {
|
|
158
|
+
if (mounted) {
|
|
159
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
160
|
+
SnackBar(content: Text('Invalid crypto ID: "$symbol"'), backgroundColor: Colors.red),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
setState(() => _isSubmitting = false);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
tradeData = {
|
|
167
|
+
'symbol': (validationResult['symbol'] as String).toUpperCase(),
|
|
168
|
+
'name': validationResult['name'] as String?,
|
|
169
|
+
'id_crypto': validationResult['id'] as String,
|
|
170
|
+
'trade_type': 'buy',
|
|
171
|
+
'shares': quantity,
|
|
172
|
+
'price': (quantity > 0) ? totalCost / quantity : 0.0,
|
|
173
|
+
'date': DateFormat('yyyy-MM-dd').format(selectedDate),
|
|
174
|
+
'investment_account': widget.accountName,
|
|
175
|
+
};
|
|
176
|
+
} else { // Sell
|
|
177
|
+
if (_livePriceForSell == null) {
|
|
178
|
+
if (mounted) {
|
|
179
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
180
|
+
const SnackBar(content: Text('Could not get live price. Please try again.'), backgroundColor: Colors.red),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
setState(() => _isSubmitting = false);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
tradeData = {
|
|
187
|
+
'symbol': symbol.toUpperCase(),
|
|
188
|
+
'trade_type': 'sell',
|
|
189
|
+
'shares': quantity,
|
|
190
|
+
'price': _livePriceForSell!,
|
|
191
|
+
'date': DateFormat('yyyy-MM-dd').format(selectedDate),
|
|
192
|
+
'investment_account': widget.accountName,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
final String? errorMessage = await _transactionService.createTrade(tradeData: tradeData, token: widget.token);
|
|
197
|
+
|
|
198
|
+
setState(() {
|
|
199
|
+
_isSubmitting = false;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (mounted) {
|
|
203
|
+
if (errorMessage != null) {
|
|
204
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
205
|
+
SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
209
|
+
SnackBar(content: Text('Trade created successfully!'), backgroundColor: Colors.green),
|
|
210
|
+
);
|
|
211
|
+
widget.onTradeCreated(tradeData);
|
|
212
|
+
_formKey.currentState!.reset();
|
|
213
|
+
_symbolController.clear();
|
|
214
|
+
_quantityController.clear();
|
|
215
|
+
_totalCostController.clear();
|
|
216
|
+
_estimatedSellValueController.clear();
|
|
217
|
+
_selectedSellSymbol = null;
|
|
218
|
+
_dateController.text = DateFormat('dd/MM/yyyy').format(DateTime.now());
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
Widget _buildBuySymbolField() {
|
|
224
|
+
return AppTextField(
|
|
225
|
+
controller: _symbolController,
|
|
226
|
+
labelText: 'Crypto ID (e.g., bitcoin, ethereum)',
|
|
227
|
+
validator: (value) => (value == null || value.isEmpty) ? 'Please enter a crypto ID' : null,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
Widget _buildSellSymbolDropdown() {
|
|
232
|
+
return DropdownButtonFormField<String>(
|
|
233
|
+
value: _selectedSellSymbol,
|
|
234
|
+
hint: const Text('Select Asset to Sell'),
|
|
235
|
+
isExpanded: true,
|
|
236
|
+
items: widget.portfolioSummary.map((position) {
|
|
237
|
+
final symbol = position['symbol'] as String;
|
|
238
|
+
final shares = double.parse(position['shares'].toString());
|
|
239
|
+
final sharesString = shares.toStringAsFixed(shares > 1 ? 2 : 6);
|
|
240
|
+
return DropdownMenuItem<String>(
|
|
241
|
+
value: symbol,
|
|
242
|
+
child: Text('$symbol ($sharesString shares)'),
|
|
243
|
+
);
|
|
244
|
+
}).toList(),
|
|
245
|
+
onChanged: (String? newValue) {
|
|
246
|
+
if (newValue != null) {
|
|
247
|
+
setState(() {
|
|
248
|
+
_selectedSellSymbol = newValue;
|
|
249
|
+
_symbolController.text = newValue;
|
|
250
|
+
_updateEstimatedSellValue();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
validator: (value) => value == null ? 'Please select an asset' : null,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@override
|
|
259
|
+
Widget build(BuildContext context) {
|
|
260
|
+
final bool isSell = _tradeType == 'sell';
|
|
261
|
+
return AppFormCard(
|
|
262
|
+
title: 'Log a New Crypto Trade',
|
|
263
|
+
child: Form(
|
|
264
|
+
key: _formKey,
|
|
265
|
+
child: Column(
|
|
266
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
267
|
+
children: [
|
|
268
|
+
SegmentedButton<String>(
|
|
269
|
+
segments: const <ButtonSegment<String>>[
|
|
270
|
+
ButtonSegment(value: 'buy', label: Text('Buy')),
|
|
271
|
+
ButtonSegment(value: 'sell', label: Text('Sell')),
|
|
272
|
+
],
|
|
273
|
+
selected: <String>{_tradeType},
|
|
274
|
+
onSelectionChanged: (Set<String> newSelection) {
|
|
275
|
+
setState(() {
|
|
276
|
+
_tradeType = newSelection.first;
|
|
277
|
+
_symbolController.clear();
|
|
278
|
+
_quantityController.clear();
|
|
279
|
+
_totalCostController.clear();
|
|
280
|
+
_estimatedSellValueController.clear();
|
|
281
|
+
_livePriceForSell = null;
|
|
282
|
+
_selectedSellSymbol = null;
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
),
|
|
286
|
+
const SizedBox(height: 16),
|
|
287
|
+
if (isSell) _buildSellSymbolDropdown() else _buildBuySymbolField(),
|
|
288
|
+
const SizedBox(height: 16),
|
|
289
|
+
AppTextField(
|
|
290
|
+
controller: _quantityController,
|
|
291
|
+
labelText: 'Quantity',
|
|
292
|
+
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
293
|
+
validator: (value) {
|
|
294
|
+
if (value == null || value.isEmpty) return 'Please enter quantity';
|
|
295
|
+
if (isSell && _symbolController.text.isNotEmpty) {
|
|
296
|
+
final position = widget.portfolioSummary.firstWhere(
|
|
297
|
+
(p) => p['symbol'] == _symbolController.text.toUpperCase(),
|
|
298
|
+
orElse: () => {});
|
|
299
|
+
if (position.isNotEmpty) {
|
|
300
|
+
final heldShares = double.parse(position['shares'].toString());
|
|
301
|
+
final sellingShares = double.tryParse(value) ?? 0.0;
|
|
302
|
+
if (sellingShares > (heldShares + 0.00000001)) {
|
|
303
|
+
return 'Cannot sell more than you hold ($heldShares)';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
},
|
|
309
|
+
),
|
|
310
|
+
const SizedBox(height: 16),
|
|
311
|
+
if (isSell) ...[
|
|
312
|
+
InputDecorator(
|
|
313
|
+
decoration: const InputDecoration(
|
|
314
|
+
labelText: 'Estimated Sell Value',
|
|
315
|
+
border: OutlineInputBorder(),
|
|
316
|
+
),
|
|
317
|
+
child: _isFetchingPrice
|
|
318
|
+
? SizedBox(
|
|
319
|
+
height: 24,
|
|
320
|
+
child: Row(
|
|
321
|
+
children: [
|
|
322
|
+
Text('Fetching price...'),
|
|
323
|
+
Spacer(),
|
|
324
|
+
CircularProgressIndicator(strokeWidth: 2.0),
|
|
325
|
+
],
|
|
326
|
+
),
|
|
327
|
+
)
|
|
328
|
+
: Text(
|
|
329
|
+
_estimatedSellValueController.text.isEmpty
|
|
330
|
+
? '$currencySymbol'
|
|
331
|
+
: _estimatedSellValueController.text,
|
|
332
|
+
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
const SizedBox(height: 16),
|
|
336
|
+
],
|
|
337
|
+
if (!isSell) AppTextField(controller: _totalCostController, labelText: 'Total Purchase Cost', isCurrency: true),
|
|
338
|
+
const SizedBox(height: 16),
|
|
339
|
+
AppTextField(
|
|
340
|
+
controller: _dateController,
|
|
341
|
+
labelText: 'Date',
|
|
342
|
+
readOnly: true,
|
|
343
|
+
onTap: () => _selectDate(context),
|
|
344
|
+
suffixIcon: IconButton(icon: const Icon(Icons.calendar_today), onPressed: () => _selectDate(context)),
|
|
345
|
+
),
|
|
346
|
+
const SizedBox(height: 20),
|
|
347
|
+
AppButton(
|
|
348
|
+
label: 'Log Trade',
|
|
349
|
+
onPressed: _isSubmitting || _isFetchingPrice ? null : _submitForm,
|
|
350
|
+
isLoading: _isSubmitting,
|
|
351
|
+
),
|
|
352
|
+
],
|
|
353
|
+
),
|
|
354
|
+
),
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
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/utils/currency_input_formatter.dart';
|
|
5
|
+
import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
|
|
6
|
+
|
|
7
|
+
class DividendLogForm extends StatefulWidget {
|
|
8
|
+
final String investmentAccount;
|
|
9
|
+
final List<Map<String, dynamic>> assets;
|
|
10
|
+
final Function onDividendLogged;
|
|
11
|
+
final String? token;
|
|
12
|
+
|
|
13
|
+
const DividendLogForm({
|
|
14
|
+
Key? key,
|
|
15
|
+
required this.investmentAccount,
|
|
16
|
+
required this.assets,
|
|
17
|
+
required this.onDividendLogged,
|
|
18
|
+
this.token,
|
|
19
|
+
}) : super(key: key);
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
_DividendLogFormState createState() => _DividendLogFormState();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class _DividendLogFormState extends State<DividendLogForm> {
|
|
26
|
+
final _formKey = GlobalKey<FormState>();
|
|
27
|
+
final TransactionService _transactionService = TransactionService();
|
|
28
|
+
|
|
29
|
+
int? _selectedAssetId;
|
|
30
|
+
final _amountController = TextEditingController();
|
|
31
|
+
final _dateController = TextEditingController(text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
|
|
32
|
+
DateTime _selectedDate = DateTime.now();
|
|
33
|
+
bool _isSubmitting = false;
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
void dispose() {
|
|
37
|
+
_amountController.dispose();
|
|
38
|
+
_dateController.dispose();
|
|
39
|
+
super.dispose();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Future<void> _selectDate(BuildContext context) async {
|
|
43
|
+
final DateTime? picked = await showDatePicker(
|
|
44
|
+
context: context,
|
|
45
|
+
initialDate: _selectedDate,
|
|
46
|
+
firstDate: DateTime(2000),
|
|
47
|
+
lastDate: DateTime(2101),
|
|
48
|
+
);
|
|
49
|
+
if (picked != null && picked != _selectedDate) {
|
|
50
|
+
setState(() {
|
|
51
|
+
_selectedDate = picked;
|
|
52
|
+
_dateController.text = DateFormat('dd/MM/yyyy').format(_selectedDate);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
Future<void> _submitForm() async {
|
|
58
|
+
if (_formKey.currentState!.validate()) {
|
|
59
|
+
setState(() {
|
|
60
|
+
_isSubmitting = true;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
final dividendData = {
|
|
64
|
+
'asset_id': _selectedAssetId!,
|
|
65
|
+
'amount': CurrencyInputFormatter.unformat(_amountController.text),
|
|
66
|
+
'date': DateFormat('yyyy-MM-dd').format(_selectedDate),
|
|
67
|
+
'investment_account': widget.investmentAccount,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
final String? errorMessage = await _transactionService.createDividend(dividendData: dividendData, token: widget.token);
|
|
71
|
+
|
|
72
|
+
setState(() {
|
|
73
|
+
_isSubmitting = false;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (mounted) {
|
|
77
|
+
if (errorMessage != null) {
|
|
78
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
79
|
+
SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
83
|
+
const SnackBar(content: Text('Dividend logged successfully!'), backgroundColor: Colors.green),
|
|
84
|
+
);
|
|
85
|
+
widget.onDividendLogged();
|
|
86
|
+
_formKey.currentState!.reset();
|
|
87
|
+
_amountController.clear();
|
|
88
|
+
_dateController.text = DateFormat('dd/MM/yyyy').format(DateTime.now());
|
|
89
|
+
setState(() {
|
|
90
|
+
_selectedAssetId = null;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
Widget build(BuildContext context) {
|
|
99
|
+
return AppFormCard(
|
|
100
|
+
title: 'Log a New Dividend',
|
|
101
|
+
child: Form(
|
|
102
|
+
key: _formKey,
|
|
103
|
+
child: Column(
|
|
104
|
+
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
105
|
+
children: [
|
|
106
|
+
DropdownButtonFormField<int>(
|
|
107
|
+
value: _selectedAssetId,
|
|
108
|
+
decoration: const InputDecoration(labelText: 'Asset'),
|
|
109
|
+
items: widget.assets.map((asset) {
|
|
110
|
+
return DropdownMenuItem<int>(
|
|
111
|
+
value: asset['id'],
|
|
112
|
+
child: Text(asset['symbol'] ?? 'Unknown'),
|
|
113
|
+
);
|
|
114
|
+
}).toList(),
|
|
115
|
+
onChanged: (int? newValue) {
|
|
116
|
+
setState(() {
|
|
117
|
+
_selectedAssetId = newValue;
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
validator: (value) =>
|
|
121
|
+
value == null ? 'Please select an asset' : null,
|
|
122
|
+
),
|
|
123
|
+
const SizedBox(height: 16),
|
|
124
|
+
AppTextField(
|
|
125
|
+
controller: _amountController,
|
|
126
|
+
labelText: 'Amount',
|
|
127
|
+
isCurrency: true,
|
|
128
|
+
),
|
|
129
|
+
const SizedBox(height: 16),
|
|
130
|
+
AppTextField(
|
|
131
|
+
controller: _dateController,
|
|
132
|
+
labelText: 'Date',
|
|
133
|
+
readOnly: true,
|
|
134
|
+
onTap: () => _selectDate(context),
|
|
135
|
+
suffixIcon: IconButton(
|
|
136
|
+
icon: const Icon(Icons.calendar_today),
|
|
137
|
+
onPressed: () => _selectDate(context),
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
const SizedBox(height: 20),
|
|
141
|
+
AppButton(
|
|
142
|
+
label: 'Log Dividend',
|
|
143
|
+
onPressed: _isSubmitting ? null : _submitForm,
|
|
144
|
+
isLoading: _isSubmitting,
|
|
145
|
+
),
|
|
146
|
+
],
|
|
147
|
+
),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import 'package:flutter/material.dart';
|
|
2
|
+
import 'package:intl/intl.dart';
|
|
3
|
+
import 'package:personal_finance_frontend_feature_management/viewmodels/manage_transactions_viewmodel.dart';
|
|
4
|
+
|
|
5
|
+
class EditTransactionDialog extends StatefulWidget {
|
|
6
|
+
final Transaction transaction;
|
|
7
|
+
final ManageTransactionsViewModel viewModel;
|
|
8
|
+
|
|
9
|
+
const EditTransactionDialog(
|
|
10
|
+
{Key? key, required this.transaction, required this.viewModel})
|
|
11
|
+
: super(key: key);
|
|
12
|
+
|
|
13
|
+
@override
|
|
14
|
+
_EditTransactionDialogState createState() => _EditTransactionDialogState();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class _EditTransactionDialogState extends State<EditTransactionDialog> {
|
|
18
|
+
final _formKey = GlobalKey<FormState>();
|
|
19
|
+
late TextEditingController _amountController;
|
|
20
|
+
late TextEditingController _descriptionController;
|
|
21
|
+
late TextEditingController _dateController;
|
|
22
|
+
late DateTime _selectedDate;
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
void initState() {
|
|
26
|
+
super.initState();
|
|
27
|
+
_amountController = TextEditingController(
|
|
28
|
+
text: NumberFormat.currency(locale: 'en_CA', symbol: '\$')
|
|
29
|
+
.format(widget.transaction.amount));
|
|
30
|
+
_descriptionController =
|
|
31
|
+
TextEditingController(text: widget.transaction.description);
|
|
32
|
+
_selectedDate = widget.transaction.date;
|
|
33
|
+
_dateController =
|
|
34
|
+
TextEditingController(text: DateFormat('yyyy-MM-dd').format(_selectedDate));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
Widget build(BuildContext context) {
|
|
39
|
+
return AlertDialog(
|
|
40
|
+
title: const Text('Edit Transaction'),
|
|
41
|
+
content: Form(
|
|
42
|
+
key: _formKey,
|
|
43
|
+
child: SingleChildScrollView(
|
|
44
|
+
child: Column(
|
|
45
|
+
mainAxisSize: MainAxisSize.min,
|
|
46
|
+
children: <Widget>[
|
|
47
|
+
TextFormField(
|
|
48
|
+
controller: _amountController,
|
|
49
|
+
decoration: const InputDecoration(labelText: 'Amount'),
|
|
50
|
+
keyboardType: TextInputType.number,
|
|
51
|
+
),
|
|
52
|
+
TextFormField(
|
|
53
|
+
controller: _descriptionController,
|
|
54
|
+
decoration: const InputDecoration(labelText: 'Description'),
|
|
55
|
+
),
|
|
56
|
+
TextFormField(
|
|
57
|
+
controller: _dateController,
|
|
58
|
+
decoration: InputDecoration(
|
|
59
|
+
labelText: 'Date',
|
|
60
|
+
suffixIcon: IconButton(
|
|
61
|
+
icon: const Icon(Icons.calendar_today),
|
|
62
|
+
onPressed: () async {
|
|
63
|
+
final pickedDate = await showDatePicker(
|
|
64
|
+
context: context,
|
|
65
|
+
initialDate: _selectedDate,
|
|
66
|
+
firstDate: DateTime(2000),
|
|
67
|
+
lastDate: DateTime(2101),
|
|
68
|
+
);
|
|
69
|
+
if (pickedDate != null) {
|
|
70
|
+
setState(() {
|
|
71
|
+
_selectedDate = pickedDate;
|
|
72
|
+
_dateController.text =
|
|
73
|
+
DateFormat('yyyy-MM-dd').format(_selectedDate);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
readOnly: true,
|
|
80
|
+
),
|
|
81
|
+
],
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
actions: <Widget>[
|
|
86
|
+
TextButton(
|
|
87
|
+
child: const Text('Cancel'),
|
|
88
|
+
onPressed: () {
|
|
89
|
+
Navigator.of(context).pop();
|
|
90
|
+
},
|
|
91
|
+
),
|
|
92
|
+
TextButton(
|
|
93
|
+
child: const Text('Save'),
|
|
94
|
+
onPressed: () {
|
|
95
|
+
if (_formKey.currentState!.validate()) {
|
|
96
|
+
final amount = double.parse(_amountController.text
|
|
97
|
+
.replaceAll(RegExp(r'[^\d.]'), ''));
|
|
98
|
+
final updatedTransaction = {
|
|
99
|
+
'amount': amount,
|
|
100
|
+
'description': _descriptionController.text,
|
|
101
|
+
'date': DateFormat('yyyy-MM-dd').format(_selectedDate),
|
|
102
|
+
};
|
|
103
|
+
widget.viewModel.updateTransaction(
|
|
104
|
+
widget.transaction.id, updatedTransaction);
|
|
105
|
+
Navigator.of(context).pop();
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
),
|
|
109
|
+
],
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|