diff --git a/assets/fonts/fa-brands-400.ttf b/assets/fonts/fa-brands-400.ttf new file mode 100644 index 0000000..989f323 Binary files /dev/null and b/assets/fonts/fa-brands-400.ttf differ diff --git a/assets/fonts/fa-regular-400.ttf b/assets/fonts/fa-regular-400.ttf new file mode 100644 index 0000000..201cc58 Binary files /dev/null and b/assets/fonts/fa-regular-400.ttf differ diff --git a/assets/fonts/fa-solid-900.ttf b/assets/fonts/fa-solid-900.ttf new file mode 100644 index 0000000..1920af1 Binary files /dev/null and b/assets/fonts/fa-solid-900.ttf differ diff --git a/lib/account_edit/view/account_edit_form.dart b/lib/account_edit/view/account_edit_form.dart index 868ef05..b40bb6f 100644 --- a/lib/account_edit/view/account_edit_form.dart +++ b/lib/account_edit/view/account_edit_form.dart @@ -3,11 +3,9 @@ import 'package:budget_app/account_edit/view/widgets/balance_input_field.dart'; import 'package:budget_app/account_edit/view/widgets/category_input_field.dart'; import 'package:budget_app/account_edit/view/widgets/include_switch.dart'; import 'package:budget_app/account_edit/view/widgets/name_input_field.dart'; -import 'package:budget_app/app/repository/budget_repository.dart'; import 'package:budget_app/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:formz/formz.dart'; class AccountEditDialog extends StatelessWidget { @@ -18,62 +16,58 @@ class AccountEditDialog extends StatelessWidget { return state.accStatus == AccountEditStatus.loading ? Center(child: CircularProgressIndicator()) : Center( - child: SingleChildScrollView( - child: Dialog( - insetPadding: EdgeInsets.all(10), - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Container( - height: 1500.h, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(15), - color: BudgetColors.teal50, + child: SingleChildScrollView( + child: Dialog( + insetPadding: EdgeInsets.all(10), + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Container( + height: 495, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: BudgetColors.teal50, + ), + padding: EdgeInsets.fromLTRB(20, 25, 20, 20), + child: Column( + children: [ + Text( + 'New Account', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleLarge + ?.fontSize), + ), + SizedBox( + height: 20, + ), + CategoryInputField(), + SizedBox( + height: 25, + ), + NameInputField(), + SizedBox( + height: 25, + ), + BalanceInputField(), + Divider(), + IncludeSwitch(), + Divider(), + _SubmitButton(), + ], + ), ), - padding: EdgeInsets.fromLTRB(20, 25, 20, 20), - child: Column( - children: [ - Text( - 'New Account', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleLarge - ?.fontSize), - ), - SizedBox( - height: 50.h, - ), - CategoryInputField(), - SizedBox( - height: 75.h, - ), - NameInputField(), - SizedBox( - height: 75.h, - ), - BalanceInputField(), - SizedBox( - height: 75.h, - ), - IncludeSwitch(), - SizedBox( - height: 75.h, - ), - _SubmitButton(), - ], - ), - ), - /*Positioned( + /*Positioned( top: -100, child: Image.network("https://i.imgur.com/2yaf2wb.png", width: 150, height: 150))*/ - ], - )), - ), - ); + ], + )), + ), + ); }, ); } @@ -87,20 +81,19 @@ class _SubmitButton extends StatelessWidget { builder: (context, state) { return state.status.isInProgress ? const CircularProgressIndicator() - : ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(30), - ), - backgroundColor: - Theme.of(context).colorScheme.primaryContainer, - ), + : TextButton( onPressed: state.isValid && state.category != null ? () => context .read() .add(AccountFormSubmitted(context: context)) : null, - child: Text('SAVE'), + child: Text('SAVE', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: state.isValid && state.category != null + ? BudgetColors.amber800 + : Colors.grey)), ); }, ); diff --git a/lib/account_edit/view/widgets/include_switch.dart b/lib/account_edit/view/widgets/include_switch.dart index e5fa36a..810ca6e 100644 --- a/lib/account_edit/view/widgets/include_switch.dart +++ b/lib/account_edit/view/widgets/include_switch.dart @@ -1,7 +1,6 @@ import 'package:budget_app/account_edit/bloc/account_edit_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; class IncludeSwitch extends StatelessWidget { const IncludeSwitch({Key? key}) : super(key: key); @@ -21,7 +20,7 @@ class IncludeSwitch extends StatelessWidget { .titleLarge ?.fontSize), ), - SizedBox(width: 50.w,), + SizedBox(width: 10), Switch( thumbIcon: _thumbIcon, value: state.isIncludeInTotals, diff --git a/lib/accounts_list/view/accounts_list_page.dart b/lib/accounts_list/view/accounts_list_page.dart index 3daab3d..9311611 100644 --- a/lib/accounts_list/view/accounts_list_page.dart +++ b/lib/accounts_list/view/accounts_list_page.dart @@ -1,12 +1,11 @@ import 'package:budget_app/account_edit/bloc/account_edit_bloc.dart'; import 'package:budget_app/account_edit/view/account_edit_form.dart'; import 'package:budget_app/accounts/repository/accounts_repository.dart'; -import 'package:budget_app/categories/models/category.dart'; import 'package:budget_app/colors.dart'; import 'package:budget_app/home/cubit/home_cubit.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../../accounts/models/account.dart'; import '../../categories/repository/categories_repository.dart'; @@ -15,8 +14,7 @@ import '../cubit/accounts_list_cubit.dart'; class AccountsListPage extends StatelessWidget { const AccountsListPage({Key? key}) : super(key: key); - static Route route( - {required HomeCubit homeCubit}) { + static Route route({required HomeCubit homeCubit}) { return MaterialPageRoute( fullscreenDialog: true, builder: (context) { @@ -47,7 +45,6 @@ class AccountsListView extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; return MultiBlocListener( listeners: [ BlocListener( @@ -68,6 +65,7 @@ class AccountsListView extends StatelessWidget { ], child: BlocBuilder( builder: (context, state) { + final scheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: Text('Accounts'), @@ -75,7 +73,7 @@ class AccountsListView extends StatelessWidget { body: Column( children: [ SizedBox( - height: 50.h, + height: 10, ), Expanded( child: ListView.builder( @@ -85,13 +83,28 @@ class AccountsListView extends StatelessWidget { return Card( elevation: Theme.of(context).cardTheme.elevation, child: ListTile( - title: Text( - account.extendName(state.accountCategories), - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleLarge! - .fontSize), + title: Row( + children: [ + Text( + account.extendName(state.accountCategories), + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleLarge! + .fontSize), + ), + Expanded(child: Container()), + FaIcon( + color: scheme.primary, + IconData( + state.accountCategories + .firstWhere((element) => + element.id == + account.categoryId) + .iconCode ?? + 0, + fontFamily: 'FontAwesomeSolid')), + ], ), leading: IconButton( icon: Icon(Icons.highlight_remove, @@ -114,7 +127,7 @@ class AccountsListView extends StatelessWidget { ListTile( tileColor: BudgetColors.amber800, title: Text( - 'New Account', + 'Add Account', style: TextStyle( fontSize: Theme.of(context).textTheme.titleLarge!.fontSize), diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index a43580c..ad7b695 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -13,7 +13,6 @@ import 'package:budget_app/transfer/repository/transfer_repository.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../constants/constants.dart'; @@ -61,12 +60,7 @@ class _AppViewState extends State { Widget build(BuildContext context) { w = MediaQuery.of(context).size.width; h = MediaQuery.of(context).size.height; - return ScreenUtilInit( - designSize: const Size(1080, 2160), - minTextAdapt: true, - splitScreenMode: true, - builder: (context, child) { - return MultiRepositoryProvider( + return MultiRepositoryProvider( providers: [ RepositoryProvider(create: (context) => BudgetRepositoryImpl()), RepositoryProvider(create: (context) => CategoriesRepositoryImpl()), @@ -147,7 +141,5 @@ class _AppViewState extends State { onGenerateRoute: (_) => SplashPage.route(), ), ); - }, - ); } } diff --git a/lib/categories/cubit/categories_cubit.dart b/lib/categories/cubit/categories_cubit.dart index fc17201..4911202 100644 --- a/lib/categories/cubit/categories_cubit.dart +++ b/lib/categories/cubit/categories_cubit.dart @@ -4,6 +4,7 @@ import 'package:bloc/bloc.dart'; import 'package:budget_app/constants/api.dart'; import 'package:budget_app/transactions/models/transaction_type.dart'; import 'package:equatable/equatable.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../models/category.dart'; import '../repository/categories_repository.dart'; @@ -29,15 +30,18 @@ class CategoriesCubit extends Cubit { final catByType = categories .where((element) => element.transactionType == state.transactionType) .toList(); - emit(state.copyWith(status: CategoriesStatus.success, categories: catByType)); + emit(state.copyWith( + status: CategoriesStatus.success, categories: catByType)); } void onNameChanged(String name) { emit(state.copyWith(name: name)); } - void onIconCodeChanged(String code) { - emit(state.copyWith(iconCode: int.parse(code))); + void onIconCodeChanged(int code) { + print('Family: ${FontAwesomeIcons.code.fontFamily}'); + print('CODE: ${FontAwesomeIcons.code.codePoint}'); + emit(state.copyWith(iconCode: code)); } void onNewCategory() { @@ -45,7 +49,10 @@ class CategoriesCubit extends Cubit { } void onCategoryEdit(Category category) { - emit(state.copyWith(editCategory: category)); + emit(state.copyWith( + editCategory: category, + name: category.name, + iconCode: category.iconCode)); } Future onSubmit() async { @@ -57,7 +64,8 @@ class CategoriesCubit extends Cubit { budgetId: await getBudgetId(), transactionType: state.transactionType); } else { - category = state.editCategory!.copyWith(name: state.name, iconCode: state.iconCode); + category = state.editCategory! + .copyWith(name: state.name, iconCode: state.iconCode); } _categoriesRepository.saveCategory(category: category); emit(state.copyWith(status: CategoriesStatus.loading)); diff --git a/lib/categories/cubit/categories_state.dart b/lib/categories/cubit/categories_state.dart index 7a49878..ed20062 100644 --- a/lib/categories/cubit/categories_state.dart +++ b/lib/categories/cubit/categories_state.dart @@ -7,7 +7,7 @@ class CategoriesState extends Equatable { final List categories; final TransactionType transactionType; final String? name; - final int? iconCode; + final int iconCode; final Category? editCategory; final String? errorMessage; @@ -16,7 +16,7 @@ class CategoriesState extends Equatable { this.categories = const [], this.transactionType = TransactionType.EXPENSE, this.name, - this.iconCode, + this.iconCode = -1, this.editCategory, this.errorMessage}); @@ -43,8 +43,8 @@ class CategoriesState extends Equatable { status: this.status, categories: this.categories, transactionType: this.transactionType, - name: this.name, - iconCode: this.iconCode, + name: null, + iconCode: 0, editCategory: null); } diff --git a/lib/categories/view/categories_page.dart b/lib/categories/view/categories_page.dart index cfb03ca..96d45d9 100644 --- a/lib/categories/view/categories_page.dart +++ b/lib/categories/view/categories_page.dart @@ -1,11 +1,11 @@ -import 'package:budget_app/app/app.dart'; import 'package:budget_app/categories/cubit/categories_cubit.dart'; import 'package:budget_app/categories/repository/categories_repository.dart'; +import 'package:budget_app/categories/view/widgets/categories_grid.dart'; import 'package:budget_app/colors.dart'; import 'package:budget_app/transactions/models/transaction_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../../constants/constants.dart'; @@ -57,7 +57,9 @@ class CategoriesView extends StatelessWidget { ), body: Column( children: [ - SizedBox(height: 50.h,), + SizedBox( + height: 10, + ), Expanded( child: ListView.builder( itemCount: state.categories.length, @@ -66,13 +68,22 @@ class CategoriesView extends StatelessWidget { return Card( elevation: Theme.of(context).cardTheme.elevation, child: ListTile( - title: Text( - category.name, - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleLarge! - .fontSize), + title: Row( + children: [ + Text( + category.name, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleLarge! + .fontSize), + ), + Expanded(child: Container()), + FaIcon( + color: scheme.primary, + IconData(category.iconCode ?? 0, + fontFamily: 'FontAwesomeSolid')), + ], ), leading: IconButton( icon: Icon(Icons.highlight_remove, @@ -98,7 +109,7 @@ class CategoriesView extends StatelessWidget { ListTile( tileColor: BudgetColors.amber800, title: Text( - 'New category', + 'Add category', style: TextStyle( fontSize: Theme.of(context).textTheme.titleLarge!.fontSize), @@ -125,38 +136,64 @@ class CategoriesView extends StatelessWidget { value: context.read(), child: BlocBuilder( builder: (context, state) { - return AlertDialog( - title: Text(state.editCategory == null - ? 'Add category' - : 'Edit category'), - content: Container( - height: h * 0.25, - child: Column( - children: [ - TextFormField( - autofocus: true, - initialValue: state.editCategory?.name, - onChanged: (name) => - context.read().onNameChanged(name), - decoration: InputDecoration(hintText: 'Enter name'), - ), - SizedBox(height: 30), - TextFormField( - keyboardType: TextInputType.number, - initialValue: state.editCategory?.iconCode.toString(), - onChanged: (code) => - context.read().onIconCodeChanged(code), - decoration: InputDecoration(hintText: 'Enter icon code'), + return Center( + child: SingleChildScrollView( + child: Dialog( + insetPadding: EdgeInsets.all(10), + child: Container( + height: h * 0.7, + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + state.editCategory == null + ? 'Add category' + : 'Edit category', + style: Theme.of(context).textTheme.titleLarge), + SizedBox(height: 20), + TextFormField( + autofocus: true, + initialValue: state.editCategory?.name, + onChanged: (name) => context + .read() + .onNameChanged(name), + decoration: + InputDecoration(hintText: 'Enter name'), + ), + SizedBox(height: 10), + Divider(), + Expanded( + child: CategoriesGrid( + selectedIconCode: state.iconCode, + onSelect: (code) => context + .read() + .onIconCodeChanged(code))), + Divider(), + TextButton( + onPressed: state.name == null || + state.name?.length == 0 || + state.iconCode < 0 + ? null + : () => _submit(context), + child: Text('SAVE', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: state.name == null || + state.name?.length == 0 || + state.iconCode < 0 + ? Colors.grey + : BudgetColors.amber800)), + ) + ], + ), ), - ], + ), ), ), - actions: [ - TextButton( - onPressed: () => _submit(context), - child: Text('SAVE'), - ) - ], ); }, ))); diff --git a/lib/categories/view/widgets/categories_grid.dart b/lib/categories/view/widgets/categories_grid.dart new file mode 100644 index 0000000..624be5f --- /dev/null +++ b/lib/categories/view/widgets/categories_grid.dart @@ -0,0 +1,39 @@ +import 'package:budget_app/shared/models/awesome_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +import '../../../colors.dart'; + +class CategoriesGrid extends StatelessWidget { + final Function(int) onSelect; + final int selectedIconCode; + + const CategoriesGrid( + {super.key, this.selectedIconCode = -1, required this.onSelect}); + + @override + Widget build(BuildContext context) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 5, + ), + itemCount: appIconsList.length, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onTap: () => onSelect(appIconsList[index].iconData.codePoint), + child: CircleAvatar( + backgroundColor: + selectedIconCode == appIconsList[index].iconData.codePoint + ? BudgetColors.amber800 + : BudgetColors.teal100, + child: Center( + child: FaIcon(appIconsList[index].iconData, + color: BudgetColors.teal900)), + ), + ), + ); + }); + } +} diff --git a/lib/charts/cubit/chart_cubit.dart b/lib/charts/cubit/chart_cubit.dart index de0617f..fb3da6e 100644 --- a/lib/charts/cubit/chart_cubit.dart +++ b/lib/charts/cubit/chart_cubit.dart @@ -1,26 +1,56 @@ +import 'dart:math'; + import 'package:bloc/bloc.dart'; +import 'package:budget_app/categories/models/category.dart'; +import 'package:budget_app/categories/repository/categories_repository.dart'; import 'package:budget_app/charts/models/year_month_sum.dart'; import 'package:budget_app/charts/repository/chart_repository.dart'; +import 'package:budget_app/transactions/models/transaction_type.dart'; import 'package:equatable/equatable.dart'; -import 'dart:math'; part 'chart_state.dart'; class ChartCubit extends Cubit { + final CategoriesRepository _categoriesRepository; final ChartRepository _chartRepository; - ChartCubit({required ChartRepository chartRepository}) + ChartCubit( + {required ChartRepository chartRepository, + required CategoriesRepository categoriesRepository}) : _chartRepository = chartRepository, + _categoriesRepository = categoriesRepository, super(ChartState()); - Future fetchChart() async { + Future changeCategory({required Category category}) async { + fetchCategoryChart(category); + } + + Future fetchTrendChart() async { emit(state.copyWith(status: ChartStatus.loading)); - await Future.delayed(Duration(milliseconds: 200)); - final chartData = await _chartRepository.fetchChart(); + final chartData = await _chartRepository.fetchTrendChartData(); emit(state.copyWith(status: ChartStatus.success, data: chartData)); } - void changeTouchedIndex(int index){ - emit(state.copyWith(touchedIndex: index)); + Future fetchCategoryChart([Category? category]) async { + final categories = await _categoriesRepository.getCategories().first; + final filteredCategories = categories + .where((element) => + element.transactionType == + (state.categoryType == 'Expenses' + ? TransactionType.EXPENSE + : TransactionType.INCOME)) + .toList(); + final chartData = await _chartRepository + .fetchCategoryChartData(category?.id ?? filteredCategories[0].id!); + emit(state.copyWith( + status: ChartStatus.success, + data: chartData, + categories: filteredCategories, + category: category != null ? category : filteredCategories[0])); + } + + void changeCategoryType({required String categoryType}) { + emit(state.copyWith(categoryType: categoryType)); + fetchCategoryChart(); } } diff --git a/lib/charts/cubit/chart_state.dart b/lib/charts/cubit/chart_state.dart index d8a6bdd..17e0d75 100644 --- a/lib/charts/cubit/chart_state.dart +++ b/lib/charts/cubit/chart_state.dart @@ -5,34 +5,58 @@ enum ChartStatus { loading, success, failure } class ChartState extends Equatable { final ChartStatus status; final List data; - final touchedIndex; + final List categories; + final Category? category; + final String categoryType; List get titles { return data.map((e) => e.date.split('-')[1]).toList(); } + List get dataPoints { + final result = []; + data.forEach((element) { + result.add(element.expenseSum); + }); + return result; + } + + List get dataPointsGrouped { + final result = []; + data.forEach((element) { + result.add(element.expenseSum); + result.add(element.incomeSum); + }); + return result; + } + double get maxValue { - final expMax = data.map((e) => e.expenseSum).reduce(max); - final incMax = data.map((e) => e.incomeSum).reduce(max); + final expMax = data.map((e) => e.expenseSum).reduce(max); + final incMax = data.map((e) => e.incomeSum).reduce(max); return max(expMax, incMax); } const ChartState( {this.status = ChartStatus.loading, this.data = const [], - this.touchedIndex = -1}); + this.categories = const [], + this.category, + this.categoryType = 'Expenses'}); - ChartState copyWith({ - ChartStatus? status, - List? data, - int? touchedIndex, - }) { + ChartState copyWith( + {ChartStatus? status, + List? data, + List? categories, + Category? category, + String? categoryType}) { return ChartState( status: status ?? this.status, data: data ?? this.data, - touchedIndex: touchedIndex ?? this.touchedIndex); + categories: categories ?? this.categories, + category: category ?? this.category, + categoryType: categoryType ?? this.categoryType); } @override - List get props => [status, data, touchedIndex]; + List get props => [status, data, categories, category, categoryType]; } diff --git a/lib/charts/repository/chart_repository.dart b/lib/charts/repository/chart_repository.dart index b3f4ba0..8bbf83b 100644 --- a/lib/charts/repository/chart_repository.dart +++ b/lib/charts/repository/chart_repository.dart @@ -1,17 +1,18 @@ import 'dart:convert'; import 'package:budget_app/charts/models/year_month_sum.dart'; +import 'package:http/http.dart' as http; import '../../constants/api.dart'; -import 'package:http/http.dart' as http; abstract class ChartRepository { - Future> fetchChart(); + Future> fetchTrendChartData(); + Future> fetchCategoryChartData(String categoryId); } class ChartRepositoryImpl extends ChartRepository { @override - Future> fetchChart() async { + Future> fetchTrendChartData() async { final url = isTestMode ? Uri.http(baseURL, '/api/charts/trend-chart', {'budgetId': await getBudgetId()}) @@ -24,4 +25,19 @@ class ChartRepositoryImpl extends ChartRepository { ).map((jsonMap) => YearMonthSum.fromJson(jsonMap)).toList(); return result; } + + @override + Future> fetchCategoryChartData(String categoryId) async { + final url = isTestMode + ? Uri.http(baseURL, '/api/charts/category-chart', + {'categoryId': categoryId}) + : Uri.https(baseURL, '/api/charts/category-chart', + {'categoryId': categoryId}); + + final response = await http.get(url, headers: await getHeaders()); + final result = List>.from( + json.decode(response.body) as List, + ).map((jsonMap) => YearMonthSum.fromJson(jsonMap)).toList(); + return result; + } } diff --git a/lib/charts/view/category_chart_page.dart b/lib/charts/view/category_chart_page.dart new file mode 100644 index 0000000..240e840 --- /dev/null +++ b/lib/charts/view/category_chart_page.dart @@ -0,0 +1,154 @@ +import 'package:budget_app/charts/cubit/chart_cubit.dart'; +import 'package:budget_app/charts/repository/chart_repository.dart'; +import 'package:budget_app/charts/view/category_table.dart'; +import 'package:budget_app/constants/constants.dart'; +import 'package:chart/chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../categories/models/category.dart'; +import '../../categories/repository/categories_repository.dart'; +import '../charts.dart'; + +class CategoryChartPage extends StatelessWidget { + const CategoryChartPage({super.key}); + + static Route route() { + final _repo = ChartRepositoryImpl(); + return MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => ChartCubit( + chartRepository: _repo, + categoriesRepository: context.read()) + ..fetchCategoryChart(), + child: CategoryChartPage(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => Scaffold( + appBar: AppBar( + title: Text(state.category?.name ?? ''), + actions: [CategoryTypeSelectButton()]), + body: Center( + child: state.status == ChartStatus.loading + ? CircularProgressIndicator() + : Column( + children: [ + AspectRatio( + aspectRatio: 1.3 / 1, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 30, horizontal: 8), + child: BlocBuilder( + builder: (context, state) { + return BarChart( + dataPoints: state.dataPoints, + labels: state.titles, + isGrouped: false, + firstColor: state.categoryType == 'Expenses' + ? Colors.red + : Colors.green, + ); + }, + ), + ), + ), + CategoryInput(), + Expanded(child: CategoryTable()) + ], + ))), + ); + } +} + +class TrendChartDesktopView extends StatelessWidget { + const TrendChartDesktopView({super.key}); + + @override + Widget build(BuildContext context) { + final _repo = ChartRepositoryImpl(); + return BlocProvider( + create: (context) => ChartCubit( + chartRepository: _repo, + categoriesRepository: context.read()) + ..fetchTrendChart(), + child: TrendChartDesktopViewBody()); + } +} + +class TrendChartDesktopViewBody extends StatelessWidget { + const TrendChartDesktopViewBody({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Center( + child: state.status == ChartStatus.loading + ? CircularProgressIndicator() + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BlocBuilder( + builder: (context, state) { + return Container( + width: w * 0.35, + height: h * 0.55, + child: BarChart( + dataPoints: state.dataPointsGrouped, + labels: state.titles, + isGrouped: true, + ), + ); + }, + ), + SizedBox(width: w * 0.01), + Padding( + padding: const EdgeInsets.only(top: 40), + child: Container( + width: w * 0.35, + height: h * 0.6, + child: TrendTable()), + ) + ], + )); + }, + ); + } +} + +class CategoryInput extends StatelessWidget { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: DropdownButtonFormField( + items: state.categories.map((Category category) { + return DropdownMenuItem( + value: category, + child: Text(category.name), + ); + }).toList(), + onChanged: (newValue) { + context.read().changeCategory(category: newValue!); + //setState(() => selectedValue = newValue); + }, + value: state.categories.contains(state.category) + ? state.category + : null, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: 'Category', + //errorText: errorSnapshot.data == 0 ? Localization.of(context).categoryEmpty : null), + )), + ); + }, + ); + } +} diff --git a/lib/charts/view/category_table.dart b/lib/charts/view/category_table.dart new file mode 100644 index 0000000..da5ad98 --- /dev/null +++ b/lib/charts/view/category_table.dart @@ -0,0 +1,135 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../cubit/chart_cubit.dart'; + +class CategoryTable extends StatelessWidget { + const CategoryTable({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final sumExp = state.data.fold(0.0, + (previousValue, element) => previousValue + element.expenseSum); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 9), + child: Column( + children: [ + Table( + border: TableBorder.all(color: Colors.grey), + columnWidths: const { + 0: FlexColumnWidth(), + 2: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + decoration: BoxDecoration( + color: Colors.white60 + ), + children: [ + Align( + alignment: Alignment.center, + child: Text( + style: TextStyle(fontSize: 18), + 'Date')), + Align( + alignment: Alignment.center, + child: Text( + style: TextStyle(fontSize: 18), + 'Spent')), + ]), + TableRow( + decoration: BoxDecoration( + color: Colors.white54 + ), + children: [ + Padding( + padding: const EdgeInsets.all(5.0), + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + style: TextStyle( + fontWeight: FontWeight.bold), + 'Total'), + Icon( + Icons.arrow_forward_outlined, + size: 15, + ) + ], + )), + ), + Padding( + padding: const EdgeInsets.all(5.0), + child: Align( + alignment: Alignment.centerRight, + child: Text( + style: TextStyle(fontWeight: FontWeight.bold), + '\$ ${sumExp.toStringAsFixed(2)}')), + ), + ]), + ], + ), + Expanded( + child: SingleChildScrollView( + child: Table( + border: TableBorder.all(color: Colors.grey), + columnWidths: const { + 0: FlexColumnWidth(), + 2: FlexColumnWidth(), + }, + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + ...state.data.reversed + .map( + (e) => TableRow( + children: [ + TableCell( + verticalAlignment: + TableCellVerticalAlignment.middle, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Text( + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize), + '${e.date}'), + ))), + TableCell( + verticalAlignment: + TableCellVerticalAlignment.middle, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.all(5.0), + child: Text( + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize), + '\$ ${e.expenseSum.toStringAsFixed(2)}'), + ))), + ], + ), + ) + .toList(), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/charts/view/chart_page.dart b/lib/charts/view/chart_page.dart deleted file mode 100644 index bf76a33..0000000 --- a/lib/charts/view/chart_page.dart +++ /dev/null @@ -1,249 +0,0 @@ -import 'package:budget_app/charts/cubit/chart_cubit.dart'; -import 'package:budget_app/charts/repository/chart_repository.dart'; -import 'package:budget_app/colors.dart'; -import 'package:budget_app/constants/constants.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../charts.dart'; - -class ChartPage extends StatelessWidget { - const ChartPage({super.key}); - - static Route route() { - final _repo = ChartRepositoryImpl(); - return MaterialPageRoute( - builder: (context) => BlocProvider( - create: (context) => ChartCubit(chartRepository: _repo)..fetchChart(), - child: ChartPage(), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Trend for last 12 months')), - body: BlocBuilder( - builder: (context, state) { - return Center( - child: state.status == ChartStatus.loading - ? CircularProgressIndicator() - : Column( - children: [ - TrendChart(), - //Divider(color: BudgetColors.teal900, indent: 20, endIndent: 20,), - Expanded(child: TrendTable()) - ], - )); - }, - ), - ); - } -} - -class TrendChartDesktopView extends StatelessWidget { - const TrendChartDesktopView({super.key}); - - @override - Widget build(BuildContext context) { - final _repo = ChartRepositoryImpl(); - return BlocProvider( - create: (context) => ChartCubit(chartRepository: _repo)..fetchChart(), - child: TrendChartDesktopViewBody()); - } -} - -class TrendChartDesktopViewBody extends StatelessWidget { - const TrendChartDesktopViewBody({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return Center( - child: state.status == ChartStatus.loading - ? CircularProgressIndicator() - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container(width: w * 0.35, height: h * 0.6, child: TrendChart()), - SizedBox(width: w * 0.01), - Padding( - padding: const EdgeInsets.only(top: 40), - child: Container(width: w * 0.35, height: h * 0.6, child: TrendTable()), - ) - ], - )); - }, - ); - } -} - -class TrendChart extends StatelessWidget { - TrendChart({super.key}); - - final Color leftBarColor = BudgetColors.teal600; - final Color rightBarColor = BudgetColors.red800; - final double width = 10; - - @override - Widget build(BuildContext context) { - final chartCubit = context.read(); - return AspectRatio( - aspectRatio: 1, - child: Padding( - padding: const EdgeInsets.all(8), - child: BlocBuilder( - builder: (context, state) { - final showingBarGroups = _buildBarChartGroups( - data: state.data, touchedIndex: state.touchedIndex); - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox( - height: 20, - ), - Expanded( - child: BarChart( - BarChartData( - maxY: state.maxValue, - barTouchData: BarTouchData( - touchTooltipData: BarTouchTooltipData( - fitInsideHorizontally: true, - tooltipBgColor: Colors.lightBlueAccent, - getTooltipItem: (group, groupIndex, rod, rodIndex) { - return BarTooltipItem( - '${group.barRods[0].toY} / ${group.barRods[1].toY}', - TextStyle(color: Colors.black)); - }, - ), - touchCallback: (FlTouchEvent event, response) { - if (!event.isInterestedForInteractions || - response == null || - response.spot == null) { - chartCubit.changeTouchedIndex(-1); - return; - } - chartCubit.changeTouchedIndex( - response.spot!.touchedBarGroupIndex); - }, - ), - titlesData: FlTitlesData( - show: true, - rightTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: const AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, meta) { - final titles = state.titles; - final Widget text = Text( - titles[value.toInt()], - style: const TextStyle( - color: Color(0xff7589a2), - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ); - - return SideTitleWidget( - axisSide: meta.axisSide, - angle: 0, - space: 16, //margin top - child: text, - ); - }, - reservedSize: 42, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - reservedSize: state.maxValue > 10000 ? 35 : 30, - interval: 1, - getTitlesWidget: (value, meta) => - _getLeftTitlesWidget(value, meta, state), - ), - ), - ), - borderData: FlBorderData( - show: false, - ), - barGroups: showingBarGroups, - gridData: const FlGridData(show: true), - ), - ), - ), - ], - ); - }, - ), - ), - ); - } - - Widget _getLeftTitlesWidget(double value, TitleMeta meta, ChartState state) { - const style = TextStyle( - color: Color(0xff7589a2), - fontWeight: FontWeight.bold, - fontSize: 14, - ); - String text; - if (value == 0) { - text = '0'; - } else if (value == (state.maxValue / 4).round()) { - text = '${(state.maxValue / 4 / 1000).toStringAsFixed(1)}K'; - } else if (value == (state.maxValue / 2).round()) { - text = '${(state.maxValue / 2 / 1000).toStringAsFixed(1)}K'; - } else if (value == (state.maxValue / 4 * 3).round()) { - text = '${(state.maxValue / 4 * 3 / 1000).toStringAsFixed(1)}K'; - } else if (value == state.maxValue) { - text = '${(state.maxValue / 1000).toStringAsFixed(1)}K'; - } else { - return Container(); - } - return SideTitleWidget( - axisSide: meta.axisSide, - space: 0, - child: Text(text, style: style), - ); - } - - List _buildBarChartGroups( - {required List data, required int touchedIndex}) { - return data - .asMap() - .entries - .map((entry) => _makeGroupData(entry.key, entry.value.incomeSum, - entry.value.expenseSum, touchedIndex)) - .toList(); - } - - BarChartGroupData _makeGroupData( - int x, double y1, double y2, int touchedIndex) { - final isTouched = touchedIndex == x; - return BarChartGroupData( - showingTooltipIndicators: isTouched ? [0] : [], - barsSpace: 1, - x: x, - barRods: [ - BarChartRodData( - toY: y1, - color: leftBarColor, - width: width, - ), - BarChartRodData( - toY: y2, - color: rightBarColor, - width: width, - ), - ], - ); - } -} diff --git a/lib/charts/view/trend_chart_page.dart b/lib/charts/view/trend_chart_page.dart new file mode 100644 index 0000000..84a1c10 --- /dev/null +++ b/lib/charts/view/trend_chart_page.dart @@ -0,0 +1,119 @@ +import 'package:budget_app/categories/repository/categories_repository.dart'; +import 'package:budget_app/charts/cubit/chart_cubit.dart'; +import 'package:budget_app/charts/repository/chart_repository.dart'; +import 'package:budget_app/constants/constants.dart'; +import 'package:chart/chart.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../charts.dart'; + +class TrendChartPage extends StatelessWidget { + const TrendChartPage({super.key}); + + static Route route() { + final _repo = ChartRepositoryImpl(); + return MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => ChartCubit( + chartRepository: _repo, + categoriesRepository: context.read()) + ..fetchTrendChart(), + child: TrendChartPage(), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Trend for last 12 months')), + body: BlocBuilder( + builder: (context, state) { + return Center( + child: state.status == ChartStatus.loading + ? CircularProgressIndicator() + : Column( + children: [ + AspectRatio( + aspectRatio: 1.3 / 1, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 30, horizontal: 8), + child: BlocBuilder( + builder: (context, state) { + return BarChart( + dataPoints: state.dataPointsGrouped, + labels: state.titles, + isGrouped: true, + ); + }, + ), + ), + ), + SizedBox(height: 20), + //Divider(color: BudgetColors.teal900, indent: 20, endIndent: 20,), + Expanded(child: TrendTable()) + ], + )); + }, + ), + ); + } +} + +class TrendChartDesktopView extends StatelessWidget { + const TrendChartDesktopView({super.key}); + + @override + Widget build(BuildContext context) { + final _repo = ChartRepositoryImpl(); + return BlocProvider( + create: (context) => ChartCubit( + chartRepository: _repo, + categoriesRepository: context.read()) + ..fetchTrendChart(), + child: TrendChartDesktopViewBody()); + } +} + +class TrendChartDesktopViewBody extends StatelessWidget { + const TrendChartDesktopViewBody({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Center( + child: state.status == ChartStatus.loading + ? CircularProgressIndicator() + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BlocBuilder( + builder: (context, state) { + return Container( + width: w * 0.35, + height: h * 0.55, + child: BarChart( + dataPoints: state.dataPointsGrouped, + labels: state.titles, + isGrouped: true, + ), + ); + }, + ), + SizedBox(width: w * 0.01), + Padding( + padding: const EdgeInsets.only(top: 40), + child: Container( + width: w * 0.35, + height: h * 0.6, + child: TrendTable()), + ) + ], + )); + }, + ); + } +} diff --git a/lib/charts/view/trend_table.dart b/lib/charts/view/trend_table.dart index b965b67..a1da373 100644 --- a/lib/charts/view/trend_table.dart +++ b/lib/charts/view/trend_table.dart @@ -168,9 +168,13 @@ class TrendTable extends StatelessWidget { verticalAlignment: TableCellVerticalAlignment.middle, child: Container( - color: e.incomeSum - e.expenseSum <= 0 - ? Color.fromRGBO(255, 227, 227, 1.0) - : Color.fromRGBO(221, 255, 216, 1.0), + color: e.incomeSum - e.expenseSum == 0 + ? null + : e.incomeSum - e.expenseSum <= 0 + ? Color.fromRGBO( + 255, 227, 227, 1.0) + : Color.fromRGBO( + 221, 255, 216, 1.0), child: Align( alignment: Alignment.centerRight, child: Padding( diff --git a/lib/charts/view/view.dart b/lib/charts/view/view.dart index 7623de6..cfd2e4b 100644 --- a/lib/charts/view/view.dart +++ b/lib/charts/view/view.dart @@ -1,2 +1,3 @@ +export 'trend_chart_page.dart'; export 'trend_table.dart'; -export 'chart_page.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/charts/view/widgets/category_type_button.dart b/lib/charts/view/widgets/category_type_button.dart new file mode 100644 index 0000000..d66d724 --- /dev/null +++ b/lib/charts/view/widgets/category_type_button.dart @@ -0,0 +1,38 @@ +import 'package:budget_app/charts/cubit/chart_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CategoryTypeSelectButton extends StatelessWidget { + const CategoryTypeSelectButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return PopupMenuButton( + shape: const ContinuousRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + initialValue: state.categoryType, + tooltip: 'Choose type', + onSelected: (type) { + context.read().changeCategoryType(categoryType: type); + }, + itemBuilder: (context) { + return [ + PopupMenuItem( + value: 'Expenses', + child: Text('Expenses'), + ), + PopupMenuItem( + value: 'Income', + child: Text('Income'), + ), + ]; + }, + icon: const Icon(Icons.filter_2), + ); + }, + ); + } +} diff --git a/lib/charts/view/widgets/widgets.dart b/lib/charts/view/widgets/widgets.dart new file mode 100644 index 0000000..380c336 --- /dev/null +++ b/lib/charts/view/widgets/widgets.dart @@ -0,0 +1 @@ +export 'category_type_button.dart'; diff --git a/lib/constants/api.dart b/lib/constants/api.dart index a1a66ce..6249d5c 100644 --- a/lib/constants/api.dart +++ b/lib/constants/api.dart @@ -1,8 +1,8 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:budget_app/shared/models/budget.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:shared_preferences/shared_preferences.dart'; const bool isTestMode = true; diff --git a/lib/debt_payoff_planner/debt_form/view/debt_dialog.dart b/lib/debt_payoff_planner/debt_form/view/debt_dialog.dart index da7b24e..5992bdc 100644 --- a/lib/debt_payoff_planner/debt_form/view/debt_dialog.dart +++ b/lib/debt_payoff_planner/debt_form/view/debt_dialog.dart @@ -3,7 +3,6 @@ import 'package:budget_app/debt_payoff_planner/cubits/debt_cubit/debts_cubit.dar import 'package:budget_app/debt_payoff_planner/debt_form/debt_form.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:formz/formz.dart'; class DebtDialog extends StatelessWidget { @@ -37,7 +36,7 @@ class DebtDialog extends StatelessWidget { alignment: Alignment.center, children: [ Container( - height: 1465.h, + height: 500, width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15),), @@ -51,15 +50,15 @@ class DebtDialog extends StatelessWidget { .textTheme .titleLarge ?.fontSize)), - SizedBox(height: 50.h), + SizedBox(height: 15), NameInputField(), - SizedBox(height: 75.h), + SizedBox(height: 20), BalanceInput(), - SizedBox(height: 75.h), + SizedBox(height: 20), MinInputField(), - SizedBox(height: 75.h), + SizedBox(height: 20), AprInputField(), - SizedBox(height: 75.h), + SizedBox(height: 20), _SubmitButton(), ], ), diff --git a/lib/drawer/main_drawer.dart b/lib/drawer/main_drawer.dart index bf618d6..c7e5ed7 100644 --- a/lib/drawer/main_drawer.dart +++ b/lib/drawer/main_drawer.dart @@ -1,9 +1,11 @@ -import 'package:budget_app/charts/view/chart_page.dart'; +import 'package:budget_app/charts/view/category_chart_page.dart'; +import 'package:budget_app/charts/view/trend_chart_page.dart'; import 'package:budget_app/colors.dart'; import 'package:budget_app/constants/constants.dart'; import 'package:budget_app/debt_payoff_planner/view/payoff_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../app/bloc/app_bloc.dart'; import '../home/view/home_page.dart'; @@ -36,105 +38,119 @@ class _MainDrawerState extends State { width: double.infinity, child: Image.asset('assets/images/piggy_logo.png', fit: BoxFit.contain))), - ListTile( - tileColor: - widget.tabController?.index == 0 ? BudgetColors.teal100 : null, - leading: Icon(Icons.monetization_on_outlined, - size: 26, color: BudgetColors.teal900), - title: Text('Home', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () { - isDisplayDesktop(context) - ? {widget.tabController?.index = 0, setState(() {},)} - : Navigator.of(context).pushNamedAndRemoveUntil( - HomePage.routeName, - (route) => false, - ); - }, - ), - ListTile( - tileColor: - widget.tabController?.index == 1 ? BudgetColors.teal100 : null, - leading: - Icon(Icons.summarize_outlined, size: 26, color: BudgetColors.teal900), - title: Text('Summary', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () { - isDisplayDesktop(context) - ? {widget.tabController?.index = 1, setState(() {}),} - : { - Navigator.pop(context), - Navigator.of(context).push(SummaryPage.route()) - }; - }, - ), - ListTile( - tileColor: - widget.tabController?.index == 2 ? BudgetColors.teal100 : null, - leading: - Icon(Icons.bar_chart, size: 26, color: BudgetColors.teal900), - title: Text('Trend', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () { - isDisplayDesktop(context) - ? {widget.tabController?.index = 2, setState(() {}),} - : { - Navigator.pop(context), - Navigator.of(context).push(ChartPage.route()) - }; - }, - ), - ListTile( - tileColor: - widget.tabController?.index == 3 ? BudgetColors.teal100 : null, - leading: Icon(Icons.money_outlined, - size: 26, color: BudgetColors.teal900), - title: Text('Debt payoff planner', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () { - isDisplayDesktop(context) - ? {widget.tabController?.index = 3, setState(() {}),} - : { - Navigator.pop(context), - Navigator.push(context, DebtPayoffPage.route()) - }; - }, - ), - ListTile( - leading: - Icon(Icons.settings, size: 26, color: BudgetColors.teal900), - title: Text('Settings', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () {}, - ), - ListTile( - leading: Icon(Icons.logout, size: 26, color: BudgetColors.teal900), - title: Text('Log out', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith(color: BudgetColors.teal900)), - onTap: () { - context.read().add(const AppLogoutRequested()); - }, - ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: Column( + children: [ + _buildMenuItem( + menuIndex: 0, + title: 'Budgets', + icon: FaIcon(FontAwesomeIcons.coins, + color: BudgetColors.teal900), + route: HomePage.route()), + Divider(color: BudgetColors.teal900), + _buildMenuItem( + menuIndex: 1, + title: 'Summary', + icon: FaIcon(FontAwesomeIcons.listUl, + color: BudgetColors.teal900), + route: SummaryPage.route()), + Divider(color: BudgetColors.teal900), + ExpansionTile( + shape: Border.all(color: Colors.transparent), + title: Row( + children: [ + FaIcon(FontAwesomeIcons.chartSimple, + color: BudgetColors.teal900), + SizedBox(width: 20), + Text('Charts', + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: BudgetColors.teal900)), + ], + ), + children: [ + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: _buildMenuItem( + menuIndex: 2, + title: 'Trend', + icon: null, + route: TrendChartPage.route())), + Padding( + padding: const EdgeInsets.only(left: 20.0), + child: _buildMenuItem( + menuIndex: 2, + title: 'Sum by Category', + icon: null, + route: CategoryChartPage.route())), + ], + ), + Divider(color: BudgetColors.teal900), + _buildMenuItem( + menuIndex: 3, + title: 'Debt payoff planner', + icon: FaIcon(FontAwesomeIcons.moneyCheckDollar, + color: BudgetColors.teal900), + route: DebtPayoffPage.route()), + Divider(color: BudgetColors.teal900), + ListTile( + leading: FaIcon(FontAwesomeIcons.gear, + color: BudgetColors.teal900), + title: Text('Settings', + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: BudgetColors.teal900)), + onTap: () {}, + ), + Divider(color: BudgetColors.teal900), + ListTile( + leading: FaIcon(FontAwesomeIcons.rightFromBracket, + color: BudgetColors.teal900), + title: Text('Log out', + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: BudgetColors.teal900)), + onTap: () { + context.read().add(const AppLogoutRequested()); + }, + ), + Divider(color: BudgetColors.teal900) + ], + ), + ), + ), + ) ], ), ); } + + ListTile _buildMenuItem( + {required int menuIndex, + required String title, + required Widget? icon, + required Route route}) { + return ListTile( + tileColor: widget.tabController?.index == menuIndex + ? BudgetColors.teal100 + : null, + leading: icon, + title: Text(title, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith(color: BudgetColors.teal900)), + onTap: () { + isDisplayDesktop(context) + ? {widget.tabController?.index = menuIndex, setState(() {})} + : {Navigator.of(context).pop(), Navigator.of(context).push(route)}; + }, + ); + } } diff --git a/lib/home/cubit/home_cubit.dart b/lib/home/cubit/home_cubit.dart index 4c71295..684f72d 100644 --- a/lib/home/cubit/home_cubit.dart +++ b/lib/home/cubit/home_cubit.dart @@ -122,7 +122,7 @@ class HomeCubit extends Cubit { var summaries = await _getSummariesByCategory( transactions: state.transactions, categories: categories); emit(state.copyWith( - summaryList: summaries, + summaryList: summaries, categories: categories )); } diff --git a/lib/home/view/home_desktop_page.dart b/lib/home/view/home_desktop_page.dart index 5e2b423..e6b54a2 100644 --- a/lib/home/view/home_desktop_page.dart +++ b/lib/home/view/home_desktop_page.dart @@ -2,6 +2,7 @@ import 'package:budget_app/charts/charts.dart'; import 'package:budget_app/constants/constants.dart'; import 'package:budget_app/drawer/main_drawer.dart'; import 'package:budget_app/home/view/widgets/accounts_summaries.dart'; +import 'package:budget_app/summary/view/summary_page.dart'; import 'package:budget_app/transactions/models/transaction_type.dart'; import 'package:budget_app/transactions/transaction/view/transaction_page.dart'; import 'package:budget_app/transfer/view/view.dart'; @@ -59,13 +60,12 @@ class HomeGrid extends StatefulWidget { } class _HomeGridState extends State with TickerProviderStateMixin { - late final TabController _tabController; @override void initState() { super.initState(); - _tabController = TabController(length: 3, vsync: this); + _tabController = TabController(length: 4, vsync: this); } @override @@ -84,6 +84,7 @@ class _HomeGridState extends State with TickerProviderStateMixin { controller: _tabController, children: [ HomeViewDesktop(), + Center(child: Container(width: w * 0.5, height: h * 0.9, child: SummaryPage())), TrendChartDesktopView(), DebtPayoffViewDesktop(), ], diff --git a/lib/home/view/home_mobile_page.dart b/lib/home/view/home_mobile_page.dart index b20c1b3..34b9c4d 100644 --- a/lib/home/view/home_mobile_page.dart +++ b/lib/home/view/home_mobile_page.dart @@ -41,19 +41,12 @@ class HomeMobileView extends StatelessWidget { child: Scaffold( backgroundColor: BudgetColors.teal50, appBar: AppBar( - title: AnimatedSwitcher( - duration: Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: state.tab != HomeTab.accounts - ? MonthPaginator( - onLeft: (date) => - context.read().changeDate(date), - onRight: (date) => - context.read().changeDate(date), - ) - : Text('Accounts')), + title: MonthPaginator( + onLeft: (date) => + context.read().changeDate(date), + onRight: (date) => + context.read().changeDate(date), + ), centerTitle: true, actions: [ IconButton( diff --git a/lib/home/view/home_page.dart b/lib/home/view/home_page.dart index 85e03b9..e8448fa 100644 --- a/lib/home/view/home_page.dart +++ b/lib/home/view/home_page.dart @@ -14,6 +14,10 @@ import '../../transactions/repository/transactions_repository.dart'; class HomePage extends StatelessWidget { static const routeName = '/home'; + static Route route(){ + return MaterialPageRoute(builder: (context) => HomePage()); + } + const HomePage({Key? key}) : super(key: key); @override diff --git a/lib/home/view/widgets/accounts_summaries.dart b/lib/home/view/widgets/accounts_summaries.dart index e259541..9630a69 100644 --- a/lib/home/view/widgets/accounts_summaries.dart +++ b/lib/home/view/widgets/accounts_summaries.dart @@ -1,6 +1,8 @@ +import 'package:budget_app/home/home.dart'; import 'package:budget_app/transfer/bloc/transfer_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../../../accounts/cubit/accounts_cubit.dart'; import '../../../accounts/models/accounts_view_filter.dart'; @@ -39,7 +41,7 @@ class AccountsSummariesView extends StatelessWidget { dividerColor: BudgetColors.teal900, expansionCallback: (int index, bool isExpanded) { context.read().changeExpanded(index); - if(isDisplayDesktop(context)){ + if (isDisplayDesktop(context)) { context.read().add(TransferFormLoaded()); } }, @@ -49,8 +51,17 @@ class AccountsSummariesView extends StatelessWidget { backgroundColor: BudgetColors.teal100, headerBuilder: (BuildContext context, bool isExpanded) { return ListTile( - leading: Icon(Icons.account_balance_outlined, - color: scheme.primary), + leading: Builder(builder: (context) { + final categories = context + .select((HomeCubit cubit) => cubit.state.categories); + return FaIcon( + color: scheme.primary, + IconData( + categories.firstWhere((element) => + element.id == acc.categoryId) + .iconCode ?? 0, + fontFamily: 'FontAwesomeSolid')); + }), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/home/view/widgets/category_summaries.dart b/lib/home/view/widgets/category_summaries.dart index bc0130a..a8a094f 100644 --- a/lib/home/view/widgets/category_summaries.dart +++ b/lib/home/view/widgets/category_summaries.dart @@ -2,6 +2,7 @@ import 'package:budget_app/transactions/models/transaction_type.dart'; import 'package:budget_app/transactions/transaction/bloc/transaction_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import '../../../colors.dart'; import '../../../constants/constants.dart'; @@ -37,15 +38,15 @@ class CategorySummaries extends StatelessWidget { backgroundColor: BudgetColors.teal100, headerBuilder: (BuildContext context, bool isExpanded) { return ListTile( - leading: Icon( + leading: FaIcon( color: scheme.primary, IconData(tile.iconCodePoint, - fontFamily: 'MaterialIcons')), + fontFamily: 'FontAwesomeSolid')), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Text( - '\$ ${tile.total.toString()}', + '\$ ${tile.total.toStringAsFixed(2)}', style: TextStyle( fontSize: textTheme.titleLarge!.fontSize, fontWeight: FontWeight.bold, diff --git a/lib/shared/models/awesome_icons.dart b/lib/shared/models/awesome_icons.dart new file mode 100644 index 0000000..014eb79 --- /dev/null +++ b/lib/shared/models/awesome_icons.dart @@ -0,0 +1,63 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/widgets.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class IconTile extends Equatable { + final IconData iconData; + final String title; + + IconTile({required this.iconData, required this.title}); + + @override + List get props => [title]; +} + +List appIconsList = [ + IconTile(iconData: FontAwesomeIcons.umbrellaBeach, title: 'umbrellaBeach'), + IconTile(iconData: FontAwesomeIcons.lightbulb, title: 'lightbulb'), + IconTile(iconData: FontAwesomeIcons.gift, title: 'gift'), + IconTile(iconData: FontAwesomeIcons.burger, title: 'burger'), + IconTile(iconData: FontAwesomeIcons.prescription, title: 'prescription'), + IconTile(iconData: FontAwesomeIcons.hammer, title: 'hammer'), + IconTile(iconData: FontAwesomeIcons.graduationCap, title: 'graduationCap'), + IconTile(iconData: FontAwesomeIcons.sackDollar, title: 'sackDollar'), + IconTile(iconData: FontAwesomeIcons.chartPie, title: 'chartPie'), + IconTile(iconData: FontAwesomeIcons.heart, title: 'heart'), + IconTile(iconData: FontAwesomeIcons.cartShopping, title: 'cartShopping'), + IconTile(iconData: FontAwesomeIcons.plane, title: 'plane'), + IconTile(iconData: FontAwesomeIcons.users, title: 'users'), + IconTile(iconData: FontAwesomeIcons.shirt, title: 'shirt'), + IconTile(iconData: FontAwesomeIcons.wallet, title: 'wallet'), + IconTile(iconData: FontAwesomeIcons.seedling, title: 'seedling'), + IconTile(iconData: FontAwesomeIcons.wrench, title: 'wrench'), + IconTile(iconData: FontAwesomeIcons.crown, title: 'crown'), + IconTile(iconData: FontAwesomeIcons.wineBottle, title: 'wineBottle'), + IconTile(iconData: FontAwesomeIcons.venus, title: 'venus'), + IconTile(iconData: FontAwesomeIcons.tent, title: 'tent'), + IconTile(iconData: FontAwesomeIcons.shrimp, title: 'shrimp'), + IconTile(iconData: FontAwesomeIcons.scissors, title: 'scissors'), + IconTile(iconData: FontAwesomeIcons.scaleUnbalanced, title: 'scaleUnbalanced'), + IconTile(iconData: FontAwesomeIcons.plane, title: 'plane'), + IconTile(iconData: FontAwesomeIcons.personHiking, title: 'personHiking'), + IconTile(iconData: FontAwesomeIcons.moneyBills, title: 'moneyBills'), + IconTile(iconData: FontAwesomeIcons.martiniGlassEmpty, title: 'martiniGlass'), + IconTile(iconData: FontAwesomeIcons.couch, title: 'couch'), + IconTile(iconData: FontAwesomeIcons.carRear, title: 'carRear'), + IconTile(iconData: FontAwesomeIcons.buildingColumns, title: 'buildingColumns'), + IconTile(iconData: FontAwesomeIcons.babyCarriage, title: 'babyCarriage'), + IconTile(iconData: FontAwesomeIcons.faceKissWinkHeart, title: 'faceKissWinkHeart'), + IconTile(iconData: FontAwesomeIcons.piggyBank, title: 'piggyBank'), + IconTile(iconData: FontAwesomeIcons.house, title: 'house'), + IconTile(iconData: FontAwesomeIcons.mugHot, title: 'mugHot'), + IconTile(iconData: FontAwesomeIcons.truck, title: 'truck'), + IconTile(iconData: FontAwesomeIcons.gamepad, title: 'gamepad'), + IconTile(iconData: FontAwesomeIcons.handHoldingHeart, title: 'handHoldingHeart'), + IconTile(iconData: FontAwesomeIcons.store, title: 'store'), + IconTile(iconData: FontAwesomeIcons.userTie, title: 'userTie'), + IconTile(iconData: FontAwesomeIcons.tooth, title: 'tooth'), + IconTile(iconData: FontAwesomeIcons.pizzaSlice, title: 'pizzaSlice'), + IconTile(iconData: FontAwesomeIcons.penClip, title: 'penClip'), + IconTile(iconData: FontAwesomeIcons.dog, title: 'dog'), + IconTile(iconData: FontAwesomeIcons.cakeCandles, title: 'cakeCandles'), + +]; diff --git a/lib/subcategories/view/subcategories_page.dart b/lib/subcategories/view/subcategories_page.dart index f2a80a7..8c57d1d 100644 --- a/lib/subcategories/view/subcategories_page.dart +++ b/lib/subcategories/view/subcategories_page.dart @@ -1,7 +1,6 @@ import 'package:budget_app/colors.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import '../../categories/models/category.dart'; import '../cubit/subcategories_cubit.dart'; @@ -36,7 +35,6 @@ class SubcategoriesView extends StatelessWidget { @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; return BlocConsumer( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { @@ -58,7 +56,7 @@ class SubcategoriesView extends StatelessWidget { body: Column( children: [ SizedBox( - height: 50.h, + height: 10, ), Expanded( child: ListView.builder( diff --git a/lib/transactions/view/widgets/transaction_list_tile.dart b/lib/transactions/view/widgets/transaction_list_tile.dart index 8397d95..5a936e7 100644 --- a/lib/transactions/view/widgets/transaction_list_tile.dart +++ b/lib/transactions/view/widgets/transaction_list_tile.dart @@ -2,7 +2,6 @@ import 'package:budget_app/colors.dart'; import 'package:budget_app/transactions/models/transaction_tile.dart'; import 'package:budget_app/transactions/models/transaction_type.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:intl/intl.dart'; class TransactionListTile extends StatelessWidget { @@ -23,7 +22,6 @@ class TransactionListTile extends StatelessWidget { onDismissed: onDismissed, direction: DismissDirection.startToEnd, background: Container( - //alignment: Alignment.centerRight, color: theme.error, padding: const EdgeInsets.symmetric(horizontal: 16), child: const Icon( @@ -49,7 +47,7 @@ class TransactionListTile extends StatelessWidget { fontWeight: FontWeight.bold), ), SizedBox( - width: 30.w, + width: 10, ), Icon(Icons.chevron_right), ], diff --git a/packages/chart/lib/chart.dart b/packages/chart/lib/chart.dart new file mode 100644 index 0000000..9056bb3 --- /dev/null +++ b/packages/chart/lib/chart.dart @@ -0,0 +1,3 @@ +library chart; + +export 'src/chart.dart'; diff --git a/packages/chart/lib/src/chart.dart b/packages/chart/lib/src/chart.dart new file mode 100644 index 0000000..213e438 --- /dev/null +++ b/packages/chart/lib/src/chart.dart @@ -0,0 +1,413 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class BarChart extends StatelessWidget { + final List dataPoints; + final List labels; + final bool isGrouped; + final Color firstColor; + final Color secondColor; + + const BarChart( + {super.key, + required this.dataPoints, + required this.labels, + required this.isGrouped, + this.firstColor = Colors.green, + this.secondColor = Colors.red}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return _BarChart( + dataPoints: dataPoints, + labels: labels, + isGrouped: isGrouped, + width: constraints.maxWidth, + height: constraints.maxHeight, + firstColor: firstColor, + secondColor: secondColor, + ); + }); + } +} + +class _BarChart extends StatefulWidget { + final List dataPoints; + final List labels; + final double width; + final double height; + final bool isGrouped; + final Color firstColor; + final Color secondColor; + + const _BarChart( + {required this.dataPoints, + required this.labels, + required this.width, + required this.height, + required this.isGrouped, + required this.firstColor, + required this.secondColor}); + + @override + _BarChartState createState() => _BarChartState(); +} + +class _BarChartState extends State<_BarChart> with TickerProviderStateMixin { + late AnimationController controller; + late BarChartTween tween; + late List _dataPoints; + late List _labels; + late bool _isGrouped; + late double maxBarHeight; + int _tappedIndex = -1; + + @override + void initState() { + super.initState(); + controller = AnimationController( + duration: const Duration(milliseconds: 1000), + vsync: this, + ); + _dataPoints = widget.dataPoints; + _labels = widget.labels; + _isGrouped = widget.isGrouped; + maxBarHeight = _dataPoints.reduce(max); + final multiplier = widget.height / maxBarHeight; + tween = BarChartTween( + BarChartModel.empty( + amount: _dataPoints.length, + isGrouped: _isGrouped, + firstColor: widget.firstColor, + secondColor: widget.secondColor), + BarChartModel.fromArray( + dataPoints: _dataPoints, + scaledData: _dataPoints.map((e) => e * multiplier).toList(), + isGrouped: _isGrouped, + firstColor: widget.firstColor, + secondColor: widget.secondColor)); + controller.forward(); + } + + void animateTappedBar(int index) { + setState(() { + _tappedIndex = index; + }); + } + + @override + void didUpdateWidget(covariant _BarChart oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.dataPoints != oldWidget.dataPoints) { + updateData(widget.dataPoints); + } + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + void updateData(List newData) { + var maxBar = newData.reduce(max); + if (maxBar == 0) { + maxBar = 1.0; + } + final multiplier = widget.height / maxBar; + setState(() { + _tappedIndex = -1; + _dataPoints = newData; + maxBarHeight = maxBar; + tween = BarChartTween( + tween.evaluate(controller), + BarChartModel.fromArray( + dataPoints: newData, + scaledData: _dataPoints.map((e) => e * multiplier).toList(), + isGrouped: _isGrouped, + firstColor: widget.firstColor, + secondColor: widget.secondColor)); + controller.forward(from: 0.0); + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onPanDown: (details) { + final barWidth = (widget.width - 40) / _dataPoints.length; + final index = ((details.localPosition.dx - 40) / barWidth).floor(); + if (index >= 0 && index < _dataPoints.length) { + animateTappedBar(index); + } else { + animateTappedBar(-1); + } + }, + child: CustomPaint( + size: Size(widget.width, widget.height), + painter: GridPainter(maxHeight: maxBarHeight, labels: _labels), + foregroundPainter: BarChartPainter(tween.animate(controller), + isGrouped: _isGrouped, + tappedIndex: _tappedIndex, + maxWidth: widget.width), + ), + ); + } +} + +class BarModel { + final double height; + final Color color; + + BarModel({required this.height, required this.color}); + + factory BarModel.empty() => BarModel(height: 0.0, color: Colors.transparent); + + static BarModel lerp( + {required BarModel begin, required BarModel end, required double t}) { + return BarModel( + height: lerpDouble(begin.height, end.height, t)!, + color: Color.lerp(begin.color, end.color, t)!); + } + + BarModel copyWith({double? height, Color? color}) { + return BarModel(height: height ?? this.height, color: color ?? this.color); + } +} + +class BarChartModel { + final List bars; + final List dataPoints; + + BarChartModel({this.bars = const [], this.dataPoints = const []}) {} + + factory BarChartModel.empty( + {required int amount, + required bool isGrouped, + required Color firstColor, + required Color secondColor}) { + return BarChartModel( + bars: List.generate( + amount, + (index) => BarModel( + height: 0.0, + color: isGrouped + ? index % 2 == 0 + ? secondColor + : firstColor + : firstColor))); + } + + factory BarChartModel.fromArray( + {required List scaledData, + required List dataPoints, + required bool isGrouped, + required Color firstColor, + required Color secondColor}) { + return BarChartModel( + dataPoints: dataPoints, + bars: List.generate( + scaledData.length, + (index) => BarModel( + height: scaledData[index], + color: isGrouped + ? index % 2 == 0 + ? secondColor + : firstColor + : firstColor))); + } + + static BarChartModel lerp(BarChartModel begin, BarChartModel end, double t) { + return BarChartModel( + dataPoints: end.dataPoints, + bars: List.generate( + begin.bars.length, + (i) => + BarModel.lerp(begin: begin.bars[i], end: end.bars[i], t: t))); + } +} + +class BarChartTween extends Tween { + BarChartTween(BarChartModel begin, BarChartModel end) + : super(begin: begin, end: end); + + @override + BarChartModel lerp(double t) => BarChartModel.lerp(begin!, end!, t); +} + +class BarChartPainter extends CustomPainter { + late final barDistance; + late final barWidth; + + final Animation animation; + final int tappedIndex; + final bool isGrouped; + + BarChartPainter(Animation animation, + {required this.isGrouped, + this.tappedIndex = -1, + required double maxWidth}) + : animation = animation, + barDistance = isGrouped ? maxWidth / 31 : maxWidth / 20, + barWidth = isGrouped ? maxWidth / 34 : maxWidth / 20, + super(repaint: animation); + + @override + void paint(Canvas canvas, Size size) { + final chart = animation.value; + var offsetX = 35.0; + var tappedBarX = 0.0; + + for (var i = 0; i < chart.bars.length; i++) { + final bar = chart.bars[i]; + _drawBar(bar, offsetX, Paint()..color = bar.color, canvas, size); + if (i == tappedIndex) { + tappedBarX = offsetX; + } + isGrouped + ? offsetX += (barDistance + barWidth / 3 * (i % 2)) + : offsetX += barDistance * 1.5; + } + if (tappedIndex >= 0) { + _drawLabel(chart.bars[tappedIndex], chart.dataPoints[tappedIndex], + tappedBarX, canvas, size); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + + void _drawBar(BarModel bar, double x, Paint paint, Canvas canvas, Size size) { + canvas.drawRect( + Rect.fromLTWH(x, size.height - bar.height, barWidth, bar.height), + paint); + } + + void _drawText(Offset position, double width, TextStyle style, String text, + Canvas canvas) { + final textSpan = TextSpan(text: text, style: style); + final textPainter = + TextPainter(text: textSpan, textDirection: TextDirection.ltr); + textPainter.layout(minWidth: 0, maxWidth: width); + textPainter.paint(canvas, position); + } + + void _drawLabel(BarModel bar, double labelValue, double tappedBarX, + Canvas canvas, Size size) { + final txt = '\$ $labelValue'; + double labelX = 0.0; + double labelY = 0.0; + if (tappedBarX - (9.0 * txt.length / 2) < 0) { + labelX = 0.0; + } else if (tappedBarX - (9.0 * txt.length / 2) > 0 && + tappedBarX + (9.0 * txt.length / 2) < size.width) { + labelX = tappedBarX - (9.0 * txt.length / 2); + } else { + labelX = size.width - (9.0 * txt.length); + } + if ((size.height - bar.height - 35) < 0) { + labelY = 0.0; + } else { + labelY = (size.height - bar.height - 35); + } + var labelStyle = TextStyle( + color: Colors.black, + fontSize: 15, + fontWeight: FontWeight.bold, + ); + final border = Paint() + ..color = Colors.black38 + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + final paint = Paint() + ..color = Colors.white + ..style = PaintingStyle.fill; + final paintBorder = Paint() + ..color = Colors.black + ..strokeWidth = 2 + ..style = PaintingStyle.stroke; + final rect = RRect.fromRectAndRadius( + Rect.fromLTWH(labelX, labelY, 9.0 * txt.length, 28), + Radius.circular(5), + ); + _drawBar(bar, tappedBarX, border, canvas, size); + canvas.drawRRect(rect, paint); + canvas.drawRRect(rect, paintBorder); + _drawText(Offset(labelX + 5, labelY + 5), 150, labelStyle, txt, canvas); + } +} + +class GridPainter extends CustomPainter { + final double maxHeight; + final List labels; + + GridPainter({required this.maxHeight, required this.labels}); + + @override + void paint(Canvas canvas, Size size) { + final double width = size.width; + final double height = size.height; + final double stepX = width / 13; // Adjust the step as needed + final double stepY = height / 13; // Adjust the step as needed + + final Paint gridPaint = Paint() + ..color = Colors.green + ..style = PaintingStyle.fill; + + for (double x = 35; x <= width; x += stepX) { + canvas.drawLine(Offset(x, 0), Offset(x, height), gridPaint); + } + + for (double y = 1; y <= height; y += stepY) { + canvas.drawLine(Offset(35, y), Offset(width, y), gridPaint); + } + + drawText(Canvas canvas, Offset position, double width, TextStyle style, + String text) { + final textSpan = TextSpan(text: text, style: style); + final textPainter = + TextPainter(text: textSpan, textDirection: TextDirection.ltr); + textPainter.layout(minWidth: 0, maxWidth: width); + textPainter.paint(canvas, position); + } + + void drawLabels(Canvas canvas, Rect rect, TextStyle labelStyle) { + var colW = size.width / 13.5; + // draw x Label + var x = rect.left + 37; + for (var i = 0; i < 12; i++) { + drawText(canvas, Offset(x, height + 5), 20, labelStyle, labels[i]); + x += colW; + } + + //draw y Label + drawText(canvas, rect.bottomLeft + Offset(0, -15), 40, labelStyle, + '0.0K'); // print min value + drawText(canvas, rect.topLeft + Offset(0, size.height / 4 * 3 - 10), 40, + labelStyle, '${(maxHeight / 4 / 1000).toStringAsFixed(1)}K'); + drawText(canvas, rect.topLeft + Offset(0, size.height / 2 - 10), 40, + labelStyle, '${(maxHeight / 2 / 1000).toStringAsFixed(1)}K'); + drawText(canvas, rect.topLeft + Offset(0, size.height / 4 - 10), 40, + labelStyle, '${(maxHeight / 4 / 1000 * 3).toStringAsFixed(1)}K'); + drawText(canvas, rect.topLeft + Offset(0, 0 - 10), 40, labelStyle, + '${(maxHeight / 1000).toStringAsFixed(1)}K'); // print max value + } + + var labelStyle = TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 15, + fontWeight: FontWeight.bold, + ); + + drawLabels( + canvas, Rect.fromLTWH(0, 0, size.width, size.height), labelStyle); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/packages/chart/pubspec.yaml b/packages/chart/pubspec.yaml new file mode 100644 index 0000000..b9d62b5 --- /dev/null +++ b/packages/chart/pubspec.yaml @@ -0,0 +1,15 @@ +name: chart +description: My chart +version: 1.0.0 +publish_to: none + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/pubspec.lock b/pubspec.lock index fa18c85..2915bc6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -175,6 +175,13 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + chart: + dependency: "direct main" + description: + path: "packages/chart" + relative: true + source: path + version: "1.0.0" checked_yaml: dependency: transitive description: @@ -356,14 +363,6 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.3" - flutter_screenutil: - dependency: "direct main" - description: - name: flutter_screenutil - sha256: "1b61f8c4cbf965104b6ca7160880ff1af6755aad7fec70b58444245132453745" - url: "https://pub.dev" - source: hosted - version: "5.8.4" flutter_test: dependency: "direct dev" description: flutter @@ -374,6 +373,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: "5fb789145cae1f4c3245c58b3f8fb287d055c26323879eab57a7bf0cfd1e45f3" + url: "https://pub.dev" + source: hosted + version: "10.5.0" form_inputs: dependency: "direct main" description: @@ -1020,4 +1027,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.0 <3.0.6" - flutter: ">=3.10.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml index fcd7de5..be513fd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: budget_app description: A Budget app. -version: 2.4.0 +version: 2.5.0 publish_to: none environment: @@ -11,7 +11,6 @@ dependencies: bloc: ^8.1.0 rxdart: ^0.27.5 equatable: ^2.0.3 - flutter_screenutil: ^5.7.0 flutter: sdk: flutter flutter_bloc: ^8.1.1 @@ -21,6 +20,8 @@ dependencies: uuid: ^3.0.0 json_annotation: ^4.8.0 http: ^1.1.0 + chart: + path: packages/chart authentication_repository: path: packages/authentication_repository form_inputs: @@ -32,6 +33,7 @@ dependencies: carousel_slider: ^4.2.1 adaptive_breakpoints: ^0.1.6 fl_chart: ^0.63.0 + font_awesome_flutter: ^10.5.0 dev_dependencies: bloc_test: ^9.0.0 @@ -47,3 +49,16 @@ flutter: uses-material-design: true assets: - assets/images/ + fonts: + - family: FontAwesomeBrands + fonts: + - asset: assets/fonts/fa-brands-400.ttf + weight: 400 + - family: FontAwesomeRegular + fonts: + - asset: assets/fonts/fa-regular-400.ttf + weight: 400 + - family: FontAwesomeSolid + fonts: + - asset: assets/fonts/fa-solid-900.ttf + weight: 900