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