diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart index 067f6325..d1065260 100644 --- a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_bloc.dart @@ -15,7 +15,7 @@ part 'glossary_screen_state.dart'; class GlossaryBloc extends Bloc { GlossaryBloc(GlossaryState initialState) : super(initialState) { on(_onInitialize); - on(_deleteEntry); + on(_changeProductGroup); } _onInitialize( @@ -30,31 +30,39 @@ class GlossaryBloc extends Bloc { AppRoutes.authScreen, ); } - final String? glossaryID = state.glossaryModelObj?.glossaryID; - final response = await http.get( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/show/$glossaryID'), + final responseProductGroups = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/getProductGroups'), headers: { 'Authorization': 'Bearer $token', }, ); - if (response.statusCode == 200) { - final data = jsonDecode(response.body); - if (data is Map) { + if (responseProductGroups.statusCode == 200) { + final dataProductGroups = jsonDecode(responseProductGroups.body); + if (dataProductGroups is List) { if (state is GlossaryLoadedState) { var currentState = state as GlossaryLoadedState; emit(currentState.copyWith( glossaryModelObj: currentState.glossaryModelObj, - glossary: currentState.glossary)); + token: currentState.token, + productGroups: currentState.productGroups, + productList: currentState.productList)); } else { - emit(GlossaryLoadedState(glossary: data)); + List productGroupsModel = + _fillProductGroups(dataProductGroups); + emit(GlossaryLoadedState( + token: token, + productGroups: productGroupsModel, + productList: [])); + await _getProductListById(token, emit, productGroupsModel[0].id); } } else { // Один или несколько ключевых полей отсутствуют, обрабатываем ошибку - emit(GlossaryErrorState('Некоторые необходимые данные отсутствуют.')); + emit(GlossaryErrorState('Список категорий некорректного формата')); } } else { // Статус ответа не равен 200, обрабатываем ошибку - emit(GlossaryErrorState('Ошибка сервера: ${response.statusCode}')); + emit(GlossaryErrorState( + 'Ошибка сервера: ${responseProductGroups.statusCode}')); } } catch (e) { // Ошибка при декодировании JSON или другая ошибка выполнения @@ -62,27 +70,46 @@ class GlossaryBloc extends Bloc { } } - FutureOr _deleteEntry( - DeleteEntry event, Emitter emit) async { - final box = await Hive.openBox('token'); - final token = box.get('token'); - if (token == null) { - NavigatorService.popAndPushNamed( - AppRoutes.authScreen, - ); + void _changeProductGroup( + ChangeProductGroup event, Emitter emit) async { + if (state is GlossaryLoadedState) { + var currentState = state as GlossaryLoadedState; + await _getProductListById(currentState.token, emit, event.id); } - final int id = event.id; - final response = await http.post( - Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/delete/$id'), + } + + Future _getProductListById( + token, Emitter emit, int? productGroupID) async { + final responseProductList = await http.get( + Uri.parse(dotenv.env['SERVER_ROOT']! + '/glossary/index/$productGroupID'), headers: { + 'Content-Type': 'application/json', 'Authorization': 'Bearer $token', }, ); - if (response.statusCode == 200) { - NavigatorService.pushNamed(AppRoutes.glossaryScreen); + final dataProductList = jsonDecode(responseProductList.body); + if (dataProductList is List) { + var currentState = state as GlossaryLoadedState; + emit(currentState.copyWith( + glossaryModelObj: currentState.glossaryModelObj, + productGroups: currentState.productGroups, + productList: dataProductList)); } else { - // Статус ответа не равен 200, обрабатываем ошибку - emit(GlossaryErrorState('Ошибка: ${response.statusCode}')); + emit(GlossaryErrorState('Список продуктов некорректного формата')); } } + + List _fillProductGroups(List productGroups) { + List result = []; + for (int i = 0; i < productGroups.length; i++) { + result.add( + SelectionPopupModel( + id: productGroups[i]['id'], + title: productGroups[i]['name'], + isSelected: i == 0, + ), + ); + } + return result; + } } diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart index 3bc7f131..f3fb4dd2 100644 --- a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_event.dart @@ -21,3 +21,10 @@ class DeleteEntry extends GlossaryEvent { @override List get props => [id]; } + +class ChangeProductGroup extends GlossaryEvent { + ChangeProductGroup(this.id); + final int id; + @override + List get props => [id]; +} diff --git a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart index cd23333a..75363864 100644 --- a/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart +++ b/lib/presentation/glossary/glossary_screen/bloc/glossary_screen_state.dart @@ -22,17 +22,29 @@ class GlossaryState extends Equatable { } class GlossaryLoadedState extends GlossaryState { - GlossaryLoadedState({this.glossaryModelObj, this.glossary}); + GlossaryLoadedState( + {this.glossaryModelObj, + this.productGroups, + this.productList, + this.token}); GlossaryModel? glossaryModelObj; - Map? glossary; + String? token; + List? productGroups; + List? productList; @override - List get props => [glossaryModelObj, glossary]; + List get props => + [glossaryModelObj, productGroups, productList, token]; GlossaryLoadedState copyWith( - {GlossaryModel? glossaryModelObj, Map? glossary}) { + {GlossaryModel? glossaryModelObj, + String? token, + List? productGroups, + List? productList}) { return GlossaryLoadedState( glossaryModelObj: glossaryModelObj ?? this.glossaryModelObj, - glossary: glossary ?? this.glossary, + productGroups: productGroups ?? this.productGroups, + productList: productList ?? this.productList, + token: token ?? this.token, ); } } diff --git a/lib/presentation/glossary/glossary_screen/glossary_screen.dart b/lib/presentation/glossary/glossary_screen/glossary_screen.dart index d7b76a00..c805d75a 100644 --- a/lib/presentation/glossary/glossary_screen/glossary_screen.dart +++ b/lib/presentation/glossary/glossary_screen/glossary_screen.dart @@ -1,10 +1,14 @@ import 'bloc/glossary_screen_bloc.dart'; import 'models/glossary_screen_model.dart'; import 'package:eatvio_app/core/app_export.dart'; -import 'package:flutter/material.dart'; -import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:eatvio_app/widgets/custom_bottom_bar.dart'; +import 'package:eatvio_app/widgets/custom_outlined_button.dart'; +import 'package:eatvio_app/widgets/custom_drop_down.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart'; +import 'package:eatvio_app/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart'; class GlossaryScreen extends StatelessWidget { const GlossaryScreen({Key? key}) : super(key: key); @@ -42,80 +46,257 @@ class GlossaryScreen extends StatelessWidget { _buildGlossary(BuildContext context, GlossaryLoadedState state) { mediaQueryData = MediaQuery.of(context); return SafeArea( - child: Scaffold( - resizeToAvoidBottomInset: false, - body: Container( - width: double.maxFinite, - decoration: AppDecoration.fillGray, - child: ListView( + child: Scaffold( + backgroundColor: Colors.grey[200], + resizeToAvoidBottomInset: true, + body: SingleChildScrollView( + child: Container( + width: double.maxFinite, + decoration: BoxDecoration(color: Colors.grey[200]), + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(height: 30.v), - Container( - margin: EdgeInsets.only( - left: 16.h, - right: 20.h, - bottom: 10.h, - ), - padding: EdgeInsets.all(12.h), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(12)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.5), - spreadRadius: 2, - blurRadius: 5, - offset: Offset(0, 3), - ), - ], - ), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - CachedNetworkImage( - imageUrl: dotenv.env['STORAGE_URL']! + - (state.glossary?['image'] ?? ''), - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundImage: imageProvider, - radius: 25, - ), - placeholder: (context, url) => - CircularProgressIndicator(), - errorWidget: (context, url, error) => CircleAvatar( - child: Icon(Icons.person), - backgroundColor: Colors.white, - radius: 25, - ), - ), - SizedBox(height: 16.0), - Text(state.glossary?['title'], - style: CustomTextStyles.titleMediumBlack), - SizedBox(height: 8.0), - Text( - state.glossary?['description'], - ), - SizedBox(height: 16.0), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ElevatedButton( - onPressed: () => Navigator.pushNamed( - context, AppRoutes.glossaryScreen, - arguments: state.glossary?['id'].toString()), - child: Text('Редактировать'), - ), - ElevatedButton( - onPressed: () => context - .read() - .add(DeleteEntry(state.glossary?['id'])), - child: Text('Удалить'), - ), - ], - ) - ]), - ) + SizedBox(height: 20), + _buildSearch(context), + SizedBox(height: 20), + _buildMenu(context), + SizedBox(height: 20), + _buildProducts(context, state) ], ), ), - bottomNavigationBar: _buildBottomBar(context))); + ), + ), + bottomNavigationBar: _buildBottomBar(context), + ), + ); + } + + _buildSearch(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + CustomSearchView( + hintText: "Поиск записей", + ), + SizedBox(height: 10), + CustomOutlinedButton( + text: "Найти продукт", + buttonStyle: CustomButtonStyles.outlinePrimaryTL15, + ), + ], + ), + ), + ); + } + + _buildMenu(BuildContext context) { + return Container( + padding: EdgeInsets.all(12), + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Поиск по алфавиту", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox( + height: 8, + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(33, _fillAlphabet), + ), + ), + ]), + ); + } + + _buildProducts(BuildContext context, GlossaryLoadedState state) { + return Container( + padding: EdgeInsets.all(12), + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "Группа продуктов", + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + SizedBox( + height: 8, + ), + CustomDropDown( + items: state.productGroups, + defaultValue: state.productGroups?[0], + fillColor: Colors.grey[200]!, + hintText: 'Выберите категорию', + borderDecoration: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide(color: Colors.grey[200]!, width: 1), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 9, + ), + onChanged: _changeProductGroup(context, state), + width: MediaQuery.of(context) + .size + .width, // Set a percentage value as per your layout + ), + SizedBox( + height: 8, + ), + Align( + alignment: Alignment.centerLeft, + child: Text("*КБЖУ указано за 100гр")), + SizedBox( + height: 8, + ), + _buildProductList(context, state) + ]), + ); + } + + _buildProductList(BuildContext context, GlossaryLoadedState state) { + List postWidgets = []; + + if (state.productList != null) { + for (var post in state.productList!) { + postWidgets.add(_buildProductEntry(post, context)); + } + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: postWidgets, + ); + } + + _buildProductEntry(product, BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all(Radius.circular(12)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + spreadRadius: 1, + blurRadius: 3, + offset: Offset(0, 3), + ), + ], + ), + child: Column(children: [ + ListTile( + trailing: CachedNetworkImage( + imageUrl: dotenv.env['STORAGE_URL']! + (product['image'] ?? ''), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundImage: imageProvider, + radius: 25, + ), + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => CircleAvatar( + child: Icon(Icons.person), + backgroundColor: Colors.white, + radius: 25, + ), + ), + title: Text( + product?['name'] ?? '', + style: CustomTextStyles.titleMediumSemiBold, + ), + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row(children: [ + Column(children: [ + Text("Ккал - ${product['energy_on_100gr'].toString()}"), + SizedBox(height: 8), + Text("Белки - ${product['proteins_on_100gr'].toString()}") + ]), + Spacer(), + Padding( + padding: const EdgeInsets.only(right: 24.0), + child: Column(children: [ + Text("Жиры - ${product['fats_on_100gr'].toString()}"), + SizedBox(height: 8), + Text( + "Углеводы - ${product['carbohydrates_on_100gr'].toString()}") + ]), + ) + ]) + ], + ), + ), + ])); + } + + Widget _fillAlphabet(int index) { + return ScrollItem(title: String.fromCharCode(1040 + index)); + } + + List fillProductGroups(List productGroups) { + List result = []; + + for (int i = 0; i < productGroups.length; i++) { + result.add( + SelectionPopupModel( + id: productGroups[i]['id'], + title: productGroups[i]['name'], + isSelected: true, + ), + ); + } + return result; } ///Handling route based on bottom click actions @@ -145,8 +326,9 @@ class GlossaryScreen extends StatelessWidget { return CustomBottomBar(onChanged: (BottomBarEnum type) {}); } - /// Navigates to the previous screen. - onTapArrowleftone(BuildContext context) { - NavigatorService.goBack(); + _changeProductGroup(BuildContext context, GlossaryLoadedState state) { + return (value) { + context.read().add(ChangeProductGroup(value.id)); + }; } } diff --git a/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart b/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart new file mode 100644 index 00000000..2c407394 --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/widgets/glossary_scroll_item.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class ScrollItem extends StatelessWidget { + final String title; + + const ScrollItem({required this.title}); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5), + margin: EdgeInsets.only(right: 10), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(20)), + ), + child: Text( + title, + style: CustomTextStyles.titleMediumSemiBold, + ), + ); + } +} diff --git a/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart b/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart new file mode 100644 index 00000000..20cce2ed --- /dev/null +++ b/lib/presentation/glossary/glossary_screen/widgets/glossary_search_widget.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:eatvio_app/core/app_export.dart'; + +class CustomSearchView extends StatelessWidget { + CustomSearchView( + {Key? key, + this.alignment, + this.width, + this.scrollPadding, + this.controller, + this.focusNode, + this.autofocus = false, + this.textStyle, + this.textInputType = TextInputType.text, + this.maxLines, + this.hintText, + this.hintStyle, + this.prefix, + this.prefixConstraints, + this.suffix, + this.suffixConstraints, + this.contentPadding, + this.borderDecoration, + this.fillColor, + this.filled = true, + this.validator, + this.onChanged}) + : super( + key: key, + ); + + final Alignment? alignment; + + final double? width; + + final TextEditingController? scrollPadding; + + final TextEditingController? controller; + + final FocusNode? focusNode; + + final bool? autofocus; + + final TextStyle? textStyle; + + final TextInputType? textInputType; + + final int? maxLines; + + final String? hintText; + + final TextStyle? hintStyle; + + final Widget? prefix; + + final BoxConstraints? prefixConstraints; + + final Widget? suffix; + + final BoxConstraints? suffixConstraints; + + final EdgeInsets? contentPadding; + + final InputBorder? borderDecoration; + + final Color? fillColor; + + final bool? filled; + + final FormFieldValidator? validator; + + final Function(String)? onChanged; + + @override + Widget build(BuildContext context) { + return alignment != null + ? Align( + alignment: alignment ?? Alignment.center, + child: searchViewWidget(context)) + : searchViewWidget(context); + } + + searchViewWidget(BuildContext context) => SizedBox( + width: width ?? double.maxFinite, + child: TextFormField( + scrollPadding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + controller: controller, + focusNode: focusNode, + onTapOutside: (event) => onTapOutside(), + autofocus: autofocus!, + style: textStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + keyboardType: textInputType, + maxLines: maxLines ?? 1, + decoration: decoration, + validator: validator, + onChanged: (String value) => onChanged?.call(value), + ), + ); + + void onTapOutside() { + if (focusNode != null) { + focusNode?.unfocus(); + } else { + FocusManager.instance.primaryFocus?.unfocus(); + } + } + + InputDecoration get decoration => InputDecoration( + hintText: hintText ?? "", + hintStyle: hintStyle ?? CustomTextStyles.bodySmallMulishBluegray800, + prefixIcon: prefix ?? + SizedBox( + child: Padding( + padding: EdgeInsets.only(left: 10), + child: Icon(Icons.search)), + ), + prefixIconConstraints: prefixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + suffixIconConstraints: suffixConstraints ?? + BoxConstraints( + maxHeight: 40.v, + ), + isDense: true, + contentPadding: contentPadding ?? + EdgeInsets.only( + top: 11.v, + right: 11.h, + bottom: 11.v, + ), + fillColor: fillColor ?? Colors.grey[200], + filled: filled, + border: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + enabledBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + focusedBorder: borderDecoration ?? + OutlineInputBorder( + borderRadius: BorderRadius.circular(8.h), + borderSide: BorderSide.none, + ), + ); +} diff --git a/lib/widgets/custom_drop_down.dart b/lib/widgets/custom_drop_down.dart index 55f1231f..759efc1a 100644 --- a/lib/widgets/custom_drop_down.dart +++ b/lib/widgets/custom_drop_down.dart @@ -23,6 +23,7 @@ class CustomDropDown extends StatelessWidget { this.filled = true, this.validator, this.onChanged, + this.defaultValue, }) : super( key: key, ); @@ -61,6 +62,8 @@ class CustomDropDown extends StatelessWidget { final bool? filled; + final SelectionPopupModel? defaultValue; + final FormFieldValidator? validator; final Function(SelectionPopupModel)? onChanged; @@ -77,18 +80,19 @@ class CustomDropDown extends StatelessWidget { _buildDropDownWidget(BuildContext context) { return SizedBox( - width: width ?? double.maxFinite, + width: width ?? MediaQuery.of(context).size.width - 20, child: DropdownButtonFormField( focusNode: focusNode ?? FocusNode(), icon: icon, autofocus: autofocus!, + isExpanded: true, style: textStyle ?? Theme.of(context).textTheme.titleMedium, items: items?.map((SelectionPopupModel item) { return DropdownMenuItem( value: item, child: Text( item.title, - overflow: TextOverflow.ellipsis, + overflow: TextOverflow.fade, style: hintStyle ?? Theme.of(context).textTheme.titleMedium, ), );