From 1d1b5f062020167604fd951030562c10626b1e49 Mon Sep 17 00:00:00 2001 From: Nathan Anderson Date: Thu, 17 Aug 2023 13:34:30 -0600 Subject: [PATCH] Updates from idk when... --- .gitignore | 3 + .../budget/screens/budget_overview.dart | 24 +- .../budget/screens/transactions_listview.dart | 265 +++++++++++++++++- .../widgets/add_budget_category_dialog.dart | 162 ++++++++--- .../widgets/add_transaction_dialog.dart | 27 +- .../budget/widgets/budget_category_bar.dart | 157 ++++++----- .../budget/widgets/budget_net_bar.dart | 9 +- .../budget/widgets/transaction_list_item.dart | 59 +++- lib/features/notes/screens/notes_screen.dart | 7 +- lib/global/api.dart | 3 +- lib/global/store.dart | 22 ++ lib/global/styles.dart | 3 + lib/main.dart | 6 +- lib/models/budget.dart | 2 + lib/models/budget.g.dart | 2 + lib/models/budget_category_model.dart | 14 + lib/models/transaction_model.dart | 6 +- lib/models/transaction_model.g.dart | 4 +- 18 files changed, 614 insertions(+), 161 deletions(-) diff --git a/.gitignore b/.gitignore index 24476c5..1b0b1d8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +#generated files +lib/**/*.g.dart diff --git a/lib/features/budget/screens/budget_overview.dart b/lib/features/budget/screens/budget_overview.dart index c26b74f..70b597b 100644 --- a/lib/features/budget/screens/budget_overview.dart +++ b/lib/features/budget/screens/budget_overview.dart @@ -9,6 +9,7 @@ import 'package:rluv/global/styles.dart'; import 'package:rluv/global/utils.dart'; import '../../../global/store.dart'; +import '../../../global/widgets/ui_button.dart'; import '../../../models/transaction_model.dart'; import '../widgets/add_budget_category_dialog.dart'; @@ -26,11 +27,13 @@ class _BudgetOverviewScreenState extends ConsumerState { final budgetListScrollController = ScrollController(); @override Widget build(BuildContext context) { + final budget = ref.watch(budgetProvider); final budgetCategories = ref.watch(budgetCategoriesProvider); final transactions = ref.watch(transactionsProvider); final screen = BuildMedia(context).size; double netExpense = 0.0; double netIncome = 0.0; + double expectedExpenses = 0.0; Map budgetCategoryNetMap = {}; netExpense = transactions .where((t) => t.type == TransactionType.expense) @@ -40,6 +43,7 @@ class _BudgetOverviewScreenState extends ConsumerState { .fold(netIncome, (net, t) => net + t.amount); for (final bud in budgetCategories) { double net = 0.0; + expectedExpenses += bud.amount; net = transactions .where((t) => t.budgetCategoryId == bud.id) .fold(net, (net, t) => net + t.amount); @@ -68,9 +72,15 @@ class _BudgetOverviewScreenState extends ConsumerState { style: TextStyle(fontSize: 42, color: Styles.electricBlue)), const Spacer(flex: 2), - BudgetNetBar(isPositive: true, net: netIncome), + BudgetNetBar( + isPositive: true, + net: netIncome, + expected: budget?.expectedIncome ?? 0.0), const Spacer(), - BudgetNetBar(isPositive: false, net: netExpense), + BudgetNetBar( + isPositive: false, + net: netExpense, + expected: expectedExpenses), const Spacer(), ], ), @@ -113,7 +123,7 @@ class _BudgetOverviewScreenState extends ConsumerState { child: Text( 'No budget categories created yet, add some!'), ), - ElevatedButton( + UiButton( onPressed: ref.watch(budgetProvider) == null @@ -127,9 +137,9 @@ class _BudgetOverviewScreenState extends ConsumerState { Styles .dialogColor, child: - const AddBudgetCategoryDialog()), + const BudgetCategoryDialog()), ), - child: const Text('Add Category'), + text: 'Add Category', ), ], ), @@ -181,7 +191,7 @@ class _BudgetOverviewScreenState extends ConsumerState { Styles .dialogColor, child: - const AddBudgetCategoryDialog()), + const BudgetCategoryDialog()), ), child: const Text( 'Add Category'), @@ -259,7 +269,7 @@ class _BudgetOverviewScreenState extends ConsumerState { return Dialog( backgroundColor: Styles.dialogColor, shape: Styles.dialogShape, - child: const AddTransactionDialog()); + child: const TransactionDialog()); }, ); }, diff --git a/lib/features/budget/screens/transactions_listview.dart b/lib/features/budget/screens/transactions_listview.dart index 14afc2b..b6c1bb1 100644 --- a/lib/features/budget/screens/transactions_listview.dart +++ b/lib/features/budget/screens/transactions_listview.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.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/global/styles.dart'; +import 'package:rluv/models/transaction_model.dart'; import '../../../global/store.dart'; +import '../../../models/budget_category_model.dart'; class TransactionsListview extends ConsumerStatefulWidget { const TransactionsListview({super.key}); @@ -14,18 +17,180 @@ class TransactionsListview extends ConsumerStatefulWidget { } class _TransactionsListviewState extends ConsumerState { + final scaffoldKey = GlobalKey(); + + @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 Widget build(BuildContext context) { - final transactions = ref.watch(transactionsProvider); - transactions.sort( - (a, b) => b.date.compareTo(a.date), - ); + final budgetCategories = ref.watch(budgetCategoriesProvider); + if (ref.read(selectedCategory) == null && budgetCategories.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) => + ref.read(selectedCategory.notifier).state = budgetCategories.first); + } + final sortType = ref.watch(selectedTransactionSortProvider); + final sortDate = ref.watch(selectedSortDateProvider); + List 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( + endDrawer: Drawer( + backgroundColor: Styles.purpleNurple, + child: SafeArea( + child: Column( + children: [ + SizedBox(height: BuildMedia(context).height * 0.15), + DropdownButton( + 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( + 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( + 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, body: SafeArea( child: Stack( children: [ 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( child: Text('No transactions'), ), @@ -33,12 +198,33 @@ class _TransactionsListviewState extends ConsumerState { if (transactions.isNotEmpty) Column( children: [ - const SizedBox(height: 28), - const Text( - 'Transaction History', - style: TextStyle(fontSize: 28), - textAlign: TextAlign.center, - ), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + 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', + 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( child: ListView.builder( physics: const BouncingScrollPhysics(), @@ -52,12 +238,65 @@ class _TransactionsListviewState extends ConsumerState { ), ], ), - 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( + (ref) => TransactionSort.all, +); + +final selectedSortDateProvider = + StateProvider((ref) => SortDate.decending); + +final transactionHistoryListProvider = StateProvider>( + (ref) => [], +); + +final selectedCategory = StateProvider((ref) => null); diff --git a/lib/features/budget/widgets/add_budget_category_dialog.dart b/lib/features/budget/widgets/add_budget_category_dialog.dart index 840ef0b..34fb6c6 100644 --- a/lib/features/budget/widgets/add_budget_category_dialog.dart +++ b/lib/features/budget/widgets/add_budget_category_dialog.dart @@ -11,16 +11,18 @@ import '../../../global/store.dart'; import '../../../global/utils.dart'; import '../../../global/widgets/ui_button.dart'; -class AddBudgetCategoryDialog extends ConsumerStatefulWidget { - const AddBudgetCategoryDialog({super.key}); +class BudgetCategoryDialog extends ConsumerStatefulWidget { + const BudgetCategoryDialog({super.key, this.category}); + + final BudgetCategory? category; @override - ConsumerState createState() => + ConsumerState createState() => _AddBudgetCategoryDialogState(); } class _AddBudgetCategoryDialogState - extends ConsumerState { + extends ConsumerState { late final Budget? budget; final categoryNameController = TextEditingController(); final amountController = TextEditingController(); @@ -32,12 +34,20 @@ class _AddBudgetCategoryDialogState int selectedColorIndex = -1; final formKey = GlobalKey(); + @override void initState() { budget = ref.read(budgetProvider); WidgetsBinding.instance.addPostFrameCallback((_) { 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(); } @@ -66,22 +76,25 @@ class _AddBudgetCategoryDialogState child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Padding( - padding: EdgeInsets.only(bottom: 18.0), - child: Text('Add Category:'), + Padding( + padding: const EdgeInsets.only(bottom: 18.0), + child: Text(widget.category == null + ? 'Add Category:' + : 'Edit Category'), ), Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const Text('Name:'), const SizedBox(width: 30), Container( - width: 120, - height: 30, + width: 150, + height: 40, decoration: Styles.boxLavenderBubble, child: TextFormField( validator: (text) { - if (text == null || text.length < 3) { - return 'Invalid Category'; + if (text == null || text.isEmpty) { + return 'Cannot be blank'; } return null; }, @@ -99,12 +112,13 @@ class _AddBudgetCategoryDialogState ), const SizedBox(height: 18), Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const Text('Amount:'), const SizedBox(width: 30), Container( - width: 80, - height: 30, + width: 150, + height: 40, decoration: Styles.boxLavenderBubble, child: TextFormField( 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( showLoading: true, color: Styles.lavender, text: 'SAVE', - onPressed: () => + onPressed: () { + if (formKey.currentState != null && + formKey.currentState!.validate()) { submitCategory(colors[selectedColorIndex]).then((_) { - Navigator.pop(context); - showSnack( - ref: ref, - text: 'Budget Category Added!', - type: SnackType.success, - ); - }), + Navigator.pop(context); + showSnack( + ref: ref, + text: 'Budget Category Added!', + type: SnackType.success, + ); + }); + } + }, ), ], ), @@ -237,24 +285,70 @@ class _AddBudgetCategoryDialogState printPink('Failed validation'); return; } - final newBudget = BudgetCategory( - id: null, - amount: double.parse(amountController.text), - budgetId: budget!.id!, - color: categoryColor, - name: categoryNameController.text, - ); + Map? budgetData; + bool success = false; + if (widget.category == null) { + final newBudget = BudgetCategory( + id: null, + 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 removeCategory() async { + final res = await ref .read(apiProvider.notifier) - .post(path: 'budget_category', data: newBudget.toJson()); - final success = budgetData != null ? budgetData['success'] as bool : false; + .delete(path: 'budget_category', data: {'id': widget.category!.id}); + final success = res != null ? res['success'] as bool : false; if (success) { ref .read(dashboardProvider.notifier) - .add({'budget_categories': budgetData}); - } else { - showSnack(ref: ref, text: 'Could not add budget', type: SnackType.error); + .removeWithId('budget_categories', widget.category!.id!); } + return success; } } diff --git a/lib/features/budget/widgets/add_transaction_dialog.dart b/lib/features/budget/widgets/add_transaction_dialog.dart index 5466350..b6e0d6f 100644 --- a/lib/features/budget/widgets/add_transaction_dialog.dart +++ b/lib/features/budget/widgets/add_transaction_dialog.dart @@ -10,21 +10,23 @@ import '../../../global/styles.dart'; import '../../../global/widgets/ui_button.dart'; import '../../../models/budget_category_model.dart'; import '../../../models/transaction_model.dart'; +import '../../../models/user.dart'; -class AddTransactionDialog extends ConsumerStatefulWidget { - const AddTransactionDialog({super.key, this.transaction}); +class TransactionDialog extends ConsumerStatefulWidget { + const TransactionDialog({super.key, this.transaction}); final Transaction? transaction; @override - ConsumerState createState() => + ConsumerState createState() => _AddTransactionDialogState(); } -class _AddTransactionDialogState extends ConsumerState { +class _AddTransactionDialogState extends ConsumerState { late DateTime selectedDate; late final TextEditingController amountController; late final TextEditingController memoController; + User? user; final amountFocusNode = FocusNode(); final memoFocusNode = FocusNode(); @@ -49,11 +51,20 @@ class _AddTransactionDialogState extends ConsumerState { if (categories.isNotEmpty) { selectedBudgetCategory = categories.first; } + final u = ref.read(userProvider); + if (u == null) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => ref.read(jwtProvider.notifier).revokeToken()); + } else { + user = u; + } + super.initState(); } @override Widget build(BuildContext context) { + if (user == null) return Container(); final List budgetCategories = ref.read(budgetCategoriesProvider); return SizedBox( @@ -274,7 +285,7 @@ class _AddTransactionDialogState extends ConsumerState { Map? data; if (widget.transaction != null) { data = await ref.read(apiProvider.notifier).put( - path: 'transactions', + path: 'transaction', data: Transaction.copyWith(widget.transaction!, { 'memo': memoController.text.isNotEmpty ? memoController.text : null, 'amount': double.parse(amountController.text), @@ -284,14 +295,14 @@ class _AddTransactionDialogState extends ConsumerState { }).toJson()); } else { data = await ref.read(apiProvider.notifier).post( - path: 'transactions', + path: 'transaction', data: Transaction( amount: double.parse(amountController.text), - addedByUserId: 1, + createdByUserId: user!.id!, budgetCategoryId: transactionType == TransactionType.income ? null : selectedBudgetCategory.id, - budgetId: 1, + budgetId: user!.budgetId, date: DateTime.now(), type: transactionType, memo: memoController.text.isNotEmpty diff --git a/lib/features/budget/widgets/budget_category_bar.dart b/lib/features/budget/widgets/budget_category_bar.dart index 5dea869..14cd7f4 100644 --- a/lib/features/budget/widgets/budget_category_bar.dart +++ b/lib/features/budget/widgets/budget_category_bar.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.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/models/budget_category_model.dart'; @@ -27,18 +28,22 @@ class BudgetCategoryBar extends StatefulWidget { class _BudgetCategoryBarState extends State { double percentSpent = 0.0; + double setTo = 0.0; @override void initState() { - Future.delayed(Duration(milliseconds: min(1600, 200 * widget.index)), () { - setState(() => - percentSpent = (widget.currentAmount / widget.budgetCategory.amount)); - }); super.initState(); } @override 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 isBright = getBrightness(widget.budgetCategory.color) == Brightness.light; @@ -50,85 +55,95 @@ class _BudgetCategoryBarState extends State { : isBright ? Colors.black87 : Colors.white); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Row( - children: [ - 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), + 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), + child: Row( + children: [ + 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( - child: Padding( - padding: const EdgeInsets.only(right: 18.0), - child: Stack( - children: [ - Container( - height: widget.height, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(13.0), - ), - ), - Padding( - padding: EdgeInsets.all(widget.innerPadding), - child: Container( - height: innerHeight, + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 18.0), + child: Stack( + children: [ + Container( + height: widget.height, decoration: BoxDecoration( - color: Styles.emptyBarGrey, - borderRadius: BorderRadius.circular(12.0), + color: Colors.black, + borderRadius: BorderRadius.circular(13.0), ), ), - ), - Padding( - padding: EdgeInsets.all(widget.innerPadding), - child: SizedBox( - 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), + Padding( + padding: EdgeInsets.all(widget.innerPadding), + child: Container( + height: innerHeight, + decoration: BoxDecoration( + color: Styles.emptyBarGrey, + borderRadius: BorderRadius.circular(12.0), + ), + ), + ), + Padding( + padding: EdgeInsets.all(widget.innerPadding), + child: SizedBox( + 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( - height: widget.height, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - '${widget.currentAmount.currency()} / ${widget.budgetCategory.amount.currency()}', - style: textStyle), + SizedBox( + height: widget.height, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + '${widget.currentAmount.currency()} / ${widget.budgetCategory.amount.currency()}', + style: textStyle), + ), ), - ), - ], + ], + ), ), - ), - ], - ), - )) - ], + ], + ), + )) + ], + ), ), ); } diff --git a/lib/features/budget/widgets/budget_net_bar.dart b/lib/features/budget/widgets/budget_net_bar.dart index 06e7384..a700c0b 100644 --- a/lib/features/budget/widgets/budget_net_bar.dart +++ b/lib/features/budget/widgets/budget_net_bar.dart @@ -4,10 +4,15 @@ import 'package:rluv/global/styles.dart'; import 'package:rluv/global/utils.dart'; 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 double net; + final double expected; @override Widget build(BuildContext context) { @@ -29,7 +34,7 @@ class BudgetNetBar extends StatelessWidget { ), ), Text( - net.currency(), + '${net.currency()} / ${expected.currency()}', style: TextStyle( fontSize: 20, color: isPositive ? Styles.incomeGreen : Styles.expensesRed), diff --git a/lib/features/budget/widgets/transaction_list_item.dart b/lib/features/budget/widgets/transaction_list_item.dart index 86604f7..6d08d9c 100644 --- a/lib/features/budget/widgets/transaction_list_item.dart +++ b/lib/features/budget/widgets/transaction_list_item.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/misc_build/build_media.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/global/api.dart'; import 'package:rluv/global/utils.dart'; import 'package:rluv/models/transaction_model.dart'; @@ -26,7 +28,7 @@ class _TransactionListItemState extends ConsumerState { double cardHeight = 70.0; BudgetCategory? budgetCategory; - late final Transaction transaction; + Transaction? transaction; @override void initState() { @@ -35,12 +37,16 @@ class _TransactionListItemState extends ConsumerState { @override 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); - if (transaction.type == TransactionType.expense) { + if (transaction!.type == TransactionType.expense) { budgetCategory = budgetCategories.singleWhere( - (category) => category.id == transaction.budgetCategoryId, + (category) => category.id == transaction!.budgetCategoryId, ); + } else { + budgetCategory = null; } return GestureDetector( onTap: () => toggleDetails(), @@ -67,7 +73,7 @@ class _TransactionListItemState extends ConsumerState { duration: const Duration(milliseconds: 200), height: cardHeight, width: 6, - color: transaction.type == TransactionType.income + color: transaction!.type == TransactionType.income ? Styles.incomeBlue : Styles.expensesOrange), SizedBox( @@ -79,7 +85,7 @@ class _TransactionListItemState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(DateFormat('EEE MMM d, h:mm a') - .format(transaction.date)), + .format(transaction!.date)), Row( children: [ const SizedBox( @@ -94,7 +100,7 @@ class _TransactionListItemState extends ConsumerState { ), ], Text( - transaction.type == TransactionType.income + transaction!.type == TransactionType.income ? 'Income' : budgetCategory!.name, style: const TextStyle(fontSize: 20), @@ -103,7 +109,7 @@ class _TransactionListItemState extends ConsumerState { ), if (showDetails) Text( - transaction.memo ?? '', + transaction!.memo ?? '', style: const TextStyle(fontSize: 16), ) ], @@ -115,14 +121,14 @@ class _TransactionListItemState extends ConsumerState { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( - transaction.amount.currency(), + transaction!.amount.currency(), style: TextStyle( fontSize: 24, - color: transaction.type == TransactionType.income + color: transaction!.type == TransactionType.income ? Styles.incomeGreen : Styles.expensesRed), ), - if (showDetails) + if (showDetails) ...[ IconButton( icon: const Icon( Icons.edit_rounded, @@ -133,12 +139,20 @@ class _TransactionListItemState extends ConsumerState { builder: (context) => Dialog( backgroundColor: Styles.dialogColor, shape: Styles.dialogShape, - child: AddTransactionDialog( - transaction: transaction), + child: + TransactionDialog(transaction: transaction), ), ); }, ), + IconButton( + icon: const Icon( + Icons.delete, + color: Styles.expensesRed, + ), + onPressed: () => deleteTransaction(), + ), + ], ], ), const SizedBox( @@ -165,4 +179,23 @@ class _TransactionListItemState extends ConsumerState { }); } } + + 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); + } + } } diff --git a/lib/features/notes/screens/notes_screen.dart b/lib/features/notes/screens/notes_screen.dart index cd47158..e47945b 100644 --- a/lib/features/notes/screens/notes_screen.dart +++ b/lib/features/notes/screens/notes_screen.dart @@ -69,7 +69,7 @@ class _SharedNotesScreenState extends ConsumerState { children: [ FittedBox( child: Text(note.title, - style: TextStyle(fontSize: 20))), + style: const TextStyle(fontSize: 20))), Flexible( child: Text( note.content.length > 80 @@ -183,7 +183,6 @@ class _NoteBottomSheetState extends ConsumerState { @override Widget build(BuildContext context) { final screen = BuildMedia(context).size; - final bottomInset = MediaQuery.of(context).viewInsets.bottom; return Container( width: screen.width, decoration: BoxDecoration( @@ -232,11 +231,11 @@ class _NoteBottomSheetState extends ConsumerState { if (widget.index == null) { res = await ref .read(apiProvider) - .post('shared_notes', data: newNote.toJson()); + .post('shared_note', data: newNote.toJson()); } else { res = await ref .read(apiProvider) - .put('shared_notes', data: newNote.toJson()); + .put('shared_note', data: newNote.toJson()); } if (res.data != null && res.data['success']) { if (widget.index == null) { diff --git a/lib/global/api.dart b/lib/global/api.dart index 0b6a40d..c559969 100644 --- a/lib/global/api.dart +++ b/lib/global/api.dart @@ -54,7 +54,7 @@ final apiProvider = StateNotifierProvider<_ApiNotifier, Dio>((ref) { class _ApiNotifier extends StateNotifier { _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 = "https://rluv.fosscat.com/"; dio.interceptors.addAll([ @@ -93,6 +93,7 @@ class _ApiNotifier extends StateNotifier { if (err.response?.statusCode == 403) { ref.read(jwtProvider.notifier).revokeToken(); } + return handler.next(err); }), _LoggingInterceptor(), ]); diff --git a/lib/global/store.dart b/lib/global/store.dart index 5f84de1..f966a74 100644 --- a/lib/global/store.dart +++ b/lib/global/store.dart @@ -189,4 +189,26 @@ class DashBoardStateNotifier extends StateNotifier?> { 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; + final newState = state; + subStateList + .removeWhere((e) => (e as Map)['id'] == id); + newState![stateKey] = subStateList; + state = {...newState}; + // printBlue(state); + break; + default: + break; + } + } } diff --git a/lib/global/styles.dart b/lib/global/styles.dart index b160ad6..434b33b 100644 --- a/lib/global/styles.dart +++ b/lib/global/styles.dart @@ -59,6 +59,9 @@ class Styles { contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), labelText: labelText, + errorStyle: const TextStyle( + color: Styles.expensesRed, + ), focusColor: Styles.blushingPink, hoverColor: Styles.blushingPink, fillColor: Styles.lavender, diff --git a/lib/main.dart b/lib/main.dart index 67bf294..1c243a6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -98,7 +98,7 @@ class _HomeState extends ConsumerState { initData = {}; } return Scaffold( - key: ref.read(scaffoldKeyProvider), + key: ref.read(_scaffoldKeyProvider), resizeToAvoidBottomInset: false, drawer: Drawer( backgroundColor: Styles.purpleNurple, @@ -159,7 +159,7 @@ class _HomeState extends ConsumerState { } toggleDrawer() async { - final key = ref.read(scaffoldKeyProvider); + final key = ref.read(_scaffoldKeyProvider); if (key.currentState != null && key.currentState!.isDrawerOpen) { key.currentState!.openEndDrawer(); } else if (key.currentState != null) { @@ -168,7 +168,7 @@ class _HomeState extends ConsumerState { } } -final scaffoldKeyProvider = Provider>( +final _scaffoldKeyProvider = Provider>( (ref) => GlobalKey(), ); diff --git a/lib/models/budget.dart b/lib/models/budget.dart index e0a90ec..687fe53 100644 --- a/lib/models/budget.dart +++ b/lib/models/budget.dart @@ -10,6 +10,7 @@ class Budget { this.id, required this.familyId, required this.name, + required this.expectedIncome, required this.createdAt, required this.updatedAt, this.hide = false, @@ -17,6 +18,7 @@ class Budget { final int? id; final int familyId; + final double? expectedIncome; final String name; @JsonKey(fromJson: boolFromJson, toJson: boolToJson) diff --git a/lib/models/budget.g.dart b/lib/models/budget.g.dart index 204b6bc..6073daf 100644 --- a/lib/models/budget.g.dart +++ b/lib/models/budget.g.dart @@ -10,6 +10,7 @@ Budget _$BudgetFromJson(Map json) => Budget( id: json['id'] as int?, familyId: json['family_id'] as int, name: json['name'] as String, + expectedIncome: (json['expected_income'] as num?)?.toDouble(), createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), @@ -18,6 +19,7 @@ Budget _$BudgetFromJson(Map json) => Budget( Map _$BudgetToJson(Budget instance) => { 'id': instance.id, 'family_id': instance.familyId, + 'expected_income': instance.expectedIncome, 'name': instance.name, 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), diff --git a/lib/models/budget_category_model.dart b/lib/models/budget_category_model.dart index 635f1bc..c9af882 100644 --- a/lib/models/budget_category_model.dart +++ b/lib/models/budget_category_model.dart @@ -40,4 +40,18 @@ class BudgetCategory { _$BudgetCategoryFromJson(json); Map toJson() => _$BudgetCategoryToJson(this); + + factory BudgetCategory.copyWith( + {required BudgetCategory category, + required Map 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, + ); } diff --git a/lib/models/transaction_model.dart b/lib/models/transaction_model.dart index 244867b..3c76244 100644 --- a/lib/models/transaction_model.dart +++ b/lib/models/transaction_model.dart @@ -17,7 +17,7 @@ class Transaction { required this.type, required this.budgetId, this.budgetCategoryId, - required this.addedByUserId, + required this.createdByUserId, required this.date, this.memo, this.createdAt, @@ -26,7 +26,7 @@ class Transaction { }); final int? id, budgetCategoryId; - final int budgetId, addedByUserId; + final int budgetId, createdByUserId; final double amount; final String? memo; final TransactionType type; @@ -53,7 +53,7 @@ class Transaction { type: data['type'] ?? trans.type, budgetId: data['budget_id'] ?? trans.budgetId, 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, memo: data['memo'] ?? trans.memo, createdAt: data['created_at'] ?? trans.createdAt, diff --git a/lib/models/transaction_model.g.dart b/lib/models/transaction_model.g.dart index 0f0b6b4..2fba12a 100644 --- a/lib/models/transaction_model.g.dart +++ b/lib/models/transaction_model.g.dart @@ -12,7 +12,7 @@ Transaction _$TransactionFromJson(Map json) => Transaction( type: $enumDecode(_$TransactionTypeEnumMap, json['type']), budgetId: json['budget_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), memo: json['memo'] as String?, createdAt: dateFromJson(json['created_at'] as int), @@ -25,7 +25,7 @@ Map _$TransactionToJson(Transaction instance) => 'id': instance.id, 'budget_category_id': instance.budgetCategoryId, 'budget_id': instance.budgetId, - 'added_by_user_id': instance.addedByUserId, + 'created_by_user_id': instance.createdByUserId, 'amount': instance.amount, 'memo': instance.memo, 'type': _$TransactionTypeEnumMap[instance.type]!,