@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.
- package/.circleci/config.yml +23 -0
- package/.metadata +10 -0
- package/CHANGELOG.md +16 -0
- package/LICENSE +1 -0
- package/README.md +31 -0
- package/analysis_options.yaml +4 -0
- package/lib/personal_finance_frontend_feature_management.dart +5 -0
- package/lib/screens/manage_transactions_screen.dart +307 -0
- package/lib/screens/reconciliation_screen.dart +335 -0
- package/lib/viewmodels/manage_transactions_viewmodel.dart +291 -0
- package/package.json +29 -0
- package/pubspec.yaml +59 -0
- package/test/personal_finance_frontend_feature_management_test.dart +12 -0
|
@@ -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,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
|
+
}
|