From 803588a289be53eaaa82153b045c551f70b12a8b Mon Sep 17 00:00:00 2001 From: Username1548 Date: Fri, 14 Mar 2025 19:56:55 +0700 Subject: [PATCH] chats have been updated to work with consultatons --- lib/core/const.dart | 2 +- lib/core/theme/theme_helper.dart | 1 + lib/core/widgets/item_widget.dart | 47 ++ .../auth/presentation/bloc/auth_cubit.dart | 8 +- .../auth/presentation/pages/auth_screen.dart | 12 +- lib/features/chat/models/chat_room_model.dart | 21 +- .../chat/models/chat_room_model.freezed.dart | 267 +++++++++-- .../chat/models/chat_room_model.g.dart | 20 +- .../models/chat_room_pagination_model.dart | 2 + .../chat_room_pagination_model.freezed.dart | 62 ++- .../models/chat_room_pagination_model.g.dart | 2 + .../bloc/chat_room_cubit/chat_room_cubit.dart | 13 +- .../chat/presentation/pages/chat_screen.dart | 442 +++++++++++------- .../chat/presentation/widgets/order_card.dart | 132 +++++- .../chat/repositories/chat_repository.dart | 10 + lib/features/chat/services/chat_service.dart | 12 +- .../chat/services/chat_service.g.dart | 19 + .../menu/presentation/pages/menu_screen.dart | 4 +- .../plans/models/chosen_plan_model.dart | 11 +- .../models/chosen_plan_model.freezed.dart | 78 +++- .../plans/models/chosen_plan_model.g.dart | 4 + .../presentation/widgets/item_widget.dart | 27 -- .../presentation/widgets/option_widget.dart | 2 +- lib/generated/intl/messages_en.dart | 5 + lib/generated/l10n.dart | 30 ++ lib/l10n/intl_en.arb | 5 +- 26 files changed, 957 insertions(+), 281 deletions(-) create mode 100644 lib/core/widgets/item_widget.dart delete mode 100644 lib/features/plans/presentation/widgets/item_widget.dart diff --git a/lib/core/const.dart b/lib/core/const.dart index 68cdc249..4f38a54e 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ // String baseUrl = 'https://eatvio.ru'; -String baseUrl = 'http://10.55.5.123:8080'; +String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/theme/theme_helper.dart b/lib/core/theme/theme_helper.dart index 2c319dd8..d4326bf4 100644 --- a/lib/core/theme/theme_helper.dart +++ b/lib/core/theme/theme_helper.dart @@ -184,6 +184,7 @@ class PrimaryColors { Color get dottedBorder => Color(0xFFE6E6E6); Color get red => Color(0xFFCD4141); + Color get strongRed => Color(0xFFD01717); Color get transparentRed => Color(0xFFCD4141).withValues(alpha: 0.75); diff --git a/lib/core/widgets/item_widget.dart b/lib/core/widgets/item_widget.dart new file mode 100644 index 00000000..c7af63c5 --- /dev/null +++ b/lib/core/widgets/item_widget.dart @@ -0,0 +1,47 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class ItemWidget extends StatelessWidget { + const ItemWidget({super.key, required this.text, this.reversed = false, this.iconSize = 20}); + final String text; + final bool reversed; + final double iconSize; + @override + Widget build(BuildContext context) { + if (reversed) { + return Row( + children: [ + SvgPicture.asset( + 'assets/icons/ok.svg', + width: iconSize, + height: iconSize, + ), + SizedBox(width: 8), + Expanded( + child: Text( + text, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + ], + ); + } + return Row( + children: [ + Expanded( + child: Text( + text, + style: TextStyles.textMMedium.copyWith(color: appTheme.black), + ), + ), + SvgPicture.asset( + 'assets/icons/ok.svg', + width: iconSize, + height: iconSize, + ), + ], + ); + } +} diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index da1c3fef..dc521829 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -1,10 +1,8 @@ import 'package:eatvio_app/core/app_router.dart'; -import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; -import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -15,7 +13,7 @@ class AuthCubit extends Cubit { required this.authRepository, }) : super(AuthState()); - getTokenByCredentials(String email, String password) async { + Future getTokenByCredentials(String email, String password) async { emit(AuthState.loading()); try { final token = (await authRepository.getTokenByCredentials( @@ -23,7 +21,6 @@ class AuthCubit extends Cubit { password, )) .token; - print(token); emit( AuthState.loaded( token: token, @@ -31,12 +28,13 @@ class AuthCubit extends Cubit { ); showSuccessToast('Успешный вход'); await TokenStorage.saveTokenInStorage(token); - await sl().getMyProfile(); AppRouter.navigatorKey.currentContext?.go('/my-profile-screen'); + return true; } catch (e) { print(e); showErrorToast('Неверный адрес электронной почты или пароль'); emit(AuthState.error(errorMessage: '$e')); + return false; } } } diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 818578ae..3ba8097e 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -4,6 +4,8 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/widgets/custom_text_form_field.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; @@ -140,10 +142,16 @@ class _AuthScreenState extends State { backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400, onPressed: state is AuthStateLoading ? null - : () => authCubit.getTokenByCredentials( + : () async { + final isAuthenticated = await authCubit.getTokenByCredentials( emailController.text, passwordController.text, - ), + ); + if (isAuthenticated) { + context.read().getMyProfile(); + context.read().getChatRoomsWithLoading(); + } + }, label: state is AuthStateLoading ? CupertinoActivityIndicator() : Text( diff --git a/lib/features/chat/models/chat_room_model.dart b/lib/features/chat/models/chat_room_model.dart index 13c12d0e..9d77fdf0 100644 --- a/lib/features/chat/models/chat_room_model.dart +++ b/lib/features/chat/models/chat_room_model.dart @@ -11,16 +11,23 @@ class ChatRoomModel with _$ChatRoomModel { const factory ChatRoomModel({ required String id, @JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings, - required Map users, - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) required ChosenPlanModel? chosenPlan, + required Users users, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) required ChosenPlanModel? chosenPlan, }) = _ChatRoomModel; factory ChatRoomModel.fromJson(Map json) => _$ChatRoomModelFromJson(json); } -ChosenPlanModel? _parseChosenPlan(json) { - if (json is Map) { - return ChosenPlanModel.fromJson(json); - } - return null; +@freezed +class Users with _$Users { + const factory Users({ + + @JsonKey(name: 'from_chat_user') required ChatUserModel sender, + @JsonKey(name: 'to_chat_user') required ChatUserModel receiver, + + }) = _Users; + + factory Users.fromJson(Map json) => _$UsersFromJson(json); } + + diff --git a/lib/features/chat/models/chat_room_model.freezed.dart b/lib/features/chat/models/chat_room_model.freezed.dart index 7f46b373..829dd69f 100644 --- a/lib/features/chat/models/chat_room_model.freezed.dart +++ b/lib/features/chat/models/chat_room_model.freezed.dart @@ -24,8 +24,8 @@ mixin _$ChatRoomModel { @JsonKey(name: 'chat_room_setting') ChatRoomSettingsModel get chatRoomSettings => throw _privateConstructorUsedError; - Map get users => throw _privateConstructorUsedError; - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) + Users get users => throw _privateConstructorUsedError; + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) ChosenPlanModel? get chosenPlan => throw _privateConstructorUsedError; /// Serializes this ChatRoomModel to a JSON map. @@ -48,11 +48,12 @@ abstract class $ChatRoomModelCopyWith<$Res> { {String id, @JsonKey(name: 'chat_room_setting') ChatRoomSettingsModel chatRoomSettings, - Map users, - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) + Users users, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) ChosenPlanModel? chosenPlan}); $ChatRoomSettingsModelCopyWith<$Res> get chatRoomSettings; + $UsersCopyWith<$Res> get users; $ChosenPlanModelCopyWith<$Res>? get chosenPlan; } @@ -88,7 +89,7 @@ class _$ChatRoomModelCopyWithImpl<$Res, $Val extends ChatRoomModel> users: null == users ? _value.users : users // ignore: cast_nullable_to_non_nullable - as Map, + as Users, chosenPlan: freezed == chosenPlan ? _value.chosenPlan : chosenPlan // ignore: cast_nullable_to_non_nullable @@ -107,6 +108,16 @@ class _$ChatRoomModelCopyWithImpl<$Res, $Val extends ChatRoomModel> }); } + /// Create a copy of ChatRoomModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UsersCopyWith<$Res> get users { + return $UsersCopyWith<$Res>(_value.users, (value) { + return _then(_value.copyWith(users: value) as $Val); + }); + } + /// Create a copy of ChatRoomModel /// with the given fields replaced by the non-null parameter values. @override @@ -134,13 +145,15 @@ abstract class _$$ChatRoomModelImplCopyWith<$Res> {String id, @JsonKey(name: 'chat_room_setting') ChatRoomSettingsModel chatRoomSettings, - Map users, - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) + Users users, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) ChosenPlanModel? chosenPlan}); @override $ChatRoomSettingsModelCopyWith<$Res> get chatRoomSettings; @override + $UsersCopyWith<$Res> get users; + @override $ChosenPlanModelCopyWith<$Res>? get chosenPlan; } @@ -172,9 +185,9 @@ class __$$ChatRoomModelImplCopyWithImpl<$Res> : chatRoomSettings // ignore: cast_nullable_to_non_nullable as ChatRoomSettingsModel, users: null == users - ? _value._users + ? _value.users : users // ignore: cast_nullable_to_non_nullable - as Map, + as Users, chosenPlan: freezed == chosenPlan ? _value.chosenPlan : chosenPlan // ignore: cast_nullable_to_non_nullable @@ -189,10 +202,9 @@ class _$ChatRoomModelImpl implements _ChatRoomModel { const _$ChatRoomModelImpl( {required this.id, @JsonKey(name: 'chat_room_setting') required this.chatRoomSettings, - required final Map users, - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) - required this.chosenPlan}) - : _users = users; + required this.users, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + required this.chosenPlan}); factory _$ChatRoomModelImpl.fromJson(Map json) => _$$ChatRoomModelImplFromJson(json); @@ -202,16 +214,10 @@ class _$ChatRoomModelImpl implements _ChatRoomModel { @override @JsonKey(name: 'chat_room_setting') final ChatRoomSettingsModel chatRoomSettings; - final Map _users; @override - Map get users { - if (_users is EqualUnmodifiableMapView) return _users; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_users); - } - + final Users users; @override - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) final ChosenPlanModel? chosenPlan; @override @@ -227,15 +233,15 @@ class _$ChatRoomModelImpl implements _ChatRoomModel { (identical(other.id, id) || other.id == id) && (identical(other.chatRoomSettings, chatRoomSettings) || other.chatRoomSettings == chatRoomSettings) && - const DeepCollectionEquality().equals(other._users, _users) && + (identical(other.users, users) || other.users == users) && (identical(other.chosenPlan, chosenPlan) || other.chosenPlan == chosenPlan)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, chatRoomSettings, - const DeepCollectionEquality().hash(_users), chosenPlan); + int get hashCode => + Object.hash(runtimeType, id, chatRoomSettings, users, chosenPlan); /// Create a copy of ChatRoomModel /// with the given fields replaced by the non-null parameter values. @@ -258,8 +264,8 @@ abstract class _ChatRoomModel implements ChatRoomModel { {required final String id, @JsonKey(name: 'chat_room_setting') required final ChatRoomSettingsModel chatRoomSettings, - required final Map users, - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) + required final Users users, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) required final ChosenPlanModel? chosenPlan}) = _$ChatRoomModelImpl; factory _ChatRoomModel.fromJson(Map json) = @@ -271,9 +277,9 @@ abstract class _ChatRoomModel implements ChatRoomModel { @JsonKey(name: 'chat_room_setting') ChatRoomSettingsModel get chatRoomSettings; @override - Map get users; + Users get users; @override - @JsonKey(name: 'chosen_plan', fromJson: _parseChosenPlan) + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) ChosenPlanModel? get chosenPlan; /// Create a copy of ChatRoomModel @@ -283,3 +289,208 @@ abstract class _ChatRoomModel implements ChatRoomModel { _$$ChatRoomModelImplCopyWith<_$ChatRoomModelImpl> get copyWith => throw _privateConstructorUsedError; } + +Users _$UsersFromJson(Map json) { + return _Users.fromJson(json); +} + +/// @nodoc +mixin _$Users { + @JsonKey(name: 'from_chat_user') + ChatUserModel get sender => throw _privateConstructorUsedError; + @JsonKey(name: 'to_chat_user') + ChatUserModel get receiver => throw _privateConstructorUsedError; + + /// Serializes this Users to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UsersCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UsersCopyWith<$Res> { + factory $UsersCopyWith(Users value, $Res Function(Users) then) = + _$UsersCopyWithImpl<$Res, Users>; + @useResult + $Res call( + {@JsonKey(name: 'from_chat_user') ChatUserModel sender, + @JsonKey(name: 'to_chat_user') ChatUserModel receiver}); + + $ChatUserModelCopyWith<$Res> get sender; + $ChatUserModelCopyWith<$Res> get receiver; +} + +/// @nodoc +class _$UsersCopyWithImpl<$Res, $Val extends Users> + implements $UsersCopyWith<$Res> { + _$UsersCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? sender = null, + Object? receiver = null, + }) { + return _then(_value.copyWith( + sender: null == sender + ? _value.sender + : sender // ignore: cast_nullable_to_non_nullable + as ChatUserModel, + receiver: null == receiver + ? _value.receiver + : receiver // ignore: cast_nullable_to_non_nullable + as ChatUserModel, + ) as $Val); + } + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChatUserModelCopyWith<$Res> get sender { + return $ChatUserModelCopyWith<$Res>(_value.sender, (value) { + return _then(_value.copyWith(sender: value) as $Val); + }); + } + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChatUserModelCopyWith<$Res> get receiver { + return $ChatUserModelCopyWith<$Res>(_value.receiver, (value) { + return _then(_value.copyWith(receiver: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$UsersImplCopyWith<$Res> implements $UsersCopyWith<$Res> { + factory _$$UsersImplCopyWith( + _$UsersImpl value, $Res Function(_$UsersImpl) then) = + __$$UsersImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'from_chat_user') ChatUserModel sender, + @JsonKey(name: 'to_chat_user') ChatUserModel receiver}); + + @override + $ChatUserModelCopyWith<$Res> get sender; + @override + $ChatUserModelCopyWith<$Res> get receiver; +} + +/// @nodoc +class __$$UsersImplCopyWithImpl<$Res> + extends _$UsersCopyWithImpl<$Res, _$UsersImpl> + implements _$$UsersImplCopyWith<$Res> { + __$$UsersImplCopyWithImpl( + _$UsersImpl _value, $Res Function(_$UsersImpl) _then) + : super(_value, _then); + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? sender = null, + Object? receiver = null, + }) { + return _then(_$UsersImpl( + sender: null == sender + ? _value.sender + : sender // ignore: cast_nullable_to_non_nullable + as ChatUserModel, + receiver: null == receiver + ? _value.receiver + : receiver // ignore: cast_nullable_to_non_nullable + as ChatUserModel, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$UsersImpl implements _Users { + const _$UsersImpl( + {@JsonKey(name: 'from_chat_user') required this.sender, + @JsonKey(name: 'to_chat_user') required this.receiver}); + + factory _$UsersImpl.fromJson(Map json) => + _$$UsersImplFromJson(json); + + @override + @JsonKey(name: 'from_chat_user') + final ChatUserModel sender; + @override + @JsonKey(name: 'to_chat_user') + final ChatUserModel receiver; + + @override + String toString() { + return 'Users(sender: $sender, receiver: $receiver)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UsersImpl && + (identical(other.sender, sender) || other.sender == sender) && + (identical(other.receiver, receiver) || + other.receiver == receiver)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, sender, receiver); + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UsersImplCopyWith<_$UsersImpl> get copyWith => + __$$UsersImplCopyWithImpl<_$UsersImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UsersImplToJson( + this, + ); + } +} + +abstract class _Users implements Users { + const factory _Users( + {@JsonKey(name: 'from_chat_user') required final ChatUserModel sender, + @JsonKey(name: 'to_chat_user') + required final ChatUserModel receiver}) = _$UsersImpl; + + factory _Users.fromJson(Map json) = _$UsersImpl.fromJson; + + @override + @JsonKey(name: 'from_chat_user') + ChatUserModel get sender; + @override + @JsonKey(name: 'to_chat_user') + ChatUserModel get receiver; + + /// Create a copy of Users + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UsersImplCopyWith<_$UsersImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/chat/models/chat_room_model.g.dart b/lib/features/chat/models/chat_room_model.g.dart index 834f2d0e..fb9e6257 100644 --- a/lib/features/chat/models/chat_room_model.g.dart +++ b/lib/features/chat/models/chat_room_model.g.dart @@ -11,11 +11,8 @@ _$ChatRoomModelImpl _$$ChatRoomModelImplFromJson(Map json) => id: json['id'] as String, chatRoomSettings: ChatRoomSettingsModel.fromJson( json['chat_room_setting'] as Map), - users: (json['users'] as Map).map( - (k, e) => - MapEntry(k, ChatUserModel.fromJson(e as Map)), - ), - chosenPlan: _parseChosenPlan(json['chosen_plan']), + users: Users.fromJson(json['users'] as Map), + chosenPlan: parseChosenPlan(json['chosen_plan']), ); Map _$$ChatRoomModelImplToJson(_$ChatRoomModelImpl instance) => @@ -25,3 +22,16 @@ Map _$$ChatRoomModelImplToJson(_$ChatRoomModelImpl instance) => 'users': instance.users, 'chosen_plan': instance.chosenPlan, }; + +_$UsersImpl _$$UsersImplFromJson(Map json) => _$UsersImpl( + sender: ChatUserModel.fromJson( + json['from_chat_user'] as Map), + receiver: + ChatUserModel.fromJson(json['to_chat_user'] as Map), + ); + +Map _$$UsersImplToJson(_$UsersImpl instance) => + { + 'from_chat_user': instance.sender, + 'to_chat_user': instance.receiver, + }; diff --git a/lib/features/chat/models/chat_room_pagination_model.dart b/lib/features/chat/models/chat_room_pagination_model.dart index 6d44a685..2d7e875d 100644 --- a/lib/features/chat/models/chat_room_pagination_model.dart +++ b/lib/features/chat/models/chat_room_pagination_model.dart @@ -1,4 +1,5 @@ import 'package:eatvio_app/features/chat/models/chat_message_model.dart'; +import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'chat_room_pagination_model.freezed.dart'; @@ -33,6 +34,7 @@ class ChatRoom with _$ChatRoom { @JsonKey(name: "last_message", fromJson: parseLastMessage) required ChatMessageModel? lastMessage, @JsonKey(name: "users") required Users users, @JsonKey(name: "unread_messages_count") required int unreadMessagesCount, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) required ChosenPlanModel? chosenPlan, }) = _ChatRoom; factory ChatRoom.fromJson(Map json) => _$ChatRoomFromJson(json); diff --git a/lib/features/chat/models/chat_room_pagination_model.freezed.dart b/lib/features/chat/models/chat_room_pagination_model.freezed.dart index 42d8ef5f..54e56ee3 100644 --- a/lib/features/chat/models/chat_room_pagination_model.freezed.dart +++ b/lib/features/chat/models/chat_room_pagination_model.freezed.dart @@ -445,6 +445,8 @@ mixin _$ChatRoom { Users get users => throw _privateConstructorUsedError; @JsonKey(name: "unread_messages_count") int get unreadMessagesCount => throw _privateConstructorUsedError; + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + ChosenPlanModel? get chosenPlan => throw _privateConstructorUsedError; /// Serializes this ChatRoom to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -467,10 +469,13 @@ abstract class $ChatRoomCopyWith<$Res> { @JsonKey(name: "last_message", fromJson: parseLastMessage) ChatMessageModel? lastMessage, @JsonKey(name: "users") Users users, - @JsonKey(name: "unread_messages_count") int unreadMessagesCount}); + @JsonKey(name: "unread_messages_count") int unreadMessagesCount, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + ChosenPlanModel? chosenPlan}); $ChatMessageModelCopyWith<$Res>? get lastMessage; $UsersCopyWith<$Res> get users; + $ChosenPlanModelCopyWith<$Res>? get chosenPlan; } /// @nodoc @@ -493,6 +498,7 @@ class _$ChatRoomCopyWithImpl<$Res, $Val extends ChatRoom> Object? lastMessage = freezed, Object? users = null, Object? unreadMessagesCount = null, + Object? chosenPlan = freezed, }) { return _then(_value.copyWith( id: null == id @@ -515,6 +521,10 @@ class _$ChatRoomCopyWithImpl<$Res, $Val extends ChatRoom> ? _value.unreadMessagesCount : unreadMessagesCount // ignore: cast_nullable_to_non_nullable as int, + chosenPlan: freezed == chosenPlan + ? _value.chosenPlan + : chosenPlan // ignore: cast_nullable_to_non_nullable + as ChosenPlanModel?, ) as $Val); } @@ -541,6 +551,20 @@ class _$ChatRoomCopyWithImpl<$Res, $Val extends ChatRoom> return _then(_value.copyWith(users: value) as $Val); }); } + + /// Create a copy of ChatRoom + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $ChosenPlanModelCopyWith<$Res>? get chosenPlan { + if (_value.chosenPlan == null) { + return null; + } + + return $ChosenPlanModelCopyWith<$Res>(_value.chosenPlan!, (value) { + return _then(_value.copyWith(chosenPlan: value) as $Val); + }); + } } /// @nodoc @@ -557,12 +581,16 @@ abstract class _$$ChatRoomImplCopyWith<$Res> @JsonKey(name: "last_message", fromJson: parseLastMessage) ChatMessageModel? lastMessage, @JsonKey(name: "users") Users users, - @JsonKey(name: "unread_messages_count") int unreadMessagesCount}); + @JsonKey(name: "unread_messages_count") int unreadMessagesCount, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + ChosenPlanModel? chosenPlan}); @override $ChatMessageModelCopyWith<$Res>? get lastMessage; @override $UsersCopyWith<$Res> get users; + @override + $ChosenPlanModelCopyWith<$Res>? get chosenPlan; } /// @nodoc @@ -583,6 +611,7 @@ class __$$ChatRoomImplCopyWithImpl<$Res> Object? lastMessage = freezed, Object? users = null, Object? unreadMessagesCount = null, + Object? chosenPlan = freezed, }) { return _then(_$ChatRoomImpl( id: null == id @@ -605,6 +634,10 @@ class __$$ChatRoomImplCopyWithImpl<$Res> ? _value.unreadMessagesCount : unreadMessagesCount // ignore: cast_nullable_to_non_nullable as int, + chosenPlan: freezed == chosenPlan + ? _value.chosenPlan + : chosenPlan // ignore: cast_nullable_to_non_nullable + as ChosenPlanModel?, )); } } @@ -618,8 +651,9 @@ class _$ChatRoomImpl implements _ChatRoom { @JsonKey(name: "last_message", fromJson: parseLastMessage) required this.lastMessage, @JsonKey(name: "users") required this.users, - @JsonKey(name: "unread_messages_count") - required this.unreadMessagesCount}); + @JsonKey(name: "unread_messages_count") required this.unreadMessagesCount, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + required this.chosenPlan}); factory _$ChatRoomImpl.fromJson(Map json) => _$$ChatRoomImplFromJson(json); @@ -639,10 +673,13 @@ class _$ChatRoomImpl implements _ChatRoom { @override @JsonKey(name: "unread_messages_count") final int unreadMessagesCount; + @override + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + final ChosenPlanModel? chosenPlan; @override String toString() { - return 'ChatRoom(id: $id, roomName: $roomName, lastMessage: $lastMessage, users: $users, unreadMessagesCount: $unreadMessagesCount)'; + return 'ChatRoom(id: $id, roomName: $roomName, lastMessage: $lastMessage, users: $users, unreadMessagesCount: $unreadMessagesCount, chosenPlan: $chosenPlan)'; } @override @@ -657,13 +694,15 @@ class _$ChatRoomImpl implements _ChatRoom { other.lastMessage == lastMessage) && (identical(other.users, users) || other.users == users) && (identical(other.unreadMessagesCount, unreadMessagesCount) || - other.unreadMessagesCount == unreadMessagesCount)); + other.unreadMessagesCount == unreadMessagesCount) && + (identical(other.chosenPlan, chosenPlan) || + other.chosenPlan == chosenPlan)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, id, roomName, lastMessage, users, unreadMessagesCount); + int get hashCode => Object.hash(runtimeType, id, roomName, lastMessage, users, + unreadMessagesCount, chosenPlan); /// Create a copy of ChatRoom /// with the given fields replaced by the non-null parameter values. @@ -689,7 +728,9 @@ abstract class _ChatRoom implements ChatRoom { required final ChatMessageModel? lastMessage, @JsonKey(name: "users") required final Users users, @JsonKey(name: "unread_messages_count") - required final int unreadMessagesCount}) = _$ChatRoomImpl; + required final int unreadMessagesCount, + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + required final ChosenPlanModel? chosenPlan}) = _$ChatRoomImpl; factory _ChatRoom.fromJson(Map json) = _$ChatRoomImpl.fromJson; @@ -709,6 +750,9 @@ abstract class _ChatRoom implements ChatRoom { @override @JsonKey(name: "unread_messages_count") int get unreadMessagesCount; + @override + @JsonKey(name: 'chosen_plan', fromJson: parseChosenPlan) + ChosenPlanModel? get chosenPlan; /// Create a copy of ChatRoom /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/chat/models/chat_room_pagination_model.g.dart b/lib/features/chat/models/chat_room_pagination_model.g.dart index 3b9ac87b..d6f6e057 100644 --- a/lib/features/chat/models/chat_room_pagination_model.g.dart +++ b/lib/features/chat/models/chat_room_pagination_model.g.dart @@ -43,6 +43,7 @@ _$ChatRoomImpl _$$ChatRoomImplFromJson(Map json) => lastMessage: parseLastMessage(json['last_message']), users: Users.fromJson(json['users'] as Map), unreadMessagesCount: (json['unread_messages_count'] as num).toInt(), + chosenPlan: parseChosenPlan(json['chosen_plan']), ); Map _$$ChatRoomImplToJson(_$ChatRoomImpl instance) => @@ -52,6 +53,7 @@ Map _$$ChatRoomImplToJson(_$ChatRoomImpl instance) => 'last_message': instance.lastMessage, 'users': instance.users, 'unread_messages_count': instance.unreadMessagesCount, + 'chosen_plan': instance.chosenPlan, }; _$UsersImpl _$$UsersImplFromJson(Map json) => _$UsersImpl( diff --git a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart index 3ba4422c..1e456fc6 100644 --- a/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart +++ b/lib/features/chat/presentation/bloc/chat_room_cubit/chat_room_cubit.dart @@ -86,11 +86,12 @@ class ChatRoomCubit extends Cubit { } } - Future updateChatRoom(String chatId) async{ + Future cancelChosenPlan({required String chatId, required int planId}) async { try { - final chatRoom = await chatRepository.getChatRoom(chatId); + await chatRepository.cancelChosenPlan(id: planId); + final chatRoom = await chatRepository.getChatRoom(chatId); emit( - (state as ChatRoomStateLoaded).copyWith(chatRoom: chatRoom), + (state as ChatRoomStateLoaded).copyWith(chatRoom: chatRoom), ); } catch (e) { emit(ChatRoomState.error(errorMessage: 'Ошибка получеия данных')); @@ -104,7 +105,7 @@ class ChatRoomCubit extends Cubit { if (client == null) return; await wsManager.initPrivateDirectChatChannel(chatId); wsManager.subscribeDirectChatChannel(); - wsManager.directChatStream?.listen((event) { + messagesSubscription = wsManager.directChatStream?.listen((event) { AudioPlayer().play((AssetSource('audio/recive_message.wav'))); final message = ChatMessageModel.fromJson(jsonDecode(event.data)['message']); List messagesList = [ @@ -122,7 +123,7 @@ class ChatRoomCubit extends Cubit { ); }); - wsManager.clientTypingStream?.listen((event) { + typingSubscription = wsManager.clientTypingStream?.listen((event) { emit( ChatRoomState.loaded( chatRoom: (state as ChatRoomStateLoaded).chatRoom, @@ -191,6 +192,8 @@ class ChatRoomCubit extends Cubit { @override Future close() async { wsManager.unsubscribeDirectChatChannel(); + messagesSubscription?.cancel(); + typingSubscription?.cancel(); super.close(); } } diff --git a/lib/features/chat/presentation/pages/chat_screen.dart b/lib/features/chat/presentation/pages/chat_screen.dart index 1d0b8b0c..c39947b0 100644 --- a/lib/features/chat/presentation/pages/chat_screen.dart +++ b/lib/features/chat/presentation/pages/chat_screen.dart @@ -11,9 +11,13 @@ import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/widgets/order_card.dart'; +import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; import 'package:visibility_detector/visibility_detector.dart'; @@ -34,6 +38,7 @@ class _ChatScreenState extends State { final ChatRoomCubit chatRoomCubit = sl(); final List<({int id, int index})> unreadMessagesIds = []; final List<({int id, int index})> unreadMessagesIdsCopy = []; + final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates(); @override void initState() { @@ -51,6 +56,7 @@ class _ChatScreenState extends State { @override void dispose() { scrollController.dispose(); + chatRoomCubit.close(); super.dispose(); } @@ -78,6 +84,9 @@ class _ChatScreenState extends State { } _buildChat(BuildContext context, ChatRoomStateLoaded state) { + final chosenPlan = state.chatRoom.chosenPlan; + final amIConsultant = chosenPlan?.consultantId == state.chatRoom.users.sender.id; + final showAboutOrderButton = showableChatBadgeStates.contains(chosenPlan?.chosenPlanState); return Scaffold( resizeToAvoidBottomInset: true, appBar: AppBar( @@ -86,12 +95,12 @@ class _ChatScreenState extends State { centerTitle: false, title: Row( children: [ - if (state.chatRoom.users['to_chat_user']!.profilePhotoUrl != null) + if (state.chatRoom.users.receiver.profilePhotoUrl != null) CircleAvatar( radius: 20, - foregroundImage: NetworkImage(state.chatRoom.users['to_chat_user']!.profilePhotoUrl!), + foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!), ), - if (state.chatRoom.users['to_chat_user']!.profilePhotoUrl == null) + if (state.chatRoom.users.receiver.profilePhotoUrl == null) Container( width: 40, height: 40, @@ -101,7 +110,7 @@ class _ChatScreenState extends State { shape: BoxShape.circle, ), child: Text( - state.chatRoom.users['to_chat_user']!.name.split(' ').first[0] + state.chatRoom.users['to_chat_user']!.name.split(' ')[1][0], + state.chatRoom.users.receiver.name.split(' ').first[0] + state.chatRoom.users.receiver.name.split(' ')[1][0], style: TextStyle( color: Color(0xFF7F9CF5), fontWeight: FontWeight.w400, @@ -114,7 +123,7 @@ class _ChatScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - state.chatRoom.users['to_chat_user']!.name, + state.chatRoom.users.receiver.name, style: CustomTextStyles.titleSmallBold.copyWith( color: PrimaryColors().black, ), @@ -136,28 +145,38 @@ class _ChatScreenState extends State { ), ), body: SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, + child: Stack( children: [ - Expanded( - child: Container( - color: appTheme.whiteA700, - width: double.maxFinite, - child: CustomScrollView( - physics: BouncingScrollPhysics(), - controller: scrollController, - reverse: true, - slivers: [ - SliverPadding( - padding: EdgeInsets.symmetric(vertical: 16), - sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index < state.chatMessages.length) { + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Container( + color: appTheme.whiteA700, + width: double.maxFinite, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + controller: scrollController, + reverse: true, + child: Column( + children: [ + SizedBox(height: 16), + ListView.builder( + physics: NeverScrollableScrollPhysics(), + reverse: true, + shrinkWrap: true, + itemBuilder: (context, index) { + if (index == state.chatMessages.length) { + return Padding( + padding: const EdgeInsets.all(8), + child: const Loader(), + ); + } + if ((index + 1 != state.chatMessages.length && state.chatMessages[index].updatedAt.day != state.chatMessages[index + 1].updatedAt.day) || index + 1 == state.chatMessages.length) { - final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId; + final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; return Column( children: [ _messageDate(state.chatMessages[index].updatedAt), @@ -191,7 +210,7 @@ class _ChatScreenState extends State { ], ); } - final isCurrentUserAuthor = state.chatRoom.users['to_chat_user']!.id != state.chatMessages[index].authorId; + final isCurrentUserAuthor = state.chatRoom.users.receiver.id != state.chatMessages[index].authorId; return VisibilityDetector( key: Key(state.chatMessages[index].id.toString()), onVisibilityChanged: (VisibilityInfo info) { @@ -217,157 +236,238 @@ class _ChatScreenState extends State { mediaData: state.chatMessages[index].mediaData ?? [], ), ); - } else { - return Padding( - padding: const EdgeInsets.all(8), - child: const Loader(), - ); - } - }, - childCount: state.chatMessages.length + (state.isLoading ? 1 : 0), - ), + }, + itemCount: state.chatMessages.length + (state.isLoading ? 1 : 0), + ), + SizedBox(height: 16), + if (chosenPlan != null) + OrderCard( + plan: chosenPlan.plan, + isUserView: !amIConsultant, + optionType: chosenPlan.optionType, + onCancel: () async { + await context.read().cancelChosenPlan(chatId: state.chatRoom.id, planId: chosenPlan.id); + }, + ), + // switch (chosenPlan.chosenPlanState) { + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateInited => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateStartDateHasBeenSet => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateInCoordination => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateStarted => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateInProgress => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.statePendingPayment => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateConfirmedByConsultant => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.statePaid => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateCanceledByTheInitiator => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateLastDayStarted => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateLastDayFinished => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateComplained => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateDone => throw UnimplementedError(), + // // TODO: Handle this case. + // ChosenPlanStateEnum.stateChargeWasCollected => throw UnimplementedError(), + // }, + ], ), ), - ], + ), + ), + Container( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + SizedBox(width: 8), + InkWell( + onTap: () async { + FilePickerResult? result = await FilePicker.platform.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: [ + 'pdf', + 'doc', + 'docx', + 'json', + 'ppt', + 'pptx', + 'rtf', + 'txt', + 'xls', + 'xlsx', + 'csv', + 'jpeg', + 'jpg', + 'png', + 'gif', + 'svg', + 'xml', + 'webp', + ], + ); + + if (result != null) { + List files = result.paths.map((path) => File(path!)).toList(); + await chatRoomCubit.sendMessage( + chatId: widget.chatId, + files: files, + ); + } else { + // User canceled the picker + } + }, + customBorder: CircleBorder(), + child: Ink( + height: 40, + width: 40, + child: Transform.rotate( + angle: pi / 4, + child: Icon( + Icons.attach_file_rounded, + size: 30, + color: Color(0xFF858E99), + ), + ), + ), + ), + SizedBox(width: 8), + Expanded( + child: SizedBox( + height: 40, + child: TextFormField( + expands: true, + controller: textEditingController, + maxLines: null, + minLines: null, + keyboardType: TextInputType.multiline, + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), + hintText: 'Сообщение', + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1, + color: Color(0xFFAEAEB2), + ), + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(100), + borderSide: BorderSide( + color: Color(0xFFD1D1D6), + width: 1, + ), + ), + ), + ), + ), + ), + SizedBox(width: 8), + InkWell( + customBorder: CircleBorder(), + onTap: () { + if (textEditingController.text.isNotEmpty) { + chatRoomCubit.sendMessage( + chatId: widget.chatId, + data: textEditingController.text.trim(), + ); + setState(() { + textEditingController.clear(); + }); + } + }, + child: Ink( + decoration: BoxDecoration( + color: appTheme.blueA400, + shape: BoxShape.circle, + ), + height: 40, + width: 40, + child: Icon( + Icons.arrow_upward_rounded, + size: 24, + color: Colors.white, + ), + ), + ), + SizedBox(width: 8), + ], + ), + ), + ], + ), + if (showAboutOrderButton) + Positioned( + top: 8, + right: 15, + left: 16, + child: GestureDetector( + onTap: () {}, + child: Container( + height: 44, + decoration: BoxDecoration( + color: appTheme.gray50, + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + Text( + S.current.more_about_order, + style: TextStyles.textSMedium.copyWith(color: appTheme.black), + ), + Container( + margin: EdgeInsets.only(right: 4), + width: 36, + height: 36, + decoration: BoxDecoration( + color: appTheme.whiteA700, + shape: BoxShape.circle, + ), + child: Center( + child: SvgPicture.asset( + 'assets/icons/right_arrow.svg', + height: 14, + width: 8, + colorFilter: ColorFilter.mode( + appTheme.blueA400, + BlendMode.srcIn, + ), + ), + ), + ), + ], + ), + ), ), ), - ), - Container( - padding: EdgeInsets.symmetric(vertical: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - SizedBox(width: 8), - InkWell( - onTap: () async { - FilePickerResult? result = await FilePicker.platform.pickFiles( - allowMultiple: true, - type: FileType.custom, - allowedExtensions: [ - 'pdf', - 'doc', - 'docx', - 'json', - 'ppt', - 'pptx', - 'rtf', - 'txt', - 'xls', - 'xlsx', - 'csv', - 'jpeg', - 'jpg', - 'png', - 'gif', - 'svg', - 'xml', - 'webp', - ], - ); - - if (result != null) { - List files = result.paths.map((path) => File(path!)).toList(); - await chatRoomCubit.sendMessage( - chatId: widget.chatId, - files: files, - ); - } else { - // User canceled the picker - } - }, - customBorder: CircleBorder(), - child: Ink( - height: 40, - width: 40, - child: Transform.rotate( - angle: pi / 4, - child: Icon( - Icons.attach_file_rounded, - size: 30, - color: Color(0xFF858E99), - ), - ), - ), - ), - SizedBox(width: 8), - Expanded( - child: SizedBox( - height: 40, - child: TextFormField( - expands: true, - controller: textEditingController, - maxLines: null, - minLines: null, - keyboardType: TextInputType.multiline, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black, height: 1), - decoration: InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 10), - hintText: 'Сообщение', - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1, - color: Color(0xFFAEAEB2), - ), - filled: true, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(100), - borderSide: BorderSide( - color: Color(0xFFD1D1D6), - width: 1, - ), - ), - ), - ), - ), - ), - SizedBox(width: 8), - InkWell( - customBorder: CircleBorder(), - onTap: () { - if (textEditingController.text.isNotEmpty) { - chatRoomCubit.sendMessage( - chatId: widget.chatId, - data: textEditingController.text.trim(), - ); - setState(() { - textEditingController.clear(); - }); - } - }, - child: Ink( - decoration: BoxDecoration( - color: appTheme.blueA400, - shape: BoxShape.circle, - ), - height: 40, - width: 40, - child: Icon( - Icons.arrow_upward_rounded, - size: 24, - color: Colors.white, - ), - ), - ), - SizedBox(width: 8), - ], - ), - ), ], ), ), diff --git a/lib/features/chat/presentation/widgets/order_card.dart b/lib/features/chat/presentation/widgets/order_card.dart index 5f57f362..fdb8180d 100644 --- a/lib/features/chat/presentation/widgets/order_card.dart +++ b/lib/features/chat/presentation/widgets/order_card.dart @@ -1,18 +1,136 @@ +import 'package:eatvio_app/core/const.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/core/utils/localization_utils.dart'; +import 'package:eatvio_app/core/utils/string_extentions.dart'; +import 'package:eatvio_app/core/widgets/item_widget.dart'; +import 'package:eatvio_app/core/widgets/loader.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; -class OrderCard extends StatelessWidget { +class OrderCard extends StatefulWidget { final PlanModel plan; + final String optionType; final bool isUserView; - const OrderCard({super.key, required this.plan, required this.isUserView}); + final Future Function() onCancel; + const OrderCard({super.key, required this.plan, required this.isUserView, required this.optionType, required this.onCancel}); @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only(top: 8, bottom: 12, left: 8, right: 8), - child: Column( - children: [ + State createState() => _OrderCardState(); +} +class _OrderCardState extends State { + bool isLoading = false; + @override + Widget build(BuildContext context) { + final optionIndex = int.parse(widget.optionType.characters.last); + final option = switch (optionIndex) { + 1 => widget.plan.packet1Options, + 2 => widget.plan.packet2Options, + 3 => widget.plan.packet3Options, + _ => null, + }; + final s = S.of(context); + return Container( + margin: EdgeInsets.symmetric(horizontal: 16), + padding: EdgeInsets.only(top: 8, bottom: 12, left: 8, right: 8), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.gray60004), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 200, + width: double.infinity, + child: Stack( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + baseUrl + '/storage/' + widget.plan.image, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Positioned( + left: 0, + bottom: 44, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: appTheme.blueA400, + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Text( + '${widget.plan.duration} ${getDayForm(widget.plan.duration, s).capitalize()}', + style: TextStyles.textSMedium.copyWith(color: appTheme.whiteA700), + ), + ), + ), + Positioned( + left: 0, + bottom: 12, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: appTheme.greenA700, + borderRadius: BorderRadius.only( + topRight: Radius.circular(12), + bottomRight: Radius.circular(12), + ), + ), + child: Text( + '${s.from} ${widget.plan.minPriceOfOptions.toStringAsFixed(0)}₽', + style: TextStyles.textSMedium.copyWith(color: appTheme.whiteA700), + ), + ), + ), + ], + ), + ), + SizedBox(height: 12), + Text( + '${widget.plan.name}', + style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 12), + Text( + '${s.option}: ${option?.short ?? ''}', + style: TextStyles.textMSemibold.copyWith(color: appTheme.black), + ), + SizedBox(height: 8), + if (option != null) + ...option.items + .map( + (element) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: ItemWidget( + reversed: true, + iconSize: 12, + text: element, + ), + ), + ) + .toList(), + if (widget.isUserView) ...[ + SizedBox(height: 8), + isLoading + ? Loader() + : GestureDetector( + onTap: () async { + setState(() => isLoading = true); + await widget.onCancel(); + setState(() => isLoading = false); + }, + child: Text( + '${s.cancel_order}', + style: TextStyles.textSMedium.copyWith(color: appTheme.strongRed), + ), + ), + ], ], ), ); diff --git a/lib/features/chat/repositories/chat_repository.dart b/lib/features/chat/repositories/chat_repository.dart index bdc537f8..c6b1bf8d 100644 --- a/lib/features/chat/repositories/chat_repository.dart +++ b/lib/features/chat/repositories/chat_repository.dart @@ -93,4 +93,14 @@ class ChatRepository { rethrow; } } + + Future cancelChosenPlan({ + required int id, + }) async { + try { + await chatService.cancelChosenPlan(id: id); + } catch (e) { + rethrow; + } + } } diff --git a/lib/features/chat/services/chat_service.dart b/lib/features/chat/services/chat_service.dart index 65910a34..3eae96fd 100644 --- a/lib/features/chat/services/chat_service.dart +++ b/lib/features/chat/services/chat_service.dart @@ -12,7 +12,7 @@ abstract class ChatService { @GET('/api/chat/index') Future getChatRooms({ - @Query('page') required int page, + @Query('page') required int page, @Query('find') String? query, }); @@ -41,5 +41,13 @@ abstract class ChatService { }); @POST('/api/chat/{id}/messages/markAsRead') - Future markMessagesAsRead({@Path() required String id, @Body() required Map> body}); + Future markMessagesAsRead({ + @Path() required String id, + @Body() required Map> body, + }); + + @POST('/api/chosen_plans/{id}/cancel') + Future cancelChosenPlan({ + @Path() required int id, + }); } diff --git a/lib/features/chat/services/chat_service.g.dart b/lib/features/chat/services/chat_service.g.dart index 69a70741..849f0ac3 100644 --- a/lib/features/chat/services/chat_service.g.dart +++ b/lib/features/chat/services/chat_service.g.dart @@ -193,6 +193,25 @@ class _ChatService implements ChatService { await _dio.fetch(_options); } + @override + Future cancelChosenPlan({required int id}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/chosen_plans/${id}/cancel', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + RequestOptions _setStreamType(RequestOptions requestOptions) { if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 73a3a358..8fbb2dbb 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -4,12 +4,14 @@ import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/image_constant.dart'; import 'package:eatvio_app/core/locator_service.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart'; import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart'; import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; @@ -155,7 +157,7 @@ class MenuScreen extends StatelessWidget { ); } catch (e) { } finally { - await TokenStorage.removeTokenInStorage(); + await TokenStorage.removeTokenInStorage(); GoRouter.of(context).go('/auth-screen', extra: true); } }, diff --git a/lib/features/plans/models/chosen_plan_model.dart b/lib/features/plans/models/chosen_plan_model.dart index f42d6d80..670e6380 100644 --- a/lib/features/plans/models/chosen_plan_model.dart +++ b/lib/features/plans/models/chosen_plan_model.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -13,8 +14,16 @@ class ChosenPlanModel with _$ChosenPlanModel { @JsonKey(name: 'start_date') required DateTime startDate, @JsonKey(name: 'last_state') required ChosenPlanStateEnum chosenPlanState, @JsonKey(name: 'chat_id') required String chatId, - + @JsonKey(name: 'consultant_id') required int consultantId, + @JsonKey(name: 'plan') required PlanModel plan, }) = _ChosenPlanModel; factory ChosenPlanModel.fromJson(Map json) => _$ChosenPlanModelFromJson(json); +} + +ChosenPlanModel? parseChosenPlan(json) { + if (json is Map) { + return ChosenPlanModel.fromJson(json); + } + return null; } \ No newline at end of file diff --git a/lib/features/plans/models/chosen_plan_model.freezed.dart b/lib/features/plans/models/chosen_plan_model.freezed.dart index 3c7d7325..859c9ad6 100644 --- a/lib/features/plans/models/chosen_plan_model.freezed.dart +++ b/lib/features/plans/models/chosen_plan_model.freezed.dart @@ -30,6 +30,10 @@ mixin _$ChosenPlanModel { ChosenPlanStateEnum get chosenPlanState => throw _privateConstructorUsedError; @JsonKey(name: 'chat_id') String get chatId => throw _privateConstructorUsedError; + @JsonKey(name: 'consultant_id') + int get consultantId => throw _privateConstructorUsedError; + @JsonKey(name: 'plan') + PlanModel get plan => throw _privateConstructorUsedError; /// Serializes this ChosenPlanModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -52,7 +56,11 @@ abstract class $ChosenPlanModelCopyWith<$Res> { @JsonKey(name: 'option_type') String optionType, @JsonKey(name: 'start_date') DateTime startDate, @JsonKey(name: 'last_state') ChosenPlanStateEnum chosenPlanState, - @JsonKey(name: 'chat_id') String chatId}); + @JsonKey(name: 'chat_id') String chatId, + @JsonKey(name: 'consultant_id') int consultantId, + @JsonKey(name: 'plan') PlanModel plan}); + + $PlanModelCopyWith<$Res> get plan; } /// @nodoc @@ -75,6 +83,8 @@ class _$ChosenPlanModelCopyWithImpl<$Res, $Val extends ChosenPlanModel> Object? startDate = null, Object? chosenPlanState = null, Object? chatId = null, + Object? consultantId = null, + Object? plan = null, }) { return _then(_value.copyWith( id: null == id @@ -97,8 +107,26 @@ class _$ChosenPlanModelCopyWithImpl<$Res, $Val extends ChosenPlanModel> ? _value.chatId : chatId // ignore: cast_nullable_to_non_nullable as String, + consultantId: null == consultantId + ? _value.consultantId + : consultantId // ignore: cast_nullable_to_non_nullable + as int, + plan: null == plan + ? _value.plan + : plan // ignore: cast_nullable_to_non_nullable + as PlanModel, ) as $Val); } + + /// Create a copy of ChosenPlanModel + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PlanModelCopyWith<$Res> get plan { + return $PlanModelCopyWith<$Res>(_value.plan, (value) { + return _then(_value.copyWith(plan: value) as $Val); + }); + } } /// @nodoc @@ -114,7 +142,12 @@ abstract class _$$ChosenPlanModelImplCopyWith<$Res> @JsonKey(name: 'option_type') String optionType, @JsonKey(name: 'start_date') DateTime startDate, @JsonKey(name: 'last_state') ChosenPlanStateEnum chosenPlanState, - @JsonKey(name: 'chat_id') String chatId}); + @JsonKey(name: 'chat_id') String chatId, + @JsonKey(name: 'consultant_id') int consultantId, + @JsonKey(name: 'plan') PlanModel plan}); + + @override + $PlanModelCopyWith<$Res> get plan; } /// @nodoc @@ -135,6 +168,8 @@ class __$$ChosenPlanModelImplCopyWithImpl<$Res> Object? startDate = null, Object? chosenPlanState = null, Object? chatId = null, + Object? consultantId = null, + Object? plan = null, }) { return _then(_$ChosenPlanModelImpl( id: null == id @@ -157,6 +192,14 @@ class __$$ChosenPlanModelImplCopyWithImpl<$Res> ? _value.chatId : chatId // ignore: cast_nullable_to_non_nullable as String, + consultantId: null == consultantId + ? _value.consultantId + : consultantId // ignore: cast_nullable_to_non_nullable + as int, + plan: null == plan + ? _value.plan + : plan // ignore: cast_nullable_to_non_nullable + as PlanModel, )); } } @@ -169,7 +212,9 @@ class _$ChosenPlanModelImpl implements _ChosenPlanModel { @JsonKey(name: 'option_type') required this.optionType, @JsonKey(name: 'start_date') required this.startDate, @JsonKey(name: 'last_state') required this.chosenPlanState, - @JsonKey(name: 'chat_id') required this.chatId}); + @JsonKey(name: 'chat_id') required this.chatId, + @JsonKey(name: 'consultant_id') required this.consultantId, + @JsonKey(name: 'plan') required this.plan}); factory _$ChosenPlanModelImpl.fromJson(Map json) => _$$ChosenPlanModelImplFromJson(json); @@ -189,10 +234,16 @@ class _$ChosenPlanModelImpl implements _ChosenPlanModel { @override @JsonKey(name: 'chat_id') final String chatId; + @override + @JsonKey(name: 'consultant_id') + final int consultantId; + @override + @JsonKey(name: 'plan') + final PlanModel plan; @override String toString() { - return 'ChosenPlanModel(id: $id, optionType: $optionType, startDate: $startDate, chosenPlanState: $chosenPlanState, chatId: $chatId)'; + return 'ChosenPlanModel(id: $id, optionType: $optionType, startDate: $startDate, chosenPlanState: $chosenPlanState, chatId: $chatId, consultantId: $consultantId, plan: $plan)'; } @override @@ -207,13 +258,16 @@ class _$ChosenPlanModelImpl implements _ChosenPlanModel { other.startDate == startDate) && (identical(other.chosenPlanState, chosenPlanState) || other.chosenPlanState == chosenPlanState) && - (identical(other.chatId, chatId) || other.chatId == chatId)); + (identical(other.chatId, chatId) || other.chatId == chatId) && + (identical(other.consultantId, consultantId) || + other.consultantId == consultantId) && + (identical(other.plan, plan) || other.plan == plan)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash( - runtimeType, id, optionType, startDate, chosenPlanState, chatId); + int get hashCode => Object.hash(runtimeType, id, optionType, startDate, + chosenPlanState, chatId, consultantId, plan); /// Create a copy of ChosenPlanModel /// with the given fields replaced by the non-null parameter values. @@ -239,7 +293,9 @@ abstract class _ChosenPlanModel implements ChosenPlanModel { @JsonKey(name: 'start_date') required final DateTime startDate, @JsonKey(name: 'last_state') required final ChosenPlanStateEnum chosenPlanState, - @JsonKey(name: 'chat_id') required final String chatId}) = + @JsonKey(name: 'chat_id') required final String chatId, + @JsonKey(name: 'consultant_id') required final int consultantId, + @JsonKey(name: 'plan') required final PlanModel plan}) = _$ChosenPlanModelImpl; factory _ChosenPlanModel.fromJson(Map json) = @@ -260,6 +316,12 @@ abstract class _ChosenPlanModel implements ChosenPlanModel { @override @JsonKey(name: 'chat_id') String get chatId; + @override + @JsonKey(name: 'consultant_id') + int get consultantId; + @override + @JsonKey(name: 'plan') + PlanModel get plan; /// Create a copy of ChosenPlanModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/features/plans/models/chosen_plan_model.g.dart b/lib/features/plans/models/chosen_plan_model.g.dart index 7e64a4e4..d3e8613c 100644 --- a/lib/features/plans/models/chosen_plan_model.g.dart +++ b/lib/features/plans/models/chosen_plan_model.g.dart @@ -15,6 +15,8 @@ _$ChosenPlanModelImpl _$$ChosenPlanModelImplFromJson( chosenPlanState: $enumDecode(_$ChosenPlanStateEnumEnumMap, json['last_state']), chatId: json['chat_id'] as String, + consultantId: (json['consultant_id'] as num).toInt(), + plan: PlanModel.fromJson(json['plan'] as Map), ); Map _$$ChosenPlanModelImplToJson( @@ -25,6 +27,8 @@ Map _$$ChosenPlanModelImplToJson( 'start_date': instance.startDate.toIso8601String(), 'last_state': _$ChosenPlanStateEnumEnumMap[instance.chosenPlanState]!, 'chat_id': instance.chatId, + 'consultant_id': instance.consultantId, + 'plan': instance.plan, }; const _$ChosenPlanStateEnumEnumMap = { diff --git a/lib/features/plans/presentation/widgets/item_widget.dart b/lib/features/plans/presentation/widgets/item_widget.dart deleted file mode 100644 index 5ec3b288..00000000 --- a/lib/features/plans/presentation/widgets/item_widget.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:eatvio_app/core/theme/custom_text_style.dart'; -import 'package:eatvio_app/core/theme/theme_helper.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class ItemWidget extends StatelessWidget { - const ItemWidget({super.key, required this.text}); - final String text; - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Text( - text, - style: TextStyles.textMMedium.copyWith(color: appTheme.black), - ), - ), - SvgPicture.asset( - 'assets/icons/ok.svg', - width: 20, - height: 20, - ), - ], - ); - } -} diff --git a/lib/features/plans/presentation/widgets/option_widget.dart b/lib/features/plans/presentation/widgets/option_widget.dart index 96849dfb..6251dbe4 100644 --- a/lib/features/plans/presentation/widgets/option_widget.dart +++ b/lib/features/plans/presentation/widgets/option_widget.dart @@ -3,7 +3,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/widgets/custom_divider.dart'; import 'package:eatvio_app/core/widgets/main_background_bouble.dart'; import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; -import 'package:eatvio_app/features/plans/presentation/widgets/item_widget.dart'; +import 'package:eatvio_app/core/widgets/item_widget.dart'; import 'package:eatvio_app/features/plans/presentation/widgets/option_duration_widget.dart'; import 'package:flutter/material.dart'; diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 6d76f87e..e087b400 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -65,6 +65,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Не удалось загрузить данные. Попробуйте обновить страницу"), "cancel_diet": MessageLookupByLibrary.simpleMessage("Отменить диету?"), + "cancel_order": MessageLookupByLibrary.simpleMessage("Отменить заказ"), "carb_type": MessageLookupByLibrary.simpleMessage("Мучное"), "carb_type_desc": MessageLookupByLibrary.simpleMessage( "Если вы любите мучные продукты, но хотите придерживаться диеты, вам подходит диета с добавленим мучных блюд на основе цельнозерновых вариантов. В них больше сложных углеводов и клетчатки, что способствует длительному чувству сытости и поддерживает здоровье пищеварительной системы."), @@ -288,6 +289,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Введение в заблуждение"), "misrepresentation_description": MessageLookupByLibrary.simpleMessage( "Новости, которые могут вызвать широкий резонанс или несут угрозу, распространяемые с целью введения людей в заблуждение."), + "more_about_order": + MessageLookupByLibrary.simpleMessage("Подробнее о заказе"), "most_new": MessageLookupByLibrary.simpleMessage("Самые новые"), "most_old": MessageLookupByLibrary.simpleMessage("Самые старые"), "msg_network_err": @@ -387,6 +390,8 @@ class MessageLookup extends MessageLookupByLibrary { "recipe_author": MessageLookupByLibrary.simpleMessage("Автор рецепта"), "recipe_publishing": MessageLookupByLibrary.simpleMessage("Публикация рецепта"), + "refuse_order": + MessageLookupByLibrary.simpleMessage("Отказаться от заказа"), "removeFromActive": MessageLookupByLibrary.simpleMessage("Снять с публикации"), "save": MessageLookupByLibrary.simpleMessage("Сохранить"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 3c20bf57..81cfa1c9 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -3251,6 +3251,36 @@ class S { args: [], ); } + + /// `Подробнее о заказе` + String get more_about_order { + return Intl.message( + 'Подробнее о заказе', + name: 'more_about_order', + desc: '', + args: [], + ); + } + + /// `Отменить заказ` + String get cancel_order { + return Intl.message( + 'Отменить заказ', + name: 'cancel_order', + desc: '', + args: [], + ); + } + + /// `Отказаться от заказа` + String get refuse_order { + return Intl.message( + 'Отказаться от заказа', + name: 'refuse_order', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3594ada0..07020bcd 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -319,7 +319,10 @@ "choose_date": "Выбрать дату", "date_has_ben_set": "Дата старта начала консультации установлена", "tap_on_link_to)pay": "Перейдите по ссылке для оплаты консультации", - "pay": "Оплатить" + "pay": "Оплатить", + "more_about_order": "Подробнее о заказе", + "cancel_order": "Отменить заказ", + "refuse_order":"Отказаться от заказа" }