@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,238 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter/services.dart';
3
+ import 'package:personal_finance_frontend_core_ui/utils/currency_input_formatter.dart';
4
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
5
+ import 'package:personal_finance_frontend_core_services/data/app_data.dart';
6
+ import 'package:intl/intl.dart';
7
+ import 'package:personal_finance_frontend_core_services/models/payment_method.dart';
8
+ import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
9
+
10
+ class ExpenseForm extends StatefulWidget {
11
+ final GlobalKey<FormState> formKey;
12
+ final VoidCallback onTransactionSuccess;
13
+ final String? token; // Add token parameter
14
+
15
+ const ExpenseForm({
16
+ Key? key,
17
+ required this.formKey,
18
+ required this.onTransactionSuccess,
19
+ this.token, // Initialize token
20
+ }) : super(key: key);
21
+
22
+ @override
23
+ _ExpenseFormState createState() => _ExpenseFormState();
24
+ }
25
+
26
+ class _ExpenseFormState extends State<ExpenseForm> {
27
+ Key _autocompleteKey = UniqueKey();
28
+ final _amountController = TextEditingController();
29
+ final _descriptionController = TextEditingController();
30
+ final _dateController = TextEditingController(
31
+ text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
32
+ final _subCategoryController = TextEditingController();
33
+ final TransactionService _transactionService = TransactionService();
34
+
35
+ DateTime _selectedDate = DateTime.now();
36
+ String? _selectedCategory; // This will be inferred
37
+ String?
38
+ _selectedSubCategory; // This will be from the text field or autocomplete
39
+ String _transactionType = 'debit'; // Default to debit
40
+ PaymentMethod? _selectedPaymentMethod; // Default to a non-cash option
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
+ // Function to get all subcategories as a flat list
58
+ List<String> _getAllSubcategories() {
59
+ List<String> allSubcategories = [];
60
+ categories.values.forEach((sublist) {
61
+ allSubcategories.addAll(sublist);
62
+ });
63
+ allSubcategories.sort(); // Sort the list alphabetically
64
+ return allSubcategories;
65
+ }
66
+
67
+ // Function to infer category from subcategory
68
+ String? _inferCategory(String subcategory) {
69
+ return subcategoryToCategory[subcategory];
70
+ }
71
+
72
+ @override
73
+ Widget build(BuildContext context) {
74
+ return AppFormCard(
75
+ title: _transactionType == 'debit' ? 'Add Expense' : 'Add Refund',
76
+ child: Form(
77
+ key: widget.formKey,
78
+ child: Column(
79
+ crossAxisAlignment: CrossAxisAlignment.stretch,
80
+ children: [
81
+ SegmentedButton<String>(
82
+ segments: const <ButtonSegment<String>>[
83
+ ButtonSegment(value: 'debit', label: Text('Expense (Debit)')),
84
+ ButtonSegment(
85
+ value: 'credit', label: Text('Refund (Credit)')),
86
+ ],
87
+ selected: <String>{_transactionType},
88
+ onSelectionChanged: (Set<String> newSelection) {
89
+ setState(() {
90
+ _transactionType = newSelection.first;
91
+ });
92
+ },
93
+ ),
94
+ const SizedBox(height: 16),
95
+
96
+ // Dropdown Payment Method
97
+ AppDropdown<PaymentMethod>(
98
+ value: _selectedPaymentMethod,
99
+ items: PaymentMethod.values.map((PaymentMethod method) {
100
+ return DropdownMenuItem<PaymentMethod>(
101
+ value: method,
102
+ child: Text(paymentMethodToString(method)),
103
+ );
104
+ }).toList(),
105
+ onChanged: (PaymentMethod? newValue) {
106
+ if (newValue != null) {
107
+ setState(() {
108
+ _selectedPaymentMethod = newValue;
109
+ });
110
+ }
111
+ },
112
+ hint: 'Payment Method',
113
+ validator: (value) =>
114
+ value == null ? 'Please select a payment method' : null,
115
+ ),
116
+
117
+ const SizedBox(height: 16),
118
+ Autocomplete<String>(
119
+ key: _autocompleteKey,
120
+ optionsBuilder: (TextEditingValue textEditingValue) {
121
+ if (textEditingValue.text == '') {
122
+ return _getAllSubcategories();
123
+ }
124
+ return _getAllSubcategories().where((String option) {
125
+ return option
126
+ .toLowerCase()
127
+ .startsWith(textEditingValue.text.toLowerCase());
128
+ });
129
+ },
130
+ onSelected: (String selection) {
131
+ setState(() {
132
+ _subCategoryController.text = selection;
133
+ _selectedSubCategory = selection;
134
+ _selectedCategory = _inferCategory(selection);
135
+ });
136
+ },
137
+ fieldViewBuilder: (BuildContext context,
138
+ TextEditingController textEditingController,
139
+ FocusNode focusNode,
140
+ VoidCallback onFieldSubmitted) {
141
+ return AppTextField(
142
+ controller: textEditingController,
143
+ focusNode: focusNode,
144
+ labelText: 'Subcategory',
145
+ validator: (value) {
146
+ if (value == null || value.isEmpty) {
147
+ return 'Please enter a subcategory';
148
+ }
149
+ if (!_getAllSubcategories().contains(value)) {
150
+ return 'Please select a valid subcategory';
151
+ }
152
+ return null;
153
+ },
154
+ );
155
+ },
156
+ ),
157
+ const SizedBox(height: 16),
158
+
159
+ /// AMOUNT
160
+ AppTextField(
161
+ controller: _amountController,
162
+ labelText: 'Amount',
163
+ isCurrency: true,
164
+ ),
165
+
166
+ const SizedBox(height: 16),
167
+
168
+ /// Campo Description
169
+ AppTextField(
170
+ controller: _descriptionController,
171
+ labelText: 'Description',
172
+ ),
173
+ const SizedBox(height: 16),
174
+
175
+ // DATE
176
+ AppTextField(
177
+ controller: _dateController,
178
+ labelText: 'Date',
179
+ readOnly: true,
180
+ onTap: () => _selectDate(context),
181
+ suffixIcon: IconButton(
182
+ icon: const Icon(Icons.calendar_today, color: Colors.white70),
183
+ onPressed: () => _selectDate(context),
184
+ ),
185
+ ),
186
+
187
+ const SizedBox(height: 20),
188
+ AppButton(
189
+ label: _transactionType == 'debit' ? 'Add Expense' : 'Add Refund',
190
+ onPressed: () async {
191
+ if (widget.formKey.currentState!.validate()) {
192
+ double amount = CurrencyInputFormatter.unformat(_amountController.text);
193
+
194
+ await _transactionService.submitTransaction(
195
+ _transactionType,
196
+ amount,
197
+ _selectedCategory ??
198
+ 'Other', // Use inferred category or 'Other'
199
+ _selectedSubCategory ??
200
+ _subCategoryController
201
+ .text, // Use selected or typed subcategory
202
+ _descriptionController.text,
203
+ _selectedDate, // Pass the selected date
204
+ _selectedPaymentMethod,
205
+ token: widget.token, // Pass the token
206
+ );
207
+ widget.onTransactionSuccess();
208
+ _amountController.text = '0.00';
209
+ _descriptionController.clear();
210
+ _subCategoryController.clear();
211
+ _dateController.text =
212
+ DateFormat('dd/MM/yyyy').format(DateTime.now());
213
+ setState(() {
214
+ _autocompleteKey = UniqueKey();
215
+ _selectedDate = DateTime.now();
216
+ _selectedCategory = null;
217
+ _selectedSubCategory = null;
218
+ _selectedPaymentMethod = null; // Reset to default
219
+ });
220
+ }
221
+ },
222
+ ),
223
+ ],
224
+ ),
225
+ ),
226
+ );
227
+ }
228
+
229
+ // Helper function to find category for a given subcategory
230
+ String? findCategoryForSubcategory(String subcategory) {
231
+ for (var entry in categories.entries) {
232
+ if (entry.value.contains(subcategory)) {
233
+ return entry.key;
234
+ }
235
+ }
236
+ return null;
237
+ }
238
+ }
@@ -0,0 +1,223 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter/services.dart';
3
+ import 'package:intl/intl.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
+ // TOP-LEVEL CURRENCY FORMATTING
9
+ final String currencySymbol = r'$';
10
+
11
+ String _formatCurrency(double value) {
12
+ final format = NumberFormat.currency(
13
+ symbol: currencySymbol,
14
+ decimalDigits: 2,
15
+ locale: 'en_US', // garante vírgula de milhar e ponto decimal no padrão US
16
+ );
17
+ return format.format(value);
18
+ }
19
+
20
+ class InvestmentForm extends StatefulWidget {
21
+ final GlobalKey<FormState> formKey;
22
+ final Map<String, double> accountBalances;
23
+ final double cashBalance;
24
+ final VoidCallback onTransactionSuccess;
25
+ final String? token; // Add token parameter
26
+
27
+ const InvestmentForm({
28
+ Key? key,
29
+ required this.formKey,
30
+ required this.accountBalances,
31
+ required this.cashBalance,
32
+ required this.onTransactionSuccess,
33
+ this.token, // Initialize token
34
+ }) : super(key: key);
35
+
36
+ @override
37
+ _InvestmentFormState createState() => _InvestmentFormState();
38
+ }
39
+
40
+ class _InvestmentFormState extends State<InvestmentForm> {
41
+ final _amountController = TextEditingController();
42
+ final _dateController = TextEditingController(
43
+ text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
44
+
45
+ final TransactionService _transactionService = TransactionService();
46
+
47
+ String? _selectedSourceAccount;
48
+ String? _selectedDestinationAccount;
49
+ DateTime _selectedDate = DateTime.now();
50
+ bool _isSubmitting = false;
51
+
52
+ // Combine cash and investment accounts
53
+ late final Map<String, double> _allAccounts;
54
+
55
+ @override
56
+ void initState() {
57
+ super.initState();
58
+ _allAccounts = {
59
+ 'CASH': widget.cashBalance,
60
+ ...widget.accountBalances,
61
+ // Manually add RRSP Sun Life if not covered by balances endpoint
62
+ if (!widget.accountBalances.containsKey('RRSP Sun Life'))
63
+ 'RRSP Sun Life': 0.0,
64
+ };
65
+ }
66
+
67
+ Future<void> _selectDate(BuildContext context) async {
68
+ final DateTime? picked = await showDatePicker(
69
+ context: context,
70
+ initialDate: _selectedDate,
71
+ firstDate: DateTime(2000),
72
+ lastDate: DateTime(2101),
73
+ );
74
+ if (picked != null && picked != _selectedDate) {
75
+ setState(() {
76
+ _selectedDate = picked;
77
+ _dateController.text = DateFormat('dd/MM/yyyy').format(_selectedDate);
78
+ });
79
+ }
80
+ }
81
+
82
+ Future<void> _submitForm() async {
83
+ if (widget.formKey.currentState!.validate()) {
84
+ setState(() {
85
+ _isSubmitting = true;
86
+ });
87
+
88
+ final String? errorMessage = await _transactionService.submitInvestment(
89
+ amount: CurrencyInputFormatter.unformat(_amountController.text),
90
+ sourceAccount: _selectedSourceAccount!,
91
+ destinationAccount: _selectedDestinationAccount!,
92
+ date: _selectedDate,
93
+ token: widget.token, // Pass the token
94
+ );
95
+
96
+ setState(() {
97
+ _isSubmitting = false;
98
+ });
99
+
100
+ if (mounted) {
101
+ if (errorMessage != null) {
102
+ showDialog(
103
+ context: context,
104
+ builder: (ctx) => AlertDialog(
105
+ title: const Text('Transaction Error'),
106
+ content: Text(errorMessage),
107
+ actions: <Widget>[
108
+ TextButton(
109
+ child: const Text('Transfer successful'),
110
+ onPressed: () {
111
+ Navigator.of(ctx).pop();
112
+ },
113
+ )
114
+ ],
115
+ ),
116
+ );
117
+ } else {
118
+ widget.onTransactionSuccess(); // Call the callback here
119
+ widget.formKey.currentState!.reset();
120
+ _amountController.clear();
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ @override
127
+ Widget build(BuildContext context) {
128
+ // Atualiza os saldos das contas
129
+ _allAccounts['CASH'] = widget.cashBalance;
130
+ widget.accountBalances.forEach((key, value) {
131
+ _allAccounts[key] = value;
132
+ });
133
+
134
+ return AppFormCard(
135
+ title: 'Move Money',
136
+ child: Form(
137
+ key: widget.formKey,
138
+ child: Column(
139
+ crossAxisAlignment: CrossAxisAlignment.stretch,
140
+ children: [
141
+ // Conta de origem
142
+ AppDropdown<String>(
143
+ value: _selectedSourceAccount,
144
+ hint: 'From (Source)',
145
+ items: _allAccounts.keys.map((String account) {
146
+ final balance = _allAccounts[account] ?? 0.0;
147
+ final formattedBalance = _formatCurrency(balance);
148
+ return DropdownMenuItem<String>(
149
+ value: account,
150
+ child: Text('$account - $formattedBalance'),
151
+ );
152
+ }).toList(),
153
+ onChanged: (String? newValue) {
154
+ setState(() {
155
+ _selectedSourceAccount = newValue;
156
+ });
157
+ },
158
+ validator: (value) =>
159
+ value == null ? 'Please select a source account' : null,
160
+ ),
161
+ const SizedBox(height: 16),
162
+
163
+ // Conta de destino
164
+ AppDropdown<String>(
165
+ value: _selectedDestinationAccount,
166
+ hint: 'To (Destination)',
167
+ items: _allAccounts.keys.map((String account) {
168
+ final balance = _allAccounts[account] ?? 0.0;
169
+ final formattedBalance = _formatCurrency(balance);
170
+ return DropdownMenuItem<String>(
171
+ value: account,
172
+ child: Text('$account - $formattedBalance'),
173
+ );
174
+ }).toList(),
175
+ onChanged: (String? newValue) {
176
+ setState(() {
177
+ _selectedDestinationAccount = newValue;
178
+ });
179
+ },
180
+ validator: (value) {
181
+ if (value == null) {
182
+ return 'Please select a destination account';
183
+ }
184
+ if (value == _selectedSourceAccount) {
185
+ return 'Destination cannot be the same as source';
186
+ }
187
+ return null;
188
+ },
189
+ ),
190
+ const SizedBox(height: 16),
191
+
192
+ // Campo de valor (com formatação de moeda)
193
+ AppTextField(
194
+ controller: _amountController,
195
+ labelText: 'Amount',
196
+ isCurrency: true,
197
+ ), const SizedBox(height: 16),
198
+
199
+ // Campo de data (com seletor de calendário)
200
+ AppTextField(
201
+ controller: _dateController,
202
+ labelText: 'Date',
203
+ readOnly: true,
204
+ onTap: () => _selectDate(context),
205
+ suffixIcon: IconButton(
206
+ icon: const Icon(Icons.calendar_today, color: Colors.white70),
207
+ onPressed: () => _selectDate(context),
208
+ ),
209
+ ),
210
+ const SizedBox(height: 16),
211
+
212
+ // BOTÃO — aqui é onde entra o AppButton customizado
213
+ AppButton(
214
+ label: _isSubmitting ? 'Processing...' : 'Add Transfer',
215
+ onPressed: _submitForm,
216
+ isLoading: _isSubmitting,
217
+ ),
218
+ ],
219
+ ),
220
+ ),
221
+ );
222
+ }
223
+ }
@@ -0,0 +1,157 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:flutter/services.dart';
3
+ import 'package:intl/intl.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 RrspContributionForm extends StatefulWidget {
9
+ final String investmentAccount;
10
+ final Function() onContributionLogged;
11
+ final String? token;
12
+
13
+ const RrspContributionForm({Key? key, required this.investmentAccount, required this.onContributionLogged, this.token}) : super(key: key);
14
+
15
+ @override
16
+ _RrspContributionFormState createState() => _RrspContributionFormState();
17
+ }
18
+
19
+ class _RrspContributionFormState extends State<RrspContributionForm> {
20
+ final _formKey = GlobalKey<FormState>();
21
+ final TransactionService _transactionService = TransactionService();
22
+
23
+ final _rrspAmountController = TextEditingController();
24
+ final _dpspAmountController = TextEditingController();
25
+ final _returnAmountController = TextEditingController();
26
+ final _dateController = TextEditingController(text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
27
+ DateTime _selectedDate = DateTime.now();
28
+ bool _isSubmitting = false;
29
+
30
+ @override
31
+ void dispose() {
32
+ _rrspAmountController.dispose();
33
+ _dpspAmountController.dispose();
34
+ _returnAmountController.dispose();
35
+ _dateController.dispose();
36
+ super.dispose();
37
+ }
38
+
39
+ Future<void> _selectDate(BuildContext context) async {
40
+ final DateTime? picked = await showDatePicker(
41
+ context: context,
42
+ initialDate: _selectedDate,
43
+ firstDate: DateTime(2000),
44
+ lastDate: DateTime(2101),
45
+ );
46
+ if (picked != null && picked != _selectedDate) {
47
+ setState(() {
48
+ _selectedDate = picked;
49
+ _dateController.text = DateFormat('dd/MM/yyyy').format(_selectedDate);
50
+ });
51
+ }
52
+ }
53
+
54
+ Future<void> _submitForm() async {
55
+ if (_formKey.currentState!.validate()) {
56
+ setState(() {
57
+ _isSubmitting = true;
58
+ });
59
+
60
+ final rrspAmount = CurrencyInputFormatter.unformat(_rrspAmountController.text);
61
+ final dpspAmount = _dpspAmountController.text.isEmpty ? 0.0 : CurrencyInputFormatter.unformat(_dpspAmountController.text);
62
+ final returnAmount = _returnAmountController.text.isEmpty ? 0.0 : CurrencyInputFormatter.unformat(_returnAmountController.text);
63
+
64
+ final contributionData = {
65
+ 'date': DateFormat('yyyy-MM-dd').format(_selectedDate),
66
+ 'rrsp_amount': rrspAmount,
67
+ 'dpsp_amount': dpspAmount,
68
+ 'return_amount': returnAmount,
69
+ 'investment_account': widget.investmentAccount,
70
+ };
71
+
72
+ final String? errorMessage = await _transactionService.createRrspContribution(contributionData: contributionData, token: widget.token);
73
+
74
+ setState(() {
75
+ _isSubmitting = false;
76
+ });
77
+
78
+ if (mounted) {
79
+ if (errorMessage != null) {
80
+ ScaffoldMessenger.of(context).showSnackBar(
81
+ SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
82
+ );
83
+ } else {
84
+ ScaffoldMessenger.of(context).showSnackBar(
85
+ const SnackBar(content: Text('RRSP Contribution logged successfully!'), backgroundColor: Colors.green),
86
+ );
87
+ widget.onContributionLogged(); // Notify parent to refresh
88
+ _formKey.currentState!.reset();
89
+ _rrspAmountController.clear();
90
+ _dpspAmountController.clear();
91
+ _returnAmountController.clear();
92
+ _dateController.text = DateFormat('dd/MM/yyyy').format(DateTime.now());
93
+ setState(() {
94
+ _selectedDate = DateTime.now();
95
+ });
96
+ }
97
+ }
98
+ }
99
+ }
100
+
101
+ @override
102
+ Widget build(BuildContext context) {
103
+ return AppFormCard(
104
+ title: 'Log RRSP Contribution',
105
+ child: Form(
106
+ key: _formKey,
107
+ child: Column(
108
+ crossAxisAlignment: CrossAxisAlignment.stretch,
109
+ children: [
110
+ AppTextField(
111
+ controller: _rrspAmountController,
112
+ labelText: 'Your RRSP Contribution',
113
+ isCurrency: true,
114
+ validator: (value) {
115
+ if (value == null || value.isEmpty || CurrencyInputFormatter.unformat(value) == 0) {
116
+ return 'Please enter your contribution';
117
+ }
118
+ return null;
119
+ },
120
+ ),
121
+ const SizedBox(height: 16),
122
+ AppTextField(
123
+ controller: _dpspAmountController,
124
+ labelText: 'DPSP Amount (Optional)',
125
+ isCurrency: true,
126
+ validator: (value) => null, // Always valid
127
+ ),
128
+ const SizedBox(height: 16),
129
+ AppTextField(
130
+ controller: _returnAmountController,
131
+ labelText: 'Return Amount (Optional)',
132
+ isCurrency: true,
133
+ validator: (value) => null, // Always valid
134
+ ),
135
+ const SizedBox(height: 16),
136
+ AppTextField(
137
+ controller: _dateController,
138
+ labelText: 'Date',
139
+ readOnly: true,
140
+ onTap: () => _selectDate(context),
141
+ suffixIcon: IconButton(
142
+ icon: const Icon(Icons.calendar_today),
143
+ onPressed: () => _selectDate(context),
144
+ ),
145
+ ),
146
+ const SizedBox(height: 20),
147
+ AppButton(
148
+ label: 'Add Contribution',
149
+ onPressed: _isSubmitting ? null : _submitForm,
150
+ isLoading: _isSubmitting,
151
+ ),
152
+ ],
153
+ ),
154
+ ),
155
+ );
156
+ }
157
+ }