chats have been updated to work with consultatons

This commit is contained in:
Username1548
2025-03-14 19:56:55 +07:00
parent 9d53760137
commit 803588a289
26 changed files with 957 additions and 281 deletions

View File

@@ -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<int> durationValues = [7, 14, 21];

View File

@@ -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);

View File

@@ -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,
),
],
);
}
}

View File

@@ -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<AuthState> {
required this.authRepository,
}) : super(AuthState());
getTokenByCredentials(String email, String password) async {
Future<bool> getTokenByCredentials(String email, String password) async {
emit(AuthState.loading());
try {
final token = (await authRepository.getTokenByCredentials(
@@ -23,7 +21,6 @@ class AuthCubit extends Cubit<AuthState> {
password,
))
.token;
print(token);
emit(
AuthState.loaded(
token: token,
@@ -31,12 +28,13 @@ class AuthCubit extends Cubit<AuthState> {
);
showSuccessToast('Успешный вход');
await TokenStorage.saveTokenInStorage(token);
await sl<ProfileAvatarCubit>().getMyProfile();
AppRouter.navigatorKey.currentContext?.go('/my-profile-screen');
return true;
} catch (e) {
print(e);
showErrorToast('Неверный адрес электронной почты или пароль');
emit(AuthState.error(errorMessage: '$e'));
return false;
}
}
}

View File

@@ -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<AuthScreen> {
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<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
}
},
label: state is AuthStateLoading
? CupertinoActivityIndicator()
: Text(

View File

@@ -11,16 +11,23 @@ class ChatRoomModel with _$ChatRoomModel {
const factory ChatRoomModel({
required String id,
@JsonKey(name: 'chat_room_setting') required ChatRoomSettingsModel chatRoomSettings,
required Map<String, ChatUserModel> 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<String, Object?> json) => _$ChatRoomModelFromJson(json);
}
ChosenPlanModel? _parseChosenPlan(json) {
if (json is Map<String, dynamic>) {
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<String, Object?> json) => _$UsersFromJson(json);
}

View File

@@ -24,8 +24,8 @@ mixin _$ChatRoomModel {
@JsonKey(name: 'chat_room_setting')
ChatRoomSettingsModel get chatRoomSettings =>
throw _privateConstructorUsedError;
Map<String, ChatUserModel> 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<String, ChatUserModel> 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<String, ChatUserModel>,
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<String, ChatUserModel> 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<String, ChatUserModel>,
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<String, ChatUserModel> 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<String, dynamic> json) =>
_$$ChatRoomModelImplFromJson(json);
@@ -202,16 +214,10 @@ class _$ChatRoomModelImpl implements _ChatRoomModel {
@override
@JsonKey(name: 'chat_room_setting')
final ChatRoomSettingsModel chatRoomSettings;
final Map<String, ChatUserModel> _users;
@override
Map<String, ChatUserModel> 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<String, ChatUserModel> 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<String, dynamic> json) =
@@ -271,9 +277,9 @@ abstract class _ChatRoomModel implements ChatRoomModel {
@JsonKey(name: 'chat_room_setting')
ChatRoomSettingsModel get chatRoomSettings;
@override
Map<String, ChatUserModel> 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<String, dynamic> 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<String, dynamic> 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<Users> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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;
}

View File

@@ -11,11 +11,8 @@ _$ChatRoomModelImpl _$$ChatRoomModelImplFromJson(Map<String, dynamic> json) =>
id: json['id'] as String,
chatRoomSettings: ChatRoomSettingsModel.fromJson(
json['chat_room_setting'] as Map<String, dynamic>),
users: (json['users'] as Map<String, dynamic>).map(
(k, e) =>
MapEntry(k, ChatUserModel.fromJson(e as Map<String, dynamic>)),
),
chosenPlan: _parseChosenPlan(json['chosen_plan']),
users: Users.fromJson(json['users'] as Map<String, dynamic>),
chosenPlan: parseChosenPlan(json['chosen_plan']),
);
Map<String, dynamic> _$$ChatRoomModelImplToJson(_$ChatRoomModelImpl instance) =>
@@ -25,3 +22,16 @@ Map<String, dynamic> _$$ChatRoomModelImplToJson(_$ChatRoomModelImpl instance) =>
'users': instance.users,
'chosen_plan': instance.chosenPlan,
};
_$UsersImpl _$$UsersImplFromJson(Map<String, dynamic> json) => _$UsersImpl(
sender: ChatUserModel.fromJson(
json['from_chat_user'] as Map<String, dynamic>),
receiver:
ChatUserModel.fromJson(json['to_chat_user'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$UsersImplToJson(_$UsersImpl instance) =>
<String, dynamic>{
'from_chat_user': instance.sender,
'to_chat_user': instance.receiver,
};

View File

@@ -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<String, dynamic> json) => _$ChatRoomFromJson(json);

View File

@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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.

View File

@@ -43,6 +43,7 @@ _$ChatRoomImpl _$$ChatRoomImplFromJson(Map<String, dynamic> json) =>
lastMessage: parseLastMessage(json['last_message']),
users: Users.fromJson(json['users'] as Map<String, dynamic>),
unreadMessagesCount: (json['unread_messages_count'] as num).toInt(),
chosenPlan: parseChosenPlan(json['chosen_plan']),
);
Map<String, dynamic> _$$ChatRoomImplToJson(_$ChatRoomImpl instance) =>
@@ -52,6 +53,7 @@ Map<String, dynamic> _$$ChatRoomImplToJson(_$ChatRoomImpl instance) =>
'last_message': instance.lastMessage,
'users': instance.users,
'unread_messages_count': instance.unreadMessagesCount,
'chosen_plan': instance.chosenPlan,
};
_$UsersImpl _$$UsersImplFromJson(Map<String, dynamic> json) => _$UsersImpl(

View File

@@ -86,11 +86,12 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
}
}
Future<void> updateChatRoom(String chatId) async{
Future<void> 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<ChatRoomState> {
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<ChatMessageModel> messagesList = [
@@ -122,7 +123,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
);
});
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<ChatRoomState> {
@override
Future<void> close() async {
wsManager.unsubscribeDirectChatChannel();
messagesSubscription?.cancel();
typingSubscription?.cancel();
super.close();
}
}

View File

@@ -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<ChatScreen> {
final ChatRoomCubit chatRoomCubit = sl<ChatRoomCubit>();
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<ChatScreen> {
@override
void dispose() {
scrollController.dispose();
chatRoomCubit.close();
super.dispose();
}
@@ -78,6 +84,9 @@ class _ChatScreenState extends State<ChatScreen> {
}
_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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
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<ChatScreen> {
),
),
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<ChatScreen> {
],
);
}
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<ChatScreen> {
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<ChatRoomCubit>().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<File> 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<File> 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),
],
),
),
],
),
),

View File

@@ -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<void> 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<OrderCard> createState() => _OrderCardState();
}
class _OrderCardState extends State<OrderCard> {
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),
),
),
],
],
),
);

View File

@@ -93,4 +93,14 @@ class ChatRepository {
rethrow;
}
}
Future<void> cancelChosenPlan({
required int id,
}) async {
try {
await chatService.cancelChosenPlan(id: id);
} catch (e) {
rethrow;
}
}
}

View File

@@ -12,7 +12,7 @@ abstract class ChatService {
@GET('/api/chat/index')
Future<ChatRoomPaginationModel> 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<void> markMessagesAsRead({@Path() required String id, @Body() required Map<String, List<int>> body});
Future<void> markMessagesAsRead({
@Path() required String id,
@Body() required Map<String, List<int>> body,
});
@POST('/api/chosen_plans/{id}/cancel')
Future<void> cancelChosenPlan({
@Path() required int id,
});
}

View File

@@ -193,6 +193,25 @@ class _ChatService implements ChatService {
await _dio.fetch<void>(_options);
}
@override
Future<void> cancelChosenPlan({required int id}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<void>(
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<void>(_options);
}
RequestOptions _setStreamType<T>(RequestOptions requestOptions) {
if (T != dynamic &&
!(requestOptions.responseType == ResponseType.bytes ||

View File

@@ -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);
}
},

View File

@@ -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<String, dynamic> json) => _$ChosenPlanModelFromJson(json);
}
ChosenPlanModel? parseChosenPlan(json) {
if (json is Map<String, dynamic>) {
return ChosenPlanModel.fromJson(json);
}
return null;
}

View File

@@ -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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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.

View File

@@ -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<String, dynamic>),
);
Map<String, dynamic> _$$ChosenPlanModelImplToJson(
@@ -25,6 +27,8 @@ Map<String, dynamic> _$$ChosenPlanModelImplToJson(
'start_date': instance.startDate.toIso8601String(),
'last_state': _$ChosenPlanStateEnumEnumMap[instance.chosenPlanState]!,
'chat_id': instance.chatId,
'consultant_id': instance.consultantId,
'plan': instance.plan,
};
const _$ChosenPlanStateEnumEnumMap = {

View File

@@ -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,
),
],
);
}
}

View File

@@ -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';

View File

@@ -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("Сохранить"),

View File

@@ -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<S> {

View File

@@ -319,7 +319,10 @@
"choose_date": "Выбрать дату",
"date_has_ben_set": "Дата старта начала консультации установлена",
"tap_on_link_to)pay": "Перейдите по ссылке для оплаты консультации",
"pay": "Оплатить"
"pay": "Оплатить",
"more_about_order": "Подробнее о заказе",
"cancel_order": "Отменить заказ",
"refuse_order":"Отказаться от заказа"
}