card adding and selecting default one

This commit is contained in:
Username1548
2025-03-21 20:40:48 +07:00
parent fa817548d9
commit cdc16b9a9c
30 changed files with 1254 additions and 296 deletions

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "ba393198430278b6595976de84fe170f553cc728"
revision: "09de023485e95e6d1225c2baa44b8feb85e0d45f"
channel: "stable"
project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
- platform: android
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
- platform: ios
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
- platform: linux
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
- platform: macos
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
- platform: web
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
- platform: windows
create_revision: ba393198430278b6595976de84fe170f553cc728
base_revision: ba393198430278b6595976de84fe170f553cc728
create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f
# User provided section

View File

@@ -0,0 +1,3 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.30775 15.5C1.80258 15.5 1.375 15.325 1.025 14.975C0.675 14.625 0.5 14.1974 0.5 13.6923V2.30775C0.5 1.80258 0.675 1.375 1.025 1.025C1.375 0.675 1.80258 0.5 2.30775 0.5H17.6923C18.1974 0.5 18.625 0.675 18.975 1.025C19.325 1.375 19.5 1.80258 19.5 2.30775V6.69625C19.5 6.94492 19.4153 7.15708 19.2458 7.33275C19.0761 7.50825 18.8659 7.596 18.6152 7.596H2V13.6923C2 13.7692 2.03208 13.8398 2.09625 13.9038C2.16025 13.9679 2.23075 14 2.30775 14H11.1155C11.328 14 11.5061 14.0719 11.6497 14.2158C11.7934 14.3596 11.8652 14.5378 11.8652 14.7503C11.8652 14.9629 11.7934 15.141 11.6497 15.2845C11.5061 15.4282 11.328 15.5 11.1155 15.5H2.30775ZM2 4.404H18V2.30775C18 2.23075 17.9679 2.16025 17.9038 2.09625C17.8398 2.03208 17.7693 2 17.6923 2H2.30775C2.23075 2 2.16025 2.03208 2.09625 2.09625C2.03208 2.16025 2 2.23075 2 2.30775V4.404ZM17.25 14.75H15C14.7875 14.75 14.6094 14.6781 14.4658 14.5343C14.3219 14.3904 14.25 14.2122 14.25 13.9998C14.25 13.7871 14.3219 13.609 14.4658 13.4655C14.6094 13.3218 14.7875 13.25 15 13.25H17.25V11C17.25 10.7875 17.3219 10.6094 17.4657 10.4658C17.6096 10.3219 17.7878 10.25 18.0002 10.25C18.2129 10.25 18.391 10.3219 18.5345 10.4658C18.6782 10.6094 18.75 10.7875 18.75 11V13.25H21C21.2125 13.25 21.3906 13.3219 21.5343 13.4658C21.6781 13.6096 21.75 13.7878 21.75 14.0003C21.75 14.2129 21.6781 14.391 21.5343 14.5345C21.3906 14.6782 21.2125 14.75 21 14.75H18.75V17C18.75 17.2125 18.6781 17.3906 18.5343 17.5343C18.3904 17.6781 18.2122 17.75 17.9998 17.75C17.7871 17.75 17.609 17.6781 17.4655 17.5343C17.3218 17.3906 17.25 17.2125 17.25 17V14.75Z" fill="#323F4B"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

5
assets/icons/card.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="40" height="28" viewBox="0 0 40 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.00809 0.40625C1.4534 0.5 0.984652 0.765625 0.594027 1.19531C-0.0465986 1.90625 0.000276472 0.875 0.000276472 14C0.000276472 27.3281 -0.0622236 26.1328 0.695589 26.8984C1.00809 27.2109 1.25809 27.375 1.56278 27.4844C1.98465 27.625 2.47684 27.6328 20.0003 27.6328C37.5237 27.6328 38.0159 27.625 38.4378 27.4844C38.7425 27.375 38.9925 27.2109 39.305 26.8984C40.0628 26.1328 40.0003 27.3281 40.0003 14C40.0003 3.26562 39.9925 2.28125 39.8675 1.92188C39.6956 1.41406 39.0081 0.726562 38.4768 0.515625C38.094 0.367188 37.6565 0.367188 20.2347 0.359375C10.4143 0.351562 2.21903 0.375 2.00809 0.40625ZM37.9143 2.13281C38.3284 2.32031 38.3597 2.48438 38.3597 4.69531V6.73438H20.0003H1.6409V4.67969C1.64871 2.49219 1.66434 2.35938 2.03934 2.15625C2.19559 2.07031 5.3909 2.05469 19.9768 2.04688C32.5784 2.04688 37.7815 2.07031 37.9143 2.13281ZM38.3597 18.5C38.3597 24.7422 38.3518 25.3594 38.2268 25.5625C38.1565 25.6875 38.0159 25.8203 37.9143 25.8672C37.6331 25.9922 2.35184 25.9844 2.07059 25.8594C1.62528 25.6562 1.6409 25.9219 1.6409 18.4922V11.6562H20.0003H38.3597V18.5Z" fill="#2F80ED"/>
<path d="M6.82867 16.4687C6.26617 16.7422 6.21929 17.5625 6.74273 17.9062C6.89117 18.0078 7.99273 18.0234 14.2193 18.0469C19.2584 18.0625 21.6099 18.0469 21.813 17.9844C22.4302 17.8047 22.6255 17.1172 22.1959 16.6406L21.9693 16.3828L14.5162 16.3672C7.93023 16.3515 7.03961 16.3594 6.82867 16.4687Z" fill="#2F80ED"/>
<path d="M6.82867 19.6719C6.26617 19.9453 6.21929 20.7656 6.74273 21.1094C6.88336 21.2109 7.58648 21.2344 11.0162 21.25C13.7974 21.2656 15.2115 21.25 15.4068 21.1875C16.024 21.0078 16.2193 20.3203 15.7896 19.8437L15.563 19.5859L11.313 19.5703C7.60992 19.5469 7.03961 19.5625 6.82867 19.6719Z" fill="#2F80ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -365,7 +365,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -383,7 +383,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.47;
MARKETING_VERSION = 1.0.49;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -514,7 +514,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -532,7 +532,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.47;
MARKETING_VERSION = 1.0.49;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -551,7 +551,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 47;
CURRENT_PROJECT_VERSION = 49;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -569,7 +569,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0.47;
MARKETING_VERSION = 1.0.49;
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

View File

@@ -158,8 +158,8 @@ class AppRouter {
builder: (context, state) => UserFinanceScreen(),
),
GoRoute(
path: '/card-settings-screen',
name: 'cardSettingsScreen',
path: CardSettingsScreen.path,
name: CardSettingsScreen.name,
builder: (context, state) => CardSettingsScreen(),
),
GoRoute(

View File

@@ -1,5 +1,5 @@
// String baseUrl = 'https://eatvio.ru';
String baseUrl = 'https://dev2.eatvio.com';
String baseUrl = 'https://eatvio.ru';
// String baseUrl = 'https://dev2.eatvio.com';
const List<int> durationValues = [7, 14, 21];

View File

@@ -49,6 +49,9 @@ import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cub
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart';
import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart';
@@ -117,6 +120,7 @@ Future<void> init() async {
sl.registerFactory<AboutRecipeCubit>(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl()));
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
sl.registerFactory<ComplainingCubit>(() => ComplainingCubit(repository: sl()));
sl.registerFactory<UserCardsCubit>(() => UserCardsCubit(cardsRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -139,6 +143,7 @@ Future<void> init() async {
sl.registerLazySingleton<AboutRecipeRepository>(() => AboutRecipeRepository(recipeService: sl()));
sl.registerLazySingleton<ComplainingRepository>(() => ComplainingRepository(service: sl()));
sl.registerLazySingleton<AuthRepository>(() => AuthRepository(authService: sl()));
sl.registerLazySingleton<CardsRepository>(() => CardsRepository(cardsService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -162,6 +167,7 @@ Future<void> init() async {
sl.registerLazySingleton<AboutRecipeService>(() => AboutRecipeService(sl()));
sl.registerLazySingleton<ComplainingService>(() => ComplainingService(sl()));
sl.registerLazySingleton<AuthService>(() => AuthService(sl()));
sl.registerLazySingleton<CardsService>(() => CardsService(sl()));
// External
sl.registerLazySingleton(

View File

@@ -63,7 +63,7 @@ class RecipesCubit extends Cubit<RecipesState> {
lastPage = currentPage;
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(state.copyWith(errorMessage: e.toString()));
emit(state.copyWith(errorMessage: e.toString(), isLoading: false));
}
}
@@ -94,7 +94,7 @@ class RecipesCubit extends Cubit<RecipesState> {
}
lastPage = currentPage;
} catch (e) {
emit(state.copyWith(errorMessage: e.toString()));
emit(state.copyWith(errorMessage: e.toString(), isLoading: false));
}
}
}

View File

@@ -16,54 +16,32 @@ import 'package:go_router/go_router.dart';
class MenuScreen extends StatelessWidget {
final ProfileModel profileModel;
const MenuScreen({
super.key,
required this.profileModel,
});
const MenuScreen({super.key, required this.profileModel});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text('Меню'),
),
appBar: AppBar(centerTitle: false, title: Text('Меню')),
body: SingleChildScrollView(
child: Column(
children: [
_tile(
title: 'Настройки профиля',
leading: ImageConstant.icnProfileSettings,
onTap: () => GoRouter.of(context).push('/settings-screen'),
),
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: ImageConstant.icnRecipes,
title: 'Мои рецепты',
onTap: () => GoRouter.of(context).push('/recipe-select-screen'),
),
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push('/recipe-select-screen')),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: 'assets/icons/diets.svg',
title: 'Мои диеты',
onTap: () => GoRouter.of(context).push(MyDietsScreen.path),
),
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(
leading: ImageConstant.icnPlans,
title: 'Мои планы',
onTap: () => GoRouter.of(context).push(MyPlansScreen.path),
),
_tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)),
_divider(context),
_tile(
leading: 'assets/icons/finance.svg',
title: 'Мои финансы',
onTap: () {
if (profileModel.roles.contains('consultant')) {
GoRouter.of(context).pushNamed('consultantFinanceScreen');
GoRouter.of(context).pushNamed('userFinanceScreen');
} else {
GoRouter.of(context).pushNamed('userFinanceScreen');
}
@@ -76,17 +54,9 @@ class MenuScreen extends StatelessWidget {
// onTap: () => GoRouter.of(context).push('/calendar-screen'),
// ),
_divider(context),
_tile(
leading: ImageConstant.icnGlossary,
title: 'Глоссарий',
onTap: () => GoRouter.of(context).push('/glossary-screen'),
),
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context),
_tile(
leading: ImageConstant.icnSupport,
title: 'Поддержка',
onTap: () => GoRouter.of(context).push('/support-screen'),
),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context),
_tile(
leading: ImageConstant.icnDevTools,
@@ -96,36 +66,19 @@ class MenuScreen extends StatelessWidget {
},
),
_divider(context),
_tile(
leading: ImageConstant.icnExit,
title: 'Выход',
onTap: _onTapLogout(context),
),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
],
),
),
);
}
_tile({
required String title,
required String leading,
required Function() onTap,
}) {
_tile({required String title, required String leading, required Function() onTap}) {
return ListTile(
onTap: onTap,
leading: SvgPicture.asset(
leading,
height: 24,
width: 24,
colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn),
),
leading: SvgPicture.asset(leading, height: 24, width: 24, colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)),
title: Text(title),
trailing: Icon(
Icons.arrow_forward_ios_rounded,
size: 12,
color: Colors.black38,
),
trailing: Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
);
}
@@ -133,10 +86,7 @@ class MenuScreen extends StatelessWidget {
return Container(
height: 0.4,
width: MediaQuery.of(context).size.width - 32,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(16),
),
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
);
}
@@ -144,15 +94,14 @@ class MenuScreen extends StatelessWidget {
return () async {
showDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
builder:
(context) => CupertinoAlertDialog(
title: Text("Выход"),
actions: [
CupertinoDialogAction(
onPressed: () async {
try {
await sl<Dio>().post(
baseUrl + '/api/profile/logout',
);
await sl<Dio>().post(baseUrl + '/api/profile/logout');
} catch (e) {
} finally {
await TokenStorage.removeTokenInStorage();

View File

@@ -0,0 +1,23 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'card_model.freezed.dart';
part 'card_model.g.dart';
@freezed
class CardModel with _$CardModel {
factory CardModel({
required int id,
@JsonKey(name: 'bank_pan') required String cardNumber,
@JsonKey(name: 'is_default', fromJson: parseBool) required bool isDefault,
}) = _CardModel;
factory CardModel.fromJson(Map<String, dynamic> json) => _$CardModelFromJson(json);
}
bool parseBool(json) {
if (json == 1 || json == true) {
return true;
}
return false;
}

View File

@@ -0,0 +1,232 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'card_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
CardModel _$CardModelFromJson(Map<String, dynamic> json) {
return _CardModel.fromJson(json);
}
/// @nodoc
mixin _$CardModel {
int get id => throw _privateConstructorUsedError;
@JsonKey(name: 'bank_pan')
String get cardNumber => throw _privateConstructorUsedError;
@JsonKey(name: 'is_default', fromJson: parseBool)
bool get isDefault => throw _privateConstructorUsedError;
/// Serializes this CardModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of CardModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CardModelCopyWith<CardModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CardModelCopyWith<$Res> {
factory $CardModelCopyWith(CardModel value, $Res Function(CardModel) then) =
_$CardModelCopyWithImpl<$Res, CardModel>;
@useResult
$Res call({
int id,
@JsonKey(name: 'bank_pan') String cardNumber,
@JsonKey(name: 'is_default', fromJson: parseBool) bool isDefault,
});
}
/// @nodoc
class _$CardModelCopyWithImpl<$Res, $Val extends CardModel>
implements $CardModelCopyWith<$Res> {
_$CardModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CardModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? cardNumber = null,
Object? isDefault = null,
}) {
return _then(
_value.copyWith(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
cardNumber:
null == cardNumber
? _value.cardNumber
: cardNumber // ignore: cast_nullable_to_non_nullable
as String,
isDefault:
null == isDefault
? _value.isDefault
: isDefault // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$CardModelImplCopyWith<$Res>
implements $CardModelCopyWith<$Res> {
factory _$$CardModelImplCopyWith(
_$CardModelImpl value,
$Res Function(_$CardModelImpl) then,
) = __$$CardModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
int id,
@JsonKey(name: 'bank_pan') String cardNumber,
@JsonKey(name: 'is_default', fromJson: parseBool) bool isDefault,
});
}
/// @nodoc
class __$$CardModelImplCopyWithImpl<$Res>
extends _$CardModelCopyWithImpl<$Res, _$CardModelImpl>
implements _$$CardModelImplCopyWith<$Res> {
__$$CardModelImplCopyWithImpl(
_$CardModelImpl _value,
$Res Function(_$CardModelImpl) _then,
) : super(_value, _then);
/// Create a copy of CardModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? cardNumber = null,
Object? isDefault = null,
}) {
return _then(
_$CardModelImpl(
id:
null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
cardNumber:
null == cardNumber
? _value.cardNumber
: cardNumber // ignore: cast_nullable_to_non_nullable
as String,
isDefault:
null == isDefault
? _value.isDefault
: isDefault // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$CardModelImpl implements _CardModel {
_$CardModelImpl({
required this.id,
@JsonKey(name: 'bank_pan') required this.cardNumber,
@JsonKey(name: 'is_default', fromJson: parseBool) required this.isDefault,
});
factory _$CardModelImpl.fromJson(Map<String, dynamic> json) =>
_$$CardModelImplFromJson(json);
@override
final int id;
@override
@JsonKey(name: 'bank_pan')
final String cardNumber;
@override
@JsonKey(name: 'is_default', fromJson: parseBool)
final bool isDefault;
@override
String toString() {
return 'CardModel(id: $id, cardNumber: $cardNumber, isDefault: $isDefault)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CardModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.cardNumber, cardNumber) ||
other.cardNumber == cardNumber) &&
(identical(other.isDefault, isDefault) ||
other.isDefault == isDefault));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, cardNumber, isDefault);
/// Create a copy of CardModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CardModelImplCopyWith<_$CardModelImpl> get copyWith =>
__$$CardModelImplCopyWithImpl<_$CardModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$CardModelImplToJson(this);
}
}
abstract class _CardModel implements CardModel {
factory _CardModel({
required final int id,
@JsonKey(name: 'bank_pan') required final String cardNumber,
@JsonKey(name: 'is_default', fromJson: parseBool)
required final bool isDefault,
}) = _$CardModelImpl;
factory _CardModel.fromJson(Map<String, dynamic> json) =
_$CardModelImpl.fromJson;
@override
int get id;
@override
@JsonKey(name: 'bank_pan')
String get cardNumber;
@override
@JsonKey(name: 'is_default', fromJson: parseBool)
bool get isDefault;
/// Create a copy of CardModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CardModelImplCopyWith<_$CardModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'card_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$CardModelImpl _$$CardModelImplFromJson(Map<String, dynamic> json) =>
_$CardModelImpl(
id: (json['id'] as num).toInt(),
cardNumber: json['bank_pan'] as String,
isDefault: parseBool(json['is_default']),
);
Map<String, dynamic> _$$CardModelImplToJson(_$CardModelImpl instance) =>
<String, dynamic>{
'id': instance.id,
'bank_pan': instance.cardNumber,
'is_default': instance.isDefault,
};

View File

@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'payment_url_model.freezed.dart';
part 'payment_url_model.g.dart';
@freezed
class PaymentUrlModel with _$PaymentUrlModel {
factory PaymentUrlModel({
required String paymentUrl,
}) = _PaymentUrlModel;
factory PaymentUrlModel.fromJson(Map<String, dynamic> json) => _$PaymentUrlModelFromJson(json);
}

View File

@@ -0,0 +1,174 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'payment_url_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
PaymentUrlModel _$PaymentUrlModelFromJson(Map<String, dynamic> json) {
return _PaymentUrlModel.fromJson(json);
}
/// @nodoc
mixin _$PaymentUrlModel {
String get paymentUrl => throw _privateConstructorUsedError;
/// Serializes this PaymentUrlModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of PaymentUrlModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$PaymentUrlModelCopyWith<PaymentUrlModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $PaymentUrlModelCopyWith<$Res> {
factory $PaymentUrlModelCopyWith(
PaymentUrlModel value,
$Res Function(PaymentUrlModel) then,
) = _$PaymentUrlModelCopyWithImpl<$Res, PaymentUrlModel>;
@useResult
$Res call({String paymentUrl});
}
/// @nodoc
class _$PaymentUrlModelCopyWithImpl<$Res, $Val extends PaymentUrlModel>
implements $PaymentUrlModelCopyWith<$Res> {
_$PaymentUrlModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of PaymentUrlModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? paymentUrl = null}) {
return _then(
_value.copyWith(
paymentUrl:
null == paymentUrl
? _value.paymentUrl
: paymentUrl // ignore: cast_nullable_to_non_nullable
as String,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$PaymentUrlModelImplCopyWith<$Res>
implements $PaymentUrlModelCopyWith<$Res> {
factory _$$PaymentUrlModelImplCopyWith(
_$PaymentUrlModelImpl value,
$Res Function(_$PaymentUrlModelImpl) then,
) = __$$PaymentUrlModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String paymentUrl});
}
/// @nodoc
class __$$PaymentUrlModelImplCopyWithImpl<$Res>
extends _$PaymentUrlModelCopyWithImpl<$Res, _$PaymentUrlModelImpl>
implements _$$PaymentUrlModelImplCopyWith<$Res> {
__$$PaymentUrlModelImplCopyWithImpl(
_$PaymentUrlModelImpl _value,
$Res Function(_$PaymentUrlModelImpl) _then,
) : super(_value, _then);
/// Create a copy of PaymentUrlModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? paymentUrl = null}) {
return _then(
_$PaymentUrlModelImpl(
paymentUrl:
null == paymentUrl
? _value.paymentUrl
: paymentUrl // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$PaymentUrlModelImpl implements _PaymentUrlModel {
_$PaymentUrlModelImpl({required this.paymentUrl});
factory _$PaymentUrlModelImpl.fromJson(Map<String, dynamic> json) =>
_$$PaymentUrlModelImplFromJson(json);
@override
final String paymentUrl;
@override
String toString() {
return 'PaymentUrlModel(paymentUrl: $paymentUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$PaymentUrlModelImpl &&
(identical(other.paymentUrl, paymentUrl) ||
other.paymentUrl == paymentUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, paymentUrl);
/// Create a copy of PaymentUrlModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$PaymentUrlModelImplCopyWith<_$PaymentUrlModelImpl> get copyWith =>
__$$PaymentUrlModelImplCopyWithImpl<_$PaymentUrlModelImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$PaymentUrlModelImplToJson(this);
}
}
abstract class _PaymentUrlModel implements PaymentUrlModel {
factory _PaymentUrlModel({required final String paymentUrl}) =
_$PaymentUrlModelImpl;
factory _PaymentUrlModel.fromJson(Map<String, dynamic> json) =
_$PaymentUrlModelImpl.fromJson;
@override
String get paymentUrl;
/// Create a copy of PaymentUrlModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$PaymentUrlModelImplCopyWith<_$PaymentUrlModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'payment_url_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$PaymentUrlModelImpl _$$PaymentUrlModelImplFromJson(
Map<String, dynamic> json,
) => _$PaymentUrlModelImpl(paymentUrl: json['paymentUrl'] as String);
Map<String, dynamic> _$$PaymentUrlModelImplToJson(
_$PaymentUrlModelImpl instance,
) => <String, dynamic>{'paymentUrl': instance.paymentUrl};

View File

@@ -0,0 +1,66 @@
import 'dart:developer';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class UserCardsCubit extends Cubit<UserCardsState> {
final CardsRepository cardsRepository;
UserCardsCubit({required this.cardsRepository}) : super(UserCardsState());
Future<PaymentUrlModel?> addCard() async {
try {
return await cardsRepository.addCard();
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
return null;
}
Future<void> getCards() async {
try {
if (isClosed) return;
final cards = await cardsRepository.getCards();
emit(state.copyWith(cards: cards));
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
Future<void> markAsDefault({required int id}) async {
final oldCards = [...state.cards];
if (isClosed) return;
final cards =
oldCards.map((card) {
if (card.id == id) {
return card.copyWith(isDefault: true);
}
return card.copyWith(isDefault: false);
}).toList();
emit(state.copyWith(cards: cards));
EasyDebounce.debounce('markAsDefault', Duration(milliseconds: id), () async {
try {
await cardsRepository.markAsDefault(id: id);
} catch (e, st) {
emit(state.copyWith(cards: oldCards));
log(e.toString(), stackTrace: st);
}
});
}
Future<void> deleteCard({required int id}) async {
final oldCards = [...state.cards];
try {
if (isClosed) return;
final cards = oldCards.where((card) => card.id != id).toList();
emit(state.copyWith(cards: cards));
await cardsRepository.deleteCard(id: id);
} catch (e, st) {
emit(state.copyWith(cards: oldCards));
log(e.toString(), stackTrace: st);
}
}
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_cards_state.freezed.dart';
@freezed
class UserCardsState with _$UserCardsState {
factory UserCardsState({
@Default([]) List<CardModel> cards,
@Default(false) bool isLoading,
}) = _UserCardsState;
}

View File

@@ -0,0 +1,186 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'user_cards_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$UserCardsState {
List<CardModel> get cards => throw _privateConstructorUsedError;
bool get isLoading => throw _privateConstructorUsedError;
/// Create a copy of UserCardsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserCardsStateCopyWith<UserCardsState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserCardsStateCopyWith<$Res> {
factory $UserCardsStateCopyWith(
UserCardsState value,
$Res Function(UserCardsState) then,
) = _$UserCardsStateCopyWithImpl<$Res, UserCardsState>;
@useResult
$Res call({List<CardModel> cards, bool isLoading});
}
/// @nodoc
class _$UserCardsStateCopyWithImpl<$Res, $Val extends UserCardsState>
implements $UserCardsStateCopyWith<$Res> {
_$UserCardsStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UserCardsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? cards = null, Object? isLoading = null}) {
return _then(
_value.copyWith(
cards:
null == cards
? _value.cards
: cards // ignore: cast_nullable_to_non_nullable
as List<CardModel>,
isLoading:
null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$UserCardsStateImplCopyWith<$Res>
implements $UserCardsStateCopyWith<$Res> {
factory _$$UserCardsStateImplCopyWith(
_$UserCardsStateImpl value,
$Res Function(_$UserCardsStateImpl) then,
) = __$$UserCardsStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<CardModel> cards, bool isLoading});
}
/// @nodoc
class __$$UserCardsStateImplCopyWithImpl<$Res>
extends _$UserCardsStateCopyWithImpl<$Res, _$UserCardsStateImpl>
implements _$$UserCardsStateImplCopyWith<$Res> {
__$$UserCardsStateImplCopyWithImpl(
_$UserCardsStateImpl _value,
$Res Function(_$UserCardsStateImpl) _then,
) : super(_value, _then);
/// Create a copy of UserCardsState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? cards = null, Object? isLoading = null}) {
return _then(
_$UserCardsStateImpl(
cards:
null == cards
? _value._cards
: cards // ignore: cast_nullable_to_non_nullable
as List<CardModel>,
isLoading:
null == isLoading
? _value.isLoading
: isLoading // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$UserCardsStateImpl implements _UserCardsState {
_$UserCardsStateImpl({
final List<CardModel> cards = const [],
this.isLoading = false,
}) : _cards = cards;
final List<CardModel> _cards;
@override
@JsonKey()
List<CardModel> get cards {
if (_cards is EqualUnmodifiableListView) return _cards;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_cards);
}
@override
@JsonKey()
final bool isLoading;
@override
String toString() {
return 'UserCardsState(cards: $cards, isLoading: $isLoading)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserCardsStateImpl &&
const DeepCollectionEquality().equals(other._cards, _cards) &&
(identical(other.isLoading, isLoading) ||
other.isLoading == isLoading));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_cards),
isLoading,
);
/// Create a copy of UserCardsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserCardsStateImplCopyWith<_$UserCardsStateImpl> get copyWith =>
__$$UserCardsStateImplCopyWithImpl<_$UserCardsStateImpl>(
this,
_$identity,
);
}
abstract class _UserCardsState implements UserCardsState {
factory _UserCardsState({final List<CardModel> cards, final bool isLoading}) =
_$UserCardsStateImpl;
@override
List<CardModel> get cards;
@override
bool get isLoading;
/// Create a copy of UserCardsState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserCardsStateImplCopyWith<_$UserCardsStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -1,21 +1,35 @@
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/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart';
import 'package:eatvio_app/features/my_finance/presentation/widgets/card_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:collection/collection.dart';
class CardSettingsScreen extends StatelessWidget {
static const path = '/card-settings-screen';
static const name = 'cardSettingsScreen';
const CardSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => sl<UserCardsCubit>()..getCards(), child: CardsSettingsContent());
}
}
class CardsSettingsContent extends StatelessWidget {
CardsSettingsContent({super.key});
@override
Widget build(BuildContext context) {
final s = S.of(context);
return Scaffold(
backgroundColor: Color(0xFFF0F2F5),
appBar: AppBar(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(12),
),
),
title: Text('Мои карты'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))),
title: Text(s.my_cards),
centerTitle: true,
),
body: Column(
@@ -30,41 +44,42 @@ class CardSettingsScreen extends StatelessWidget {
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Привязанные карты',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1,
),
),
child: Text('Привязанные карты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w600, height: 1)),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Выплату будут производиться\nна карту ** 0278',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1,
),
child: BlocBuilder<UserCardsCubit, UserCardsState>(
builder: (context, state) {
final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber;
if (defaultCardNumber != null) {
return Text(
'Выплату будут производиться\nна карту ${defaultCardNumber.substring(defaultCardNumber.length - 7)}',
style: TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, height: 1),
);
}
return Text(s.no_default_cards, style: TextStyles.textMMedium.copyWith(color: appTheme.black));
},
),
),
const SizedBox(height: 16),
SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
child: Row(
children: [
_card(true),
const SizedBox(width: 8),
_card(),
const SizedBox(width: 8),
_card(),
],
child: BlocBuilder<UserCardsCubit, UserCardsState>(
builder: (context, state) {
if (state.isLoading) {}
return Row(
spacing: 12,
children: List.generate(state.cards.length + 1, (index) {
if (index < state.cards.length) {
final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7);
return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber);
}
return CardWidget(enabledAddCard: true);
}),
);
},
),
),
const SizedBox(height: 16),
@@ -75,40 +90,4 @@ class CardSettingsScreen extends StatelessWidget {
),
);
}
_card([bool isSelected = false]) {
return Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? appTheme.blueA400 : Color(0xFFF1F2F6),
width: 2,
),
),
child: Container(
width: 148,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
color: Color(0xFFF1F2F6),
borderRadius: BorderRadius.circular(4),
),
child: Column(
children: [
SvgPicture.asset('assets/icons/mir.svg'),
const SizedBox(height: 24),
Text(
'** 0278',
style: TextStyle(
color: Color(0xFF323F4B),
fontSize: 14,
height: 1,
fontWeight: FontWeight.w600,
),
),
],
),
),
);
}
}

View File

@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/my_finance/entities/consultant_finance_entity.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@@ -13,11 +14,7 @@ class UserFinanceScreen extends StatelessWidget {
return Scaffold(
backgroundColor: Color(0xFFF0F2F5),
appBar: AppBar(
shape: RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.vertical(
bottom: Radius.circular(12),
),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))),
title: Text('История операций'),
centerTitle: true,
),
@@ -28,26 +25,16 @@ class UserFinanceScreen extends StatelessWidget {
const SizedBox(height: 8),
InkWell(
onTap: () {
GoRouter.of(context).pushNamed('cardSettingsScreen');
GoRouter.of(context).pushNamed(CardSettingsScreen.name);
},
borderRadius: BorderRadius.circular(20),
child: Ink(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
width: MediaQuery.of(context).size.width - 32,
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Управление картами',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: Text('Управление картами', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
),
),
),
@@ -62,15 +49,13 @@ class UserFinanceScreen extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
child: Column(
children: [
CustomDropDownButton(
items: [],
onChanged: (value) {},
),
CustomDropDownButton(items: [], onChanged: (value) {}),
const SizedBox(height: 12),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Material(
itemBuilder:
(context, index) => Material(
color: Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
child: Padding(
@@ -83,51 +68,28 @@ class UserFinanceScreen extends StatelessWidget {
children: [
Text(
testFinanceEntity.consultationsList[index].id,
style: TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontWeight: FontWeight.w500,
),
style: TextStyle(color: Color(0xFF323F4B), height: 1, fontWeight: FontWeight.w500),
),
Text(
testFinanceEntity.consultationsList[index].date.format('dd.MM.yyy', 'RU'),
style: TextStyle(
color: Color(0xFF6D6F71),
height: 1,
fontWeight: FontWeight.w500,
),
style: TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500),
),
],
),
const SizedBox(height: 8),
Text(
testFinanceEntity.consultationsList[index].title,
style: TextStyle(
color: appTheme.blueA400,
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
Text(
'Покупатель: ' + testFinanceEntity.consultationsList[index].clientName,
style: TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Text(
testFinanceEntity.consultationsList[index].planDuration.toString() + ' дней',
style: TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
style: TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Row(
@@ -135,12 +97,7 @@ class UserFinanceScreen extends StatelessWidget {
children: [
Text(
testFinanceEntity.consultationsList[index].price.toString() + ' ₽',
style: TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
style: TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600),
),
Material(
color: Color(0xFF0BB450).withAlpha(11),
@@ -149,12 +106,7 @@ class UserFinanceScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
testFinanceEntity.consultationsList[index].status.name,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF0BB450),
height: 1,
),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w400, color: Color(0xFF0BB450), height: 1),
),
),
),

View File

@@ -0,0 +1,68 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:url_launcher/url_launcher.dart';
class CardWidget extends StatelessWidget {
const CardWidget({super.key, this.isSelected = false, this.enabledAddCard = false, this.cardId = 0, this.cardNumber = ''});
final int cardId;
final bool isSelected;
final bool enabledAddCard;
final String cardNumber;
@override
Widget build(BuildContext context) {
final s = S.of(context);
return GestureDetector(
onTap: enabledAddCard
? () async {
final addCardUrl = await context.read<UserCardsCubit>().addCard();
if (addCardUrl != null) {
final uri = Uri.parse(addCardUrl.paymentUrl);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
}
: () {
context.read<UserCardsCubit>().markAsDefault(id: cardId);
},
child: Container(
padding: EdgeInsets.all(4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(color: isSelected ? appTheme.blueA400 : Color(0xFFF1F2F6), width: 2),
),
child: enabledAddCard
? Container(
width: 148,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)),
child: Column(
children: [
SvgPicture.asset('assets/icons/add_card.svg'),
const SizedBox(height: 24),
Text(s.new_card, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
],
),
)
: Container(
width: 148,
padding: EdgeInsets.all(10),
decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)),
child: Column(
children: [
SvgPicture.asset('assets/icons/card.svg'),
const SizedBox(height: 24),
Text(cardNumber, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,39 @@
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart';
import 'package:eatvio_app/features/my_finance/services/cards_service.dart';
class CardsRepository {
final CardsService cardsService;
CardsRepository({required this.cardsService});
Future<PaymentUrlModel> addCard() async {
try {
return await cardsService.addCard();
} catch (e) {
rethrow;
}
}
Future<List<CardModel>> getCards() async {
try {
return await cardsService.getCards();
} catch (e) {
rethrow;
}
}
Future<void> markAsDefault({required int id}) async {
try {
await cardsService.markAsDefault(id: id);
} catch (e) {
rethrow;
}
}
Future<void> deleteCard({required int id}) async {
try {
await cardsService.deleteCard(id: id);
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,28 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart';
import 'package:retrofit/retrofit.dart';
part 'cards_service.g.dart';
@RestApi()
abstract class CardsService {
factory CardsService(Dio dio, {String baseUrl}) = _CardsService;
@GET('/api/finances/cards')
Future<List<CardModel>> getCards();
@POST('/api/finances/cards/add')
Future<PaymentUrlModel> addCard();
@POST('/api/finances/cards/{id}/default')
Future<void> markAsDefault({
@Path('id') required int id,
});
@POST('/api/finances/cards/{id}/remove')
Future<void> deleteCard({
@Path('id') required int id,
});
}

View File

@@ -0,0 +1,141 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'cards_service.dart';
// **************************************************************************
// RetrofitGenerator
// **************************************************************************
// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations
class _CardsService implements CardsService {
_CardsService(this._dio, {this.baseUrl, this.errorLogger});
final Dio _dio;
String? baseUrl;
final ParseErrorLogger? errorLogger;
@override
Future<List<CardModel>> getCards() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<List<CardModel>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'/api/finances/cards',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<List<dynamic>>(_options);
late List<CardModel> _value;
try {
_value =
_result.data!
.map((dynamic i) => CardModel.fromJson(i as Map<String, dynamic>))
.toList();
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<PaymentUrlModel> addCard() async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<PaymentUrlModel>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'/api/finances/cards/add',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late PaymentUrlModel _value;
try {
_value = PaymentUrlModel.fromJson(_result.data!);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
return _value;
}
@override
Future<void> markAsDefault({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/finances/cards/${id}/default',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
await _dio.fetch<void>(_options);
}
@override
Future<void> deleteCard({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/finances/cards/${id}/remove',
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 ||
requestOptions.responseType == ResponseType.stream)) {
if (T == String) {
requestOptions.responseType = ResponseType.plain;
} else {
requestOptions.responseType = ResponseType.json;
}
}
return requestOptions;
}
String _combineBaseUrls(String dioBaseUrl, String? baseUrl) {
if (baseUrl == null || baseUrl.trim().isEmpty) {
return dioBaseUrl;
}
final url = Uri.parse(baseUrl);
if (url.isAbsolute) {
return url.toString();
}
return Uri.parse(dioBaseUrl).resolveUri(url).toString();
}
}

View File

@@ -363,6 +363,7 @@ class MessageLookup extends MessageLookupByLibrary {
"Что то пошло не так!",
),
"my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"),
"my_cards": MessageLookupByLibrary.simpleMessage("Мои карты"),
"my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"),
"my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"),
"my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"),
@@ -372,9 +373,13 @@ class MessageLookup extends MessageLookupByLibrary {
"name_short": MessageLookupByLibrary.simpleMessage(
"В имени пользователя должно быть минимум 6 символов",
),
"new_card": MessageLookupByLibrary.simpleMessage("Добавить карту"),
"nicotine": MessageLookupByLibrary.simpleMessage("Никотин"),
"no": MessageLookupByLibrary.simpleMessage("Нет"),
"no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"),
"no_default_cards": MessageLookupByLibrary.simpleMessage(
"У вас нет привязанных карт. Что бы начать работу, привяжите карту",
),
"not_liked_groceries": MessageLookupByLibrary.simpleMessage(
"Не любимые продукты",
),

View File

@@ -2631,6 +2631,26 @@ class S {
args: [],
);
}
/// `Мои карты`
String get my_cards {
return Intl.message('Мои карты', name: 'my_cards', desc: '', args: []);
}
/// `Добавить карту`
String get new_card {
return Intl.message('Добавить карту', name: 'new_card', desc: '', args: []);
}
/// `У вас нет привязанных карт. Что бы начать работу, привяжите карту`
String get no_default_cards {
return Intl.message(
'У вас нет привязанных карт. Что бы начать работу, привяжите карту',
name: 'no_default_cards',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<S> {

View File

@@ -324,7 +324,10 @@
"more_about_order": "Подробнее о заказе",
"cancel_order": "Отменить заказ",
"refuse_order":"Отказаться от заказа",
"user_info": "Анкета пользователя"
"user_info": "Анкета пользователя",
"my_cards": "Мои карты",
"new_card": "Добавить карту",
"no_default_cards": "У вас нет привязанных карт. Что бы начать работу, привяжите карту"
}

View File

@@ -1,6 +1,6 @@
name: eatvio_app
description: A new Flutter project.
version: 1.0.47+47
version: 1.0.49+50
publish_to: 'none'
environment:

30
test/widget_test.dart Normal file
View File

@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:eatvio_app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}