@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.
Files changed (139) hide show
  1. package/.circleci/config.yml +23 -0
  2. package/.metadata +45 -0
  3. package/CHANGELOG.md +26 -0
  4. package/README.md +16 -0
  5. package/analysis_options.yaml +28 -0
  6. package/android/app/build.gradle.kts +44 -0
  7. package/android/app/src/debug/AndroidManifest.xml +7 -0
  8. package/android/app/src/main/AndroidManifest.xml +45 -0
  9. package/android/app/src/main/kotlin/com/example/personal_finance_frontend_core_ui/MainActivity.kt +5 -0
  10. package/android/app/src/main/res/drawable/launch_background.xml +12 -0
  11. package/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  12. package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  13. package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  14. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  15. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  16. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  17. package/android/app/src/main/res/values/styles.xml +18 -0
  18. package/android/app/src/main/res/values-night/styles.xml +18 -0
  19. package/android/app/src/profile/AndroidManifest.xml +7 -0
  20. package/android/build.gradle.kts +21 -0
  21. package/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  22. package/android/gradle.properties +3 -0
  23. package/android/settings.gradle.kts +25 -0
  24. package/ios/Flutter/AppFrameworkInfo.plist +26 -0
  25. package/ios/Flutter/Debug.xcconfig +1 -0
  26. package/ios/Flutter/Release.xcconfig +1 -0
  27. package/ios/Runner/AppDelegate.swift +13 -0
  28. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  29. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  30. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  31. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  32. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  33. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  34. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  35. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  36. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  37. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  38. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  39. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  40. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  41. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  42. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  43. package/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  44. package/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  45. package/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  46. package/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  47. package/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  48. package/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  49. package/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  50. package/ios/Runner/Base.lproj/Main.storyboard +26 -0
  51. package/ios/Runner/Info.plist +49 -0
  52. package/ios/Runner/Runner-Bridging-Header.h +1 -0
  53. package/ios/Runner.xcodeproj/project.pbxproj +619 -0
  54. package/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  55. package/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  56. package/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  57. package/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  58. package/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  59. package/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  60. package/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  61. package/ios/RunnerTests/RunnerTests.swift +12 -0
  62. package/lib/main.dart +122 -0
  63. package/lib/personal_finance_frontend_core_ui.dart +1 -0
  64. package/lib/utils/currency_input_formatter.dart +49 -0
  65. package/lib/utils/currency_utils.dart +14 -0
  66. package/lib/utils/theme_notifier.dart +33 -0
  67. package/lib/widgets/app_widgets.dart +405 -0
  68. package/lib/widgets/crypto_trade_form.dart +357 -0
  69. package/lib/widgets/dividend_log_form.dart +151 -0
  70. package/lib/widgets/edit_transaction_dialog.dart +112 -0
  71. package/lib/widgets/expense_form.dart +238 -0
  72. package/lib/widgets/investment_form.dart +223 -0
  73. package/lib/widgets/rrsp_contribution_form.dart +157 -0
  74. package/lib/widgets/salary_form.dart +152 -0
  75. package/lib/widgets/trade_form.dart +374 -0
  76. package/lib/widgets/user_profile_avatar.dart +60 -0
  77. package/linux/CMakeLists.txt +128 -0
  78. package/linux/flutter/CMakeLists.txt +88 -0
  79. package/linux/flutter/generated_plugin_registrant.cc +11 -0
  80. package/linux/flutter/generated_plugin_registrant.h +15 -0
  81. package/linux/flutter/generated_plugins.cmake +23 -0
  82. package/linux/runner/CMakeLists.txt +26 -0
  83. package/linux/runner/main.cc +6 -0
  84. package/linux/runner/my_application.cc +130 -0
  85. package/linux/runner/my_application.h +18 -0
  86. package/macos/Flutter/Flutter-Debug.xcconfig +1 -0
  87. package/macos/Flutter/Flutter-Release.xcconfig +1 -0
  88. package/macos/Flutter/GeneratedPluginRegistrant.swift +10 -0
  89. package/macos/Runner/AppDelegate.swift +13 -0
  90. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  91. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  92. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  93. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  94. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  95. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  96. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  97. package/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  98. package/macos/Runner/Base.lproj/MainMenu.xib +343 -0
  99. package/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  100. package/macos/Runner/Configs/Debug.xcconfig +2 -0
  101. package/macos/Runner/Configs/Release.xcconfig +2 -0
  102. package/macos/Runner/Configs/Warnings.xcconfig +13 -0
  103. package/macos/Runner/DebugProfile.entitlements +12 -0
  104. package/macos/Runner/Info.plist +32 -0
  105. package/macos/Runner/MainFlutterWindow.swift +15 -0
  106. package/macos/Runner/Release.entitlements +8 -0
  107. package/macos/Runner.xcodeproj/project.pbxproj +705 -0
  108. package/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  109. package/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  110. package/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  111. package/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  112. package/macos/RunnerTests/RunnerTests.swift +12 -0
  113. package/package.json +29 -0
  114. package/pubspec.yaml +94 -0
  115. package/test/widget_test.dart +30 -0
  116. package/web/favicon.png +0 -0
  117. package/web/icons/Icon-192.png +0 -0
  118. package/web/icons/Icon-512.png +0 -0
  119. package/web/icons/Icon-maskable-192.png +0 -0
  120. package/web/icons/Icon-maskable-512.png +0 -0
  121. package/web/index.html +38 -0
  122. package/web/manifest.json +35 -0
  123. package/windows/CMakeLists.txt +108 -0
  124. package/windows/flutter/CMakeLists.txt +109 -0
  125. package/windows/flutter/generated_plugin_registrant.cc +11 -0
  126. package/windows/flutter/generated_plugin_registrant.h +15 -0
  127. package/windows/flutter/generated_plugins.cmake +23 -0
  128. package/windows/runner/CMakeLists.txt +40 -0
  129. package/windows/runner/Runner.rc +121 -0
  130. package/windows/runner/flutter_window.cpp +71 -0
  131. package/windows/runner/flutter_window.h +33 -0
  132. package/windows/runner/main.cpp +43 -0
  133. package/windows/runner/resource.h +16 -0
  134. package/windows/runner/resources/app_icon.ico +0 -0
  135. package/windows/runner/runner.exe.manifest +14 -0
  136. package/windows/runner/utils.cpp +65 -0
  137. package/windows/runner/utils.h +19 -0
  138. package/windows/runner/win32_window.cpp +288 -0
  139. 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
+ }