Skip to content

Commit 1aad093

Browse files
committed
feat: add transaction categories with icons and selection UI
- Create `lib\values\categories.dart` with predefined expense/income categories and helper functions - Update dashboard and transactions pages to display category-specific icons with handling the old non-categorize data on firebase - Add category selection UI to quick add expense/income forms - Use Material Design icons for better visual representation of transaction types
1 parent 3d9dc13 commit 1aad093

File tree

5 files changed

+274
-59
lines changed

5 files changed

+274
-59
lines changed

mobile/lib/ui/app/dashboard/dashboard.dart

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
import 'package:financial_planner_mobile/cubit/balances_cubit.dart';
32
import 'package:financial_planner_mobile/cubit/currency_cubit.dart';
43
import 'package:financial_planner_mobile/cubit/transactions_cubit.dart';
@@ -9,6 +8,7 @@ import 'package:financial_planner_mobile/ui/app/quick_add/quick_add_expense.dart
98
import 'package:financial_planner_mobile/ui/app/quick_add/quick_add_income.dart';
109
import 'package:financial_planner_mobile/ui/app/transactions/transactions.dart';
1110
import 'package:financial_planner_mobile/util/theme.dart';
11+
import 'package:financial_planner_mobile/values/categories.dart';
1212
import 'package:flutter/material.dart';
1313
import 'package:flutter_bloc/flutter_bloc.dart';
1414
import 'package:intl/intl.dart';
@@ -66,7 +66,8 @@ class _DashboardPageState extends State<DashboardPage> {
6666
});
6767
if (isHidden) {
6868
return Padding(
69-
padding: const EdgeInsets.symmetric(vertical: 8.5),
69+
padding:
70+
const EdgeInsets.symmetric(vertical: 8.5),
7071
child: Container(
7172
width: 120,
7273
height: 40,
@@ -256,21 +257,20 @@ class _DashboardPageState extends State<DashboardPage> {
256257
decoration: BoxDecoration(
257258
color: darkTheme.surfaceBright,
258259
borderRadius: BorderRadius.circular(10)),
259-
child: Column(
260-
children: [
261-
if (transactions[index]["type"] ==
262-
"expense")
263-
Icon(
264-
Icons.payments_rounded,
265-
size: 25,
266-
)
267-
else if (transactions[index]["type"] ==
268-
"income")
269-
Icon(
270-
Icons.file_download_rounded,
271-
size: 25,
272-
)
273-
],
260+
child: Icon(
261+
getCategoryIcon(
262+
transactions[index]
263+
.data()
264+
.containsKey("category")
265+
? transactions[index]["category"]
266+
: null,
267+
defaultIcon: transactions[index]
268+
["type"] ==
269+
"expense"
270+
? Icons.payments_rounded
271+
: Icons.file_download_rounded,
272+
),
273+
size: 25,
274274
),
275275
),
276276
Expanded(

mobile/lib/ui/app/quick_add/quick_add_expense.dart

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:cloud_firestore/cloud_firestore.dart';
22
import 'package:financial_planner_mobile/ui/common/themed_input_field.dart';
33
import 'package:financial_planner_mobile/util/theme.dart';
4+
import 'package:financial_planner_mobile/values/categories.dart';
45
import 'package:financial_planner_mobile/values/spaces.dart';
56
import 'package:firebase_auth/firebase_auth.dart';
67
import 'package:flutter/material.dart';
@@ -25,6 +26,7 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
2526
int balanceIndex = -1;
2627
DateTime dateTime = DateTime.now();
2728
bool addToBudget = true;
29+
Category? selectedCategory;
2830

2931
@override
3032
void initState() {
@@ -75,8 +77,7 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
7577

7678
String formattedDate =
7779
"${fullDateTime.day.toString().padLeft(2, '0')}/${fullDateTime.month.toString().padLeft(2, '0')}/${fullDateTime.year}";
78-
int hour =
79-
pickedTime.hourOfPeriod == 0 ? 12 : pickedTime.hourOfPeriod;
80+
int hour = pickedTime.hourOfPeriod == 0 ? 12 : pickedTime.hourOfPeriod;
8081
String period = pickedTime.period == DayPeriod.am ? "AM" : "PM";
8182
String formattedTime =
8283
"${hour.toString().padLeft(2, '0')}:${pickedTime.minute.toString().padLeft(2, '0')} $period";
@@ -139,6 +140,73 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
139140
],
140141
),
141142
SizedBox(height: 40),
143+
Column(
144+
crossAxisAlignment: CrossAxisAlignment.start,
145+
children: [
146+
Text(
147+
"Category:",
148+
style: TextStyle(color: Colors.grey),
149+
),
150+
SizedBox(height: 10),
151+
SingleChildScrollView(
152+
scrollDirection: Axis.horizontal,
153+
child: Row(
154+
spacing: 8,
155+
children: expenseCategories.map((category) {
156+
var isSelected =
157+
selectedCategory == category;
158+
return GestureDetector(
159+
onTap: () {
160+
setState(() {
161+
selectedCategory = category;
162+
});
163+
},
164+
child: Container(
165+
padding: EdgeInsets.symmetric(
166+
horizontal: 16, vertical: 10),
167+
decoration: BoxDecoration(
168+
color: isSelected
169+
? darkTheme.primary
170+
: darkTheme.surfaceContainer,
171+
borderRadius:
172+
BorderRadius.circular(20),
173+
border: isSelected
174+
? null
175+
: Border.all(
176+
color: Colors.grey.shade700),
177+
),
178+
child: Row(
179+
mainAxisSize: MainAxisSize.min,
180+
spacing: 6,
181+
children: [
182+
Icon(
183+
category.icon,
184+
size: 18,
185+
color: isSelected
186+
? Colors.black
187+
: Colors.white,
188+
),
189+
Text(
190+
category.name,
191+
style: TextStyle(
192+
color: isSelected
193+
? Colors.black
194+
: Colors.white,
195+
fontWeight: isSelected
196+
? FontWeight.bold
197+
: FontWeight.normal,
198+
),
199+
),
200+
],
201+
),
202+
),
203+
);
204+
}).toList(),
205+
),
206+
),
207+
],
208+
),
209+
SizedBox(height: 20),
142210
ThemedInputField(
143211
label: "Name",
144212
controller: nameController,
@@ -214,8 +282,9 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
214282
onTap: () => _setDate(context),
215283
),
216284
SizedBox(height: 20),
217-
BlocBuilder<BudgetCubit, dynamic>(builder: (context, state) {
218-
if(state != null) {
285+
BlocBuilder<BudgetCubit, dynamic>(
286+
builder: (context, state) {
287+
if (state != null) {
219288
return GestureDetector(
220289
onTap: () {
221290
setState(() {
@@ -229,10 +298,14 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
229298
width: 30,
230299
height: 30,
231300
decoration: BoxDecoration(
232-
color: addToBudget ? darkTheme.primary : darkTheme.surfaceContainer,
233-
borderRadius: BorderRadius.circular(5)
234-
),
235-
child: addToBudget ? Icon(Icons.check) : null,
301+
color: addToBudget
302+
? darkTheme.primary
303+
: darkTheme.surfaceContainer,
304+
borderRadius:
305+
BorderRadius.circular(5)),
306+
child: addToBudget
307+
? Icon(Icons.check)
308+
: null,
236309
),
237310
Text("Add to budget")
238311
],
@@ -303,7 +376,8 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
303376
}
304377

305378
if (context.mounted &&
306-
context.read<BudgetCubit>().state != null && addToBudget) {
379+
context.read<BudgetCubit>().state != null &&
380+
addToBudget) {
307381
await FirebaseFirestore.instance
308382
.collection("users")
309383
.doc(FirebaseAuth.instance.currentUser?.uid)
@@ -330,6 +404,7 @@ class _QuickAddExpenseState extends State<QuickAddExpense> {
330404
"name": nameController.text,
331405
"value": double.parse(valueController.text),
332406
"date": dateTime,
407+
"category": selectedCategory?.name,
333408
"source": balanceIndex != -1
334409
? balances[balanceIndex]["name"]
335410
: null

mobile/lib/ui/app/quick_add/quick_add_income.dart

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:cloud_firestore/cloud_firestore.dart';
22
import 'package:financial_planner_mobile/ui/common/themed_input_field.dart';
33
import 'package:financial_planner_mobile/util/theme.dart';
4+
import 'package:financial_planner_mobile/values/categories.dart';
45
import 'package:financial_planner_mobile/values/spaces.dart';
56
import 'package:firebase_auth/firebase_auth.dart';
67
import 'package:flutter/material.dart';
@@ -23,12 +24,14 @@ class _QuickAddIncomeState extends State<QuickAddIncome> {
2324
TextEditingController dateController = TextEditingController();
2425
int balanceIndex = -1;
2526
DateTime dateTime = DateTime.now();
27+
Category? selectedCategory;
2628

2729
@override
2830
void initState() {
2931
super.initState();
3032

31-
String date = "${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}";
33+
String date =
34+
"${dateTime.day.toString().padLeft(2, '0')}/${dateTime.month.toString().padLeft(2, '0')}/${dateTime.year}";
3235

3336
int hour = dateTime.hour;
3437
int minute = dateTime.minute;
@@ -37,7 +40,8 @@ class _QuickAddIncomeState extends State<QuickAddIncome> {
3740
hour = hour % 12;
3841
if (hour == 0) hour = 12;
3942

40-
String time = "${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period";
43+
String time =
44+
"${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')} $period";
4145

4246
setState(() {
4347
dateController.text = "$date $time";
@@ -69,10 +73,12 @@ class _QuickAddIncomeState extends State<QuickAddIncome> {
6973
pickedTime.minute,
7074
);
7175

72-
String formattedDate = "${fullDateTime.day.toString().padLeft(2, '0')}/${fullDateTime.month.toString().padLeft(2, '0')}/${fullDateTime.year}";
76+
String formattedDate =
77+
"${fullDateTime.day.toString().padLeft(2, '0')}/${fullDateTime.month.toString().padLeft(2, '0')}/${fullDateTime.year}";
7378
int hour = pickedTime.hourOfPeriod == 0 ? 12 : pickedTime.hourOfPeriod;
7479
String period = pickedTime.period == DayPeriod.am ? "AM" : "PM";
75-
String formattedTime = "${hour.toString().padLeft(2, '0')}:${pickedTime.minute.toString().padLeft(2, '0')} $period";
80+
String formattedTime =
81+
"${hour.toString().padLeft(2, '0')}:${pickedTime.minute.toString().padLeft(2, '0')} $period";
7682

7783
setState(() {
7884
dateTime = fullDateTime;
@@ -131,6 +137,73 @@ class _QuickAddIncomeState extends State<QuickAddIncome> {
131137
],
132138
),
133139
SizedBox(height: 40),
140+
Column(
141+
crossAxisAlignment: CrossAxisAlignment.start,
142+
children: [
143+
Text(
144+
"Category:",
145+
style: TextStyle(color: Colors.grey),
146+
),
147+
SizedBox(height: 10),
148+
SingleChildScrollView(
149+
scrollDirection: Axis.horizontal,
150+
child: Row(
151+
spacing: 8,
152+
children: incomeCategories.map((category) {
153+
var isSelected =
154+
selectedCategory == category;
155+
return GestureDetector(
156+
onTap: () {
157+
setState(() {
158+
selectedCategory = category;
159+
});
160+
},
161+
child: Container(
162+
padding: EdgeInsets.symmetric(
163+
horizontal: 16, vertical: 10),
164+
decoration: BoxDecoration(
165+
color: isSelected
166+
? darkTheme.primary
167+
: darkTheme.surfaceContainer,
168+
borderRadius:
169+
BorderRadius.circular(20),
170+
border: isSelected
171+
? null
172+
: Border.all(
173+
color: Colors.grey.shade700),
174+
),
175+
child: Row(
176+
mainAxisSize: MainAxisSize.min,
177+
spacing: 6,
178+
children: [
179+
Icon(
180+
category.icon,
181+
size: 18,
182+
color: isSelected
183+
? Colors.black
184+
: Colors.white,
185+
),
186+
Text(
187+
category.name,
188+
style: TextStyle(
189+
color: isSelected
190+
? Colors.black
191+
: Colors.white,
192+
fontWeight: isSelected
193+
? FontWeight.bold
194+
: FontWeight.normal,
195+
),
196+
),
197+
],
198+
),
199+
),
200+
);
201+
}).toList(),
202+
),
203+
),
204+
],
205+
),
206+
SizedBox(height: 20),
134207
ThemedInputField(
135208
label: "Name",
136209
controller: nameController,
@@ -276,6 +349,7 @@ class _QuickAddIncomeState extends State<QuickAddIncome> {
276349
"name": nameController.text,
277350
"value": double.parse(valueController.text),
278351
"date": dateTime,
352+
"category": selectedCategory?.name,
279353
"source": balanceIndex != -1
280354
? balances[balanceIndex]["name"]
281355
: null

0 commit comments

Comments
 (0)