Updates from idk when...

This commit is contained in:
Nathan Anderson 2023-08-17 13:34:30 -06:00
parent 6fae83674b
commit 1d1b5f0620
18 changed files with 614 additions and 161 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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());
}, },
); );
}, },

View File

@ -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(
padding: const EdgeInsets.only(left: 8.0, top: 16.0),
child: IconButton(
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', 'Transaction History',
style: TextStyle(fontSize: 28), style: TextStyle(fontSize: 28),
textAlign: TextAlign.center, 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);

View File

@ -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,11 +219,43 @@ 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(
@ -217,7 +263,9 @@ class _AddBudgetCategoryDialogState
text: 'Budget Category Added!', text: 'Budget Category Added!',
type: SnackType.success, type: SnackType.success,
); );
}), });
}
},
), ),
], ],
), ),
@ -237,6 +285,9 @@ class _AddBudgetCategoryDialogState
printPink('Failed validation'); printPink('Failed validation');
return; return;
} }
Map<String, dynamic>? budgetData;
bool success = false;
if (widget.category == null) {
final newBudget = BudgetCategory( final newBudget = BudgetCategory(
id: null, id: null,
amount: double.parse(amountController.text), amount: double.parse(amountController.text),
@ -245,16 +296,59 @@ class _AddBudgetCategoryDialogState
name: categoryNameController.text, name: categoryNameController.text,
); );
final budgetData = await ref budgetData = await ref
.read(apiProvider.notifier) .read(apiProvider.notifier)
.post(path: 'budget_category', data: newBudget.toJson()); .post(path: 'budget_category', data: newBudget.toJson());
final success = budgetData != null ? budgetData['success'] as bool : false; success = budgetData != null ? budgetData['success'] as bool : false;
if (success) { if (success) {
ref ref
.read(dashboardProvider.notifier) .read(dashboardProvider.notifier)
.add({'budget_categories': budgetData}); .add({'budget_categories': budgetData});
}
} else { } else {
showSnack(ref: ref, text: 'Could not add budget', type: SnackType.error); 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)
.delete(path: 'budget_category', data: {'id': widget.category!.id});
final success = res != null ? res['success'] as bool : false;
if (success) {
ref
.read(dashboardProvider.notifier)
.removeWithId('budget_categories', widget.category!.id!);
}
return success;
}
} }

View File

@ -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

View File

@ -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,7 +55,16 @@ class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
: isBright : isBright
? Colors.black87 ? Colors.black87
: Colors.white); : Colors.white);
return Padding( return InkWell(
onLongPress: () async {
showDialog(
context: context,
builder: (context) => Dialog(
shape: Styles.dialogShape,
backgroundColor: Styles.dialogColor,
child: BudgetCategoryDialog(category: widget.budgetCategory)));
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row( child: Row(
children: [ children: [
@ -130,6 +144,7 @@ class _BudgetCategoryBarState extends State<BudgetCategoryBar> {
)) ))
], ],
), ),
),
); );
} }

View File

@ -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),

View File

@ -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);
}
}
} }

View File

@ -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) {

View File

@ -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(),
]); ]);

View File

@ -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;
}
}
} }

View File

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

View File

@ -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>(),
); );

View File

@ -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)

View File

@ -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),

View File

@ -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,
);
} }

View File

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

View File

@ -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]!,