@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,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
|
+
}
|