Updates from idk when...
This commit is contained in:
parent
6fae83674b
commit
1d1b5f0620
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -42,3 +42,6 @@ app.*.map.json
|
||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
|
||||||
|
#generated files
|
||||||
|
lib/**/*.g.dart
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:rluv/global/styles.dart';
|
||||||
import 'package:rluv/global/utils.dart';
|
import 'package:rluv/global/utils.dart';
|
||||||
|
|
||||||
import '../../../global/store.dart';
|
import '../../../global/store.dart';
|
||||||
|
import '../../../global/widgets/ui_button.dart';
|
||||||
import '../../../models/transaction_model.dart';
|
import '../../../models/transaction_model.dart';
|
||||||
import '../widgets/add_budget_category_dialog.dart';
|
import '../widgets/add_budget_category_dialog.dart';
|
||||||
|
|
||||||
|
@ -26,11 +27,13 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
final budgetListScrollController = ScrollController();
|
final budgetListScrollController = ScrollController();
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final budget = ref.watch(budgetProvider);
|
||||||
final budgetCategories = ref.watch(budgetCategoriesProvider);
|
final budgetCategories = ref.watch(budgetCategoriesProvider);
|
||||||
final transactions = ref.watch(transactionsProvider);
|
final transactions = ref.watch(transactionsProvider);
|
||||||
final screen = BuildMedia(context).size;
|
final screen = BuildMedia(context).size;
|
||||||
double netExpense = 0.0;
|
double netExpense = 0.0;
|
||||||
double netIncome = 0.0;
|
double netIncome = 0.0;
|
||||||
|
double expectedExpenses = 0.0;
|
||||||
Map<int, double> budgetCategoryNetMap = {};
|
Map<int, double> budgetCategoryNetMap = {};
|
||||||
netExpense = transactions
|
netExpense = transactions
|
||||||
.where((t) => t.type == TransactionType.expense)
|
.where((t) => t.type == TransactionType.expense)
|
||||||
|
@ -40,6 +43,7 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
.fold(netIncome, (net, t) => net + t.amount);
|
.fold(netIncome, (net, t) => net + t.amount);
|
||||||
for (final bud in budgetCategories) {
|
for (final bud in budgetCategories) {
|
||||||
double net = 0.0;
|
double net = 0.0;
|
||||||
|
expectedExpenses += bud.amount;
|
||||||
net = transactions
|
net = transactions
|
||||||
.where((t) => t.budgetCategoryId == bud.id)
|
.where((t) => t.budgetCategoryId == bud.id)
|
||||||
.fold(net, (net, t) => net + t.amount);
|
.fold(net, (net, t) => net + t.amount);
|
||||||
|
@ -68,9 +72,15 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
style:
|
style:
|
||||||
TextStyle(fontSize: 42, color: Styles.electricBlue)),
|
TextStyle(fontSize: 42, color: Styles.electricBlue)),
|
||||||
const Spacer(flex: 2),
|
const Spacer(flex: 2),
|
||||||
BudgetNetBar(isPositive: true, net: netIncome),
|
BudgetNetBar(
|
||||||
|
isPositive: true,
|
||||||
|
net: netIncome,
|
||||||
|
expected: budget?.expectedIncome ?? 0.0),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
BudgetNetBar(isPositive: false, net: netExpense),
|
BudgetNetBar(
|
||||||
|
isPositive: false,
|
||||||
|
net: netExpense,
|
||||||
|
expected: expectedExpenses),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -113,7 +123,7 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
child: Text(
|
child: Text(
|
||||||
'No budget categories created yet, add some!'),
|
'No budget categories created yet, add some!'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
UiButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
ref.watch(budgetProvider) ==
|
ref.watch(budgetProvider) ==
|
||||||
null
|
null
|
||||||
|
@ -127,9 +137,9 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
Styles
|
Styles
|
||||||
.dialogColor,
|
.dialogColor,
|
||||||
child:
|
child:
|
||||||
const AddBudgetCategoryDialog()),
|
const BudgetCategoryDialog()),
|
||||||
),
|
),
|
||||||
child: const Text('Add Category'),
|
text: 'Add Category',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -181,7 +191,7 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
Styles
|
Styles
|
||||||
.dialogColor,
|
.dialogColor,
|
||||||
child:
|
child:
|
||||||
const AddBudgetCategoryDialog()),
|
const BudgetCategoryDialog()),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Add Category'),
|
'Add Category'),
|
||||||
|
@ -259,7 +269,7 @@ class _BudgetOverviewScreenState extends ConsumerState<BudgetOverviewScreen> {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
backgroundColor: Styles.dialogColor,
|
backgroundColor: Styles.dialogColor,
|
||||||
shape: Styles.dialogShape,
|
shape: Styles.dialogShape,
|
||||||
child: const AddTransactionDialog());
|
child: const TransactionDialog());
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:helpers/helpers/misc_build/build_media.dart';
|
||||||
import 'package:rluv/features/budget/widgets/transaction_list_item.dart';
|
import 'package:rluv/features/budget/widgets/transaction_list_item.dart';
|
||||||
import 'package:rluv/global/styles.dart';
|
import 'package:rluv/global/styles.dart';
|
||||||
|
import 'package:rluv/models/transaction_model.dart';
|
||||||
|
|
||||||
import '../../../global/store.dart';
|
import '../../../global/store.dart';
|
||||||
|
import '../../../models/budget_category_model.dart';
|
||||||
|
|
||||||
class TransactionsListview extends ConsumerStatefulWidget {
|
class TransactionsListview extends ConsumerStatefulWidget {
|
||||||
const TransactionsListview({super.key});
|
const TransactionsListview({super.key});
|
||||||
|
@ -14,18 +17,180 @@ class TransactionsListview extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TransactionsListviewState extends ConsumerState<TransactionsListview> {
|
class _TransactionsListviewState extends ConsumerState<TransactionsListview> {
|
||||||
|
final scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
// TODO: implement initState
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ref.read(selectedTransactionSortProvider.notifier).state =
|
||||||
|
TransactionSort.all;
|
||||||
|
ref.read(selectedSortDateProvider.notifier).state = SortDate.decending;
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final transactions = ref.watch(transactionsProvider);
|
final budgetCategories = ref.watch(budgetCategoriesProvider);
|
||||||
transactions.sort(
|
if (ref.read(selectedCategory) == null && budgetCategories.isNotEmpty) {
|
||||||
(a, b) => b.date.compareTo(a.date),
|
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||||
);
|
ref.read(selectedCategory.notifier).state = budgetCategories.first);
|
||||||
|
}
|
||||||
|
final sortType = ref.watch(selectedTransactionSortProvider);
|
||||||
|
final sortDate = ref.watch(selectedSortDateProvider);
|
||||||
|
List<Transaction> transactions = [...ref.watch(transactionsProvider)];
|
||||||
|
// Removes transactions by category
|
||||||
|
switch (sortType) {
|
||||||
|
case TransactionSort.income:
|
||||||
|
transactions = transactions
|
||||||
|
.where(
|
||||||
|
(element) => element.type == TransactionType.income,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
break;
|
||||||
|
case TransactionSort.expense:
|
||||||
|
transactions = transactions
|
||||||
|
.where(
|
||||||
|
(element) => element.type == TransactionType.expense,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
break;
|
||||||
|
case TransactionSort.category:
|
||||||
|
if (ref.read(selectedCategory) != null) {
|
||||||
|
transactions = transactions
|
||||||
|
.where(
|
||||||
|
(element) =>
|
||||||
|
element.budgetCategoryId == ref.read(selectedCategory)!.id,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case TransactionSort.all:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Sorts transactions by date
|
||||||
|
switch (sortDate) {
|
||||||
|
case SortDate.ascending:
|
||||||
|
transactions.sort((a, b) => a.date.compareTo(b.date));
|
||||||
|
break;
|
||||||
|
case SortDate.decending:
|
||||||
|
transactions.sort((a, b) => b.date.compareTo(a.date));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) =>
|
||||||
|
ref.read(transactionHistoryListProvider.notifier).state = transactions);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
endDrawer: Drawer(
|
||||||
|
backgroundColor: Styles.purpleNurple,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(height: BuildMedia(context).height * 0.15),
|
||||||
|
DropdownButton<TransactionSort>(
|
||||||
|
dropdownColor: Styles.lavender,
|
||||||
|
value: ref.watch(selectedTransactionSortProvider),
|
||||||
|
items: TransactionSort.values
|
||||||
|
.map(
|
||||||
|
(e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
sortTypeToIcon(e),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(e.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (TransactionSort? value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref.read(selectedTransactionSortProvider.notifier).state =
|
||||||
|
value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
DropdownButton<SortDate>(
|
||||||
|
dropdownColor: Styles.lavender,
|
||||||
|
value: ref.watch(selectedSortDateProvider),
|
||||||
|
items: SortDate.values
|
||||||
|
.map(
|
||||||
|
(e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
sortDateToIcon(e),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(e.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (SortDate? value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref.read(selectedSortDateProvider.notifier).state = value;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (ref.read(selectedTransactionSortProvider) ==
|
||||||
|
TransactionSort.category)
|
||||||
|
DropdownButton<BudgetCategory>(
|
||||||
|
dropdownColor: Styles.lavender,
|
||||||
|
value: ref.watch(selectedCategory),
|
||||||
|
items: budgetCategories
|
||||||
|
.map(
|
||||||
|
(e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 18,
|
||||||
|
width: 18,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.black, width: 1.5),
|
||||||
|
color: e.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(e.name),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (BudgetCategory? value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref.read(selectedCategory.notifier).state = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
key: scaffoldKey,
|
||||||
backgroundColor: Styles.purpleNurple,
|
backgroundColor: Styles.purpleNurple,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (transactions.isEmpty) ...[
|
if (transactions.isEmpty) ...[
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, top: 16.0),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.chevron_left),
|
||||||
|
onPressed: () => Navigator.pop(context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Center(
|
const Center(
|
||||||
child: Text('No transactions'),
|
child: Text('No transactions'),
|
||||||
),
|
),
|
||||||
|
@ -33,12 +198,33 @@ class _TransactionsListviewState extends ConsumerState<TransactionsListview> {
|
||||||
if (transactions.isNotEmpty)
|
if (transactions.isNotEmpty)
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 28),
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
const Text(
|
Padding(
|
||||||
'Transaction History',
|
padding: const EdgeInsets.only(left: 8.0, top: 16.0),
|
||||||
style: TextStyle(fontSize: 28),
|
child: IconButton(
|
||||||
textAlign: TextAlign.center,
|
icon: const Icon(Icons.chevron_left),
|
||||||
),
|
onPressed: () => Navigator.pop(context)),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
const Padding(
|
||||||
|
padding:
|
||||||
|
EdgeInsets.symmetric(vertical: 18.0, horizontal: 0.0),
|
||||||
|
child: Text(
|
||||||
|
'Transaction History',
|
||||||
|
style: TextStyle(fontSize: 28),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0, top: 16.0),
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.sort),
|
||||||
|
onPressed: () {
|
||||||
|
toggleDrawer();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
]),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
|
@ -52,12 +238,65 @@ class _TransactionsListviewState extends ConsumerState<TransactionsListview> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.chevron_left),
|
|
||||||
onPressed: () => Navigator.pop(context)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void toggleDrawer() {
|
||||||
|
if (scaffoldKey.currentState != null &&
|
||||||
|
scaffoldKey.currentState!.isDrawerOpen) {
|
||||||
|
scaffoldKey.currentState!.openDrawer();
|
||||||
|
} else if (scaffoldKey.currentState != null) {
|
||||||
|
scaffoldKey.currentState!.openEndDrawer();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TransactionSort {
|
||||||
|
all,
|
||||||
|
income,
|
||||||
|
expense,
|
||||||
|
category,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SortDate {
|
||||||
|
decending,
|
||||||
|
ascending,
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon sortTypeToIcon(TransactionSort s) {
|
||||||
|
switch (s) {
|
||||||
|
case TransactionSort.all:
|
||||||
|
return const Icon(Icons.donut_large);
|
||||||
|
case TransactionSort.income:
|
||||||
|
return const Icon(Icons.attach_money);
|
||||||
|
case TransactionSort.expense:
|
||||||
|
return const Icon(Icons.shopping_bag);
|
||||||
|
case TransactionSort.category:
|
||||||
|
return const Icon(Icons.sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon sortDateToIcon(SortDate s) {
|
||||||
|
switch (s) {
|
||||||
|
case SortDate.ascending:
|
||||||
|
return const Icon(Icons.arrow_circle_up);
|
||||||
|
case SortDate.decending:
|
||||||
|
return const Icon(Icons.arrow_circle_down);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedTransactionSortProvider = StateProvider<TransactionSort>(
|
||||||
|
(ref) => TransactionSort.all,
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedSortDateProvider =
|
||||||
|
StateProvider<SortDate>((ref) => SortDate.decending);
|
||||||
|
|
||||||
|
final transactionHistoryListProvider = StateProvider<List<Transaction>>(
|
||||||
|
(ref) => [],
|
||||||
|
);
|
||||||
|
|
||||||
|
final selectedCategory = StateProvider<BudgetCategory?>((ref) => null);
|
||||||
|
|
|
@ -11,16 +11,18 @@ import '../../../global/store.dart';
|
||||||
import '../../../global/utils.dart';
|
import '../../../global/utils.dart';
|
||||||
import '../../../global/widgets/ui_button.dart';
|
import '../../../global/widgets/ui_button.dart';
|
||||||
|
|
||||||
class AddBudgetCategoryDialog extends ConsumerStatefulWidget {
|
class BudgetCategoryDialog extends ConsumerStatefulWidget {
|
||||||
const AddBudgetCategoryDialog({super.key});
|
const BudgetCategoryDialog({super.key, this.category});
|
||||||
|
|
||||||
|
final BudgetCategory? category;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<AddBudgetCategoryDialog> createState() =>
|
ConsumerState<BudgetCategoryDialog> createState() =>
|
||||||
_AddBudgetCategoryDialogState();
|
_AddBudgetCategoryDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddBudgetCategoryDialogState
|
class _AddBudgetCategoryDialogState
|
||||||
extends ConsumerState<AddBudgetCategoryDialog> {
|
extends ConsumerState<BudgetCategoryDialog> {
|
||||||
late final Budget? budget;
|
late final Budget? budget;
|
||||||
final categoryNameController = TextEditingController();
|
final categoryNameController = TextEditingController();
|
||||||
final amountController = TextEditingController();
|
final amountController = TextEditingController();
|
||||||
|
@ -32,12 +34,20 @@ class _AddBudgetCategoryDialogState
|
||||||
int selectedColorIndex = -1;
|
int selectedColorIndex = -1;
|
||||||
|
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
budget = ref.read(budgetProvider);
|
budget = ref.read(budgetProvider);
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
categoryFocusNode.requestFocus();
|
categoryFocusNode.requestFocus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (widget.category != null) {
|
||||||
|
categoryNameController.text = widget.category!.name;
|
||||||
|
amountController.text = widget.category!.amount.toString();
|
||||||
|
selectedColorIndex = colors.indexOf(widget.category!.color);
|
||||||
|
}
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,22 +76,25 @@ class _AddBudgetCategoryDialogState
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(bottom: 18.0),
|
padding: const EdgeInsets.only(bottom: 18.0),
|
||||||
child: Text('Add Category:'),
|
child: Text(widget.category == null
|
||||||
|
? 'Add Category:'
|
||||||
|
: 'Edit Category'),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
const Text('Name:'),
|
const Text('Name:'),
|
||||||
const SizedBox(width: 30),
|
const SizedBox(width: 30),
|
||||||
Container(
|
Container(
|
||||||
width: 120,
|
width: 150,
|
||||||
height: 30,
|
height: 40,
|
||||||
decoration: Styles.boxLavenderBubble,
|
decoration: Styles.boxLavenderBubble,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
validator: (text) {
|
validator: (text) {
|
||||||
if (text == null || text.length < 3) {
|
if (text == null || text.isEmpty) {
|
||||||
return 'Invalid Category';
|
return 'Cannot be blank';
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
@ -99,12 +112,13 @@ class _AddBudgetCategoryDialogState
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
const Text('Amount:'),
|
const Text('Amount:'),
|
||||||
const SizedBox(width: 30),
|
const SizedBox(width: 30),
|
||||||
Container(
|
Container(
|
||||||
width: 80,
|
width: 150,
|
||||||
height: 30,
|
height: 40,
|
||||||
decoration: Styles.boxLavenderBubble,
|
decoration: Styles.boxLavenderBubble,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
validator: (text) {
|
validator: (text) {
|
||||||
|
@ -205,19 +219,53 @@ class _AddBudgetCategoryDialogState
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.category != null && widget.category?.id != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
|
child: UiButton(
|
||||||
|
showLoading: true,
|
||||||
|
color: Styles.expensesRed,
|
||||||
|
text: 'DELETE',
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState != null &&
|
||||||
|
formKey.currentState!.validate()) {
|
||||||
|
removeCategory().then((success) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
if (success) {
|
||||||
|
showSnack(
|
||||||
|
ref: ref,
|
||||||
|
text: 'Budget Category Removed',
|
||||||
|
type: SnackType.success,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showSnack(
|
||||||
|
ref: ref,
|
||||||
|
text: 'Unable to Remove Budget Category',
|
||||||
|
type: SnackType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
UiButton(
|
UiButton(
|
||||||
showLoading: true,
|
showLoading: true,
|
||||||
color: Styles.lavender,
|
color: Styles.lavender,
|
||||||
text: 'SAVE',
|
text: 'SAVE',
|
||||||
onPressed: () =>
|
onPressed: () {
|
||||||
|
if (formKey.currentState != null &&
|
||||||
|
formKey.currentState!.validate()) {
|
||||||
submitCategory(colors[selectedColorIndex]).then((_) {
|
submitCategory(colors[selectedColorIndex]).then((_) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
showSnack(
|
showSnack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
text: 'Budget Category Added!',
|
text: 'Budget Category Added!',
|
||||||
type: SnackType.success,
|
type: SnackType.success,
|
||||||
);
|
);
|
||||||
}),
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -237,24 +285,70 @@ class _AddBudgetCategoryDialogState
|
||||||
printPink('Failed validation');
|
printPink('Failed validation');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final newBudget = BudgetCategory(
|
Map<String, dynamic>? budgetData;
|
||||||
id: null,
|
bool success = false;
|
||||||
amount: double.parse(amountController.text),
|
if (widget.category == null) {
|
||||||
budgetId: budget!.id!,
|
final newBudget = BudgetCategory(
|
||||||
color: categoryColor,
|
id: null,
|
||||||
name: categoryNameController.text,
|
amount: double.parse(amountController.text),
|
||||||
);
|
budgetId: budget!.id!,
|
||||||
|
color: categoryColor,
|
||||||
|
name: categoryNameController.text,
|
||||||
|
);
|
||||||
|
|
||||||
final budgetData = await ref
|
budgetData = await ref
|
||||||
|
.read(apiProvider.notifier)
|
||||||
|
.post(path: 'budget_category', data: newBudget.toJson());
|
||||||
|
success = budgetData != null ? budgetData['success'] as bool : false;
|
||||||
|
if (success) {
|
||||||
|
ref
|
||||||
|
.read(dashboardProvider.notifier)
|
||||||
|
.add({'budget_categories': budgetData});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final newBudget =
|
||||||
|
BudgetCategory.copyWith(category: widget.category!, data: {
|
||||||
|
'amount': double.parse(amountController.text),
|
||||||
|
'color': categoryColor,
|
||||||
|
'name': categoryNameController.text
|
||||||
|
});
|
||||||
|
budgetData = await ref
|
||||||
|
.read(apiProvider.notifier)
|
||||||
|
.put(path: 'budget_category', data: newBudget.toJson());
|
||||||
|
success = budgetData != null ? budgetData['success'] as bool : false;
|
||||||
|
if (success) {
|
||||||
|
ref
|
||||||
|
.read(dashboardProvider.notifier)
|
||||||
|
.update({'budget_categories': budgetData});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
showSnack(
|
||||||
|
ref: ref,
|
||||||
|
text: widget.category == null
|
||||||
|
? 'Added budget category!'
|
||||||
|
: 'Updated category!',
|
||||||
|
type: SnackType.error);
|
||||||
|
} else {
|
||||||
|
showSnack(
|
||||||
|
ref: ref,
|
||||||
|
text: widget.category == null
|
||||||
|
? 'Could not add budget category'
|
||||||
|
: 'Could not update category',
|
||||||
|
type: SnackType.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> removeCategory() async {
|
||||||
|
final res = await ref
|
||||||
.read(apiProvider.notifier)
|
.read(apiProvider.notifier)
|
||||||
.post(path: 'budget_category', data: newBudget.toJson());
|
.delete(path: 'budget_category', data: {'id': widget.category!.id});
|
||||||
final success = budgetData != null ? budgetData['success'] as bool : false;
|
final success = res != null ? res['success'] as bool : false;
|
||||||
if (success) {
|
if (success) {
|
||||||
ref
|
ref
|
||||||
.read(dashboardProvider.notifier)
|
.read(dashboardProvider.notifier)
|
||||||
.add({'budget_categories': budgetData});
|
.removeWithId('budget_categories', widget.category!.id!);
|
||||||
} else {
|
|
||||||
showSnack(ref: ref, text: 'Could not add budget', type: SnackType.error);
|
|
||||||
}
|
}
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,21 +10,23 @@ import '../../../global/styles.dart';
|
||||||
import '../../../global/widgets/ui_button.dart';
|
import '../../../global/widgets/ui_button.dart';
|
||||||
import '../../../models/budget_category_model.dart';
|
import '../../../models/budget_category_model.dart';
|
||||||
import '../../../models/transaction_model.dart';
|
import '../../../models/transaction_model.dart';
|
||||||
|
import '../../../models/user.dart';
|
||||||
|
|
||||||
class AddTransactionDialog extends ConsumerStatefulWidget {
|
class TransactionDialog extends ConsumerStatefulWidget {
|
||||||
const AddTransactionDialog({super.key, this.transaction});
|
const TransactionDialog({super.key, this.transaction});
|
||||||
|
|
||||||
final Transaction? transaction;
|
final Transaction? transaction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<AddTransactionDialog> createState() =>
|
ConsumerState<TransactionDialog> createState() =>
|
||||||
_AddTransactionDialogState();
|
_AddTransactionDialogState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
|
class _AddTransactionDialogState extends ConsumerState<TransactionDialog> {
|
||||||
late DateTime selectedDate;
|
late DateTime selectedDate;
|
||||||
late final TextEditingController amountController;
|
late final TextEditingController amountController;
|
||||||
late final TextEditingController memoController;
|
late final TextEditingController memoController;
|
||||||
|
User? user;
|
||||||
|
|
||||||
final amountFocusNode = FocusNode();
|
final amountFocusNode = FocusNode();
|
||||||
final memoFocusNode = FocusNode();
|
final memoFocusNode = FocusNode();
|
||||||
|
@ -49,11 +51,20 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
|
||||||
if (categories.isNotEmpty) {
|
if (categories.isNotEmpty) {
|
||||||
selectedBudgetCategory = categories.first;
|
selectedBudgetCategory = categories.first;
|
||||||
}
|
}
|
||||||
|
final u = ref.read(userProvider);
|
||||||
|
if (u == null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => ref.read(jwtProvider.notifier).revokeToken());
|
||||||
|
} else {
|
||||||
|
user = u;
|
||||||
|
}
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (user == null) return Container();
|
||||||
final List<BudgetCategory> budgetCategories =
|
final List<BudgetCategory> budgetCategories =
|
||||||
ref.read(budgetCategoriesProvider);
|
ref.read(budgetCategoriesProvider);
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
|
@ -274,7 +285,7 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
|
||||||
Map<String, dynamic>? data;
|
Map<String, dynamic>? data;
|
||||||
if (widget.transaction != null) {
|
if (widget.transaction != null) {
|
||||||
data = await ref.read(apiProvider.notifier).put(
|
data = await ref.read(apiProvider.notifier).put(
|
||||||
path: 'transactions',
|
path: 'transaction',
|
||||||
data: Transaction.copyWith(widget.transaction!, {
|
data: Transaction.copyWith(widget.transaction!, {
|
||||||
'memo': memoController.text.isNotEmpty ? memoController.text : null,
|
'memo': memoController.text.isNotEmpty ? memoController.text : null,
|
||||||
'amount': double.parse(amountController.text),
|
'amount': double.parse(amountController.text),
|
||||||
|
@ -284,14 +295,14 @@ class _AddTransactionDialogState extends ConsumerState<AddTransactionDialog> {
|
||||||
}).toJson());
|
}).toJson());
|
||||||
} else {
|
} else {
|
||||||
data = await ref.read(apiProvider.notifier).post(
|
data = await ref.read(apiProvider.notifier).post(
|
||||||
path: 'transactions',
|
path: 'transaction',
|
||||||
data: Transaction(
|
data: Transaction(
|
||||||
amount: double.parse(amountController.text),
|
amount: double.parse(amountController.text),
|
||||||
addedByUserId: 1,
|
createdByUserId: user!.id!,
|
||||||
budgetCategoryId: transactionType == TransactionType.income
|
budgetCategoryId: transactionType == TransactionType.income
|
||||||
? null
|
? null
|
||||||
: selectedBudgetCategory.id,
|
: selectedBudgetCategory.id,
|
||||||
budgetId: 1,
|
budgetId: user!.budgetId,
|
||||||
date: DateTime.now(),
|
date: DateTime.now(),
|
||||||
type: transactionType,
|
type: transactionType,
|
||||||
memo: memoController.text.isNotEmpty
|
memo: memoController.text.isNotEmpty
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:helpers/helpers/print.dart';
|
import 'package:helpers/helpers/print.dart';
|
||||||
|
import 'package:rluv/features/budget/widgets/add_budget_category_dialog.dart';
|
||||||
import 'package:rluv/global/utils.dart';
|
import 'package:rluv/global/utils.dart';
|
||||||
import 'package:rluv/models/budget_category_model.dart';
|
import 'package:rluv/models/budget_category_model.dart';
|
||||||
|
|
||||||
|
@ -27,18 +28,22 @@ class BudgetCategoryBar extends StatefulWidget {
|
||||||
|
|
||||||
class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
|
class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
|
||||||
double percentSpent = 0.0;
|
double percentSpent = 0.0;
|
||||||
|
double setTo = 0.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
Future.delayed(Duration(milliseconds: min(1600, 200 * widget.index)), () {
|
|
||||||
setState(() =>
|
|
||||||
percentSpent = (widget.currentAmount / widget.budgetCategory.amount));
|
|
||||||
});
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
Future.delayed(Duration(milliseconds: min(1600, 200 * widget.index)), () {
|
||||||
|
final valToSetTo = widget.currentAmount / widget.budgetCategory.amount;
|
||||||
|
if (valToSetTo != setTo) {
|
||||||
|
setTo = valToSetTo;
|
||||||
|
setState(() => percentSpent = valToSetTo);
|
||||||
|
}
|
||||||
|
});
|
||||||
final innerHeight = widget.height - widget.innerPadding * 2;
|
final innerHeight = widget.height - widget.innerPadding * 2;
|
||||||
final isBright =
|
final isBright =
|
||||||
getBrightness(widget.budgetCategory.color) == Brightness.light;
|
getBrightness(widget.budgetCategory.color) == Brightness.light;
|
||||||
|
@ -50,85 +55,95 @@ class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
|
||||||
: isBright
|
: isBright
|
||||||
? Colors.black87
|
? Colors.black87
|
||||||
: Colors.white);
|
: Colors.white);
|
||||||
return Padding(
|
return InkWell(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
onLongPress: () async {
|
||||||
child: Row(
|
showDialog(
|
||||||
children: [
|
context: context,
|
||||||
Padding(
|
builder: (context) => Dialog(
|
||||||
padding: const EdgeInsets.all(8.0),
|
shape: Styles.dialogShape,
|
||||||
child: SizedBox(
|
backgroundColor: Styles.dialogColor,
|
||||||
width: 90,
|
child: BudgetCategoryDialog(category: widget.budgetCategory)));
|
||||||
child: FittedBox(
|
},
|
||||||
fit: BoxFit.scaleDown,
|
child: Padding(
|
||||||
alignment: Alignment.centerLeft,
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
child: Text(
|
child: Row(
|
||||||
formatBudgetName(widget.budgetCategory.name),
|
children: [
|
||||||
style: const TextStyle(fontSize: 18.0),
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 90,
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
formatBudgetName(widget.budgetCategory.name),
|
||||||
|
style: const TextStyle(fontSize: 18.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.only(right: 18.0),
|
||||||
padding: const EdgeInsets.only(right: 18.0),
|
child: Stack(
|
||||||
child: Stack(
|
children: [
|
||||||
children: [
|
Container(
|
||||||
Container(
|
height: widget.height,
|
||||||
height: widget.height,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black,
|
|
||||||
borderRadius: BorderRadius.circular(13.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(widget.innerPadding),
|
|
||||||
child: Container(
|
|
||||||
height: innerHeight,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Styles.emptyBarGrey,
|
color: Colors.black,
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(13.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: EdgeInsets.all(widget.innerPadding),
|
||||||
padding: EdgeInsets.all(widget.innerPadding),
|
child: Container(
|
||||||
child: SizedBox(
|
height: innerHeight,
|
||||||
height: innerHeight,
|
decoration: BoxDecoration(
|
||||||
child: AnimatedFractionallySizedBox(
|
color: Styles.emptyBarGrey,
|
||||||
curve: Curves.easeOutSine,
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
heightFactor: 1.0,
|
),
|
||||||
widthFactor: min(1.0, max(0.08, percentSpent)),
|
),
|
||||||
duration: const Duration(milliseconds: 350),
|
),
|
||||||
child: Container(
|
Padding(
|
||||||
decoration: BoxDecoration(
|
padding: EdgeInsets.all(widget.innerPadding),
|
||||||
color: widget.budgetCategory.color,
|
child: SizedBox(
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
height: innerHeight,
|
||||||
|
child: AnimatedFractionallySizedBox(
|
||||||
|
curve: Curves.easeOutSine,
|
||||||
|
heightFactor: 1.0,
|
||||||
|
widthFactor: min(1.0, max(0.08, percentSpent)),
|
||||||
|
duration: const Duration(milliseconds: 350),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: widget.budgetCategory.color,
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
SizedBox(
|
||||||
SizedBox(
|
height: widget.height,
|
||||||
height: widget.height,
|
child: Row(
|
||||||
child: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
children: [
|
||||||
children: [
|
Center(
|
||||||
Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
child: Text(
|
||||||
child: Text(
|
'${widget.currentAmount.currency()} / ${widget.budgetCategory.amount.currency()}',
|
||||||
'${widget.currentAmount.currency()} / ${widget.budgetCategory.amount.currency()}',
|
style: textStyle),
|
||||||
style: textStyle),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
))
|
||||||
))
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,15 @@ import 'package:rluv/global/styles.dart';
|
||||||
import 'package:rluv/global/utils.dart';
|
import 'package:rluv/global/utils.dart';
|
||||||
|
|
||||||
class BudgetNetBar extends StatelessWidget {
|
class BudgetNetBar extends StatelessWidget {
|
||||||
const BudgetNetBar({super.key, required this.isPositive, required this.net});
|
const BudgetNetBar(
|
||||||
|
{super.key,
|
||||||
|
required this.isPositive,
|
||||||
|
required this.net,
|
||||||
|
required this.expected});
|
||||||
|
|
||||||
final bool isPositive;
|
final bool isPositive;
|
||||||
final double net;
|
final double net;
|
||||||
|
final double expected;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -29,7 +34,7 @@ class BudgetNetBar extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
net.currency(),
|
'${net.currency()} / ${expected.currency()}',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
color: isPositive ? Styles.incomeGreen : Styles.expensesRed),
|
color: isPositive ? Styles.incomeGreen : Styles.expensesRed),
|
||||||
|
|
|
@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:helpers/helpers/misc_build/build_media.dart';
|
import 'package:helpers/helpers/misc_build/build_media.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:rluv/features/budget/screens/transactions_listview.dart';
|
||||||
import 'package:rluv/features/budget/widgets/add_transaction_dialog.dart';
|
import 'package:rluv/features/budget/widgets/add_transaction_dialog.dart';
|
||||||
|
import 'package:rluv/global/api.dart';
|
||||||
import 'package:rluv/global/utils.dart';
|
import 'package:rluv/global/utils.dart';
|
||||||
import 'package:rluv/models/transaction_model.dart';
|
import 'package:rluv/models/transaction_model.dart';
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
double cardHeight = 70.0;
|
double cardHeight = 70.0;
|
||||||
|
|
||||||
BudgetCategory? budgetCategory;
|
BudgetCategory? budgetCategory;
|
||||||
late final Transaction transaction;
|
Transaction? transaction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -35,12 +37,16 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final transaction = ref.watch(transactionsProvider)[widget.index];
|
transaction =
|
||||||
|
ref.watch(transactionHistoryListProvider).elementAtOrNull(widget.index);
|
||||||
|
if (transaction == null) return Container();
|
||||||
final budgetCategories = ref.read(budgetCategoriesProvider);
|
final budgetCategories = ref.read(budgetCategoriesProvider);
|
||||||
if (transaction.type == TransactionType.expense) {
|
if (transaction!.type == TransactionType.expense) {
|
||||||
budgetCategory = budgetCategories.singleWhere(
|
budgetCategory = budgetCategories.singleWhere(
|
||||||
(category) => category.id == transaction.budgetCategoryId,
|
(category) => category.id == transaction!.budgetCategoryId,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
budgetCategory = null;
|
||||||
}
|
}
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => toggleDetails(),
|
onTap: () => toggleDetails(),
|
||||||
|
@ -67,7 +73,7 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
height: cardHeight,
|
height: cardHeight,
|
||||||
width: 6,
|
width: 6,
|
||||||
color: transaction.type == TransactionType.income
|
color: transaction!.type == TransactionType.income
|
||||||
? Styles.incomeBlue
|
? Styles.incomeBlue
|
||||||
: Styles.expensesOrange),
|
: Styles.expensesOrange),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
@ -79,7 +85,7 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(DateFormat('EEE MMM d, h:mm a')
|
Text(DateFormat('EEE MMM d, h:mm a')
|
||||||
.format(transaction.date)),
|
.format(transaction!.date)),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -94,7 +100,7 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Text(
|
Text(
|
||||||
transaction.type == TransactionType.income
|
transaction!.type == TransactionType.income
|
||||||
? 'Income'
|
? 'Income'
|
||||||
: budgetCategory!.name,
|
: budgetCategory!.name,
|
||||||
style: const TextStyle(fontSize: 20),
|
style: const TextStyle(fontSize: 20),
|
||||||
|
@ -103,7 +109,7 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
),
|
),
|
||||||
if (showDetails)
|
if (showDetails)
|
||||||
Text(
|
Text(
|
||||||
transaction.memo ?? '',
|
transaction!.memo ?? '',
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@ -115,14 +121,14 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
transaction.amount.currency(),
|
transaction!.amount.currency(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
color: transaction.type == TransactionType.income
|
color: transaction!.type == TransactionType.income
|
||||||
? Styles.incomeGreen
|
? Styles.incomeGreen
|
||||||
: Styles.expensesRed),
|
: Styles.expensesRed),
|
||||||
),
|
),
|
||||||
if (showDetails)
|
if (showDetails) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Icons.edit_rounded,
|
Icons.edit_rounded,
|
||||||
|
@ -133,12 +139,20 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
builder: (context) => Dialog(
|
builder: (context) => Dialog(
|
||||||
backgroundColor: Styles.dialogColor,
|
backgroundColor: Styles.dialogColor,
|
||||||
shape: Styles.dialogShape,
|
shape: Styles.dialogShape,
|
||||||
child: AddTransactionDialog(
|
child:
|
||||||
transaction: transaction),
|
TransactionDialog(transaction: transaction),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.delete,
|
||||||
|
color: Styles.expensesRed,
|
||||||
|
),
|
||||||
|
onPressed: () => deleteTransaction(),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
|
@ -165,4 +179,23 @@ class _TransactionListItemState extends ConsumerState<TransactionListItem> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future deleteTransaction() async {
|
||||||
|
final res = await ref
|
||||||
|
.read(apiProvider.notifier)
|
||||||
|
.delete(path: 'transaction', data: {'id': transaction!.id});
|
||||||
|
|
||||||
|
final success = res != null ? res['success'] as bool : false;
|
||||||
|
if (success) {
|
||||||
|
ref
|
||||||
|
.read(dashboardProvider.notifier)
|
||||||
|
.removeWithId('transactions', transaction!.id!);
|
||||||
|
showSnack(ref: ref, text: 'Transaction removed', type: SnackType.success);
|
||||||
|
} else {
|
||||||
|
showSnack(
|
||||||
|
ref: ref,
|
||||||
|
text: 'Could not delete transaction',
|
||||||
|
type: SnackType.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ class _SharedNotesScreenState extends ConsumerState<SharedNotesScreen> {
|
||||||
children: [
|
children: [
|
||||||
FittedBox(
|
FittedBox(
|
||||||
child: Text(note.title,
|
child: Text(note.title,
|
||||||
style: TextStyle(fontSize: 20))),
|
style: const TextStyle(fontSize: 20))),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
note.content.length > 80
|
note.content.length > 80
|
||||||
|
@ -183,7 +183,6 @@ class _NoteBottomSheetState extends ConsumerState<NoteBottomSheet> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screen = BuildMedia(context).size;
|
final screen = BuildMedia(context).size;
|
||||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
|
||||||
return Container(
|
return Container(
|
||||||
width: screen.width,
|
width: screen.width,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -232,11 +231,11 @@ class _NoteBottomSheetState extends ConsumerState<NoteBottomSheet> {
|
||||||
if (widget.index == null) {
|
if (widget.index == null) {
|
||||||
res = await ref
|
res = await ref
|
||||||
.read(apiProvider)
|
.read(apiProvider)
|
||||||
.post('shared_notes', data: newNote.toJson());
|
.post('shared_note', data: newNote.toJson());
|
||||||
} else {
|
} else {
|
||||||
res = await ref
|
res = await ref
|
||||||
.read(apiProvider)
|
.read(apiProvider)
|
||||||
.put('shared_notes', data: newNote.toJson());
|
.put('shared_note', data: newNote.toJson());
|
||||||
}
|
}
|
||||||
if (res.data != null && res.data['success']) {
|
if (res.data != null && res.data['success']) {
|
||||||
if (widget.index == null) {
|
if (widget.index == null) {
|
||||||
|
|
|
@ -54,7 +54,7 @@ final apiProvider = StateNotifierProvider<_ApiNotifier, Dio>((ref) {
|
||||||
|
|
||||||
class _ApiNotifier extends StateNotifier<Dio> {
|
class _ApiNotifier extends StateNotifier<Dio> {
|
||||||
_ApiNotifier(this.ref, this.dio) : super(dio) {
|
_ApiNotifier(this.ref, this.dio) : super(dio) {
|
||||||
// dio.options.baseUrl = "https://fe7d-136-36-2-234.ngrok-free.app";
|
// dio.options.baseUrl = "https://af70-136-36-2-234.ngrok-free.app/";
|
||||||
// dio.options.baseUrl = "http://localhost:8081/";
|
// dio.options.baseUrl = "http://localhost:8081/";
|
||||||
dio.options.baseUrl = "https://rluv.fosscat.com/";
|
dio.options.baseUrl = "https://rluv.fosscat.com/";
|
||||||
dio.interceptors.addAll([
|
dio.interceptors.addAll([
|
||||||
|
@ -93,6 +93,7 @@ class _ApiNotifier extends StateNotifier<Dio> {
|
||||||
if (err.response?.statusCode == 403) {
|
if (err.response?.statusCode == 403) {
|
||||||
ref.read(jwtProvider.notifier).revokeToken();
|
ref.read(jwtProvider.notifier).revokeToken();
|
||||||
}
|
}
|
||||||
|
return handler.next(err);
|
||||||
}),
|
}),
|
||||||
_LoggingInterceptor(),
|
_LoggingInterceptor(),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -189,4 +189,26 @@ class DashBoardStateNotifier extends StateNotifier<Map<String, dynamic>?> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void removeWithId(String stateKey, int id) {
|
||||||
|
if (state == null) {
|
||||||
|
printPink('Cant remove data, state is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (stateKey) {
|
||||||
|
case 'transactions':
|
||||||
|
case 'budget_categories':
|
||||||
|
case 'shared_noted':
|
||||||
|
final subStateList = state![stateKey] as List<dynamic>;
|
||||||
|
final newState = state;
|
||||||
|
subStateList
|
||||||
|
.removeWhere((e) => (e as Map<String, dynamic>)['id'] == id);
|
||||||
|
newState![stateKey] = subStateList;
|
||||||
|
state = {...newState};
|
||||||
|
// printBlue(state);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,9 @@ class Styles {
|
||||||
contentPadding:
|
contentPadding:
|
||||||
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
|
||||||
labelText: labelText,
|
labelText: labelText,
|
||||||
|
errorStyle: const TextStyle(
|
||||||
|
color: Styles.expensesRed,
|
||||||
|
),
|
||||||
focusColor: Styles.blushingPink,
|
focusColor: Styles.blushingPink,
|
||||||
hoverColor: Styles.blushingPink,
|
hoverColor: Styles.blushingPink,
|
||||||
fillColor: Styles.lavender,
|
fillColor: Styles.lavender,
|
||||||
|
|
|
@ -98,7 +98,7 @@ class _HomeState extends ConsumerState<Home> {
|
||||||
initData = {};
|
initData = {};
|
||||||
}
|
}
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: ref.read(scaffoldKeyProvider),
|
key: ref.read(_scaffoldKeyProvider),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
drawer: Drawer(
|
drawer: Drawer(
|
||||||
backgroundColor: Styles.purpleNurple,
|
backgroundColor: Styles.purpleNurple,
|
||||||
|
@ -159,7 +159,7 @@ class _HomeState extends ConsumerState<Home> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDrawer() async {
|
toggleDrawer() async {
|
||||||
final key = ref.read(scaffoldKeyProvider);
|
final key = ref.read(_scaffoldKeyProvider);
|
||||||
if (key.currentState != null && key.currentState!.isDrawerOpen) {
|
if (key.currentState != null && key.currentState!.isDrawerOpen) {
|
||||||
key.currentState!.openEndDrawer();
|
key.currentState!.openEndDrawer();
|
||||||
} else if (key.currentState != null) {
|
} else if (key.currentState != null) {
|
||||||
|
@ -168,7 +168,7 @@ class _HomeState extends ConsumerState<Home> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final scaffoldKeyProvider = Provider<GlobalKey<ScaffoldState>>(
|
final _scaffoldKeyProvider = Provider<GlobalKey<ScaffoldState>>(
|
||||||
(ref) => GlobalKey<ScaffoldState>(),
|
(ref) => GlobalKey<ScaffoldState>(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ class Budget {
|
||||||
this.id,
|
this.id,
|
||||||
required this.familyId,
|
required this.familyId,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
required this.expectedIncome,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
this.hide = false,
|
this.hide = false,
|
||||||
|
@ -17,6 +18,7 @@ class Budget {
|
||||||
|
|
||||||
final int? id;
|
final int? id;
|
||||||
final int familyId;
|
final int familyId;
|
||||||
|
final double? expectedIncome;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
@JsonKey(fromJson: boolFromJson, toJson: boolToJson)
|
@JsonKey(fromJson: boolFromJson, toJson: boolToJson)
|
||||||
|
|
|
@ -10,6 +10,7 @@ Budget _$BudgetFromJson(Map<String, dynamic> json) => Budget(
|
||||||
id: json['id'] as int?,
|
id: json['id'] as int?,
|
||||||
familyId: json['family_id'] as int,
|
familyId: json['family_id'] as int,
|
||||||
name: json['name'] as String,
|
name: json['name'] as String,
|
||||||
|
expectedIncome: (json['expected_income'] as num?)?.toDouble(),
|
||||||
createdAt: dateFromJson(json['created_at'] as int),
|
createdAt: dateFromJson(json['created_at'] as int),
|
||||||
updatedAt: dateFromJson(json['updated_at'] as int),
|
updatedAt: dateFromJson(json['updated_at'] as int),
|
||||||
hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int),
|
hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int),
|
||||||
|
@ -18,6 +19,7 @@ Budget _$BudgetFromJson(Map<String, dynamic> json) => Budget(
|
||||||
Map<String, dynamic> _$BudgetToJson(Budget instance) => <String, dynamic>{
|
Map<String, dynamic> _$BudgetToJson(Budget instance) => <String, dynamic>{
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'family_id': instance.familyId,
|
'family_id': instance.familyId,
|
||||||
|
'expected_income': instance.expectedIncome,
|
||||||
'name': instance.name,
|
'name': instance.name,
|
||||||
'hide': boolToJson(instance.hide),
|
'hide': boolToJson(instance.hide),
|
||||||
'created_at': dateToJson(instance.createdAt),
|
'created_at': dateToJson(instance.createdAt),
|
||||||
|
|
|
@ -40,4 +40,18 @@ class BudgetCategory {
|
||||||
_$BudgetCategoryFromJson(json);
|
_$BudgetCategoryFromJson(json);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => _$BudgetCategoryToJson(this);
|
Map<String, dynamic> toJson() => _$BudgetCategoryToJson(this);
|
||||||
|
|
||||||
|
factory BudgetCategory.copyWith(
|
||||||
|
{required BudgetCategory category,
|
||||||
|
required Map<String, dynamic> data}) =>
|
||||||
|
BudgetCategory(
|
||||||
|
id: data['id'] ?? category.id,
|
||||||
|
budgetId: data['budget_id'] ?? category.budgetId,
|
||||||
|
name: data['name'] ?? category.name,
|
||||||
|
color: data['color'] ?? category.color,
|
||||||
|
createdAt: data['created_at'] ?? category.createdAt,
|
||||||
|
updatedAt: data['updated_at'] ?? category.updatedAt,
|
||||||
|
amount: data['amount'] ?? category.amount,
|
||||||
|
hide: data['hide'] ?? category.hide,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ class Transaction {
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.budgetId,
|
required this.budgetId,
|
||||||
this.budgetCategoryId,
|
this.budgetCategoryId,
|
||||||
required this.addedByUserId,
|
required this.createdByUserId,
|
||||||
required this.date,
|
required this.date,
|
||||||
this.memo,
|
this.memo,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
|
@ -26,7 +26,7 @@ class Transaction {
|
||||||
});
|
});
|
||||||
|
|
||||||
final int? id, budgetCategoryId;
|
final int? id, budgetCategoryId;
|
||||||
final int budgetId, addedByUserId;
|
final int budgetId, createdByUserId;
|
||||||
final double amount;
|
final double amount;
|
||||||
final String? memo;
|
final String? memo;
|
||||||
final TransactionType type;
|
final TransactionType type;
|
||||||
|
@ -53,7 +53,7 @@ class Transaction {
|
||||||
type: data['type'] ?? trans.type,
|
type: data['type'] ?? trans.type,
|
||||||
budgetId: data['budget_id'] ?? trans.budgetId,
|
budgetId: data['budget_id'] ?? trans.budgetId,
|
||||||
budgetCategoryId: data['budget_category_id'] ?? trans.budgetCategoryId,
|
budgetCategoryId: data['budget_category_id'] ?? trans.budgetCategoryId,
|
||||||
addedByUserId: data['added_by_user_id'] ?? trans.addedByUserId,
|
createdByUserId: data['created_by_user_id'] ?? trans.createdByUserId,
|
||||||
date: data['date'] ?? trans.date,
|
date: data['date'] ?? trans.date,
|
||||||
memo: data['memo'] ?? trans.memo,
|
memo: data['memo'] ?? trans.memo,
|
||||||
createdAt: data['created_at'] ?? trans.createdAt,
|
createdAt: data['created_at'] ?? trans.createdAt,
|
||||||
|
|
|
@ -12,7 +12,7 @@ Transaction _$TransactionFromJson(Map<String, dynamic> json) => Transaction(
|
||||||
type: $enumDecode(_$TransactionTypeEnumMap, json['type']),
|
type: $enumDecode(_$TransactionTypeEnumMap, json['type']),
|
||||||
budgetId: json['budget_id'] as int,
|
budgetId: json['budget_id'] as int,
|
||||||
budgetCategoryId: json['budget_category_id'] as int?,
|
budgetCategoryId: json['budget_category_id'] as int?,
|
||||||
addedByUserId: json['added_by_user_id'] as int,
|
createdByUserId: json['created_by_user_id'] as int,
|
||||||
date: dateFromJson(json['date'] as int),
|
date: dateFromJson(json['date'] as int),
|
||||||
memo: json['memo'] as String?,
|
memo: json['memo'] as String?,
|
||||||
createdAt: dateFromJson(json['created_at'] as int),
|
createdAt: dateFromJson(json['created_at'] as int),
|
||||||
|
@ -25,7 +25,7 @@ Map<String, dynamic> _$TransactionToJson(Transaction instance) =>
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'budget_category_id': instance.budgetCategoryId,
|
'budget_category_id': instance.budgetCategoryId,
|
||||||
'budget_id': instance.budgetId,
|
'budget_id': instance.budgetId,
|
||||||
'added_by_user_id': instance.addedByUserId,
|
'created_by_user_id': instance.createdByUserId,
|
||||||
'amount': instance.amount,
|
'amount': instance.amount,
|
||||||
'memo': instance.memo,
|
'memo': instance.memo,
|
||||||
'type': _$TransactionTypeEnumMap[instance.type]!,
|
'type': _$TransactionTypeEnumMap[instance.type]!,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user