From 18aad2b3d52a1e67fb980050cda164a9953eeb71 Mon Sep 17 00:00:00 2001 From: Nathan Anderson Date: Sat, 22 Jul 2023 21:29:32 -0600 Subject: [PATCH] Added working edit transaction --- .../budget/screens/budget_overview.dart | 474 ++++++++++-------- .../budget/screens/transactions_listview.dart | 63 +++ .../widgets/add_budget_category_dialog.dart | 265 ++++++++++ .../widgets/add_transaction_dialog.dart | 441 +++++++++++----- .../budget/widgets/budget_category_bar.dart | 157 ++++++ .../budget/widgets/budget_net_bar.dart | 3 +- .../budget/widgets/transaction_list_item.dart | 168 +++++++ lib/features/notes/screens/notes_screen.dart | 51 ++ .../settings/screens/settings_screen.dart | 20 + lib/global/api.dart | 153 ++++-- lib/global/store.dart | 138 ++++- lib/global/styles.dart | 58 ++- lib/global/utils.dart | 61 ++- lib/global/widgets/budget_color_circle.dart | 35 ++ lib/global/widgets/drawer_button.dart | 54 ++ lib/main.dart | 87 +++- lib/models/budget.dart | 3 + lib/models/budget.g.dart | 2 + lib/models/budget_category_model.dart | 12 +- lib/models/budget_category_model.g.dart | 4 + lib/models/family_model.dart | 3 + lib/models/family_model.g.dart | 2 + lib/models/shared_note.dart | 46 ++ lib/models/shared_note.g.dart | 34 ++ lib/models/transaction_model.dart | 35 +- lib/models/transaction_model.g.dart | 15 +- lib/models/user.dart | 3 + lib/models/user.g.dart | 2 + 28 files changed, 1959 insertions(+), 430 deletions(-) create mode 100644 lib/features/budget/screens/transactions_listview.dart create mode 100644 lib/features/budget/widgets/add_budget_category_dialog.dart create mode 100644 lib/features/budget/widgets/transaction_list_item.dart create mode 100644 lib/features/notes/screens/notes_screen.dart create mode 100644 lib/features/settings/screens/settings_screen.dart create mode 100644 lib/global/widgets/budget_color_circle.dart create mode 100644 lib/global/widgets/drawer_button.dart create mode 100644 lib/models/shared_note.dart create mode 100644 lib/models/shared_note.g.dart diff --git a/lib/features/budget/screens/budget_overview.dart b/lib/features/budget/screens/budget_overview.dart index 7749807..d02f703 100644 --- a/lib/features/budget/screens/budget_overview.dart +++ b/lib/features/budget/screens/budget_overview.dart @@ -1,15 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers.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/budget_category_bar.dart'; import 'package:rluv/features/budget/widgets/budget_net_bar.dart'; import 'package:rluv/global/styles.dart'; import 'package:rluv/global/utils.dart'; import '../../../global/store.dart'; +import '../../../models/transaction_model.dart'; +import '../widgets/add_budget_category_dialog.dart'; class BudgetOverviewScreen extends ConsumerStatefulWidget { - const BudgetOverviewScreen({super.key}); + const BudgetOverviewScreen({super.key, required this.initialData}); + + final Map initialData; @override ConsumerState createState() => @@ -20,234 +26,256 @@ class _BudgetOverviewScreenState extends ConsumerState { final budgetListScrollController = ScrollController(); @override Widget build(BuildContext context) { - final budgetCategoriesRef = ref.watch(Store().budgetCategoriesProvider); + final budgetCategories = ref.watch(Store().budgetCategoriesProvider); + final transactions = ref.watch(Store().transactionsProvider); final screen = BuildMedia(context).size; - return budgetCategoriesRef.when( - data: ((budgetCategories) => RefreshIndicator( - onRefresh: () async { - final _ = await ref.refresh(Store().dashboardProvider.future); - }, + double netExpense = 0.0; + double netIncome = 0.0; + Map budgetCategoryNetMap = {}; + netExpense = transactions + .where((t) => t.type == TransactionType.expense) + .fold(netExpense, (net, t) => net + t.amount); + netIncome = transactions + .where((t) => t.type == TransactionType.income) + .fold(netIncome, (net, t) => net + t.amount); + for (final bud in budgetCategories) { + double net = 0.0; + net = transactions + .where((t) => t.budgetCategoryId == bud.id) + .fold(net, (net, t) => net + t.amount); + budgetCategoryNetMap[bud.id!] = net; + } + return RefreshIndicator( + displacement: 20.0, + onRefresh: () async { + final family = ref.read(Store().familyProvider).valueOrNull; + final _ = + ref.read(Store().dashboardProvider.notifier).fetchDashboard(family); + }, + child: Column( + children: [ + /// TOP HALF, TITLE & OVERVIEW + Container( + height: screen.height * 0.3, + width: screen.width, + color: Styles.purpleNurple, child: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - /// TOP HALF, TITLE & OVERVIEW - Container( - height: screen.height * 0.33, - width: screen.width, - color: Styles.purpleNurple, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(flex: 2), - Text( - formatDate(DateTime.now()), - style: const TextStyle( - fontSize: 16, color: Styles.electricBlue), - ), - const Spacer(), - const Text('MONTHLY', - style: TextStyle( - fontSize: 42, color: Styles.electricBlue)), - const Spacer(flex: 2), - const BudgetNetBar(isPositive: true, net: 5024.64), - const Spacer(), - const BudgetNetBar(isPositive: false, net: 2004.37), - const Spacer(), - ], - ), + const Spacer(flex: 2), + Text( + formatDate(DateTime.now()), + style: + const TextStyle(fontSize: 16, color: Styles.electricBlue), ), - - /// BOTTOM HALF, BUDGET BREAKDOWN - Expanded( - child: SizedBox( - width: screen.width, - child: Column( - mainAxisSize: MainAxisSize.max, - children: [ - Padding( - padding: const EdgeInsets.all(14.0), - child: Container( - decoration: BoxDecoration( - color: Styles.blushingPink, - borderRadius: BorderRadius.circular(16.0), - ), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'BUDGET', - style: TextStyle( - fontSize: 28, - color: Styles.electricBlue), - ), - ), - Scrollbar( - controller: budgetListScrollController, - thumbVisibility: true, - child: ListView( - controller: budgetListScrollController, - shrinkWrap: true, - children: [ - for (final budget in budgetCategories) - Row( - children: [ - Padding( - padding: - const EdgeInsets.all(8.0), - child: SizedBox( - width: 85, - child: Text( - budget.name, - style: const TextStyle( - fontSize: 16.0), - ), - ), - ), - AnimatedContainer( - duration: const Duration( - milliseconds: 500), - child: Expanded( - child: Padding( - padding: const EdgeInsets.only( - right: 18.0), - child: Stack( - children: [ - Container( - height: 30, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: - BorderRadius - .circular(10.0), - ), - ), - Padding( - padding: - const EdgeInsets.all( - 2.0), - child: Container( - height: 26, - decoration: - BoxDecoration( - color: Styles - .emptyBarGrey, - borderRadius: - BorderRadius - .circular( - 10.0), - ), - ), - ), - Padding( - padding: - const EdgeInsets.all( - 2.0), - child: SizedBox( - height: 26, - child: - FractionallySizedBox( - heightFactor: 1.0, - widthFactor: 0.5, - child: Container( - decoration: - BoxDecoration( - color: - budget.color, - borderRadius: - BorderRadius - .circular( - 10.0), - ), - ), - ), - ), - ), - ], - ), - )), - ) - ], - ), - const SizedBox(height: 20), - ], - ), - ), - ], - ), - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.only(bottom: 24.0), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only( - left: 20.0, right: 15.0), - child: Container( - height: 70, - decoration: BoxDecoration( - color: Styles.seaweedGreen, - borderRadius: BorderRadius.circular(15.0), - ), - child: InkWell( - child: const Center( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 20.0, vertical: 14.0), - child: Text( - 'Transaction History', - textAlign: TextAlign.center, - style: TextStyle(fontSize: 25), - ), - ), - ), - onTap: () { - printRed('FIXME'); - }, - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 20.0), - child: Container( - decoration: BoxDecoration( - color: Styles.purpleNurple, - borderRadius: BorderRadius.circular(40.0), - ), - height: 80, - width: 80, - child: IconButton( - icon: const Icon( - Icons.add, - size: 48, - color: Styles.lavender, - ), - onPressed: () { - showDialog( - context: context, - builder: (BuildContext context) { - return const AddTransactionDialog(); - }, - ); - }, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ) + const Spacer(), + const Text('MONTHLY', + style: TextStyle(fontSize: 42, color: Styles.electricBlue)), + const Spacer(flex: 2), + BudgetNetBar(isPositive: true, net: netIncome), + const Spacer(), + BudgetNetBar(isPositive: false, net: netExpense), + const Spacer(), ], ), - )), - loading: () => const CircularProgressIndicator(), - error: (error, stackTrace) => Text('Error: $error, \n\n$stackTrace'), + ), + + /// BOTTOM HALF, BUDGET BREAKDOWN + Expanded( + child: Container( + color: Styles.sunflower, + width: screen.width, + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(14.0), + child: Container( + decoration: BoxDecoration( + color: Styles.blushingPink, + borderRadius: BorderRadius.circular(16.0), + ), + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'BUDGET', + style: TextStyle( + fontSize: 28, color: Styles.electricBlue), + ), + ), + budgetCategories.isEmpty + ? Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: screen.width * 0.8, + child: Column( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'No budget categories created yet, add some!'), + ), + ElevatedButton( + onPressed: ref.watch( + Store().budgetProvider) == + null + ? null + : () => showDialog( + context: context, + builder: (context) => Dialog( + shape: + Styles.dialogShape, + backgroundColor: + Styles.dialogColor, + child: + const AddBudgetCategoryDialog()), + ), + child: const Text('Add Category'), + ), + ], + ), + ), + ) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2.0, vertical: 4.0), + child: SizedBox( + height: screen.height * 0.36, + child: Scrollbar( + controller: budgetListScrollController, + thumbVisibility: true, + child: ListView( + physics: const BouncingScrollPhysics(), + controller: budgetListScrollController, + shrinkWrap: true, + children: [ + ...budgetCategories + .mapIndexed((i, category) { + return BudgetCategoryBar( + budgetCategory: category, + currentAmount: + budgetCategoryNetMap[ + category.id]!, + index: i, + ); + }).toList(), + const SizedBox(height: 20), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SizedBox( + width: 140, + child: ElevatedButton( + onPressed: ref.watch(Store() + .budgetProvider) == + null + ? null + : () => showDialog( + context: context, + builder: (context) => Dialog( + shape: Styles + .dialogShape, + backgroundColor: + Styles + .dialogColor, + child: + const AddBudgetCategoryDialog()), + ), + child: const Text( + 'Add Category'), + ), + ), + ], + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ], + ), + ), + ), + const Spacer(), + Row( + children: [ + Expanded( + child: Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 15.0), + child: Container( + height: 70, + decoration: BoxDecoration( + color: Styles.seaweedGreen, + borderRadius: BorderRadius.circular(15.0), + ), + child: InkWell( + child: const Center( + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 20.0, vertical: 14.0), + child: FittedBox( + child: Text( + 'Transaction History', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 25), + ), + ), + ), + ), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const TransactionsListview())); + }, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 20.0), + child: Container( + decoration: BoxDecoration( + color: Styles.purpleNurple, + borderRadius: BorderRadius.circular(40.0), + ), + height: 80, + width: 80, + child: IconButton( + icon: const Icon( + Icons.add, + size: 48, + color: Styles.lavender, + ), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Styles.dialogColor, + shape: Styles.dialogShape, + child: const AddTransactionDialog()); + }, + ); + }, + ), + ), + ), + ], + ), + const Spacer(), + ], + ), + ), + ) + ], + ), ); } } diff --git a/lib/features/budget/screens/transactions_listview.dart b/lib/features/budget/screens/transactions_listview.dart new file mode 100644 index 0000000..8adc0c2 --- /dev/null +++ b/lib/features/budget/screens/transactions_listview.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:rluv/features/budget/widgets/transaction_list_item.dart'; +import 'package:rluv/global/styles.dart'; + +import '../../../global/store.dart'; + +class TransactionsListview extends ConsumerStatefulWidget { + const TransactionsListview({super.key}); + + @override + ConsumerState createState() => + _TransactionsListviewState(); +} + +class _TransactionsListviewState extends ConsumerState { + @override + Widget build(BuildContext context) { + final transactions = ref.watch(Store().transactionsProvider); + transactions.sort( + (a, b) => b.date.compareTo(a.date), + ); + return Scaffold( + backgroundColor: Styles.purpleNurple, + body: SafeArea( + child: Stack( + children: [ + if (transactions.isEmpty) ...[ + const Center( + child: Text('No transactions'), + ), + ], + if (transactions.isNotEmpty) + Column( + children: [ + const SizedBox(height: 28), + const Text( + 'Transaction History', + style: TextStyle(fontSize: 28), + textAlign: TextAlign.center, + ), + Expanded( + child: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: transactions.length, + itemBuilder: (BuildContext context, int index) { + return TransactionListItem( + index: index, + ); + }, + ), + ), + ], + ), + IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: () => Navigator.pop(context)), + ], + ), + ), + ); + } +} diff --git a/lib/features/budget/widgets/add_budget_category_dialog.dart b/lib/features/budget/widgets/add_budget_category_dialog.dart new file mode 100644 index 0000000..3846bd2 --- /dev/null +++ b/lib/features/budget/widgets/add_budget_category_dialog.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/misc_build/build_media.dart'; +import 'package:helpers/helpers/print.dart'; +import 'package:rluv/global/styles.dart'; +import 'package:rluv/models/budget.dart'; +import 'package:rluv/models/budget_category_model.dart'; + +import '../../../global/api.dart'; +import '../../../global/store.dart'; + +class AddBudgetCategoryDialog extends ConsumerStatefulWidget { + const AddBudgetCategoryDialog({super.key}); + + @override + ConsumerState createState() => + _AddBudgetCategoryDialogState(); +} + +class _AddBudgetCategoryDialogState + extends ConsumerState { + bool loading = false; + bool complete = false; + late final Budget? budget; + final categoryNameController = TextEditingController(); + final amountController = TextEditingController(); + + final categoryFocusNode = FocusNode(); + final amountFocusNode = FocusNode(); + + final colors = Styles.curatedColors; + int selectedColorIndex = -1; + + final formKey = GlobalKey(); + @override + void initState() { + budget = ref.read(Store().budgetProvider); + WidgetsBinding.instance.addPostFrameCallback((_) { + categoryFocusNode.requestFocus(); + }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (budget == null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Could not get your budget'), + ), + ); + }); + return Container(); + } else if (complete) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Budget Category Added!'), + ), + ); + }); + return Container(); + } + return SizedBox( + width: BuildMedia(context).width * Styles.dialogScreenWidthFactor, + child: Stack( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 18.0, horizontal: 32.0), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 18.0), + child: Text('Add Category:'), + ), + Row( + children: [ + const Text('Name:'), + const SizedBox(width: 30), + Container( + width: 120, + height: 30, + decoration: Styles.boxLavenderBubble, + child: TextFormField( + validator: (text) { + if (text == null || text.length < 3) { + return 'Invalid Category'; + } + return null; + }, + focusNode: categoryFocusNode, + textAlign: TextAlign.center, + cursorColor: Styles.blushingPink, + controller: categoryNameController, + decoration: Styles.inputLavenderBubble(), + onFieldSubmitted: (_) { + amountFocusNode.requestFocus(); + }, + ), + ), + ], + ), + const SizedBox(height: 18), + Row( + children: [ + const Text('Amount:'), + const SizedBox(width: 30), + Container( + width: 80, + height: 30, + decoration: Styles.boxLavenderBubble, + child: TextFormField( + validator: (text) { + try { + if (text == null || text.isEmpty) { + return 'Invalid Amount'; + } + final amount = double.tryParse(text); + if (amount == null) { + return 'Not A Number'; + } + if (amount < 0) { + return 'Not Positive'; + } + return null; + } catch (err) { + return 'Invalid input'; + } + }, + focusNode: amountFocusNode, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + cursorColor: Styles.blushingPink, + controller: amountController, + decoration: Styles.inputLavenderBubble(), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 6.0, horizontal: 16.0), + child: SizedBox( + height: 76, + child: ListView.builder( + itemCount: colors.length, + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + if (selectedColorIndex != index) { + setState(() => selectedColorIndex = index); + } else { + setState(() => selectedColorIndex = -1); + } + printBlue(selectedColorIndex); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedContainer( + decoration: selectedColorIndex == index + ? BoxDecoration( + borderRadius: + BorderRadius.circular(15.0), + border: Border.all( + color: Styles.washedStone, + width: 2.0, + ), + ) + : BoxDecoration( + borderRadius: + BorderRadius.circular(5.0), + border: Border.all( + color: Colors.transparent, + width: 2.0, + ), + ), + duration: const Duration(milliseconds: 400), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + height: 40, + width: 40, + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + ), + Container( + height: 40, + width: 40, + decoration: BoxDecoration( + color: colors[index], + shape: BoxShape.circle, + border: Border.all( + color: Colors.black, width: 1.5), + ), + ), + ], + ), + ), + ), + ), + ); + }, + ), + ), + ), + ElevatedButton( + onPressed: () => submitCategory(colors[selectedColorIndex]), + child: const Text('SAVE'), + ) + ], + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + if (loading) const Center(child: CircularProgressIndicator()), + ], + ), + ); + } + + Future submitCategory(Color categoryColor) async { + if (formKey.currentState != null && !formKey.currentState!.validate()) { + printPink('Failed validation'); + return; + } + setState( + () => loading = true, + ); + final newBudget = BudgetCategory( + id: null, + amount: double.parse(amountController.text), + budgetId: budget!.id!, + color: categoryColor, + name: categoryNameController.text, + ); + + final budgetData = + await Api().post(path: 'budget_category', data: newBudget.toJson()); + final success = budgetData != null ? budgetData['success'] as bool : false; + if (success) { + ref + .read(Store().dashboardProvider.notifier) + .add({'budget_categories': budgetData}); + } + complete = true; + setState(() { + loading = false; + }); + } +} diff --git a/lib/features/budget/widgets/add_transaction_dialog.dart b/lib/features/budget/widgets/add_transaction_dialog.dart index 47d70f6..92fa676 100644 --- a/lib/features/budget/widgets/add_transaction_dialog.dart +++ b/lib/features/budget/widgets/add_transaction_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/misc_build/build_media.dart'; import '../../../global/api.dart'; import '../../../global/store.dart'; @@ -9,7 +10,9 @@ import '../../../models/budget_category_model.dart'; import '../../../models/transaction_model.dart'; class AddTransactionDialog extends ConsumerStatefulWidget { - const AddTransactionDialog({super.key}); + const AddTransactionDialog({super.key, this.transaction}); + + final Transaction? transaction; @override ConsumerState createState() => @@ -17,146 +20,310 @@ class AddTransactionDialog extends ConsumerStatefulWidget { } class _AddTransactionDialogState extends ConsumerState { - final amountController = TextEditingController(); + bool loading = false; + bool complete = false; + late final TextEditingController amountController; + late final TextEditingController memoController; + + final amountFocusNode = FocusNode(); + final memoFocusNode = FocusNode(); + + TransactionType transactionType = TransactionType.expense; + late BudgetCategory selectedBudgetCategory; + + @override + void initState() { + if (widget.transaction != null) { + amountController = + TextEditingController(text: widget.transaction!.amount.toString()); + memoController = TextEditingController(text: widget.transaction!.memo); + transactionType = widget.transaction!.type; + } else { + amountController = TextEditingController(); + memoController = TextEditingController(); + } + final categories = ref.read(Store().budgetCategoriesProvider); + if (categories.isNotEmpty) { + selectedBudgetCategory = categories.first; + } + super.initState(); + } @override Widget build(BuildContext context) { - final List budgetCategories = - ref.read(Store().budgetCategoriesProvider).requireValue; - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10.0), - side: const BorderSide(color: Styles.purpleNurple, width: 5.0), - ), - child: StatefulBuilder(builder: (context, setState) { - TransactionType transactionType = TransactionType.expense; - BudgetCategory selectedBudgetCategory = budgetCategories.first; - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10.0), - color: Styles.purpleNurple, - ), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row(children: [ - InkWell( - onTap: transactionType == TransactionType.expense - ? null - : () { - setState(() => - transactionType = TransactionType.expense); - }, - child: AnimatedContainer( - height: 30, - width: 70, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8.0), - topRight: Radius.circular(8.0)), - color: transactionType == TransactionType.expense - ? Styles.lavender - : Styles.sand), - duration: const Duration(milliseconds: 300), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Expense'), - ), - ), - ), - InkWell( - onTap: transactionType == TransactionType.income - ? null - : () { - setState( - () => transactionType = TransactionType.income); - }, - child: AnimatedContainer( - height: 30, - width: 70, - decoration: BoxDecoration( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8.0), - topRight: Radius.circular(8.0)), - color: transactionType == TransactionType.income - ? Styles.lavender - : Styles.sand), - duration: const Duration(milliseconds: 300), - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Income'), - ), - ), - ), - ]), - Row( - children: [ - const Text('Amount:'), - SizedBox( - width: 80, - child: TextField( - controller: amountController, - decoration: InputDecoration( - fillColor: Styles.lavender, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10.0), - borderSide: - const BorderSide(color: Colors.transparent), - ), - ), - ), - ), - ], - ), - Row( - children: [ - const Padding( - padding: EdgeInsets.only(right: 28.0), - child: Text('Category:'), - ), - DropdownButton( - value: selectedBudgetCategory, - items: budgetCategories - .map( - (e) => DropdownMenuItem( - value: e, - child: Text(e.name), - ), - ) - .toList(), - onChanged: (BudgetCategory? value) { - if (value != null) { - if (kDebugMode) { - print('${value.name} selected'); - } - setState(() => selectedBudgetCategory = value); - } - }, - ), - ], - ), - ElevatedButton( - child: const Text('Add'), - onPressed: () { - Api().put( - path: 'transactions', - data: Transaction( - amount: double.parse(amountController.text), - addedByUserId: 1, - budgetCategoryId: 1, - budgetId: 1, - date: DateTime.now(), - transactionType: transactionType)); - Navigator.pop(context); - }, - ), - ], - ), + if (complete) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(widget.transaction != null + ? 'Transaction updated!' + : 'Transaction added!'), ), ); - }), - ); + }); + return Container(); + } + final List budgetCategories = + ref.read(Store().budgetCategoriesProvider); + return SizedBox( + width: BuildMedia(context).width * Styles.dialogScreenWidthFactor, + child: budgetCategories.isEmpty + ? const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Add budget categories to sort your transactions', + textAlign: TextAlign.center, + ), + ) + : Padding( + padding: const EdgeInsets.all(18.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.transaction == null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 8.0), + child: Row(children: [ + if (budgetCategories.isNotEmpty) + InkWell( + onTap: transactionType == TransactionType.expense + ? null + : () { + setState(() => transactionType = + TransactionType.expense); + }, + child: AnimatedContainer( + height: 38, + width: 80, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0)), + color: transactionType == + TransactionType.expense + ? Styles.lavender + : Styles.washedStone), + duration: const Duration(milliseconds: 300), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Expense', + style: TextStyle(fontSize: 16)), + ), + ), + ), + InkWell( + onTap: transactionType == TransactionType.income + ? null + : () { + setState(() => transactionType = + TransactionType.income); + }, + child: AnimatedContainer( + height: 38, + width: 80, + decoration: BoxDecoration( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8.0), + topRight: Radius.circular(8.0)), + color: + transactionType == TransactionType.income + ? Styles.lavender + : Styles.washedStone), + duration: const Duration(milliseconds: 300), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Income', + style: TextStyle(fontSize: 16)), + ), + ), + ), + ]), + ), + Row( + children: [ + const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Amount:', + style: TextStyle(fontSize: 18.0), + ), + ), + Container( + width: 120, + height: 40, + decoration: Styles.boxLavenderBubble, + child: TextFormField( + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18.0), + cursorColor: Styles.blushingPink, + decoration: Styles.inputLavenderBubble(), + focusNode: amountFocusNode, + controller: amountController, + onFieldSubmitted: (_) { + memoFocusNode.requestFocus(); + }, + ), + ), + ], + ), + if (budgetCategories.isEmpty) + const Padding( + padding: EdgeInsets.all(18.0), + child: Text( + 'Add budget categories to sort your transactions', + textAlign: TextAlign.center, + ), + ), + if (budgetCategories.isNotEmpty) + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: transactionType == TransactionType.income + ? const SizedBox(height: 12.5) + : Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Row( + children: [ + const Padding( + padding: EdgeInsets.only(right: 28.0), + child: Text( + 'Category:', + style: TextStyle(fontSize: 18.0), + ), + ), + DropdownButton( + dropdownColor: Styles.lavender, + value: selectedBudgetCategory, + 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) { + if (kDebugMode) { + print('${value.name} selected'); + } + setState(() => + selectedBudgetCategory = value); + } + }, + ), + ], + ), + ), + ), + const Padding( + padding: EdgeInsets.all(8.0), + child: Row( + children: [ + Text( + 'Memo', + style: TextStyle(fontSize: 18.0), + ), + ], + ), + ), + Container( + width: BuildMedia(context).width * 0.65, + height: 100, + decoration: Styles.boxLavenderBubble, + child: TextFormField( + style: const TextStyle(fontSize: 18.0), + maxLines: 4, + focusNode: memoFocusNode, + textAlign: TextAlign.left, + cursorColor: Styles.blushingPink, + controller: memoController, + decoration: Styles.inputLavenderBubble(), + onFieldSubmitted: (_) {}, + ), + ), + ElevatedButton( + onPressed: loading ? null : () => submitTransaction(), + child: const Text('Add'), + ), + ], + ), + )); + } + + Future submitTransaction() async { + setState(() { + loading = true; + }); + Map? data; + if (widget.transaction != null) { + data = await Api().put( + path: 'transactions', + data: Transaction.copyWith(widget.transaction!, { + 'memo': memoController.text.isNotEmpty ? memoController.text : null, + 'amount': double.parse(amountController.text), + 'budget_category_id': transactionType == TransactionType.income + ? null + : selectedBudgetCategory.id, + }).toJson()); + } else { + data = await Api().post( + path: 'transactions', + data: Transaction( + amount: double.parse(amountController.text), + addedByUserId: 1, + budgetCategoryId: transactionType == TransactionType.income + ? null + : selectedBudgetCategory.id, + budgetId: 1, + date: DateTime.now(), + type: transactionType, + memo: memoController.text.isNotEmpty + ? memoController.text + : null) + .toJson()); + } + final success = data != null ? data['success'] as bool : false; + if (success) { + if (widget.transaction != null) { + ref + .read(Store().dashboardProvider.notifier) + .update({'transactions': data}); + } else { + ref + .read(Store().dashboardProvider.notifier) + .add({'transactions': data}); + } + complete = true; + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(widget.transaction != null + ? 'Failed to edit transaction' + : 'Failed to add transaction'), + ), + ); + } + setState(() { + loading = false; + }); } } diff --git a/lib/features/budget/widgets/budget_category_bar.dart b/lib/features/budget/widgets/budget_category_bar.dart index e69de29..11d25a7 100644 --- a/lib/features/budget/widgets/budget_category_bar.dart +++ b/lib/features/budget/widgets/budget_category_bar.dart @@ -0,0 +1,157 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:helpers/helpers/print.dart'; +import 'package:rluv/global/utils.dart'; +import 'package:rluv/models/budget_category_model.dart'; + +import '../../../global/styles.dart'; + +class BudgetCategoryBar extends StatefulWidget { + const BudgetCategoryBar( + {super.key, + required this.budgetCategory, + required this.currentAmount, + required this.index, + this.height = 32, + this.innerPadding = 1.5}); + + final int index; + final double currentAmount; + final BudgetCategory budgetCategory; + final double height; + final double innerPadding; + @override + State createState() => _BudgetCategoryBarState(); +} + +class _BudgetCategoryBarState extends State { + double percentSpent = 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) { + final innerHeight = widget.height - widget.innerPadding * 2; + final isBright = + getBrightness(widget.budgetCategory.color) == Brightness.light; + final textStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: widget.currentAmount > widget.budgetCategory.amount + ? Styles.expensesRed + : 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), + ), + ), + ), + ), + 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(14.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), + ), + ), + ], + ), + ), + ], + ), + )) + ], + ), + ); + } + + String formatBudgetName(String name) { + if (name.length < 12) { + return name; + } + if (!name.contains(' ') || name.indexOf(' ') == name.length - 1) { + return name; + } + printLime('here'); + final words = name.split(' '); + int index = 0; + String firstLine = words[index]; + while (true) { + index += 1; + if (index == words.length) { + return '$firstLine\n${words.sublist(index).join(' ')}'; + } + if (firstLine.length + words[index].length > 12) { + return '$firstLine\n${words.sublist(index).join(' ')}'; + } + firstLine += ' ${words[index]}'; + } + } +} diff --git a/lib/features/budget/widgets/budget_net_bar.dart b/lib/features/budget/widgets/budget_net_bar.dart index 13d64f9..06e7384 100644 --- a/lib/features/budget/widgets/budget_net_bar.dart +++ b/lib/features/budget/widgets/budget_net_bar.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:helpers/helpers.dart'; 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}); @@ -28,7 +29,7 @@ class BudgetNetBar extends StatelessWidget { ), ), Text( - '\$$net', + net.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 new file mode 100644 index 0000000..8bbb406 --- /dev/null +++ b/lib/features/budget/widgets/transaction_list_item.dart @@ -0,0 +1,168 @@ +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/widgets/add_transaction_dialog.dart'; +import 'package:rluv/global/utils.dart'; +import 'package:rluv/models/transaction_model.dart'; + +import '../../../global/store.dart'; +import '../../../global/styles.dart'; +import '../../../global/widgets/budget_color_circle.dart'; +import '../../../models/budget_category_model.dart'; + +class TransactionListItem extends ConsumerStatefulWidget { + const TransactionListItem({super.key, required this.index}); + + final int index; + + @override + ConsumerState createState() => + _TransactionListItemState(); +} + +class _TransactionListItemState extends ConsumerState { + bool showDetails = false; + double cardHeight = 70.0; + + BudgetCategory? budgetCategory; + late final Transaction transaction; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final transaction = ref.watch(Store().transactionsProvider)[widget.index]; + final budgetCategories = ref.read(Store().budgetCategoriesProvider); + if (transaction.type == TransactionType.expense) { + budgetCategory = budgetCategories.singleWhere( + (category) => category.id == transaction.budgetCategoryId, + ); + } + return GestureDetector( + onTap: () => toggleDetails(), + child: Padding( + padding: const EdgeInsets.only(top: 8.0, left: 8.0, bottom: 8.0), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10.0), + bottomLeft: Radius.circular(10.0)), + child: AnimatedContainer( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + height: cardHeight, + decoration: const BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + bottomLeft: Radius.circular(10.0)), + color: Styles.washedStone, + ), + child: Row( + children: [ + AnimatedContainer( + curve: Curves.easeOut, + duration: const Duration(milliseconds: 200), + height: cardHeight, + width: 6, + color: transaction.type == TransactionType.income + ? Styles.incomeBlue + : Styles.expensesOrange), + SizedBox( + width: BuildMedia(context).width * 0.65, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(DateFormat('EEE MMM d, h:mm a') + .format(transaction.date)), + Row( + children: [ + const SizedBox( + width: 6, + ), + if (budgetCategory != null) ...[ + BudgetColorCircle( + color: budgetCategory!.color, + ), + const SizedBox( + width: 8, + ), + ], + Text( + transaction.type == TransactionType.income + ? 'Income' + : budgetCategory!.name, + style: const TextStyle(fontSize: 20), + ), + ], + ), + if (showDetails) + Text( + transaction.memo ?? '', + style: const TextStyle(fontSize: 16), + ) + ], + ), + ), + ), + const Spacer(), + Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + transaction.amount.currency(), + style: TextStyle( + fontSize: 24, + color: transaction.type == TransactionType.income + ? Styles.incomeGreen + : Styles.expensesRed), + ), + if (showDetails) + IconButton( + icon: const Icon( + Icons.edit_rounded, + ), + onPressed: () { + showDialog( + context: context, + builder: (context) => Dialog( + backgroundColor: Styles.dialogColor, + shape: Styles.dialogShape, + child: AddTransactionDialog( + transaction: transaction), + ), + ); + }, + ), + ], + ), + const SizedBox( + width: 14, + ), + ], + ), + ), + ), + ), + ); + } + + void toggleDetails() { + if (showDetails) { + setState(() { + showDetails = false; + cardHeight = 70; + }); + } else { + setState(() { + showDetails = true; + cardHeight = 120; + }); + } + } +} diff --git a/lib/features/notes/screens/notes_screen.dart b/lib/features/notes/screens/notes_screen.dart new file mode 100644 index 0000000..02add73 --- /dev/null +++ b/lib/features/notes/screens/notes_screen.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:helpers/helpers/misc_build/build_media.dart'; + +import '../../../global/store.dart'; + +class SharedNotesScreen extends ConsumerStatefulWidget { + const SharedNotesScreen({super.key, required this.initialData}); + + final Map initialData; + @override + ConsumerState createState() => _SharedNotesScreenState(); +} + +class _SharedNotesScreenState extends ConsumerState { + @override + Widget build(BuildContext context) { + final sharedNotes = ref.watch(Store().sharedNotesProvider); + return Column( + children: [ + const Padding( + padding: EdgeInsets.all(18.0), + child: Text( + 'Notes:', + style: TextStyle(fontSize: 20), + textAlign: TextAlign.center, + ), + ), + if (sharedNotes.isEmpty) const Text('Add notes to get started:'), + if (sharedNotes.isNotEmpty) + GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + ), + itemBuilder: (BuildContext context, int index) { + final note = sharedNotes[index]; + return SizedBox( + width: BuildMedia(context).width * 0.4, + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(note.content), + ), + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart new file mode 100644 index 0000000..c42ce36 --- /dev/null +++ b/lib/features/settings/screens/settings_screen.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class SettingsScreen extends ConsumerStatefulWidget { + const SettingsScreen({super.key}); + + @override + ConsumerState createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends ConsumerState { + @override + Widget build(BuildContext context) { + return const Column( + children: [ + Text('Settings'), + ], + ); + } +} diff --git a/lib/global/api.dart b/lib/global/api.dart index 04795e8..c4e2878 100644 --- a/lib/global/api.dart +++ b/lib/global/api.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:helpers/helpers/print.dart'; class Api { @@ -10,38 +13,40 @@ class Api { _instance.initDone = true; _instance.dio = Dio(); _instance.dio.options.baseUrl = "http://localhost:8081/"; + // _instance.dio.options.baseUrl = "https://rluv.fosscat.com/"; _instance.dio.interceptors.add( - InterceptorsWrapper( - onRequest: (RequestOptions options, RequestInterceptorHandler handler) { - // Do something before request is sent. - // If you want to resolve the request with custom data, - // you can resolve a `Response` using `handler.resolve(response)`. - // If you want to reject the request with a error message, - // you can reject with a `DioException` using `handler.reject(dioError)`. - return handler.next(options); - }, - onResponse: (Response response, ResponseInterceptorHandler handler) { - if (response.statusCode != null && - response.statusCode! < 500 && - response.statusCode! >= 400) { - return handler.reject(DioException.badResponse( - requestOptions: RequestOptions(), - response: response, - statusCode: response.statusCode!)); - } - // Do something with response data. - // If you want to reject the request with a error message, - // you can reject a `DioException` object using `handler.reject(dioError)`. - return handler.next(response); - }, - onError: (DioException e, ErrorInterceptorHandler handler) { - printPink(e); - // Do something with response error. - // If you want to resolve the request with some custom data, - // you can resolve a `Response` object using `handler.resolve(response)`. - return handler.next(e); - }, - ), + LoggingInterceptor(), + // InterceptorsWrapper( + // onRequest: (RequestOptions options, RequestInterceptorHandler handler) { + // // Do something before request is sent. + // // If you want to resolve the request with custom data, + // // you can resolve a `Response` using `handler.resolve(response)`. + // // If you want to reject the request with a error message, + // // you can reject with a `DioException` using `handler.reject(dioError)`. + // return handler.next(options); + // }, + // onResponse: (Response response, ResponseInterceptorHandler handler) { + // if (response.statusCode != null && + // response.statusCode! < 500 && + // response.statusCode! >= 400) { + // return handler.reject(DioException.badResponse( + // requestOptions: RequestOptions(), + // response: response, + // statusCode: response.statusCode!)); + // } + // // Do something with response data. + // // If you want to reject the request with a error message, + // // you can reject a `DioException` object using `handler.reject(dioError)`. + // return handler.next(response); + // }, + // onError: (DioException e, ErrorInterceptorHandler handler) { + // printPink(e); + // // Do something with response error. + // // If you want to resolve the request with some custom data, + // // you can resolve a `Response` object using `handler.resolve(response)`. + // return handler.next(e); + // }, + // ), ); return _instance; } @@ -79,6 +84,21 @@ class Api { } } + Future?> post( + {required String path, Object? data}) async { + try { + final res = await dio.post(path, data: data); + + if (res.data != null) { + return res.data as Map; + } + return null; + } catch (err) { + printRed('Error in put: $err'); + return null; + } + } + Future?> delete( {required String path, Object? data}) async { try { @@ -94,3 +114,74 @@ class Api { } } } + +class LoggingInterceptor extends Interceptor { + LoggingInterceptor(); + + @override + Future onRequest( + RequestOptions options, RequestInterceptorHandler handler) async { + logPrint('///*** REQUEST ***\\\\\\'); + printKV('URI', options.uri); + printKV('METHOD', options.method); + logPrint('HEADERS:'); + options.headers.forEach((key, v) => printKV(' - $key', v)); + logPrint('BODY:'); + printJson(options.data); + + return handler.next(options); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + logPrint('///*** ERROR RESPONSE ***\\\\\\'); + logPrint('URI: ${err.requestOptions.uri}'); + if (err.response != null) { + logPrint('STATUS CODE: ${err.response?.statusCode?.toString()}'); + } + logPrint('$err'); + if (err.response != null) { + printKV('REDIRECT', err.response?.realUri ?? ''); + logPrint('BODY:'); + printJson(err.response?.data); + } + return handler.next(err); + } + + @override + Future onResponse( + Response response, ResponseInterceptorHandler handler) async { + logPrint('///*** RESPONSE ***\\\\\\'); + printKV('URI', response.requestOptions.uri); + printKV('STATUS CODE', response.statusCode ?? ''); + // printKV('REDIRECT', response.isRedirect); + logPrint('BODY:'); + printJson(response.data); + + return handler.next(response); + } + + void printKV(String key, Object v) { + if (kDebugMode) { + printOrange('$key: $v'); + } + } + + void printJson(Map? s) { + if (kDebugMode) { + if (s == null) { + printAmber({}); + return; + } + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + String prettyprint = encoder.convert(s); + printAmber(prettyprint); + } + } + + void logPrint(String s) { + if (kDebugMode) { + printOrange(s); + } + } +} diff --git a/lib/global/store.dart b/lib/global/store.dart index 9a8fbc3..99de3e2 100644 --- a/lib/global/store.dart +++ b/lib/global/store.dart @@ -1,43 +1,70 @@ +import 'dart:convert'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:helpers/helpers/print.dart'; import 'package:rluv/global/api.dart'; import 'package:rluv/models/budget.dart'; import 'package:rluv/models/budget_category_model.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../models/family_model.dart'; +import '../models/shared_note.dart'; import '../models/transaction_model.dart'; import '../models/user.dart'; class Store { static final Store _instance = Store._internal(); bool _initDone = false; + SharedPreferences? prefs; factory Store() { if (_instance._initDone) { return _instance; } _instance._initDone = true; - _instance.dashboardProvider = FutureProvider?>( - (ref) async { - final family = await ref.watch(_instance.familyProvider.future); - return Api().get("dashboard/${family.id}"); + SharedPreferences.getInstance().then( + (value) => _instance.prefs = value, + ); + _instance.dashboardProvider = + StateNotifierProvider?>( + (ref) { + final family = ref.watch(_instance.familyProvider).valueOrNull; + return DashBoardStateNotifier(family); }, ); - _instance.budgetCategoriesProvider = - FutureProvider>((ref) async { - final dash = await ref.watch(_instance.dashboardProvider.future); - printAmber(dash); + // _instance.dashboardProvider = FutureProvider?>( + // (ref) async { + // final family = await ref.watch(_instance.familyProvider.future); + // return Api().get("dashboard/${family.id}"); + // }, + // ); + _instance.budgetProvider = Provider( + (ref) { + final dash = ref.watch(_instance.dashboardProvider); + if (dash == null) return null; + final budgetData = dash['budget'] as Map?; + if (budgetData == null) return null; + return Budget.fromJson(budgetData); + }, + ); + _instance.budgetCategoriesProvider = Provider>((ref) { + final dash = ref.watch(_instance.dashboardProvider); if (dash == null) return []; - final categories = dash['budget_categories'] as List; - return categories + final categoriesData = dash['budget_categories'] as List; + final categories = categoriesData .map( (e) => BudgetCategory.fromJson(e as Map), ) .toList(); + if (_instance.prefs != null) { + final budgetJson = jsonEncode({'budget_categories': categoriesData}); + printBlue('updated prefs stored categories'); + _instance.prefs!.setString('budget_categories', budgetJson); + } + return categories; }); - _instance.transactionsProvider = - FutureProvider>((ref) async { - final dash = await ref.watch(_instance.dashboardProvider.future); + _instance.transactionsProvider = Provider>((ref) { + final dash = ref.watch(_instance.dashboardProvider); if (dash == null) return []; final transactions = dash['transactions'] as List; return transactions @@ -72,10 +99,87 @@ class Store { createdAt: DateTime.now(), updatedAt: DateTime.now())); - late final FutureProvider> budgetCategoriesProvider; - late final FutureProvider budgetProvider; - late final FutureProvider> transactionsProvider; - late final FutureProvider?> dashboardProvider; + late final Provider> budgetCategoriesProvider; + late final Provider budgetProvider; + late final Provider> transactionsProvider; + late final Provider> sharedNotesProvider; + // late final FutureProvider?> dashboardProvider; + late final StateNotifierProvider?> dashboardProvider; void fetchDashboard() {} } + +class DashBoardStateNotifier extends StateNotifier?> { + DashBoardStateNotifier(FamilyModel? family) : super(null) { + fetchDashboard(family); + } + + Future fetchDashboard(FamilyModel? family) async { + if (family == null) { + printPink('Unable to get dashboard'); + return; + } + printAmber('Fetching dashboard'); + state = await Api().get("dashboard/${family.id}"); + } + + void update(Map data) { + if (state == null) { + printPink('Cant update data, state is null'); + return; + } + if (data.keys.length != 1 || data.values.length != 1) { + throw Exception('Only one key/val used in update'); + } + final key = data.keys.first; + switch (key) { + case 'transactions': + case 'budget_categories': + final subStateList = state![key] as List; + final subStateListObj = subStateList + .map( + (e) => e as Map, + ) + .toList(); + subStateListObj.removeWhere( + (element) => + element['id'] == + (data.values.first as Map)['id'], + ); + subStateListObj.add(data.values.first); + + final newState = state; + newState![key] = subStateListObj; + state = {...newState}; + // printBlue(state); + break; + default: + break; + } + } + + void add(Map data) { + if (state == null) { + printPink('Cant add data, state is null'); + return; + } + if (data.keys.length != 1 || data.values.length != 1) { + throw Exception('Only one key/val used in add'); + } + final key = data.keys.first; + switch (key) { + case 'transactions': + case 'budget_categories': + final subStateList = state![key] as List; + final newState = state; + subStateList.add(data.values.first); + newState![key] = subStateList; + state = {...newState}; + // printBlue(state); + break; + default: + break; + } + } +} diff --git a/lib/global/styles.dart b/lib/global/styles.dart index 4b71883..b8cce2c 100644 --- a/lib/global/styles.dart +++ b/lib/global/styles.dart @@ -1,4 +1,4 @@ -import 'dart:ui'; +import 'package:flutter/material.dart'; class Styles { // Theme Colors @@ -9,7 +9,7 @@ class Styles { static const Color seaweedGreen = Color(0xFF86BA90); static const Color emptyBarGrey = Color(0xFFC8C8C8); static const Color lavender = Color(0xFFB8B8FF); - static const Color sand = Color(0xFFD9D9D9); + static const Color washedStone = Color(0xFFD9D9D9); // Income Colors static const Color incomeBlue = Color(0xFFB8B8FF); @@ -17,5 +17,57 @@ class Styles { // Expenses Colors static const Color expensesOrange = Color(0xFFFA824C); - static const Color expensesRed = Color(0xFF9E0000); + static const Color expensesRed = Color(0xFFC73E1D); + + static const List curatedColors = [ + Color(0xFF6D326D), + Color(0xFFE58F65), + Color(0xFFD3E298), + Color(0xFFFD96A9), + Color(0xFF4F7CAC), + Color(0xFFE4B4C2), + Color(0xFFFFB8D1), + Color(0xFFDDFDFE), + Color(0xFFD7BE82), + Color(0xFFD4C5C7), + Color(0xFF8EB8E5), + Color(0xFF9893DA), + Color(0xFF99AA38), + Color(0xFFA7E2E3), + Color(0xFFF7B2BD), + Color(0xFFFEDC97), + Color(0xFFC28CAE), + Color(0xFFF1BF98), + Color(0xFFD1BCE3), + Color(0xFFBDC667), + Color(0xFFFFB563) + ]; + + // Widget Styles + static BoxDecoration boxLavenderBubble = BoxDecoration( + color: Styles.lavender, + borderRadius: BorderRadius.circular(20.0), + ); + + static InputDecoration inputLavenderBubble({String? labelText}) => + InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), + labelText: labelText, + focusColor: Styles.blushingPink, + hoverColor: Styles.blushingPink, + fillColor: Styles.lavender, + border: const OutlineInputBorder( + borderSide: BorderSide.none, + ), + ); + + static const Color dialogColor = Styles.purpleNurple; + + static RoundedRectangleBorder dialogShape = RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20.0), + ); + + // Sizing Styles + static const dialogScreenWidthFactor = 0.75; } diff --git a/lib/global/utils.dart b/lib/global/utils.dart index 1a30b0c..341a61d 100644 --- a/lib/global/utils.dart +++ b/lib/global/utils.dart @@ -1,5 +1,8 @@ -import 'dart:ui'; +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:helpers/helpers/print.dart'; import 'package:intl/intl.dart'; String formatDate(DateTime time) { @@ -8,10 +11,62 @@ String formatDate(DateTime time) { DateTime dateFromJson(int value) => DateTime.fromMillisecondsSinceEpoch(value); -int dateToJson(DateTime? value) => - value == null ? 0 : value.millisecondsSinceEpoch; +int? dateToJson(DateTime? value) => value?.millisecondsSinceEpoch; + +bool boolFromJson(int value) => value == 1; + +int boolToJson(bool hide) => hide ? 1 : 0; String colorToJson(Color color) => color.toString().split('(0x')[1].split(')')[0]; Color colorFromJson(String hex) => Color(int.parse(hex, radix: 16)); + +String? optionalColorToJson(Color? optionalColor) => optionalColor == null + ? null + : optionalColor.toString().split('(0x')[1].split(')')[0]; + +Color? optionalColorFromJson(String? hex) => + hex == null ? null : Color(int.parse(hex, radix: 16)); + +Brightness getBrightness(Color color) => + ThemeData.estimateBrightnessForColor(color); + +List generateColorList() { + List colors = []; + for (int i = 100; i <= 255; i += 30) { + // Red value variations + for (int j = 100; j <= 255; j += 30) { + // Green value variations + for (int k = 100; k <= 255; k += 30) { + // Blue value variations + final alpha = Random().nextInt(256) + 50 % 255; + colors.add(Color.fromARGB(alpha, i, j, k)); + } + } + } + colors.shuffle(); + return colors; +} + +extension MonetaryExtension on double { + String currency() { + final numStr = toStringAsFixed(2); + final pieces = numStr.split("."); + if (pieces.length == 1) { + return "\$${pieces.first}.00"; + } else { + if (pieces.length > 2) { + printBlue(pieces); + } + return '\$${pieces[0]}.${pieces[1].padRight(2, "0")}'; + } + } +} + +void setDevicePortraitOrientation() { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); +} diff --git a/lib/global/widgets/budget_color_circle.dart b/lib/global/widgets/budget_color_circle.dart new file mode 100644 index 0000000..61cfe75 --- /dev/null +++ b/lib/global/widgets/budget_color_circle.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class BudgetColorCircle extends StatelessWidget { + const BudgetColorCircle( + {super.key, required this.color, this.height = 18, this.width = 18}); + + final double height; + final double width; + final Color color; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + height: height, + width: width, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + ), + Container( + height: height, + width: width, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Colors.black, width: 1.5), + color: color, + ), + ), + ], + ); + } +} diff --git a/lib/global/widgets/drawer_button.dart b/lib/global/widgets/drawer_button.dart new file mode 100644 index 0000000..ed8dd29 --- /dev/null +++ b/lib/global/widgets/drawer_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:rluv/global/styles.dart'; + +class CuteDrawerButton extends StatelessWidget { + const CuteDrawerButton( + {super.key, + required this.text, + required this.color, + required this.onPressed}); + + final String text; + final Function onPressed; + final Color color; + final double borderRadius = 12.0; + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: InkWell( + borderRadius: BorderRadius.circular(borderRadius), + onTap: () => onPressed(), + child: Container( + decoration: BoxDecoration( + color: color, + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 2.0, + spreadRadius: 2.0, + offset: Offset(2.5, 2.5), + ) + ], + borderRadius: BorderRadius.circular(borderRadius), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + text, + style: const TextStyle( + fontSize: 14, + color: Styles.washedStone, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index f7a4a4b..7b417de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,17 @@ 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/screens/budget_overview.dart'; +import 'package:rluv/features/notes/screens/notes_screen.dart'; +import 'package:rluv/features/settings/screens/settings_screen.dart'; import 'package:rluv/global/styles.dart'; +import 'package:rluv/global/utils.dart'; +import 'package:rluv/global/widgets/drawer_button.dart'; -void main() { +import 'global/store.dart'; + +void main() async { + final _ = Store().prefs; runApp(const MyApp()); } @@ -34,12 +42,70 @@ class Home extends ConsumerStatefulWidget { } class _HomeState extends ConsumerState { + Map initData = {}; + final drawerColors = Styles.curatedColors; + @override + void initState() { + setDevicePortraitOrientation(); + super.initState(); + } + @override Widget build(BuildContext context) { + if (ref.watch(currentHomePageProvider).toString() == + "BudgetOverviewScreen") { + initData = {}; + } return Scaffold( + key: ref.read(scaffoldKeyProvider), resizeToAvoidBottomInset: false, - drawer: const Drawer( + drawer: Drawer( backgroundColor: Styles.purpleNurple, + child: SafeArea( + child: Column( + children: [ + SizedBox(height: BuildMedia(context).height * 0.15), + CuteDrawerButton( + text: 'Budget', + color: drawerColors[4], + onPressed: () { + if (ref.read(currentHomePageProvider).toString() != + "BudgetOverviewScreen") { + ref.read(currentHomePageProvider.notifier).state = + BudgetOverviewScreen(initialData: initData); + } + toggleDrawer(); + }, + ), + CuteDrawerButton( + text: 'Notes', + color: drawerColors[5], + onPressed: () { + if (ref.read(currentHomePageProvider).toString() != + "SharedNotesScreen") { + ref.read(currentHomePageProvider.notifier).state = + SharedNotesScreen(initialData: initData); + } + toggleDrawer(); + }, + ), + const Spacer(), + CuteDrawerButton( + text: 'Settings', + color: drawerColors[0], + onPressed: () { + if (ref.read(currentHomePageProvider).toString() != + "SettingsScreen") { + ref.read(currentHomePageProvider.notifier).state = + const SettingsScreen(); + } + toggleDrawer(); + }, + ), + const SizedBox(height: 24), + ], + ), + ), ), appBar: AppBar( backgroundColor: Styles.purpleNurple, @@ -50,10 +116,23 @@ class _HomeState extends ConsumerState { body: ref.watch(currentHomePageProvider), ); } + + toggleDrawer() async { + final key = ref.read(scaffoldKeyProvider); + if (key.currentState != null && key.currentState!.isDrawerOpen) { + key.currentState!.openEndDrawer(); + } else if (key.currentState != null) { + key.currentState!.openDrawer(); + } + } } +final scaffoldKeyProvider = Provider>( + (ref) => GlobalKey(), +); + final currentHomePageProvider = StateProvider( - (ref) => const BudgetOverviewScreen(), + (ref) => const BudgetOverviewScreen(initialData: {}), ); final appBarTitleProvider = Provider( @@ -62,6 +141,8 @@ final appBarTitleProvider = Provider( switch (currentPageName) { case 'BudgetOverviewScreen': return 'Budget'; + case 'SharedNotesScreen': + return 'Notes'; default: return ''; } diff --git a/lib/models/budget.dart b/lib/models/budget.dart index b28db81..afeffe9 100644 --- a/lib/models/budget.dart +++ b/lib/models/budget.dart @@ -11,11 +11,14 @@ class Budget { required this.name, required this.createdAt, required this.updatedAt, + this.hide = false, }); final int? id; final String name; + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) final DateTime createdAt; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) diff --git a/lib/models/budget.g.dart b/lib/models/budget.g.dart index 1fba5c4..3087ba4 100644 --- a/lib/models/budget.g.dart +++ b/lib/models/budget.g.dart @@ -11,11 +11,13 @@ Budget _$BudgetFromJson(Map json) => Budget( name: json['name'] as String, createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), ); Map _$BudgetToJson(Budget instance) => { 'id': instance.id, 'name': instance.name, + 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), 'updated_at': dateToJson(instance.updatedAt), }; diff --git a/lib/models/budget_category_model.dart b/lib/models/budget_category_model.dart index 90a6e34..635f1bc 100644 --- a/lib/models/budget_category_model.dart +++ b/lib/models/budget_category_model.dart @@ -13,8 +13,10 @@ class BudgetCategory { required this.budgetId, required this.name, required this.color, - required this.createdAt, + this.createdAt, + this.updatedAt, required this.amount, + this.hide = false, }); final int budgetId; @@ -22,11 +24,17 @@ class BudgetCategory { final String name; final double amount; + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; + @JsonKey(fromJson: colorFromJson, toJson: colorToJson) final Color color; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) - final DateTime createdAt; + final DateTime? createdAt; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? updatedAt; factory BudgetCategory.fromJson(Map json) => _$BudgetCategoryFromJson(json); diff --git a/lib/models/budget_category_model.g.dart b/lib/models/budget_category_model.g.dart index a47efe8..d7498c1 100644 --- a/lib/models/budget_category_model.g.dart +++ b/lib/models/budget_category_model.g.dart @@ -13,7 +13,9 @@ BudgetCategory _$BudgetCategoryFromJson(Map json) => name: json['name'] as String, color: colorFromJson(json['color'] as String), createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), amount: (json['amount'] as num).toDouble(), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), ); Map _$BudgetCategoryToJson(BudgetCategory instance) => @@ -22,6 +24,8 @@ Map _$BudgetCategoryToJson(BudgetCategory instance) => 'id': instance.id, 'name': instance.name, 'amount': instance.amount, + 'hide': boolToJson(instance.hide), 'color': colorToJson(instance.color), 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), }; diff --git a/lib/models/family_model.dart b/lib/models/family_model.dart index 8c9be80..91720e2 100644 --- a/lib/models/family_model.dart +++ b/lib/models/family_model.dart @@ -11,10 +11,13 @@ class FamilyModel { required this.budgetId, required this.createdAt, required this.updatedAt, + this.hide = false, }); final int id, budgetId; + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) final DateTime createdAt; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) diff --git a/lib/models/family_model.g.dart b/lib/models/family_model.g.dart index e411dbf..6c36909 100644 --- a/lib/models/family_model.g.dart +++ b/lib/models/family_model.g.dart @@ -11,12 +11,14 @@ FamilyModel _$FamilyModelFromJson(Map json) => FamilyModel( budgetId: json['budget_id'] as int, createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), ); Map _$FamilyModelToJson(FamilyModel instance) => { 'id': instance.id, 'budget_id': instance.budgetId, + 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), 'updated_at': dateToJson(instance.updatedAt), }; diff --git a/lib/models/shared_note.dart b/lib/models/shared_note.dart new file mode 100644 index 0000000..6cedf0e --- /dev/null +++ b/lib/models/shared_note.dart @@ -0,0 +1,46 @@ +import 'dart:ui'; + +import 'package:json_annotation/json_annotation.dart'; + +import '../global/utils.dart'; + +part 'shared_note.g.dart'; + +@JsonSerializable() +class SharedNote { + const SharedNote({ + this.id, + required this.familyId, + required this.createdByUserId, + required this.content, + this.color, + this.createdAt, + this.updatedAt, + this.isMarkdown = false, + this.hide = false, + }); + + final int? id; + final int familyId, createdByUserId; + final String content; + + @JsonKey(fromJson: optionalColorFromJson, toJson: optionalColorToJson) + final Color? color; + + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool isMarkdown; + + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? createdAt; + + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? updatedAt; + + factory SharedNote.fromJson(Map json) => + _$SharedNoteFromJson(json); + + Map toJson() => _$SharedNoteToJson(this); +} diff --git a/lib/models/shared_note.g.dart b/lib/models/shared_note.g.dart new file mode 100644 index 0000000..fb5e067 --- /dev/null +++ b/lib/models/shared_note.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shared_note.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SharedNote _$SharedNoteFromJson(Map json) => SharedNote( + id: json['id'] as int?, + familyId: json['family_id'] as int, + createdByUserId: json['created_by_user_id'] as int, + content: json['content'] as String, + color: optionalColorFromJson(json['color'] as String?), + createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), + isMarkdown: json['is_markdown'] == null + ? false + : boolFromJson(json['is_markdown'] as int), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), + ); + +Map _$SharedNoteToJson(SharedNote instance) => + { + 'id': instance.id, + 'family_id': instance.familyId, + 'created_by_user_id': instance.createdByUserId, + 'content': instance.content, + 'color': optionalColorToJson(instance.color), + 'is_markdown': boolToJson(instance.isMarkdown), + 'hide': boolToJson(instance.hide), + 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), + }; diff --git a/lib/models/transaction_model.dart b/lib/models/transaction_model.dart index b2fc6a9..244867b 100644 --- a/lib/models/transaction_model.dart +++ b/lib/models/transaction_model.dart @@ -14,18 +14,25 @@ class Transaction { const Transaction({ this.id, required this.amount, - required this.transactionType, + required this.type, required this.budgetId, - required this.budgetCategoryId, + this.budgetCategoryId, required this.addedByUserId, required this.date, + this.memo, this.createdAt, + this.updatedAt, + this.hide = false, }); - final int? id; - final int budgetId, budgetCategoryId, addedByUserId; + final int? id, budgetCategoryId; + final int budgetId, addedByUserId; final double amount; - final TransactionType transactionType; + final String? memo; + final TransactionType type; + + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) final DateTime date; @@ -33,8 +40,26 @@ class Transaction { @JsonKey(fromJson: dateFromJson, toJson: dateToJson) final DateTime? createdAt; + @JsonKey(fromJson: dateFromJson, toJson: dateToJson) + final DateTime? updatedAt; + factory Transaction.fromJson(Map json) => _$TransactionFromJson(json); + factory Transaction.copyWith(Transaction trans, Map data) => + Transaction( + id: data['id'] ?? trans.id, + amount: data['amount'] ?? trans.amount, + 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, + date: data['date'] ?? trans.date, + memo: data['memo'] ?? trans.memo, + createdAt: data['created_at'] ?? trans.createdAt, + updatedAt: data['updated_at'] ?? trans.updatedAt, + hide: data['hide'] ?? trans.hide, + ); + Map toJson() => _$TransactionToJson(this); } diff --git a/lib/models/transaction_model.g.dart b/lib/models/transaction_model.g.dart index 573be33..0f0b6b4 100644 --- a/lib/models/transaction_model.g.dart +++ b/lib/models/transaction_model.g.dart @@ -9,25 +9,30 @@ part of 'transaction_model.dart'; Transaction _$TransactionFromJson(Map json) => Transaction( id: json['id'] as int?, amount: (json['amount'] as num).toDouble(), - transactionType: - $enumDecode(_$TransactionTypeEnumMap, json['transaction_type']), + type: $enumDecode(_$TransactionTypeEnumMap, json['type']), 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, date: dateFromJson(json['date'] as int), + memo: json['memo'] as String?, createdAt: dateFromJson(json['created_at'] as int), + updatedAt: dateFromJson(json['updated_at'] as int), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), ); Map _$TransactionToJson(Transaction instance) => { 'id': instance.id, - 'budget_id': instance.budgetId, 'budget_category_id': instance.budgetCategoryId, + 'budget_id': instance.budgetId, 'added_by_user_id': instance.addedByUserId, 'amount': instance.amount, - 'transaction_type': _$TransactionTypeEnumMap[instance.transactionType]!, + 'memo': instance.memo, + 'type': _$TransactionTypeEnumMap[instance.type]!, + 'hide': boolToJson(instance.hide), 'date': dateToJson(instance.date), 'created_at': dateToJson(instance.createdAt), + 'updated_at': dateToJson(instance.updatedAt), }; const _$TransactionTypeEnumMap = { diff --git a/lib/models/user.dart b/lib/models/user.dart index dd2ca16..79eebcb 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -14,12 +14,15 @@ class User { this.createdAt, this.updatedAt, this.lastActivityAt, + this.hide = false, }); final int? id; final int familyId, budgetId; final String name; + @JsonKey(fromJson: boolFromJson, toJson: boolToJson) + final bool hide; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) final DateTime? createdAt; @JsonKey(fromJson: dateFromJson, toJson: dateToJson) diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index acd57a0..cbb3fd8 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -14,6 +14,7 @@ User _$UserFromJson(Map json) => User( createdAt: dateFromJson(json['created_at'] as int), updatedAt: dateFromJson(json['updated_at'] as int), lastActivityAt: dateFromJson(json['last_activity_at'] as int), + hide: json['hide'] == null ? false : boolFromJson(json['hide'] as int), ); Map _$UserToJson(User instance) => { @@ -21,6 +22,7 @@ Map _$UserToJson(User instance) => { 'family_id': instance.familyId, 'budget_id': instance.budgetId, 'name': instance.name, + 'hide': boolToJson(instance.hide), 'created_at': dateToJson(instance.createdAt), 'updated_at': dateToJson(instance.updatedAt), 'last_activity_at': dateToJson(instance.lastActivityAt),