@marcos_feitoza/personal-finance-frontend-feature-management 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.
@@ -0,0 +1,23 @@
1
+ version: 2.1
2
+
3
+ jobs:
4
+ release:
5
+ docker:
6
+ - image: circleci/node:latest
7
+ steps:
8
+ - checkout
9
+ - run:
10
+ name: Install dependencies
11
+ command: npm install
12
+ - run:
13
+ name: Run semantic-release
14
+ command: npx semantic-release
15
+
16
+ workflows:
17
+ build-and-release:
18
+ jobs:
19
+ - release:
20
+ filters:
21
+ branches:
22
+ only:
23
+ - main
package/.metadata ADDED
@@ -0,0 +1,10 @@
1
+ # This file tracks properties of this Flutter project.
2
+ # Used by Flutter tool to assess capabilities and perform upgrades etc.
3
+ #
4
+ # This file should be version controlled and should not be manually edited.
5
+
6
+ version:
7
+ revision: "fcf2c11572af6f390246c056bc905eca609533a0"
8
+ channel: "stable"
9
+
10
+ project_type: package
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # 1.0.0 (2025-11-28)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * add currency class ([c01940f](https://github.com/MarcosOps/personal-finance-frontend-feature-management/commit/c01940fae5b08c97e843bba46a15c3d443282be6))
7
+ * add ignore and more estilo ([82c1c33](https://github.com/MarcosOps/personal-finance-frontend-feature-management/commit/82c1c3353dbe78fc4936e2ec679ac146d46b56e9))
8
+
9
+
10
+ ### Features
11
+
12
+ * update readme.me ([de994b3](https://github.com/MarcosOps/personal-finance-frontend-feature-management/commit/de994b3f66d78329ff15d44a7c9225c8ec9f3073))
13
+
14
+ ## 0.0.1
15
+
16
+ * TODO: Describe initial release.
package/LICENSE ADDED
@@ -0,0 +1 @@
1
+ TODO: Add your license here.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Feature: Gerenciamento (Frontend Flutter)
2
+
3
+ Este pacote contém a implementação das telas para gerenciamento detalhado de transações e para o processo de conciliação na aplicação Personal Finance Frontend.
4
+
5
+ ## Propósito
6
+
7
+ O objetivo deste pacote é fornecer ao usuário ferramentas para revisar, editar e deletar transações, bem como para realizar a conciliação de contas, garantindo a exatidão dos registros financeiros.
8
+
9
+ ## Conteúdo Principal
10
+
11
+ - **`lib/screens/manage_transactions_screen.dart`**: Tela para visualizar, editar e deletar transações individuais.
12
+ - **`lib/screens/reconciliation_screen.dart`**: Tela dedicada ao processo de conciliação de contas.
13
+
14
+ ---
15
+
16
+ ## Como Usar (Instalação como Dependência)
17
+
18
+ Este pacote é uma dependência local para a aplicação principal (`personal-finance-frontend`).
19
+
20
+ No `pubspec.yaml` da aplicação principal, adicione a seguinte linha em `dependencies`:
21
+
22
+ ```yaml
23
+ personal_finance_frontend_feature_management:
24
+ path: ../personal-finance-frontend-feature-management
25
+ ```
26
+
27
+ ## Features
28
+
29
+ - **Gestão de Transações**: Permite ao usuário visualizar um histórico completo de transações, com opções de edição e exclusão.
30
+ - **Conciliação de Contas**: Oferece um fluxo guiado para conciliar transações com extratos bancários, identificando e corrigindo discrepâncias.
31
+ - **Filtros Avançados**: Ferramentas para filtrar transações por tipo, método de pagamento, data, etc.
@@ -0,0 +1,4 @@
1
+ include: package:flutter_lints/flutter.yaml
2
+
3
+ # Additional information about this file can be found at
4
+ # https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,5 @@
1
+ /// A Calculator.
2
+ class Calculator {
3
+ /// Returns [value] plus 1.
4
+ int addOne(int value) => value + 1;
5
+ }
@@ -0,0 +1,307 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:intl/intl.dart';
3
+ import 'package:provider/provider.dart';
4
+ import '../viewmodels/manage_transactions_viewmodel.dart';
5
+ import 'package:personal_finance_frontend_core_ui/widgets/edit_transaction_dialog.dart';
6
+ import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
7
+ import 'package:personal_finance_frontend_core_services/models/payment_method.dart';
8
+ import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
9
+
10
+ class ManageTransactionsScreen extends StatelessWidget {
11
+ const ManageTransactionsScreen({Key? key}) : super(key: key);
12
+
13
+ @override
14
+ Widget build(BuildContext context) {
15
+ final authProvider = Provider.of<AuthProvider>(context, listen: false);
16
+
17
+ return ChangeNotifierProvider(
18
+ create: (_) => ManageTransactionsViewModel(token: authProvider.token),
19
+ child: Scaffold(
20
+ appBar: AppBar(
21
+ title: const Text('Manage Transactions'),
22
+ actions: [
23
+ Consumer<ManageTransactionsViewModel>(
24
+ builder: (context, viewModel, child) {
25
+ return IconButton(
26
+ icon: const Icon(Icons.delete),
27
+ onPressed: viewModel.selectedTransactionIds.isEmpty
28
+ ? null
29
+ : () {
30
+ showDialog(
31
+ context: context,
32
+ builder: (BuildContext context) {
33
+ return AlertDialog(
34
+ title: const Text('Delete Transactions'),
35
+ content: Text(
36
+ 'Are you sure you want to delete ${viewModel.selectedTransactionIds.length} transaction(s)?'),
37
+ actions: <Widget>[
38
+ TextButton(
39
+ child: const Text('Cancel'),
40
+ onPressed: () {
41
+ Navigator.of(context).pop();
42
+ },
43
+ ),
44
+ TextButton(
45
+ child: const Text('Delete'),
46
+ onPressed: () {
47
+ viewModel.deleteSelectedTransactions();
48
+ Navigator.of(context).pop();
49
+ },
50
+ ),
51
+ ],
52
+ );
53
+ },
54
+ );
55
+ },
56
+ );
57
+ },
58
+ ),
59
+ ],
60
+ ),
61
+ body: Consumer<ManageTransactionsViewModel>(
62
+ builder: (context, viewModel, child) {
63
+ if (viewModel.isLoading) {
64
+ return const Center(child: CircularProgressIndicator());
65
+ }
66
+ if (viewModel.errorMessage.isNotEmpty) {
67
+ return Center(child: Text(viewModel.errorMessage));
68
+ }
69
+ return Column(
70
+ children: [
71
+ _buildFilters(context, viewModel),
72
+ Expanded(
73
+ child: SingleChildScrollView(
74
+ child: DataTable(
75
+ columns: const [
76
+ DataColumn(label: Text('Select')),
77
+ DataColumn(label: Text('Date')),
78
+ DataColumn(label: Text('Category')),
79
+ DataColumn(label: Text('Subcategory')),
80
+ DataColumn(label: Text('Method')),
81
+ DataColumn(label: Text('Amount')),
82
+ DataColumn(label: Text('Description')),
83
+ DataColumn(label: Text('Paid')), // Added Paid column
84
+ DataColumn(label: Text('Actions')),
85
+ ],
86
+ rows: viewModel.transactions.map((transaction) {
87
+ return DataRow(
88
+ selected: viewModel.selectedTransactionIds
89
+ .contains(transaction.id),
90
+ onSelectChanged: (isSelected) {
91
+ viewModel
92
+ .toggleTransactionSelection(transaction.id);
93
+ },
94
+ cells: [
95
+ DataCell(Checkbox(
96
+ value: viewModel.selectedTransactionIds
97
+ .contains(transaction.id),
98
+ onChanged: (isSelected) {
99
+ viewModel
100
+ .toggleTransactionSelection(transaction.id);
101
+ },
102
+ )),
103
+ DataCell(Text(DateFormat('yyyy-MM-dd')
104
+ .format(transaction.date))),
105
+ DataCell(Text(transaction.category)),
106
+ DataCell(Text(transaction.subcategory)),
107
+ DataCell(Text(transaction.paymentMethod)),
108
+ DataCell(Text(NumberFormat.currency(
109
+ locale: 'en_CA', symbol: '\$')
110
+ .format(transaction.amount))),
111
+ DataCell(Text(transaction.description)),
112
+ DataCell(Switch( // Added Switch for Paid status
113
+ value: transaction.isPaid,
114
+ onChanged: (bool newValue) {
115
+ print('Switch onChanged: transaction.id=${transaction.id}, newValue=$newValue'); // Added log
116
+ viewModel.updateTransactionPaidStatus(transaction.id, newValue);
117
+ },
118
+ )),
119
+ DataCell(IconButton(
120
+ icon: const Icon(Icons.edit),
121
+ onPressed: () {
122
+ showDialog(
123
+ context: context,
124
+ builder: (BuildContext context) {
125
+ return EditTransactionDialog(
126
+ transaction: transaction,
127
+ viewModel: viewModel,
128
+ );
129
+ },
130
+ );
131
+ },
132
+ )),
133
+ ],
134
+ );
135
+ }).toList(),
136
+ ),
137
+ ),
138
+ ),
139
+ ],
140
+ );
141
+ },
142
+ ),
143
+ ),
144
+ );
145
+ }
146
+
147
+ // Show Year Dialog
148
+ void _showYearFilter(BuildContext context, ManageTransactionsViewModel viewModel) async {
149
+ final selected = await showDialog<int>(
150
+ context: context,
151
+ builder: (context) => SimpleDialog(
152
+ title: const Text('Select Year'),
153
+ children: [
154
+ SimpleDialogOption(
155
+ onPressed: () => Navigator.pop(context, null),
156
+ child: const Text('All'),
157
+ ),
158
+ ...viewModel.availableYears.map((year) => SimpleDialogOption(
159
+ onPressed: () => Navigator.pop(context, year),
160
+ child: Text(year.toString()),
161
+ )),
162
+ ],
163
+ ),
164
+ );
165
+ viewModel.setSelectedYear(selected);
166
+ }
167
+
168
+ // Show Month Dialog
169
+ void _showMonthFilter(BuildContext context, ManageTransactionsViewModel viewModel) async {
170
+ final selected = await showDialog<int>(
171
+ context: context,
172
+ builder: (context) => SimpleDialog(
173
+ title: const Text('Select Month'),
174
+ children: viewModel.availableMonths.map((month) {
175
+ final text = month == null ? 'All' : DateFormat.MMM().format(DateTime(0, month));
176
+ return SimpleDialogOption(
177
+ onPressed: () => Navigator.pop(context, month),
178
+ child: Text(text),
179
+ );
180
+ }).toList(),
181
+ ),
182
+ );
183
+ viewModel.setSelectedMonth(selected);
184
+ }
185
+
186
+ // Show Category Dialog
187
+ void _showCategoryFilter(BuildContext context, ManageTransactionsViewModel viewModel) async {
188
+ final selected = await showDialog<String>(
189
+ context: context,
190
+ builder: (context) => SimpleDialog(
191
+ title: const Text('Select Category'),
192
+ children: viewModel.availableCategories.map((cat) {
193
+ return SimpleDialogOption(
194
+ onPressed: () => Navigator.pop(context, cat == 'All' ? null : cat),
195
+ child: Text(cat),
196
+ );
197
+ }).toList(),
198
+ ),
199
+ );
200
+ viewModel.setSelectedCategory(selected);
201
+ }
202
+
203
+ // Show Subcategory Dialog
204
+ void _showSubcategoryFilter(BuildContext context, ManageTransactionsViewModel viewModel) async {
205
+ final selected = await showDialog<String>(
206
+ context: context,
207
+ builder: (context) => SimpleDialog(
208
+ title: const Text('Select Subcategory'),
209
+ children: viewModel.availableSubcategories.map((sub) {
210
+ return SimpleDialogOption(
211
+ onPressed: () => Navigator.pop(context, sub == 'All' ? null : sub),
212
+ child: Text(sub),
213
+ );
214
+ }).toList(),
215
+ ),
216
+ );
217
+ viewModel.setSelectedSubcategory(selected);
218
+ }
219
+
220
+ // Show Payment Method Dialog
221
+ void _showPaymentMethodFilter(BuildContext context, ManageTransactionsViewModel viewModel) async {
222
+ final selected = await showDialog<String>(
223
+ context: context,
224
+ builder: (context) => SimpleDialog(
225
+ title: const Text('Select Payment Method'),
226
+ children: viewModel.availablePaymentMethods.map((method) {
227
+ final text = method == null ? 'All' : paymentMethodToString(paymentMethodFromString(method));
228
+ return SimpleDialogOption(
229
+ onPressed: () => Navigator.pop(context, method),
230
+ child: Text(text),
231
+ );
232
+ }).toList(),
233
+ ),
234
+ );
235
+ viewModel.setSelectedPaymentMethod(selected);
236
+ }
237
+
238
+ Widget _buildFilters(
239
+ BuildContext context, ManageTransactionsViewModel viewModel) {
240
+ String formatMonth(int? month) =>
241
+ month == null ? 'All' : DateFormat.MMM().format(DateTime(0, month));
242
+
243
+ return AppFormCard(
244
+ title: 'Filters',
245
+ child: Column(
246
+ children: [
247
+ Row(
248
+ children: [
249
+ Expanded(
250
+ child: FilterButton(
251
+ label: 'Year:',
252
+ value: viewModel.selectedYear?.toString() ?? 'All',
253
+ onPressed: () => _showYearFilter(context, viewModel),
254
+ ),
255
+ ),
256
+ const SizedBox(width: 16),
257
+ Expanded(
258
+ child: FilterButton(
259
+ label: 'Month:',
260
+ value: formatMonth(viewModel.selectedMonth),
261
+ onPressed: () => _showMonthFilter(context, viewModel),
262
+ ),
263
+ ),
264
+ ],
265
+ ),
266
+ const SizedBox(height: 8),
267
+ Row(
268
+ children: [
269
+ Expanded(
270
+ child: FilterButton(
271
+ label: 'Category:',
272
+ value: viewModel.selectedCategory ?? 'All',
273
+ onPressed: () => _showCategoryFilter(context, viewModel),
274
+ ),
275
+ ),
276
+ const SizedBox(width: 16),
277
+ Expanded(
278
+ child: FilterButton(
279
+ label: 'Subcategory:',
280
+ value: viewModel.selectedSubcategory ?? 'All',
281
+ onPressed: () => _showSubcategoryFilter(context, viewModel),
282
+ ),
283
+ ),
284
+ ],
285
+ ),
286
+ const SizedBox(height: 8),
287
+ FilterButton(
288
+ label: 'Method:',
289
+ value: viewModel.selectedPaymentMethod != null
290
+ ? paymentMethodToString(
291
+ paymentMethodFromString(viewModel.selectedPaymentMethod!))
292
+ : 'All',
293
+ onPressed: () => _showPaymentMethodFilter(context, viewModel),
294
+ ),
295
+ const SizedBox(height: 16),
296
+ Align(
297
+ alignment: Alignment.centerRight,
298
+ child: TextButton(
299
+ onPressed: viewModel.clearFilters,
300
+ child: const Text('Clear Filters'),
301
+ ),
302
+ )
303
+ ],
304
+ ),
305
+ );
306
+ }
307
+ }
@@ -0,0 +1,335 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
3
+ import 'package:intl/intl.dart';
4
+ import 'package:personal_finance_frontend_core_services/models/payment_method.dart';
5
+ import 'package:personal_finance_frontend_core_ui/widgets/app_widgets.dart';
6
+ import 'package:provider/provider.dart';
7
+ import 'package:personal_finance_frontend_core_services/providers/auth_provider.dart';
8
+
9
+ class ReconciliationScreen extends StatefulWidget {
10
+ final String paymentMethod;
11
+
12
+ const ReconciliationScreen({Key? key, required this.paymentMethod})
13
+ : super(key: key);
14
+
15
+ @override
16
+ _ReconciliationScreenState createState() => _ReconciliationScreenState();
17
+ }
18
+
19
+ class _ReconciliationScreenState extends State<ReconciliationScreen> {
20
+ final TransactionService _transactionService = TransactionService();
21
+ List<Map<String, dynamic>> _unpaidTransactions = [];
22
+ List<String> _availablePaymentMethods = []; // To hold dynamic payment methods
23
+ Set<int> _selectedIds = {};
24
+ double _selectedTotal = 0.0;
25
+ bool _isLoading = true;
26
+ String? _token;
27
+
28
+ DateTime? _startDate;
29
+ DateTime? _endDate;
30
+ String? _selectedPaymentMethod;
31
+
32
+ @override
33
+ void initState() {
34
+ super.initState();
35
+ _selectedPaymentMethod =
36
+ widget.paymentMethod.isNotEmpty ? widget.paymentMethod.toUpperCase() : null;
37
+
38
+ // Get token from AuthProvider
39
+ WidgetsBinding.instance.addPostFrameCallback((_) {
40
+ final authProvider = Provider.of<AuthProvider>(context, listen: false);
41
+ setState(() {
42
+ _token = authProvider.token;
43
+ });
44
+ _fetchInitialData();
45
+ });
46
+ }
47
+
48
+ Future<void> _fetchInitialData() async {
49
+ if (_selectedPaymentMethod != null) {
50
+ await _fetchUnpaidTransactions();
51
+ } else {
52
+ await _fetchAvailablePaymentMethods();
53
+ }
54
+ }
55
+
56
+ Future<void> _fetchAvailablePaymentMethods() async {
57
+ setState(() {
58
+ _isLoading = true;
59
+ });
60
+ final summary = await _transactionService.getSummaryByPaymentMethod(token: _token);
61
+ setState(() {
62
+ _availablePaymentMethods = summary.map((s) => s['payment_method'] as String).toList();
63
+ if (_selectedPaymentMethod != null && !_availablePaymentMethods.contains(_selectedPaymentMethod)) {
64
+ _selectedPaymentMethod = null; // Clear selection if not in the new list
65
+ }
66
+ _isLoading = false;
67
+ });
68
+ }
69
+
70
+ String currencySymbol = r'$'; // ou poderia vir de configuração, API, etc.
71
+
72
+ String _formatCurrency(double value) {
73
+ return '$currencySymbol${value.toStringAsFixed(2)}';
74
+ }
75
+
76
+ Future<void> _fetchUnpaidTransactions() async {
77
+ setState(() {
78
+ _isLoading = true;
79
+ });
80
+ final transactions = await _transactionService.fetchTransactions(
81
+ paymentMethod: _selectedPaymentMethod?.toUpperCase(),
82
+ isPaid: false,
83
+ startDate: _startDate,
84
+ endDate: _endDate,
85
+ token: _token,
86
+ );
87
+ setState(() {
88
+ _unpaidTransactions = transactions;
89
+ _isLoading = false;
90
+ });
91
+ }
92
+
93
+ void _onTransactionSelected(
94
+ bool? selected, Map<String, dynamic> transaction) {
95
+ final int transactionId = transaction['id'];
96
+ setState(() {
97
+ if (selected == true) {
98
+ _selectedIds.add(transactionId);
99
+ } else {
100
+ _selectedIds.remove(transactionId);
101
+ }
102
+ _recalculateTotal();
103
+ });
104
+ }
105
+
106
+ void _recalculateTotal() {
107
+ _selectedTotal = 0;
108
+ for (int id in _selectedIds) {
109
+ final transaction = _unpaidTransactions.firstWhere((t) => t['id'] == id);
110
+ final double amount = double.parse(transaction['amount'].toString());
111
+ final String type = transaction['type'];
112
+ _selectedTotal += type == 'debit' ? amount : -amount;
113
+ }
114
+ }
115
+
116
+ Future<void> _reconcile() async {
117
+ if (_selectedIds.isEmpty) {
118
+ ScaffoldMessenger.of(context).showSnackBar(
119
+ const SnackBar(
120
+ content: Text('Please select transactions to reconcile.')),
121
+ );
122
+ return;
123
+ }
124
+
125
+ if (_selectedPaymentMethod == null) {
126
+ ScaffoldMessenger.of(context).showSnackBar(
127
+ const SnackBar(content: Text('Please select a payment method.')),
128
+ );
129
+ return;
130
+ }
131
+
132
+ final newBalance = await _transactionService.reconcileTransactions(
133
+ _selectedIds.toList(),
134
+ _selectedPaymentMethod!,
135
+ _selectedTotal,
136
+ token: _token,
137
+ );
138
+
139
+ if (newBalance != null) {
140
+ ScaffoldMessenger.of(context).showSnackBar(
141
+ const SnackBar(content: Text('Reconciliation successful!')),
142
+ );
143
+ Navigator.of(context).pop(true); // Pop with a success result
144
+ } else {
145
+ ScaffoldMessenger.of(context).showSnackBar(
146
+ const SnackBar(
147
+ content: Text('Reconciliation failed. Please try again.')),
148
+ );
149
+ }
150
+ }
151
+
152
+ @override
153
+ Widget build(BuildContext context) {
154
+ return Scaffold(
155
+ appBar: AppBar(
156
+ title: Text('Reconcile Transactions'),
157
+ actions: [
158
+ Center(
159
+ child: Padding(
160
+ padding: const EdgeInsets.only(right: 8.0),
161
+ child: Text(
162
+ 'Total: ${_formatCurrency(_selectedTotal)}',
163
+ style: const TextStyle(fontSize: 18),
164
+ ),
165
+ ),
166
+ ),
167
+ IconButton(
168
+ icon: const Icon(Icons.check),
169
+ onPressed: _selectedPaymentMethod == null ? null : _reconcile,
170
+ tooltip: 'Mark as Paid',
171
+ ),
172
+ ],
173
+ ),
174
+ body: Column(
175
+ children: [
176
+ _buildFilters(),
177
+ Expanded(
178
+ child: _isLoading
179
+ ? const Center(child: CircularProgressIndicator())
180
+ : _unpaidTransactions.isEmpty
181
+ ? const Center(child: Text('No unpaid transactions found.'))
182
+ : SingleChildScrollView(
183
+ scrollDirection: Axis.vertical,
184
+ child: DataTable(
185
+ columns: const [
186
+ DataColumn(label: Text('Select')),
187
+ DataColumn(label: Text('Date')),
188
+ DataColumn(label: Text('Subcategory')),
189
+ DataColumn(label: Text('Method')),
190
+ DataColumn(label: Text('Amount')),
191
+ ],
192
+ rows: _unpaidTransactions.map((transaction) {
193
+ final transactionId = transaction['id'] as int;
194
+ final amount = double.parse(transaction['amount'].toString());
195
+ return DataRow(
196
+ cells: [
197
+ DataCell(Checkbox(
198
+ value: _selectedIds.contains(transactionId),
199
+ onChanged: _selectedPaymentMethod == null
200
+ ? null
201
+ : (bool? selected) {
202
+ _onTransactionSelected(
203
+ selected, transaction);
204
+ },
205
+ )),
206
+ DataCell(Text(DateFormat('yyyy-MM-dd').format(
207
+ DateTime.parse(transaction['date'])))),
208
+ DataCell(Text(transaction['subcategory'] ??
209
+ 'No subcategory')),
210
+ DataCell(Text(
211
+ transaction['payment_method'] ?? 'N/A')),
212
+ DataCell(Text(
213
+ '${_formatCurrency(amount)}',
214
+ style: TextStyle(
215
+ color: transaction['type'] == 'debit'
216
+ ? Colors.red
217
+ : Colors.green,
218
+ fontWeight: FontWeight.bold,
219
+ ),
220
+ )),
221
+ ],
222
+ );
223
+ }).toList(),
224
+ ),
225
+ ),
226
+ ),
227
+ ],
228
+ ),
229
+ );
230
+ }
231
+
232
+ void _clearFilters() {
233
+ setState(() {
234
+ // Can't fully clear payment method if it was passed via widget
235
+ if (widget.paymentMethod.isEmpty) {
236
+ _selectedPaymentMethod = null;
237
+ }
238
+ _startDate = null;
239
+ _endDate = null;
240
+ _selectedIds.clear();
241
+ _recalculateTotal();
242
+ });
243
+ _fetchUnpaidTransactions();
244
+ }
245
+
246
+ Future<void> _selectDate(BuildContext context, bool isStartDate) async {
247
+ final DateTime? picked = await showDatePicker(
248
+ context: context,
249
+ initialDate: (isStartDate ? _startDate : _endDate) ?? DateTime.now(),
250
+ firstDate: DateTime(2000),
251
+ lastDate: DateTime(2101),
252
+ );
253
+ if (picked != null) {
254
+ setState(() {
255
+ if (isStartDate) {
256
+ _startDate = picked;
257
+ } else {
258
+ _endDate = picked;
259
+ }
260
+ });
261
+ _fetchUnpaidTransactions();
262
+ }
263
+ }
264
+
265
+ void _showPaymentMethodDialog(BuildContext context) async {
266
+ final selected = await showDialog<String>(
267
+ context: context,
268
+ builder: (context) => SimpleDialog(
269
+ title: const Text('Select Payment Method'),
270
+ children: _availablePaymentMethods.map((method) {
271
+ return SimpleDialogOption(
272
+ onPressed: () => Navigator.pop(context, method),
273
+ child: Text(method.toUpperCase()),
274
+ );
275
+ }).toList(),
276
+ ),
277
+ );
278
+ if (selected != null) {
279
+ setState(() {
280
+ _selectedPaymentMethod = selected;
281
+ });
282
+ _fetchUnpaidTransactions();
283
+ }
284
+ }
285
+
286
+ Widget _buildFilters() {
287
+ return AppFormCard(
288
+ title: 'Filters',
289
+ child: Column(
290
+ children: [
291
+ // Only show payment method filter if it wasn't passed via constructor
292
+ if (widget.paymentMethod.isEmpty) ...[
293
+ FilterButton(
294
+ label: 'Method:',
295
+ value: _selectedPaymentMethod?.toUpperCase() ?? 'None',
296
+ onPressed: () => _showPaymentMethodDialog(context),
297
+ ),
298
+ const SizedBox(height: 8),
299
+ ],
300
+ Row(
301
+ children: [
302
+ Expanded(
303
+ child: FilterButton(
304
+ label: 'Start:',
305
+ value: _startDate == null
306
+ ? 'None'
307
+ : DateFormat('yyyy-MM-dd').format(_startDate!),
308
+ onPressed: () => _selectDate(context, true),
309
+ ),
310
+ ),
311
+ const SizedBox(width: 16),
312
+ Expanded(
313
+ child: FilterButton(
314
+ label: 'End:',
315
+ value: _endDate == null
316
+ ? 'None'
317
+ : DateFormat('yyyy-MM-dd').format(_endDate!),
318
+ onPressed: () => _selectDate(context, false),
319
+ ),
320
+ ),
321
+ ],
322
+ ),
323
+ const SizedBox(height: 16),
324
+ Align(
325
+ alignment: Alignment.centerRight,
326
+ child: TextButton(
327
+ onPressed: _clearFilters,
328
+ child: const Text('Clear Filters'),
329
+ ),
330
+ ),
331
+ ],
332
+ ),
333
+ );
334
+ }
335
+ }
@@ -0,0 +1,291 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:intl/intl.dart';
3
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
4
+ import 'package:personal_finance_frontend_core_services/models/payment_method.dart';
5
+
6
+ class Transaction {
7
+ final int id;
8
+ final String type;
9
+ final String category;
10
+ final String subcategory;
11
+ final double amount;
12
+ final String description;
13
+ final DateTime date;
14
+ final String paymentMethod;
15
+ final bool isPaid; // Added isPaid
16
+
17
+ Transaction({
18
+ required this.id,
19
+ required this.type,
20
+ required this.category,
21
+ required this.subcategory,
22
+ required this.amount,
23
+ required this.description,
24
+ required this.date,
25
+ required this.paymentMethod,
26
+ required this.isPaid, // Added isPaid
27
+ });
28
+
29
+ factory Transaction.fromJson(Map<String, dynamic> json) {
30
+ return Transaction(
31
+ id: json['id'],
32
+ type: json['type'],
33
+ category: json['category'],
34
+ subcategory: json['subcategory'] ?? '',
35
+ amount: double.parse(json['amount'].toString()),
36
+ description: json['description'] ?? '',
37
+ date: DateTime.parse(json['date']),
38
+ paymentMethod: json['payment_method'] ?? '',
39
+ isPaid: json['is_paid'] ?? false, // Added isPaid
40
+ );
41
+ }
42
+ }
43
+
44
+ class ManageTransactionsViewModel extends ChangeNotifier {
45
+ final TransactionService _transactionService = TransactionService();
46
+ final String? _token;
47
+
48
+ List<Transaction> _allTransactions = [];
49
+ List<Transaction> _transactions = [];
50
+ List<Transaction> get transactions => _transactions;
51
+
52
+ bool _isLoading = false;
53
+ bool get isLoading => _isLoading;
54
+
55
+ String _errorMessage = '';
56
+ String get errorMessage => _errorMessage;
57
+
58
+ Set<int> _selectedTransactionIds = {};
59
+ Set<int> get selectedTransactionIds => _selectedTransactionIds;
60
+
61
+ // Filter properties
62
+ int? _selectedYear;
63
+ int? _selectedMonth;
64
+ String? _selectedCategory;
65
+ String? _selectedSubcategory;
66
+ String? _selectedPaymentMethod;
67
+
68
+ int? get selectedYear => _selectedYear;
69
+ int? get selectedMonth => _selectedMonth;
70
+ String? get selectedCategory => _selectedCategory;
71
+ String? get selectedSubcategory => _selectedSubcategory;
72
+ String? get selectedPaymentMethod => _selectedPaymentMethod;
73
+
74
+ List<int> get availableYears {
75
+ if (_allTransactions.isEmpty) return [DateTime.now().year];
76
+ final years = _allTransactions.map((t) => t.date.year).toSet().toList();
77
+ years.sort((a, b) => b.compareTo(a));
78
+ return years;
79
+ }
80
+
81
+ List<int?> get availableMonths => [null, ...List.generate(12, (i) => i + 1)];
82
+
83
+ List<String> get availableCategories {
84
+ final categories =
85
+ _allTransactions.map((t) => t.category.toUpperCase()).toSet().toList();
86
+ categories.sort();
87
+ return ['All', ...categories];
88
+ }
89
+
90
+ List<String> get availableSubcategories {
91
+ if (_selectedCategory == null || _selectedCategory == 'All') {
92
+ return ['All'];
93
+ }
94
+ final subcategories = _allTransactions
95
+ .where((t) => t.category.toUpperCase() == _selectedCategory)
96
+ .map((t) => t.subcategory)
97
+ .toSet()
98
+ .toList();
99
+ subcategories.sort();
100
+ return ['All', ...subcategories];
101
+ }
102
+
103
+ List<String?> get availablePaymentMethods {
104
+ return [null, ...PaymentMethod.values.map((e) => e.name).toList()];
105
+ }
106
+
107
+ ManageTransactionsViewModel({String? token}) : _token = token {
108
+ print("[LOG] ManageTransactionsViewModel: Initializing...");
109
+ _selectedYear = DateTime.now().year;
110
+ fetchTransactions();
111
+ }
112
+
113
+ void setSelectedYear(int? year) {
114
+ print("[LOG] setSelectedYear: New year = $year");
115
+ _selectedYear = year;
116
+ applyFilters();
117
+ }
118
+
119
+ void setSelectedMonth(int? month) {
120
+ print("[LOG] setSelectedMonth: New month = $month");
121
+ _selectedMonth = month;
122
+ applyFilters();
123
+ }
124
+
125
+ void setSelectedCategory(String? category) {
126
+ print("[LOG] setSelectedCategory: New category = $category");
127
+ _selectedCategory = category;
128
+ _selectedSubcategory = null; // Reset subcategory when category changes
129
+ applyFilters();
130
+ }
131
+
132
+ void setSelectedSubcategory(String? subcategory) {
133
+ print("[LOG] setSelectedSubcategory: New subcategory = $subcategory");
134
+ _selectedSubcategory = subcategory;
135
+ applyFilters();
136
+ }
137
+
138
+ void setSelectedPaymentMethod(String? paymentMethod) {
139
+ print("[LOG] setSelectedPaymentMethod: New payment method = $paymentMethod");
140
+ _selectedPaymentMethod = paymentMethod;
141
+ applyFilters();
142
+ }
143
+
144
+ void clearFilters() {
145
+ print("[LOG] clearFilters: Clearing all filters.");
146
+ _selectedYear = DateTime.now().year;
147
+ _selectedMonth = null;
148
+ _selectedCategory = null;
149
+ _selectedSubcategory = null;
150
+ _selectedPaymentMethod = null;
151
+ applyFilters(); // apply default filters
152
+ }
153
+
154
+ void toggleTransactionSelection(int id) {
155
+ if (_selectedTransactionIds.contains(id)) {
156
+ _selectedTransactionIds.remove(id);
157
+ print("[LOG] toggleTransactionSelection: Deselected transaction ID = $id");
158
+ } else {
159
+ _selectedTransactionIds.add(id);
160
+ print("[LOG] toggleTransactionSelection: Selected transaction ID = $id");
161
+ }
162
+ notifyListeners();
163
+ }
164
+
165
+ Future<void> fetchTransactions() async {
166
+ print("[LOG] fetchTransactions: Starting...");
167
+ _isLoading = true;
168
+ _errorMessage = '';
169
+ notifyListeners();
170
+
171
+ try {
172
+ final data = await _transactionService.fetchTransactions(token: _token);
173
+ _allTransactions = data.map((json) => Transaction.fromJson(json)).toList();
174
+ print("[LOG] fetchTransactions: Success - Fetched ${_allTransactions.length} transactions.");
175
+ applyFilters();
176
+ } catch (e) {
177
+ _errorMessage = 'Error fetching transactions: $e';
178
+ print("[LOG] fetchTransactions: FAILED - $e");
179
+ } finally {
180
+ _isLoading = false;
181
+ notifyListeners();
182
+ }
183
+ }
184
+
185
+ void applyFilters() {
186
+ print("[LOG] applyFilters: Applying filters...");
187
+ print("[LOG] applyFilters: Year=$_selectedYear, Month=$_selectedMonth, Category=$_selectedCategory, Subcategory=$_selectedSubcategory, Method=$_selectedPaymentMethod");
188
+
189
+ List<Transaction> filtered = _allTransactions;
190
+
191
+ if (_selectedYear != null) {
192
+ filtered = filtered.where((t) => t.date.year == _selectedYear).toList();
193
+ }
194
+ if (_selectedMonth != null) {
195
+ filtered = filtered.where((t) => t.date.month == _selectedMonth).toList();
196
+ }
197
+ if (_selectedCategory != null && _selectedCategory != 'All') {
198
+ filtered = filtered
199
+ .where((t) => t.category.toUpperCase() == _selectedCategory)
200
+ .toList();
201
+ }
202
+ if (_selectedSubcategory != null && _selectedSubcategory != 'All') {
203
+ filtered = filtered
204
+ .where((t) => t.subcategory == _selectedSubcategory)
205
+ .toList();
206
+ }
207
+ if (_selectedPaymentMethod != null) {
208
+ filtered = filtered.where((t) => t.paymentMethod == _selectedPaymentMethod).toList();
209
+ }
210
+
211
+ _transactions = filtered;
212
+ print("[LOG] applyFilters: Found ${_transactions.length} transactions after filtering.");
213
+ notifyListeners();
214
+ }
215
+
216
+ Future<void> deleteSelectedTransactions() async {
217
+ if (_selectedTransactionIds.isEmpty) return;
218
+
219
+ print("[LOG] deleteSelectedTransactions: Starting deletion for IDs: $_selectedTransactionIds");
220
+ _isLoading = true;
221
+ notifyListeners();
222
+
223
+ try {
224
+ await _transactionService.deleteTransactions(_selectedTransactionIds.toList(), token: _token);
225
+ print("[LOG] deleteSelectedTransactions: Success.");
226
+ _selectedTransactionIds.clear();
227
+ await fetchTransactions(); // Refresh the list
228
+ } catch (e) {
229
+ _errorMessage = 'Error deleting transactions: $e';
230
+ print("[LOG] deleteSelectedTransactions: FAILED - $e");
231
+ } finally {
232
+ _isLoading = false;
233
+ notifyListeners();
234
+ }
235
+ }
236
+
237
+ Future<void> updateTransaction(int id, Map<String, dynamic> data) async {
238
+ print("[LOG] updateTransaction: Starting update for ID: $id with data: $data");
239
+ _isLoading = true;
240
+ notifyListeners();
241
+
242
+ try {
243
+ await _transactionService.updateTransaction(id, data, token: _token);
244
+ print("[LOG] updateTransaction: Success for ID: $id");
245
+ await fetchTransactions(); // Refresh the list
246
+ } catch (e) {
247
+ _errorMessage = 'Error updating transaction: $e';
248
+ print("[LOG] updateTransaction: FAILED for ID: $id - $e");
249
+ } finally {
250
+ _isLoading = false;
251
+ notifyListeners();
252
+ }
253
+ }
254
+
255
+ Future<void> updateTransactionPaidStatus(int id, bool isPaid) async {
256
+ print("[LOG] updateTransactionPaidStatus: Starting update for ID: $id with isPaid: $isPaid");
257
+ _isLoading = true;
258
+ notifyListeners();
259
+
260
+ try {
261
+ await _transactionService.updateTransaction(id, {'is_paid': isPaid}, token: _token);
262
+ print("[LOG] updateTransactionPaidStatus: Success for ID: $id");
263
+
264
+ // Find the updated transaction in _allTransactions and update its isPaid status
265
+ int index = _allTransactions.indexWhere((t) => t.id == id);
266
+ if (index != -1) {
267
+ _allTransactions[index] = Transaction(
268
+ id: _allTransactions[index].id,
269
+ type: _allTransactions[index].type,
270
+ category: _allTransactions[index].category,
271
+ subcategory: _allTransactions[index].subcategory,
272
+ amount: _allTransactions[index].amount,
273
+ description: _allTransactions[index].description,
274
+ date: _allTransactions[index].date,
275
+ paymentMethod: _allTransactions[index].paymentMethod,
276
+ isPaid: isPaid, // Update isPaid status
277
+ );
278
+ print("[LOG] updateTransactionPaidStatus: Local state updated for ID: $id");
279
+ } else {
280
+ print("[LOG] updateTransactionPaidStatus: WARNING - Could not find transaction ID $id in local list to update status.");
281
+ }
282
+ applyFilters(); // Re-apply filters to update the displayed list
283
+ } catch (e) {
284
+ _errorMessage = 'Error updating transaction paid status: $e';
285
+ print("[LOG] updateTransactionPaidStatus: FAILED for ID: $id - $e");
286
+ } finally {
287
+ _isLoading = false;
288
+ notifyListeners();
289
+ }
290
+ }
291
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@marcos_feitoza/personal-finance-frontend-feature-management",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "scripts": {
8
+ "release": "semantic-release"
9
+ },
10
+ "devDependencies": {
11
+ "semantic-release": "^18.0.0",
12
+ "@semantic-release/changelog": "^6.0.0",
13
+ "@semantic-release/git": "^10.0.0",
14
+ "@semantic-release/github": "^8.0.0"
15
+ },
16
+ "release": {
17
+ "branches": [
18
+ "main"
19
+ ],
20
+ "plugins": [
21
+ "@semantic-release/commit-analyzer",
22
+ "@semantic-release/release-notes-generator",
23
+ "@semantic-release/changelog",
24
+ "@semantic-release/npm",
25
+ "@semantic-release/github",
26
+ "@semantic-release/git"
27
+ ]
28
+ }
29
+ }
package/pubspec.yaml ADDED
@@ -0,0 +1,59 @@
1
+ name: personal_finance_frontend_feature_management
2
+ description: "A new Flutter package project."
3
+ version: 0.0.1
4
+ homepage:
5
+
6
+ environment:
7
+ sdk: '>=2.19.0 <3.0.0'
8
+
9
+ dependencies:
10
+ flutter:
11
+ sdk: flutter
12
+ intl: ^0.17.0
13
+ provider: ^6.0.0
14
+ personal_finance_frontend_core_services:
15
+ path: ../personal-finance-frontend-core-services
16
+ personal_finance_frontend_core_ui:
17
+ path: ../personal-finance-frontend-core-ui
18
+
19
+ dev_dependencies:
20
+ flutter_test:
21
+ sdk: flutter
22
+ flutter_lints: ^5.0.0
23
+
24
+ # For information on the generic Dart part of this file, see the
25
+ # following page: https://dart.dev/tools/pub/pubspec
26
+
27
+ # The following section is specific to Flutter packages.
28
+ flutter:
29
+
30
+ # To add assets to your package, add an assets section, like this:
31
+ # assets:
32
+ # - images/a_dot_burr.jpeg
33
+ # - images/a_dot_ham.jpeg
34
+ #
35
+ # For details regarding assets in packages, see
36
+ # https://flutter.dev/to/asset-from-package
37
+ #
38
+ # An image asset can refer to one or more resolution-specific "variants", see
39
+ # https://flutter.dev/to/resolution-aware-images
40
+
41
+ # To add custom fonts to your package, add a fonts section here,
42
+ # in this "flutter" section. Each entry in this list should have a
43
+ # "family" key with the font family name, and a "fonts" key with a
44
+ # list giving the asset and other descriptors for the font. For
45
+ # example:
46
+ # fonts:
47
+ # - family: Schyler
48
+ # fonts:
49
+ # - asset: fonts/Schyler-Regular.ttf
50
+ # - asset: fonts/Schyler-Italic.ttf
51
+ # style: italic
52
+ # - family: Trajan Pro
53
+ # fonts:
54
+ # - asset: fonts/TrajanPro.ttf
55
+ # - asset: fonts/TrajanPro_Bold.ttf
56
+ # weight: 700
57
+ #
58
+ # For details regarding fonts in packages, see
59
+ # https://flutter.dev/to/font-from-package
@@ -0,0 +1,12 @@
1
+ import 'package:flutter_test/flutter_test.dart';
2
+
3
+ import 'package:personal_finance_frontend_feature_management/personal_finance_frontend_feature_management.dart';
4
+
5
+ void main() {
6
+ test('adds one to input values', () {
7
+ final calculator = Calculator();
8
+ expect(calculator.addOne(2), 3);
9
+ expect(calculator.addOne(-7), -6);
10
+ expect(calculator.addOne(0), 1);
11
+ });
12
+ }