@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,152 @@
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:intl/intl.dart';
6
+ import 'package:personal_finance_frontend_core_services/models/payment_method.dart';
7
+ import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
8
+
9
+ class SalaryForm extends StatefulWidget {
10
+ final GlobalKey<FormState> formKey;
11
+ final VoidCallback onTransactionSuccess;
12
+ final String? token; // Add token parameter
13
+
14
+ const SalaryForm({
15
+ Key? key,
16
+ required this.formKey,
17
+ required this.onTransactionSuccess,
18
+ this.token, // Initialize token
19
+ }) : super(key: key);
20
+
21
+ @override
22
+ _SalaryFormState createState() => _SalaryFormState();
23
+ }
24
+
25
+ class _SalaryFormState extends State<SalaryForm> {
26
+ final _salaryController = TextEditingController();
27
+ final _dateController = TextEditingController(
28
+ text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
29
+ final _descriptionController = TextEditingController();
30
+ final TransactionService _transactionService = TransactionService();
31
+
32
+ DateTime _selectedDate = DateTime.now();
33
+ String? _selectedSalaryType;
34
+ bool _isSubmitting = false;
35
+
36
+ final List<String> _salaryTypes = ['Salary 1', 'Salary 2', 'Extra'];
37
+
38
+ Future<void> _selectDate(BuildContext context) async {
39
+ final DateTime? picked = await showDatePicker(
40
+ context: context,
41
+ initialDate: _selectedDate,
42
+ firstDate: DateTime(2000),
43
+ lastDate: DateTime(2101),
44
+ );
45
+ if (picked != null && picked != _selectedDate) {
46
+ setState(() {
47
+ _selectedDate = picked;
48
+ _dateController.text = DateFormat('dd/MM/yyyy').format(_selectedDate);
49
+ });
50
+ }
51
+ }
52
+
53
+ @override
54
+ Widget build(BuildContext context) {
55
+ return AppFormCard(
56
+ title: 'Add Salary',
57
+ child: Form(
58
+ key: widget.formKey,
59
+ child: Column(
60
+ crossAxisAlignment: CrossAxisAlignment.stretch,
61
+ children: [
62
+ /// Dropdown Salary type
63
+ AppDropdown<String>(
64
+ value: _selectedSalaryType,
65
+ items: _salaryTypes
66
+ .map((type) => DropdownMenuItem(
67
+ value: type,
68
+ child: Text(type),
69
+ ))
70
+ .toList(),
71
+ onChanged: (newValue) {
72
+ setState(() => _selectedSalaryType = newValue);
73
+ print('Selected Salary Type: $newValue');
74
+ },
75
+ hint: 'Salary Type',
76
+ ),
77
+
78
+ const SizedBox(height: 16),
79
+
80
+ /// AMOUNT
81
+ AppTextField(
82
+ controller: _salaryController,
83
+ labelText: 'Amount',
84
+ isCurrency: true,
85
+ ),
86
+
87
+ const SizedBox(height: 16),
88
+
89
+ // DATE
90
+ AppTextField(
91
+ controller: _dateController,
92
+ labelText: 'Date',
93
+ readOnly: true,
94
+ onTap: () => _selectDate(context),
95
+ suffixIcon: IconButton(
96
+ icon: const Icon(Icons.calendar_today, color: Colors.white70),
97
+ onPressed: () => _selectDate(context),
98
+ ),
99
+ ),
100
+
101
+ const SizedBox(height: 16),
102
+
103
+ /// Campo Description
104
+ AppTextField(
105
+ controller: _descriptionController,
106
+ labelText: 'Description',
107
+ ),
108
+
109
+ const SizedBox(height: 24),
110
+
111
+ /// BUTTON
112
+ AppButton(
113
+ label: 'Add Salary',
114
+ isLoading: _isSubmitting,
115
+ onPressed: () async {
116
+ if (widget.formKey.currentState!.validate()) {
117
+ setState(() => _isSubmitting = true);
118
+
119
+ double amount = CurrencyInputFormatter.unformat(_salaryController.text);
120
+
121
+ await _transactionService.submitTransaction(
122
+ 'credit',
123
+ amount,
124
+ _selectedSalaryType ?? 'Salary',
125
+ _descriptionController.text,
126
+ _descriptionController.text,
127
+ _selectedDate,
128
+ 'CASH',
129
+ token: widget.token, // Pass the token
130
+ );
131
+
132
+ widget.onTransactionSuccess();
133
+
134
+ _salaryController.text = '\$0.00';
135
+ _dateController.text =
136
+ DateFormat('dd/MM/yyyy').format(DateTime.now());
137
+ _descriptionController.clear();
138
+
139
+ setState(() {
140
+ _selectedDate = DateTime.now();
141
+ _selectedSalaryType = null;
142
+ _isSubmitting = false;
143
+ });
144
+ }
145
+ },
146
+ ),
147
+ ],
148
+ ),
149
+ ),
150
+ );
151
+ }
152
+ }
@@ -0,0 +1,374 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:intl/intl.dart';
3
+ import 'package:personal_finance_frontend_core_services/services/stock_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 TradeForm extends StatefulWidget {
9
+ final String accountName;
10
+ final List<Map<String, dynamic>> portfolioSummary;
11
+ final List<Map<String, dynamic>> assets;
12
+ final Function(Map<String, dynamic> tradeData) onTradeCreated;
13
+ final String? token;
14
+
15
+ const TradeForm(
16
+ {Key? key,
17
+ required this.accountName,
18
+ required this.portfolioSummary,
19
+ required this.assets,
20
+ required this.onTradeCreated,
21
+ this.token})
22
+ : super(key: key);
23
+
24
+ @override
25
+ _TradeFormState createState() => _TradeFormState();
26
+ }
27
+
28
+ class _TradeFormState extends State<TradeForm> {
29
+ final _formKey = GlobalKey<FormState>();
30
+ final TransactionService _transactionService = TransactionService();
31
+ final StockService _stockService = StockService();
32
+
33
+ String _tradeType = 'buy';
34
+ final _symbolController = TextEditingController();
35
+ final _sharesController = TextEditingController();
36
+ final _totalCostController = TextEditingController();
37
+ final _estimatedSellValueController = TextEditingController();
38
+ final _dateController = TextEditingController(
39
+ text: DateFormat('dd/MM/yyyy').format(DateTime.now()));
40
+ DateTime _selectedDate = DateTime.now();
41
+ bool _isSubmitting = false;
42
+ double? _livePriceForSell;
43
+ bool _isFetchingPrice = false;
44
+ String? _selectedSellSymbol;
45
+
46
+ String currencySymbol = r'$';
47
+
48
+ String _formatCurrency(double value) {
49
+ return '$currencySymbol${value.toStringAsFixed(2)}';
50
+ }
51
+
52
+ @override
53
+ void initState() {
54
+ super.initState();
55
+ _sharesController.addListener(_updateEstimatedSellValue);
56
+ _symbolController.addListener(_handleSymbolChange);
57
+ }
58
+
59
+ @override
60
+ void dispose() {
61
+ _symbolController.removeListener(_handleSymbolChange);
62
+ _sharesController.removeListener(_updateEstimatedSellValue);
63
+ _symbolController.dispose();
64
+ _sharesController.dispose();
65
+ _totalCostController.dispose();
66
+ _estimatedSellValueController.dispose();
67
+ _dateController.dispose();
68
+ super.dispose();
69
+ }
70
+
71
+ void _handleSymbolChange() {
72
+ if (_symbolController.text.isEmpty) {
73
+ setState(() {
74
+ _livePriceForSell = null;
75
+ _estimatedSellValueController.clear();
76
+ });
77
+ }
78
+ }
79
+
80
+ Future<void> _updateEstimatedSellValue() async {
81
+ if (_tradeType == 'sell') {
82
+ final symbol = _symbolController.text.toUpperCase();
83
+ if (symbol.isEmpty) {
84
+ setState(() {
85
+ _livePriceForSell = null;
86
+ _estimatedSellValueController.clear();
87
+ });
88
+ return;
89
+ }
90
+
91
+ setState(() {
92
+ _isFetchingPrice = true;
93
+ _estimatedSellValueController.text = "Fetching price...";
94
+ });
95
+
96
+ final livePrices = await _stockService.getLivePrices([symbol]);
97
+ final livePrice = livePrices[symbol];
98
+
99
+ if (!mounted) return;
100
+
101
+ setState(() {
102
+ _livePriceForSell = livePrice;
103
+ if (livePrice != null) {
104
+ final shares = double.tryParse(_sharesController.text) ?? 0.0;
105
+ final estimatedValue = shares * livePrice;
106
+ _estimatedSellValueController.text = _formatCurrency(estimatedValue);
107
+ } else {
108
+ _estimatedSellValueController.text = "Price not available";
109
+ }
110
+ _isFetchingPrice = false;
111
+ });
112
+ }
113
+ }
114
+
115
+ Future<void> _selectDate(BuildContext context) async {
116
+ final DateTime? picked = await showDatePicker(
117
+ context: context,
118
+ initialDate: _selectedDate,
119
+ firstDate: DateTime(2000),
120
+ lastDate: DateTime(2101),
121
+ );
122
+ if (picked != null && picked != _selectedDate) {
123
+ setState(() {
124
+ _selectedDate = picked;
125
+ _dateController.text = DateFormat('dd/MM/yyyy').format(_selectedDate);
126
+ });
127
+ }
128
+ }
129
+
130
+ Future<void> _submitForm() async {
131
+ if (!_formKey.currentState!.validate()) {
132
+ return;
133
+ }
134
+
135
+ final symbol = _symbolController.text;
136
+ final shares = double.tryParse(_sharesController.text) ?? 0.0;
137
+ final totalCost = CurrencyInputFormatter.unformat(_totalCostController.text);
138
+ final selectedDate = _selectedDate;
139
+
140
+ setState(() {
141
+ _isSubmitting = true;
142
+ });
143
+
144
+ // --- Stock Symbol Validation for 'buy' trades ---
145
+ final bool isCrypto = widget.accountName.toLowerCase().contains('crypto');
146
+ if (_tradeType == 'buy' && !isCrypto) {
147
+ final isValid = await _stockService.validateStockSymbol(symbol);
148
+ if (!isValid) {
149
+ if (mounted) {
150
+ ScaffoldMessenger.of(context).showSnackBar(
151
+ SnackBar(
152
+ content: Text('Invalid stock symbol: "$symbol"'),
153
+ backgroundColor: Colors.red),
154
+ );
155
+ }
156
+ setState(() => _isSubmitting = false);
157
+ return; // Stop submission
158
+ }
159
+ }
160
+ // --- End Validation ---
161
+
162
+ double price;
163
+ if (_tradeType == 'sell') {
164
+ if (_livePriceForSell == null) {
165
+ if (mounted) {
166
+ ScaffoldMessenger.of(context).showSnackBar(
167
+ const SnackBar(
168
+ content: Text('Could not get live price. Please try again.'),
169
+ backgroundColor: Colors.red),
170
+ );
171
+ }
172
+ setState(() => _isSubmitting = false);
173
+ return;
174
+ }
175
+ price = _livePriceForSell!;
176
+ } else {
177
+ price = (shares > 0) ? totalCost / shares : 0.0;
178
+ }
179
+
180
+ final tradeData = {
181
+ 'symbol': symbol.toUpperCase(),
182
+ 'trade_type': _tradeType,
183
+ 'shares': shares,
184
+ 'price': price,
185
+ 'date': DateFormat('yyyy-MM-dd').format(selectedDate),
186
+ 'investment_account': widget.accountName,
187
+ };
188
+
189
+ final String? errorMessage =
190
+ await _transactionService.createTrade(tradeData: tradeData, token: widget.token);
191
+
192
+ setState(() {
193
+ _isSubmitting = false;
194
+ });
195
+
196
+ if (mounted) {
197
+ if (errorMessage != null) {
198
+ ScaffoldMessenger.of(context).showSnackBar(
199
+ SnackBar(content: Text(errorMessage), backgroundColor: Colors.red),
200
+ );
201
+ } else {
202
+ ScaffoldMessenger.of(context).showSnackBar(
203
+ const SnackBar(
204
+ content: Text('Trade created successfully!'),
205
+ backgroundColor: Colors.green),
206
+ );
207
+ widget.onTradeCreated(tradeData);
208
+ _formKey.currentState!.reset();
209
+ _symbolController.clear();
210
+ _sharesController.clear();
211
+ _totalCostController.clear();
212
+ _estimatedSellValueController.clear();
213
+ _selectedSellSymbol = null;
214
+ _dateController.text = DateFormat('dd/MM/yyyy').format(DateTime.now());
215
+ }
216
+ }
217
+ }
218
+
219
+ Widget _buildBuySymbolAutocomplete() {
220
+ return Autocomplete<Map<String, dynamic>>(
221
+ displayStringForOption: (option) => option['symbol'] as String,
222
+ optionsBuilder: (TextEditingValue textEditingValue) {
223
+ _symbolController.text = textEditingValue.text;
224
+ final sourceList = widget.assets;
225
+ if (textEditingValue.text == '') {
226
+ return sourceList;
227
+ }
228
+ return sourceList.where((Map<String, dynamic> option) {
229
+ return (option['symbol'] as String)
230
+ .toLowerCase()
231
+ .contains(textEditingValue.text.toLowerCase());
232
+ });
233
+ },
234
+ onSelected: (Map<String, dynamic> selection) {
235
+ _symbolController.text = selection['symbol'] as String;
236
+ },
237
+ fieldViewBuilder: (context, textEditingController, focusNode, onFieldSubmitted) {
238
+ return TextFormField(
239
+ controller: textEditingController,
240
+ focusNode: focusNode,
241
+ decoration: const InputDecoration(labelText: 'Symbol (e.g., AAPL)'),
242
+ validator: (value) => (value == null || value.isEmpty) ? 'Please enter a symbol' : null,
243
+ );
244
+ },
245
+ );
246
+ }
247
+
248
+ Widget _buildSellSymbolDropdown() {
249
+ return DropdownButtonFormField<String>(
250
+ value: _selectedSellSymbol,
251
+ hint: const Text('Select Asset to Sell'),
252
+ isExpanded: true,
253
+ items: widget.portfolioSummary.map((position) {
254
+ final symbol = position['symbol'] as String;
255
+ final shares = (position['shares'] as num).toDouble();
256
+ final sharesString = shares.toStringAsFixed(shares > 1 ? 2 : 6);
257
+ return DropdownMenuItem<String>(
258
+ value: symbol,
259
+ child: Text('$symbol ($sharesString shares)'),
260
+ );
261
+ }).toList(),
262
+ onChanged: (String? newValue) {
263
+ if (newValue != null) {
264
+ setState(() {
265
+ _selectedSellSymbol = newValue;
266
+ _symbolController.text = newValue;
267
+ _updateEstimatedSellValue();
268
+ });
269
+ }
270
+ },
271
+ validator: (value) => value == null ? 'Please select an asset' : null,
272
+ );
273
+ }
274
+
275
+ @override
276
+ Widget build(BuildContext context) {
277
+ final bool isSell = _tradeType == 'sell';
278
+ return AppFormCard(
279
+ title: 'Log a New Trade',
280
+ child: Form(
281
+ key: _formKey,
282
+ child: Column(
283
+ crossAxisAlignment: CrossAxisAlignment.stretch,
284
+ children: [
285
+ SegmentedButton<String>(
286
+ segments: const <ButtonSegment<String>>[
287
+ ButtonSegment(value: 'buy', label: Text('Buy')),
288
+ ButtonSegment(value: 'sell', label: Text('Sell')),
289
+ ],
290
+ selected: <String>{_tradeType},
291
+ onSelectionChanged: (Set<String> newSelection) {
292
+ setState(() {
293
+ _tradeType = newSelection.first;
294
+ _symbolController.clear();
295
+ _sharesController.clear();
296
+ _totalCostController.clear();
297
+ _estimatedSellValueController.clear();
298
+ _livePriceForSell = null;
299
+ _selectedSellSymbol = null;
300
+ });
301
+ },
302
+ ),
303
+ const SizedBox(height: 16),
304
+ if (isSell) _buildSellSymbolDropdown() else _buildBuySymbolAutocomplete(),
305
+ const SizedBox(height: 16),
306
+ AppTextField(
307
+ controller: _sharesController,
308
+ labelText: 'Quantity',
309
+ keyboardType: const TextInputType.numberWithOptions(decimal: true),
310
+ validator: (value) {
311
+ if (value == null || value.isEmpty) return 'Please enter quantity';
312
+ if (isSell && _symbolController.text.isNotEmpty) {
313
+ final position = widget.portfolioSummary.firstWhere(
314
+ (p) => p['symbol'] == _symbolController.text.toUpperCase(),
315
+ orElse: () => {});
316
+ if (position.isNotEmpty) {
317
+ final heldShares = (position['shares'] as num?)?.toDouble() ?? 0.0;
318
+ final sellingShares = double.tryParse(value) ?? 0.0;
319
+ if (sellingShares > (heldShares + 0.00000001)) {
320
+ return 'Cannot sell more than you hold ($heldShares)';
321
+ }
322
+ }
323
+ }
324
+ return null;
325
+ },
326
+ ),
327
+ const SizedBox(height: 16),
328
+ if (isSell) ...[
329
+ InputDecorator(
330
+ decoration: const InputDecoration(
331
+ labelText: 'Estimated Sell Value',
332
+ border: OutlineInputBorder(),
333
+ ),
334
+ child: _isFetchingPrice
335
+ ? SizedBox(
336
+ height: 24,
337
+ child: Row(
338
+ children: [
339
+ Text('Fetching price...'),
340
+ Spacer(),
341
+ CircularProgressIndicator(strokeWidth: 2.0),
342
+ ],
343
+ ),
344
+ )
345
+ : Text(
346
+ _estimatedSellValueController.text.isEmpty
347
+ ? '\$0.00'
348
+ : _estimatedSellValueController.text,
349
+ style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
350
+ ),
351
+ ),
352
+ const SizedBox(height: 16),
353
+ ],
354
+ if (!isSell) AppTextField(controller: _totalCostController, labelText: 'Total Purchase Cost', isCurrency: true),
355
+ const SizedBox(height: 16),
356
+ AppTextField(
357
+ controller: _dateController,
358
+ labelText: 'Date',
359
+ readOnly: true,
360
+ onTap: () => _selectDate(context),
361
+ suffixIcon: IconButton(icon: const Icon(Icons.calendar_today), onPressed: () => _selectDate(context)),
362
+ ),
363
+ const SizedBox(height: 20),
364
+ AppButton(
365
+ label: 'Log Trade',
366
+ onPressed: _isSubmitting || _isFetchingPrice ? null : _submitForm,
367
+ isLoading: _isSubmitting,
368
+ ),
369
+ ],
370
+ ),
371
+ ),
372
+ );
373
+ }
374
+ }
@@ -0,0 +1,60 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:provider/provider.dart';
3
+ import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
4
+
5
+ class UserProfileAvatar extends StatelessWidget {
6
+ const UserProfileAvatar({Key? key}) : super(key: key);
7
+
8
+ String _getInitials(String email) {
9
+ if (email.isEmpty) {
10
+ return '?';
11
+ }
12
+ // Split by '@' and take the first part, then split by '.' or '_'
13
+ List<String> parts = email.split('@')[0].replaceAll('.', ' ').replaceAll('_', ' ').split(' ');
14
+
15
+ if (parts.length > 1) {
16
+ // For names like 'john.doe', take 'j' and 'd'
17
+ String first = parts.first.isNotEmpty ? parts.first[0] : '';
18
+ String last = parts.last.isNotEmpty ? parts.last[0] : '';
19
+ return (first + last).toUpperCase();
20
+ } else if (parts.first.length > 1) {
21
+ // For a single name like 'johndoe', take the first two letters
22
+ return parts.first.substring(0, 2).toUpperCase();
23
+ } else if (parts.first.length == 1) {
24
+ // For a single letter name
25
+ return parts.first.toUpperCase();
26
+ } else {
27
+ return '?';
28
+ }
29
+ }
30
+
31
+ @override
32
+ Widget build(BuildContext context) {
33
+ // Use a Consumer to react to changes in AuthProvider
34
+ return Consumer<AuthProvider>(
35
+ builder: (context, authProvider, child) {
36
+ if (!authProvider.isAuthenticated || authProvider.userEmail == null) {
37
+ // Return an empty container or a placeholder if not authenticated
38
+ return const SizedBox.shrink();
39
+ }
40
+
41
+ final email = authProvider.userEmail!;
42
+ final initials = _getInitials(email);
43
+
44
+ return Padding(
45
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
46
+ child: Tooltip(
47
+ message: email,
48
+ child: CircleAvatar(
49
+ backgroundColor: Colors.blue.shade700,
50
+ child: Text(
51
+ initials,
52
+ style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
53
+ ),
54
+ ),
55
+ ),
56
+ );
57
+ },
58
+ );
59
+ }
60
+ }