forked from eatvio/eatvio_app
Compare commits
58 Commits
diary
...
diaryFixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c3cc199c0 | ||
|
|
2584a70a09 | ||
|
|
69359b01c5 | ||
|
|
ff40e74491 | ||
|
|
0270c940fe | ||
|
|
cacfaff4cf | ||
|
|
e728b0951a | ||
|
|
cf6ad1eafe | ||
|
|
bbba15b3c7 | ||
|
|
c5e0d233f0 | ||
|
|
da8d21bb08 | ||
|
|
c361e12d35 | ||
|
|
7c354cc0a3 | ||
|
|
f0a184b800 | ||
|
|
8a9a913420 | ||
|
|
74ecbbdbff | ||
|
|
c95b610c35 | ||
|
|
35fc032d31 | ||
|
|
1cb824573b | ||
|
|
77c52a50e1 | ||
|
|
7b05c7bd36 | ||
|
|
27cea212b0 | ||
|
|
252538f52d | ||
|
|
88bf79def4 | ||
|
|
7f29ade548 | ||
|
|
d405be1ea4 | ||
|
|
10ef59f6ea | ||
|
|
ee7b03c504 | ||
|
|
9f0ce55d47 | ||
|
|
5d1a1dccc8 | ||
|
|
a90efa8caa | ||
|
|
1db528838b | ||
|
|
3addd9d238 | ||
|
|
d645910554 | ||
|
|
820b4d0a8b | ||
|
|
cd4ca7288c | ||
|
|
ac46cd159b | ||
|
|
dc1e8d5b71 | ||
|
|
db73865278 | ||
|
|
2cd4054f29 | ||
|
|
b9611bb5a1 | ||
|
|
036aca7d08 | ||
|
|
8f9aeb7b63 | ||
|
|
38031c89c2 | ||
| 64bf6e33d7 | |||
|
|
b80b4e3a9e | ||
|
|
566a96b18e | ||
|
|
242cb474cc | ||
|
|
67bf7b0510 | ||
|
|
51d3c1ad2f | ||
|
|
f38eead280 | ||
|
|
cf5b459756 | ||
|
|
29b3de325c | ||
|
|
1e1005a09c | ||
|
|
7df1323f7d | ||
|
|
8f70e9620b | ||
|
|
50b63fea7d | ||
|
|
1dfaa7cd93 |
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.29.1"
|
||||
flutter_version: "3.32.0"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test on custom docker
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.29.1"
|
||||
flutter_version: "3.32.0"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.29.3"
|
||||
flutter_version: "3.32.0"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-all.zip
|
||||
|
||||
@@ -20,7 +20,7 @@ plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.7.3" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.3.15" apply false
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
|
||||
}
|
||||
|
||||
1
assets/lottie_animations/loading.json
Normal file
1
assets/lottie_animations/loading.json
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1,2 +1,3 @@
|
||||
extensions:
|
||||
- provider: true
|
||||
- provider: true
|
||||
- shared_preferences: true
|
||||
@@ -388,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -538,7 +538,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -576,7 +576,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
CURRENT_PROJECT_VERSION = 24;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>16</string>
|
||||
<string>24</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -32,7 +32,7 @@ import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/consultant_finance_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
|
||||
@@ -40,9 +40,11 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/about_recipe_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
|
||||
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
|
||||
@@ -76,8 +78,11 @@ class AppRouter {
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
routes: [
|
||||
// GoRoute(path: 'webView-registration-screen', name: 'webViewRegistrationScreen', builder: (context, state) => const WebViewRegistrationScreen()),
|
||||
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||
GoRoute(
|
||||
path: 'registration-screen',
|
||||
name: 'registrationScreen',
|
||||
builder: (context, state) => const RegistrationScreen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
@@ -95,10 +100,6 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/my-profile-screen',
|
||||
name: 'myProfileScreen',
|
||||
redirect: (context, state) async {
|
||||
await sl<WsManager>().initialize();
|
||||
return null;
|
||||
},
|
||||
pageBuilder: (context, state) {
|
||||
return const NoTransitionPage(child: ProfileScreen());
|
||||
},
|
||||
@@ -108,11 +109,21 @@ class AppRouter {
|
||||
name: 'feedScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
|
||||
redirect: (context, state) async {
|
||||
await sl<WsManager>().initialize();
|
||||
return null;
|
||||
try {
|
||||
await sl<WsManager>().initialize();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
return '/feed-screen';
|
||||
},
|
||||
),
|
||||
GoRoute(path: '/diary-screen', name: 'diaryScreen', pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen())),
|
||||
GoRoute(
|
||||
path: '/diary-screen',
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: DiaryScreen(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
@@ -172,7 +183,7 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/consultant-finance-screen',
|
||||
name: 'consultantFinanceScreen',
|
||||
builder: (context, state) => const ConsultantFinanceScreen(),
|
||||
builder: (context, state) => const ConsultationsArchiveScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/user-finance-screen',
|
||||
@@ -201,7 +212,11 @@ class AppRouter {
|
||||
name: 'aboutRecipesScreen',
|
||||
builder: (context, state) {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
return AboutRecipeScreen(
|
||||
recipe: data['recipe'] as Recipe,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -284,7 +299,12 @@ class AppRouter {
|
||||
),
|
||||
GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
|
||||
GoRoute(
|
||||
path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', builder: (context, state) => const StepperBecomeConsultantScreen()),
|
||||
path: '/stepper-become-consultant-screen',
|
||||
name: 'stepperBecomeConsultantScreen',
|
||||
builder: (context, state) => StepperBecomeConsultantScreen(
|
||||
slug: state.extra == null ? null : state.extra as String,
|
||||
),
|
||||
),
|
||||
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
|
||||
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
|
||||
GoRoute(
|
||||
@@ -344,9 +364,12 @@ class AppRouter {
|
||||
path: UserDiaryScreen.path,
|
||||
name: UserDiaryScreen.name,
|
||||
builder: (context, state) {
|
||||
final diaryId = state.extra as int;
|
||||
final data = state.extra as Map<String, int>;
|
||||
final diaryId = data['diaryId'] as int;
|
||||
final currentPlanDay = data['currentDiaryDay'] as int;
|
||||
return UserDiaryScreen(
|
||||
diaryId: diaryId,
|
||||
currentPlanDay: currentPlanDay,
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -355,6 +378,16 @@ class AppRouter {
|
||||
name: 'verifyScreen',
|
||||
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/verify-email-screen',
|
||||
name: 'verifyEmailScreen',
|
||||
builder: (context, state) => const VerifyEmailScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: MyRecipesScreen.path,
|
||||
name: MyRecipesScreen.name,
|
||||
builder: (context, state) => const MyRecipesScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -13,6 +14,8 @@ class PhoneVerifyErrorInterceptor extends Interceptor {
|
||||
if (data is Map<String, dynamic> && data['type'] == 'unverificated_phone') {
|
||||
final String phone = await sl<ProfileRepository>().getMyPhone();
|
||||
final String passKey = await sl<ProfileRepository>().getPassKey(phone);
|
||||
await showErrorDialog(AppRouter.navigatorKey.currentContext!,
|
||||
title: 'Привяжите номер телефона', description: 'Для того, что бы выполнить это действие необходимо привязать номер телефона');
|
||||
AppRouter.navigatorKey.currentContext?.pushNamed('verifyScreen', extra: passKey);
|
||||
return handler.reject(
|
||||
DioException(
|
||||
|
||||
13
lib/core/enums/filters_comparison_enum.dart
Normal file
13
lib/core/enums/filters_comparison_enum.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
enum FiltersComparisonEnum {
|
||||
equal('\$eq'),
|
||||
notEqual('\$ne'),
|
||||
greaterThan('\$gt'),
|
||||
lessThan('\$lt'),
|
||||
greaterThanOrEqual('\$gte'),
|
||||
lessThanOrEqual('\$lte'),
|
||||
inArray('\$in'),
|
||||
notInArray('\$notIn');
|
||||
|
||||
final String value;
|
||||
const FiltersComparisonEnum(this.value);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import 'package:eatvio_app/features/complaining/services/complaining_service.dar
|
||||
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
|
||||
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
|
||||
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
|
||||
@@ -81,9 +82,13 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip
|
||||
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_service.dart';
|
||||
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
|
||||
@@ -115,9 +120,16 @@ Future<void> init() async {
|
||||
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
|
||||
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
|
||||
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl()));
|
||||
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
|
||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||
sl.registerFactory<DiaryCubit>(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl(), profileRepository: sl(), dietRepository: sl()));
|
||||
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
|
||||
chatRepository: sl(),
|
||||
diaryRepository: sl(),
|
||||
glossaryRepository: sl(),
|
||||
profileRepository: sl(),
|
||||
dietRepository: sl(),
|
||||
settingsRepository: sl(),
|
||||
));
|
||||
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
|
||||
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
|
||||
@@ -138,6 +150,9 @@ Future<void> init() async {
|
||||
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
|
||||
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
|
||||
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
@@ -165,6 +180,7 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
|
||||
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
|
||||
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
|
||||
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
@@ -193,6 +209,7 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
|
||||
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
|
||||
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
|
||||
@@ -101,8 +101,7 @@ class PrimaryColors {
|
||||
// Blue
|
||||
Color get blue400 => const Color(0XFF43A6F2);
|
||||
|
||||
Color get blueA400 => const Color(0XFF2F80ED);
|
||||
|
||||
Color get blueA400 => const Color(0XFF2F80ED);
|
||||
// BlueGray
|
||||
Color get blueGray400 => const Color(0XFF7B8794);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ class WsManager {
|
||||
PrivateChannel? _privateDirectChatChannel;
|
||||
|
||||
Stream<ChannelReadEvent>? get newMessagesStream => _privateMessagesChannel?.bind('App\\Events\\ChatMessageUpdated');
|
||||
|
||||
|
||||
Stream<ChannelReadEvent>? get newNotificationsStream => _privateNotificationChannel?.bind('Illuminate\\Notifications\\Events\\BroadcastNotificationCreated');
|
||||
|
||||
Stream<ChannelReadEvent>? get directChatStream => _privateDirectChatChannel?.bind('App\\Events\\ChatMessageUpdated');
|
||||
@@ -135,4 +135,16 @@ class WsManager {
|
||||
void unsubscribeDirectChatChannel() {
|
||||
_privateDirectChatChannel?.unsubscribe();
|
||||
}
|
||||
|
||||
Future<void> clear() async{
|
||||
await client?.disconnect();
|
||||
client = null;
|
||||
userId = null;
|
||||
_privateDirectChatChannel?.unsubscribe();
|
||||
_privateMessagesChannel?.unsubscribe();
|
||||
_privateNotificationChannel?.unsubscribe();
|
||||
_privateDirectChatChannel = null;
|
||||
_privateMessagesChannel = null;
|
||||
_privateNotificationChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,11 @@ import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit/blog_post_create_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/presentation/complaining_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -100,37 +97,10 @@ class CustomPopup extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
});
|
||||
addItem('assets/icons/edit_paint.svg', 'Редактировать', () async {
|
||||
if (complainedContentType == ComplainedContent.posts) {
|
||||
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel);
|
||||
} else if (complainedContentType == ComplainedContent.recipe) {
|
||||
GoRouter.of(context).pushNamed(RecipeCreateScreen.name, extra: recipe);
|
||||
} else if (complainedContentType == ComplainedContent.diet) {
|
||||
await showErrorDialog(context,
|
||||
title: 'Предупреждение', description: 'Диета будет перенесена в архив. Вы создаете новую диету на основе существующей.');
|
||||
await sl<MyDietsCubit>().unpublishDiet(id!);
|
||||
GoRouter.of(context).pushNamed(CreateDietScreen.name, extra: diet);
|
||||
} else {
|
||||
showInfoToast('В разработке');
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (complainedContentType == ComplainedContent.plan || complainedContentType == ComplainedContent.diet) {
|
||||
addItem('assets/icons/remove_from_public.svg', 'Снять с публикации', () async {
|
||||
if (complainedContentType == ComplainedContent.diet) {
|
||||
final result = await sl<MyDietsCubit>().unpublishDiet(id!);
|
||||
if (!result) {
|
||||
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
|
||||
return;
|
||||
}
|
||||
showSuccessToast('Диета перенесена в архив, обновите ленту');
|
||||
} else if (complainedContentType == ComplainedContent.plan) {
|
||||
final result = await sl<MyPlansCubit>().unpublish(slug!);
|
||||
if (!result) {
|
||||
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
|
||||
return;
|
||||
}
|
||||
showSuccessToast('План перенесен в архив, обновите ленту');
|
||||
if (complainedContentType == ComplainedContent.posts) {
|
||||
addItem('assets/icons/edit_paint.svg', 'Редактировать', () async {
|
||||
if (complainedContentType == ComplainedContent.posts) {
|
||||
GoRouter.of(context).pushNamed('blogPostCreateScreen', extra: feedBlogPostModel);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -145,21 +115,21 @@ class CustomPopup extends StatelessWidget {
|
||||
} else if (complainedContentType == ComplainedContent.recipe) {
|
||||
final result = await sl<RecipesCubit>().deleteRecipe(slug!);
|
||||
if (!result) {
|
||||
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
|
||||
showErrorDialog(context, title: 'Не удалось удалить рецепт');
|
||||
return;
|
||||
}
|
||||
showSuccessToast('Рецепт удален, обновите ленту');
|
||||
} else if (complainedContentType == ComplainedContent.diet) {
|
||||
final result = await sl<DietsCubit>().deleteDiet(id!);
|
||||
if (!result) {
|
||||
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
|
||||
showErrorDialog(context, title: 'Не удалось удалить диету');
|
||||
return;
|
||||
}
|
||||
showSuccessToast('Диета удалена, обновите ленту');
|
||||
} else if (complainedContentType == ComplainedContent.plan) {
|
||||
final result = await sl<MyPlansCubit>().deletePlan(slug!);
|
||||
if (!result) {
|
||||
showErrorDialog(context, title: S.current.could_not_delete_blog_post);
|
||||
showErrorDialog(context, title: 'Не удалось удалить план');
|
||||
return;
|
||||
}
|
||||
showSuccessToast('План удален, обновите ленту');
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rive/rive.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class Loader extends StatelessWidget {
|
||||
const Loader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Center(
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: RiveAnimation.asset(
|
||||
'assets/rive_animations/loading.riv',
|
||||
height: 42,
|
||||
width: 42,
|
||||
child: Lottie.asset(
|
||||
'assets/lottie_animations/loading.json',
|
||||
width: 42,
|
||||
height: 42,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/core/app_router.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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
|
||||
class AuthCubit extends Cubit<AuthState> {
|
||||
final AuthRepository authRepository;
|
||||
@@ -28,9 +27,7 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
token: token,
|
||||
),
|
||||
);
|
||||
showSuccessToast('Успешный вход');
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen');
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
|
||||
@@ -5,10 +5,13 @@ import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.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/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@@ -49,6 +52,20 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _reconnectWebSocket(BuildContext context) async {
|
||||
await sl<WsManager>().clear();
|
||||
await sl<WsManager>().initialize();
|
||||
if (context.mounted) {
|
||||
context.read<ProfileAvatarCubit>().getMyProfile();
|
||||
context.read<ChatsCubit>().getChatRoomsWithLoading();
|
||||
context.read<ChatsCubit>().connectToWebSocket();
|
||||
context.read<NotificationsCubit>().getNotifications();
|
||||
context.read<NotificationsCubit>().connectToWebSocket();
|
||||
showSuccessToast('Успешный вход');
|
||||
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
@@ -179,8 +196,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
);
|
||||
if (isAuthenticated) {
|
||||
if (context.mounted) {
|
||||
context.read<ProfileAvatarCubit>().getMyProfile();
|
||||
context.read<ChatsCubit>().getChatRoomsWithLoading();
|
||||
_reconnectWebSocket(context);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -186,7 +186,7 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
foregroundImage: NetworkImage(
|
||||
widget.blogPost.author.authorProfileIcon,
|
||||
baseUrl + widget.blogPost.author.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
|
||||
@@ -12,7 +12,11 @@ class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
||||
getMyProfile() async {
|
||||
emit(const ProfileAvatarState.loading());
|
||||
try {
|
||||
emit(ProfileAvatarState.loaded(profile: await profileRepository.getMyProfile()));
|
||||
emit(
|
||||
ProfileAvatarState.loaded(
|
||||
profile: await profileRepository.getMyProfile(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||
@@ -81,7 +82,7 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
BottomNavigationBarItem(
|
||||
label: '',
|
||||
icon: CircleAvatar(
|
||||
foregroundImage: NetworkImage(widget.profileModel.profilePhotoUrl),
|
||||
foregroundImage: NetworkImage(baseUrl + widget.profileModel.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: 16,
|
||||
),
|
||||
@@ -202,9 +203,10 @@ class _BottomNavigationBarWidgetState extends State<BottomNavigationBarWidget> {
|
||||
return badges.Badge(
|
||||
position: badges.BadgePosition.topEnd(),
|
||||
badgeAnimation: const badges.BadgeAnimation.fade(),
|
||||
badgeContent: Text(
|
||||
(state is NotificationsStateLoaded) ? state.notificationsCount.toString() : '0',
|
||||
style: const TextStyle(
|
||||
showBadge: state is NotificationsStateLoaded && state.hasUnreadNotifications,
|
||||
badgeContent: const Text(
|
||||
'',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 8,
|
||||
),
|
||||
|
||||
@@ -28,8 +28,22 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
int currentPage = 1;
|
||||
late int lastPage;
|
||||
bool isLoading = false;
|
||||
Timer? _timer;
|
||||
|
||||
ChatRoomCubit({required this.chatRepository, required this.wsManager, required this.cardsRepository}) : super(const ChatRoomState.loading());
|
||||
ChatRoomCubit({
|
||||
required this.chatRepository,
|
||||
required this.wsManager,
|
||||
required this.cardsRepository,
|
||||
}) : super(const ChatRoomState.loading());
|
||||
|
||||
void setTimer() {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 5), (_) => updateChosenPlanState());
|
||||
}
|
||||
|
||||
void cancelTimer() {
|
||||
_timer?.cancel();
|
||||
}
|
||||
|
||||
loadNextPage(String chatId) async {
|
||||
if (currentPage < lastPage && !isLoading) {
|
||||
@@ -94,13 +108,9 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
),
|
||||
);
|
||||
|
||||
if (chatRoom.chosenPlan?.consultantId == chatRoom.users.receiver.id) {
|
||||
if (chosenPlan?.consultantId == chatRoom.users.sender.id) {
|
||||
await checkCards();
|
||||
}
|
||||
|
||||
if (sendInitialMessage) {
|
||||
await chatRepository.sendMessage(chatId: chatId, data: 'Здравствуйте, хочу заказать у вас: ${chosenPlan?.plan.name ?? 'план'}');
|
||||
}
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
typingSubscription?.cancel();
|
||||
@@ -237,6 +247,7 @@ class ChatRoomCubit extends Cubit<ChatRoomState> {
|
||||
@override
|
||||
Future<void> close() async {
|
||||
wsManager.unsubscribeDirectChatChannel();
|
||||
_timer?.cancel();
|
||||
messagesSubscription?.cancel();
|
||||
typingSubscription?.cancel();
|
||||
super.close();
|
||||
|
||||
@@ -14,18 +14,14 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
final ChatRepository chatRepository;
|
||||
final ProfileRepository profileRepository;
|
||||
final WsManager wsManager;
|
||||
late StreamSubscription? newIncomingMessagesSubscription;
|
||||
late StreamSubscription? newLastMessageSubscription;
|
||||
StreamSubscription? newIncomingMessagesSubscription;
|
||||
StreamSubscription? newLastMessageSubscription;
|
||||
|
||||
ChatsCubit({
|
||||
required this.chatRepository,
|
||||
required this.wsManager,
|
||||
required this.profileRepository,
|
||||
}) : super(ChatsState()) {
|
||||
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
|
||||
_handleNewMessageViaSocket(event.data);
|
||||
});
|
||||
|
||||
}) : super(ChatsState()) {
|
||||
newLastMessageSubscription = chatRepository.newUserMessagesStream.listen((message) {
|
||||
_setNewLastMessage(message);
|
||||
});
|
||||
@@ -33,6 +29,13 @@ class ChatsCubit extends Cubit<ChatsState> {
|
||||
String? _query;
|
||||
int page = 1;
|
||||
late int lastPage;
|
||||
Future<void> connectToWebSocket() async {
|
||||
await newIncomingMessagesSubscription?.cancel();
|
||||
newIncomingMessagesSubscription = wsManager.newMessagesStream?.listen((event) {
|
||||
_handleNewMessageViaSocket(event.data);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> getChatRoomsWithLoading({String? query}) async {
|
||||
page = 1;
|
||||
_query = query;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
@@ -43,12 +44,24 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final List<({int id, int index})> unreadMessagesIds = [];
|
||||
final List<({int id, int index})> unreadMessagesIdsCopy = [];
|
||||
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
|
||||
late AppLifecycleListener _listener;
|
||||
|
||||
void _onHide() {
|
||||
chatRoomCubit.cancelTimer();
|
||||
}
|
||||
|
||||
void _onResume() {
|
||||
chatRoomCubit.updateChosenPlanState();
|
||||
chatRoomCubit.setTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController.addListener(_onScroll);
|
||||
chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false);
|
||||
chatRoomCubit.setTimer();
|
||||
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
|
||||
}
|
||||
|
||||
_onScroll() {
|
||||
@@ -60,6 +73,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
_listener.dispose();
|
||||
chatRoomCubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -95,7 +109,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
final showAboutOrderButton =
|
||||
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
|
||||
BannerType? bannerType;
|
||||
if (chosenPlan == null) {
|
||||
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
bannerType = null;
|
||||
} else if (amIConsultant && !state.hasCard) {
|
||||
bannerType = BannerType.card;
|
||||
@@ -104,7 +118,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
} else if (!amIConsultant &&
|
||||
(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) {
|
||||
bannerType = BannerType.payment;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone) {
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) {
|
||||
bannerType = BannerType.collectCharge;
|
||||
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
|
||||
bannerType = BannerType.finishPlan;
|
||||
@@ -141,7 +155,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
|
||||
foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
|
||||
@@ -302,17 +316,17 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (bannerType != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: NotificationBanner(
|
||||
bannerType: bannerType,
|
||||
chosenPlanId: chosenPlan?.id,
|
||||
planName: chosenPlan?.plan.name,
|
||||
planPrice: chosenOption?.price.toDouble(),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (bannerType != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: NotificationBanner(
|
||||
bannerType: bannerType,
|
||||
chosenPlanId: chosenPlan?.id,
|
||||
planName: chosenPlan?.plan.name,
|
||||
planPrice: chosenOption?.price.toDouble(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
@@ -41,7 +42,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
|
||||
CircleAvatar(
|
||||
radius: 24,
|
||||
foregroundImage: NetworkImage(
|
||||
widget.chatRoom.users.toChatUser.profilePhotoUrl,
|
||||
baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
backgroundColor: Colors.white,
|
||||
|
||||
@@ -8,9 +8,10 @@ part 'consultation_request_model.g.dart';
|
||||
@freezed
|
||||
abstract class ConsultationRequestModel with _$ConsultationRequestModel {
|
||||
const factory ConsultationRequestModel({
|
||||
@JsonKey(name: 'rating_place') required int ratingPlace,
|
||||
@JsonKey(name: 'rating_place') int? ratingPlace,
|
||||
@JsonKey(name: 'education_resource') required EducationModel educationModel,
|
||||
@JsonKey(name: 'cert_photos') required List<CertPhotosModel> photos,
|
||||
@JsonKey(name: 'canceled_moderation') CanceledModerationModel? canceledModeration,
|
||||
}) = _ConsultationRequestModel;
|
||||
|
||||
factory ConsultationRequestModel.fromJson(Map<String, dynamic> json) => _$ConsultationRequestModelFromJson(json);
|
||||
@@ -24,3 +25,20 @@ abstract class CertPhotosModel with _$CertPhotosModel {
|
||||
|
||||
factory CertPhotosModel.fromJson(Map<String, dynamic> json) => _$CertPhotosModelFromJson(json);
|
||||
}
|
||||
|
||||
enum ModerationStatus {
|
||||
approved,
|
||||
created,
|
||||
canceled
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class CanceledModerationModel with _$CanceledModerationModel {
|
||||
const factory CanceledModerationModel({
|
||||
required String title,
|
||||
required ModerationStatus status,
|
||||
required List<String>? additional,
|
||||
}) = _CanceledModerationModel;
|
||||
|
||||
factory CanceledModerationModel.fromJson(Map<String, dynamic> json) => _$CanceledModerationModelFromJson(json);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
final ConsultantRepository consultantRepository;
|
||||
bool _update = false;
|
||||
|
||||
RegisterConsultantCubit({
|
||||
required this.consultantRepository,
|
||||
@@ -17,12 +19,80 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
socialLinks: SocialLinksModel(),
|
||||
hasMedicalEducation: true,
|
||||
),
|
||||
loading: true,
|
||||
acceptOffer: false,
|
||||
),
|
||||
);
|
||||
|
||||
Future<File> downloadImageAsFile(String imageUrl) async {
|
||||
final response = await http.get(Uri.parse(imageUrl));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final filePath = '${tempDir.path}/${imageUrl.split('/').last}';
|
||||
final file = File(filePath);
|
||||
return await file.writeAsBytes(response.bodyBytes);
|
||||
} else {
|
||||
throw Exception('Не удалось загрузить изображение');
|
||||
}
|
||||
}
|
||||
|
||||
getMyRequestStatus(String slug) async {
|
||||
_update = true;
|
||||
final myRequestStatus = await consultantRepository.getMyRequestStatus(slug);
|
||||
|
||||
final List<String> photos = await Future.wait(
|
||||
myRequestStatus.photos.map(
|
||||
(e) async => (await downloadImageAsFile(e.url)).path,
|
||||
),
|
||||
);
|
||||
|
||||
List<CertificatesModel>? certificates = myRequestStatus.educationModel.completedCourses?.map((courseMap) {
|
||||
final course = courseMap.values.first;
|
||||
|
||||
return CertificatesModel(
|
||||
title: course.title,
|
||||
date: course.date,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
String? sanitizeLink(String? link) {
|
||||
return (link != null && link.isNotEmpty) ? link : null;
|
||||
}
|
||||
|
||||
emit(
|
||||
RegisterConsultantState(
|
||||
consultant: ConsultantProfileModel(
|
||||
photos: photos,
|
||||
certificates: certificates,
|
||||
socialLinks: SocialLinksModel(
|
||||
vkLink: sanitizeLink(myRequestStatus.educationModel.vkLink),
|
||||
tgLink: sanitizeLink(myRequestStatus.educationModel.tgLink),
|
||||
fbLink: sanitizeLink(myRequestStatus.educationModel.fbLink),
|
||||
instaLink: sanitizeLink(myRequestStatus.educationModel.instaLink),
|
||||
personalSiteLink: sanitizeLink(myRequestStatus.educationModel.personalSiteLink),
|
||||
),
|
||||
hasMedicalEducation: myRequestStatus.educationModel.hasMedicalEducation == 1,
|
||||
),
|
||||
acceptOffer: false,
|
||||
loading: false,
|
||||
requestModel: myRequestStatus,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
disableLoading() {
|
||||
emit(state.copyWith(loading: false));
|
||||
}
|
||||
|
||||
updateConsultantProfileModel(ConsultantProfileModel consultant, [bool acceptOffer = false]) {
|
||||
emit(RegisterConsultantState(consultant: consultant, acceptOffer: acceptOffer));
|
||||
emit(
|
||||
state.copyWith(
|
||||
consultant: consultant,
|
||||
acceptOffer: acceptOffer,
|
||||
loading: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
registerConsultant() async {
|
||||
@@ -35,7 +105,7 @@ class RegisterConsultantCubit extends Cubit<RegisterConsultantState> {
|
||||
),
|
||||
);
|
||||
}
|
||||
await consultantRepository.registerConsultant(consultant: state.consultant);
|
||||
await consultantRepository.registerConsultant(consultant: state.consultant, update: _update);
|
||||
}
|
||||
|
||||
Future<List<String>> uploadPhotos(List<File> files) async {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultant_profile_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'register_consultant_state.freezed.dart';
|
||||
@@ -8,5 +9,7 @@ abstract class RegisterConsultantState with _$RegisterConsultantState {
|
||||
const factory RegisterConsultantState({
|
||||
required ConsultantProfileModel consultant,
|
||||
required bool acceptOffer,
|
||||
required bool loading,
|
||||
ConsultationRequestModel? requestModel,
|
||||
}) = _RegisterConsultantState;
|
||||
}
|
||||
|
||||
@@ -30,19 +30,20 @@ class _CertificatesAndDiplomasScreenState extends State<CertificatesAndDiplomasS
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (context.read<RegisterConsultantCubit>().state.consultant.certificates != null) {
|
||||
diplomasList = context
|
||||
.read<RegisterConsultantCubit>()
|
||||
.state
|
||||
.consultant
|
||||
.certificates!
|
||||
.map(
|
||||
(e) => DiplomaEntity(
|
||||
date: DateTime.tryParse(e.date),
|
||||
textEditingController: TextEditingController(text: e.title),
|
||||
enableDeleteButton: true,
|
||||
diplomasList = [
|
||||
...context.read<RegisterConsultantCubit>().state.consultant.certificates!.map(
|
||||
(e) => DiplomaEntity(
|
||||
date: DateTime.tryParse(e.date),
|
||||
textEditingController: TextEditingController(text: e.title),
|
||||
enableDeleteButton: true,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
DiplomaEntity(
|
||||
textEditingController: TextEditingController(),
|
||||
enableDeleteButton: false,
|
||||
date: null,
|
||||
),
|
||||
];
|
||||
diplomasList.last.enableDeleteButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/bloc/register_consultant_cubit/register_consultant_state.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/additional_settings_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/certificates_and_diplomas_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/last_step_screen.dart';
|
||||
@@ -15,7 +18,9 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class StepperBecomeConsultantScreen extends StatefulWidget {
|
||||
const StepperBecomeConsultantScreen({super.key});
|
||||
final String? slug;
|
||||
|
||||
const StepperBecomeConsultantScreen({super.key, this.slug});
|
||||
|
||||
@override
|
||||
State<StepperBecomeConsultantScreen> createState() => _StepperBecomeConsultantScreenState();
|
||||
@@ -25,249 +30,328 @@ class _StepperBecomeConsultantScreenState extends State<StepperBecomeConsultantS
|
||||
double progressValue = 1 / 6;
|
||||
int step = 1;
|
||||
final pageController = PageController();
|
||||
final registrationConsultantCubit = sl<RegisterConsultantCubit>();
|
||||
late final RegisterConsultantCubit registrationConsultantCubit;
|
||||
bool isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.slug == null) {
|
||||
registrationConsultantCubit = sl<RegisterConsultantCubit>()..disableLoading();
|
||||
} else {
|
||||
registrationConsultantCubit = sl<RegisterConsultantCubit>()..getMyRequestStatus(widget.slug!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RegisterConsultantCubit>(
|
||||
create: (context) => registrationConsultantCubit,
|
||||
child: Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: MediaQuery.of(context).size.width * 0.2,
|
||||
padding: const EdgeInsets.only(left: 16, right: 4),
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'backButtonRegistration',
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: const Color(0xFFEAF2FD),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
});
|
||||
},
|
||||
label: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 48,
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
padding: const EdgeInsets.only(left: 4, right: 16),
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'nextButtonRegistration',
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: isLoading ? Colors.grey : appTheme.blueA400,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (step == 6) {
|
||||
if (registrationConsultantCubit.state.acceptOffer) {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
try {
|
||||
await registrationConsultantCubit.registerConsultant();
|
||||
if (!context.mounted) return;
|
||||
await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Спасибо!\nВаши данные отправлены на модерацию',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Lottie.asset(
|
||||
'assets/lottie_animations/success.json',
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Закрыть',
|
||||
child: BlocBuilder<RegisterConsultantCubit, RegisterConsultantState>(
|
||||
builder: (context, state) {
|
||||
if (state.loading == false) {
|
||||
return Scaffold(
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 48,
|
||||
width: MediaQuery.of(context).size.width * 0.2,
|
||||
padding: const EdgeInsets.only(left: 16, right: 4),
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'backButtonRegistration',
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: const Color(0xFFEAF2FD),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
});
|
||||
},
|
||||
label: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (state.requestModel?.educationModel.requestState == ModerationStatus.canceled)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(state.requestModel!.canceledModeration!.title),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < state.requestModel!.canceledModeration!.additional!.length; i++) ...[
|
||||
Text('• ${state.requestModel!.canceledModeration!.additional![i]}'),
|
||||
]
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
height: 48,
|
||||
width: MediaQuery.of(context).size.width * 0.6,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0x33FFCC00),
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Комментарий',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 48,
|
||||
width: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? MediaQuery.of(context).size.width * 0.2
|
||||
: MediaQuery.of(context).size.width * 0.8,
|
||||
padding: const EdgeInsets.only(left: 4, right: 16),
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'nextButtonRegistration',
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
backgroundColor: isLoading
|
||||
? Colors.grey
|
||||
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? const Color(0xFFEAF2FD)
|
||||
: appTheme.blueA400,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () async {
|
||||
if (step == 6) {
|
||||
if (registrationConsultantCubit.state.acceptOffer) {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
try {
|
||||
await registrationConsultantCubit.registerConsultant();
|
||||
if (!context.mounted) return;
|
||||
await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
showDragHandle: true,
|
||||
context: context,
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Спасибо!\nВаши данные отправлены на модерацию',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Lottie.asset(
|
||||
'assets/lottie_animations/success.json',
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'В течении трех рабочих дней Вам придет уведомление о смене статуса',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Закрыть',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Ок'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
content: Text(e.toString()),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Ок'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
content: const Text('Примите договор публичной офёрты'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Ок'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
setState(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
label: isLoading
|
||||
? const CircularProgressIndicator()
|
||||
: state.requestModel?.educationModel.requestState == ModerationStatus.canceled
|
||||
? Icon(
|
||||
Icons.arrow_forward_ios_outlined,
|
||||
color: appTheme.blueA400,
|
||||
)
|
||||
: Text(
|
||||
'Продолжить',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
content: const Text('Примите договор публичной офёрты'),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('Ок'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
setState(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
label: isLoading ? const CircularProgressIndicator() : Text(
|
||||
'Продолжить',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Text('шаг $step из 6'),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 1 / 6,
|
||||
end: progressValue,
|
||||
),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
height: 48,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Text('шаг $step из 6'),
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 1 / 6,
|
||||
end: progressValue,
|
||||
),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
minHeight: 8,
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(appTheme.blueA400),
|
||||
),
|
||||
body: PageView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
step = index + 1;
|
||||
progressValue = step / 6;
|
||||
});
|
||||
},
|
||||
children: const [
|
||||
CertificatesAndDiplomasScreen(),
|
||||
PhotoOrScansScreen(),
|
||||
AdditionalSettingsScreen(),
|
||||
SocialNetworksScreen(),
|
||||
RegistrationAsScreen(),
|
||||
LastStepScreen(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: PageView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
controller: pageController,
|
||||
onPageChanged: (index) {
|
||||
setState(() {
|
||||
step = index + 1;
|
||||
progressValue = step / 6;
|
||||
});
|
||||
},
|
||||
children: const [
|
||||
CertificatesAndDiplomasScreen(),
|
||||
PhotoOrScansScreen(),
|
||||
AdditionalSettingsScreen(),
|
||||
SocialNetworksScreen(),
|
||||
RegistrationAsScreen(),
|
||||
LastStepScreen(),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Scaffold(
|
||||
body: Loader(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,11 +14,18 @@ class ConsultantRepository {
|
||||
|
||||
Future<void> registerConsultant({
|
||||
required ConsultantProfileModel consultant,
|
||||
bool update = false,
|
||||
}) async {
|
||||
try {
|
||||
return await consultantService.consultantRegistration(
|
||||
consultantModel: consultant,
|
||||
);
|
||||
if (update) {
|
||||
return await consultantService.consultantUpdate(
|
||||
consultantModel: consultant,
|
||||
);
|
||||
} else {
|
||||
return await consultantService.consultantRegistration(
|
||||
consultantModel: consultant,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -57,4 +64,12 @@ class ConsultantRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConsultationRequestModel> getMyRequestStatus(String slug) async {
|
||||
try {
|
||||
return await consultantService.getMyRequestStatus(slug: slug);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,18 @@ abstract class ConsultantService {
|
||||
@Part(name: 'photos[]', contentType: 'multipart/form-data') List<MultipartFile>? photos,
|
||||
});
|
||||
|
||||
@GET('/api/consultant_request/{slug}/status')
|
||||
@GET('/api/profile/consultant/{slug}/status')
|
||||
Future<ConsultationRequestModel> getConsultantRequestStatus({
|
||||
@Path() required String slug,
|
||||
});
|
||||
|
||||
@GET('/api/consultant_request/{slug}/status')
|
||||
Future<ConsultationRequestModel> getMyRequestStatus({
|
||||
@Path() required String slug,
|
||||
});
|
||||
|
||||
@PUT('/api/consultant_request/update')
|
||||
Future<void> consultantUpdate({
|
||||
@Body() required ConsultantProfileModel consultantModel,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,12 +10,14 @@ abstract class ConsultationModel with _$ConsultationModel {
|
||||
factory ConsultationModel({
|
||||
required int id,
|
||||
@JsonKey(name: 'last_state') required ChosenPlanStateEnum chosenPlanState,
|
||||
required String planTitle,
|
||||
required String userName,
|
||||
required String userSlug,
|
||||
required double consultantPayment,
|
||||
@JsonKey(name: 'plan_title') required String planTitle,
|
||||
@JsonKey(name: 'user_name') required String userName,
|
||||
@JsonKey(name: 'user_slug') required String userSlug,
|
||||
@JsonKey(name: 'consultant_payment') required double consultantPayment,
|
||||
required int duration,
|
||||
required int currentDiaryDay,
|
||||
@JsonKey(name: 'current_diary_day') required int currentDiaryDay,
|
||||
@JsonKey(name: 'user_profile_photo_url') required String userProfilePhotoUrl,
|
||||
@JsonKey(name: 'consultation_option_price') required int consultationOptionPrice,
|
||||
@JsonKey(name: 'start_date', fromJson: parseDate) required DateTime startDate,
|
||||
@JsonKey(name: 'end_date', fromJson: parseDate) required DateTime endDate,
|
||||
@JsonKey(name: 'consultant_id') required int consultantId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/enums/base_state.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:eatvio_app/features/consultations/presentation/cubit/consultatio
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ConsultationsScreen extends StatelessWidget {
|
||||
static const path = '/consultations-screen';
|
||||
@@ -42,17 +44,53 @@ class ConsultationsContent extends StatelessWidget {
|
||||
),
|
||||
body: switch (state.state) {
|
||||
BaseState.loading => const Loader(),
|
||||
BaseState.error => DataErrorWidget(onReload: () async {}),
|
||||
BaseState.error => DataErrorWidget(onReload: () async {
|
||||
context.read<ConsultationsCubit>().loadConsultations();
|
||||
}),
|
||||
BaseState.initial => const Loader(),
|
||||
BaseState.success => ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
itemBuilder: (context, index) => ConsultationCardWidget(
|
||||
consultationsModel: state.consultations[index],
|
||||
BaseState.success => SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => ConsultationCardWidget(
|
||||
consultationsModel: state.consultations[index],
|
||||
),
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
itemCount: state.consultations.length,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed('consultantFinanceScreen');
|
||||
},
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Архив консультаций',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
separatorBuilder: (context, index) => const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
itemCount: state.consultations.length,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:collection/collection.dart';
|
||||
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/widgets/custom_divider.dart';
|
||||
@@ -71,8 +72,9 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
@@ -195,7 +197,10 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
|
||||
return;
|
||||
}
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: day.$2.id);
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
|
||||
'diaryId': day.$2.id,
|
||||
'currentDiaryDay': index + 1,
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
@@ -272,7 +277,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
const CustomDivider(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'${widget.consultationsModel.consultantPayment.toStringAsFixed(0)} ₽',
|
||||
'${widget.consultationsModel.consultationOptionPrice.toStringAsFixed(0)} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF019A3E),
|
||||
fontSize: 20,
|
||||
@@ -291,6 +296,10 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
)
|
||||
.lastOrNull;
|
||||
if (lastActiveDay != null) {
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
|
||||
'diaryId': lastActiveDay.$2.id,
|
||||
'currentDiaryDay': lastActiveDay.$1,
|
||||
});
|
||||
GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
|
||||
|
||||
class ConsultationsRepository {
|
||||
final ConsultationService consultationService;
|
||||
@@ -13,4 +15,31 @@ class ConsultationsRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ConsultationModel>> getFilteredConsultations({
|
||||
String? state,
|
||||
String? date,
|
||||
FiltersComparisonEnum? stateComparison,
|
||||
FiltersComparisonEnum? dateComparison,
|
||||
}) async {
|
||||
try {
|
||||
final model = await consultationService.getFilteredConsultations(
|
||||
state: state,
|
||||
date: date,
|
||||
stateComparison: stateComparison,
|
||||
dateComparison: dateComparison,
|
||||
);
|
||||
return model.data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<StatisticModel> getStatistic() async {
|
||||
try {
|
||||
return await consultationService.getStatistics();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,69 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
|
||||
|
||||
part 'consultation_service.g.dart';
|
||||
class ConsultationService {
|
||||
final Dio dio;
|
||||
ConsultationService(this.dio);
|
||||
|
||||
@RestApi()
|
||||
abstract class ConsultationService {
|
||||
factory ConsultationService(Dio dio, {String baseUrl}) = _ConsultationService;
|
||||
Future<ConsultationPaginationModel> getConsultations() async {
|
||||
try {
|
||||
final response = await dio.get('/api/consultations', queryParameters: {
|
||||
'filters[last_state][\$notIn][]': const [
|
||||
'initied',
|
||||
'in_coordination',
|
||||
'start_date_has_been_set',
|
||||
'pending_payment',
|
||||
'canceled_by_the_initiator',
|
||||
'charge_was_collected',
|
||||
],
|
||||
'sort[]': 'created_at:desc'
|
||||
});
|
||||
return ConsultationPaginationModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@GET('/api/consultations')
|
||||
Future<ConsultationPaginationModel> getConsultations();
|
||||
Future<ConsultationPaginationModel> getFilteredConsultations({
|
||||
String? state,
|
||||
String? date,
|
||||
FiltersComparisonEnum? stateComparison,
|
||||
FiltersComparisonEnum? dateComparison,
|
||||
}) async {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'/api/consultations',
|
||||
queryParameters: {
|
||||
if (stateComparison == FiltersComparisonEnum.notInArray)
|
||||
'filters[last_state][\$notIn][]': const [
|
||||
'initied',
|
||||
'in_coordination',
|
||||
'start_date_has_been_set',
|
||||
'pending_payment',
|
||||
'canceled_by_the_initiator',
|
||||
'charge_was_collected',
|
||||
]
|
||||
else if (state != null && stateComparison != null)
|
||||
'filters[last_state][${stateComparison.value}]': state,
|
||||
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
},
|
||||
);
|
||||
return ConsultationPaginationModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<StatisticModel> getStatistics() async {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'/api/consultations/statistics',
|
||||
);
|
||||
return StatisticModel.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,6 +293,7 @@ class CreateDietCubit extends Cubit<CreateDietState> {
|
||||
final diet = Diet(
|
||||
id: 0,
|
||||
name: state.dietData.name ?? '',
|
||||
slug: '',
|
||||
image: imageInBase64,
|
||||
duration: state.dietData.duration ?? 0,
|
||||
state: state.dietData.state ?? DietState.active,
|
||||
|
||||
@@ -192,7 +192,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
path: 'diet/${widget.diet.id}',
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
@@ -21,21 +21,45 @@ class OptionWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OptionWidgetState extends State<OptionWidget> {
|
||||
final customerPriceController = TextEditingController()..text = '1200₽';
|
||||
final firstOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
final secondOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
final thirdOptionCustomerPriceController = TextEditingController()..text = '1200₽';
|
||||
|
||||
final creatorPriceController = TextEditingController()..text = '1000₽';
|
||||
final optionTitleController = TextEditingController();
|
||||
final optionDescriptionController = TextEditingController();
|
||||
|
||||
final firstOptionTitleController = TextEditingController();
|
||||
final secondOptionTitleController = TextEditingController();
|
||||
final thirdOptionTitleController = TextEditingController();
|
||||
|
||||
final firstOptionDescriptionController = TextEditingController();
|
||||
final secondOptionDescriptionController = TextEditingController();
|
||||
final thirdOptionDescriptionController = TextEditingController();
|
||||
|
||||
bool firstExplanationVisible = false;
|
||||
bool isChecked = false;
|
||||
bool secondExplanationVisible = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
customerPriceController.addListener(() {
|
||||
if (!customerPriceController.text.endsWith('₽')) {
|
||||
final text = customerPriceController.text.replaceAll('₽', '');
|
||||
customerPriceController.text = '$text₽';
|
||||
customerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
firstOptionCustomerPriceController.addListener(() {
|
||||
if (!firstOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = firstOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
firstOptionCustomerPriceController.text = '$text₽';
|
||||
firstOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
secondOptionCustomerPriceController.addListener(() {
|
||||
if (!secondOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = secondOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
secondOptionCustomerPriceController.text = '$text₽';
|
||||
secondOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
thirdOptionCustomerPriceController.addListener(() {
|
||||
if (!thirdOptionCustomerPriceController.text.endsWith('₽')) {
|
||||
final text = thirdOptionCustomerPriceController.text.replaceAll('₽', '');
|
||||
thirdOptionCustomerPriceController.text = '$text₽';
|
||||
thirdOptionCustomerPriceController.selection = TextSelection.collapsed(offset: text.length);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -45,15 +69,32 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<CreatePlanCubit, CreatePlanState>(
|
||||
builder: (context, state) {
|
||||
final optionNameHintText = switch (state.selectedOptionIndex ?? 1) {
|
||||
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
|
||||
final optionNameHintText = switch (selectedOptionIndex) {
|
||||
0 => s.economy,
|
||||
2 => s.comfort,
|
||||
_ => s.standard,
|
||||
};
|
||||
final selectedOptionIndex = state.selectedOptionIndex ?? 0;
|
||||
|
||||
optionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||
optionDescriptionController.text = state.planData.options[selectedOptionIndex].description;
|
||||
final optionTitleController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionTitleController,
|
||||
2 => thirdOptionTitleController,
|
||||
_ => secondOptionTitleController,
|
||||
};
|
||||
final optionDescriptionController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionDescriptionController,
|
||||
2 => thirdOptionDescriptionController,
|
||||
_ => secondOptionDescriptionController,
|
||||
};
|
||||
final customerPriceController = switch (selectedOptionIndex) {
|
||||
0 => firstOptionCustomerPriceController,
|
||||
2 => thirdOptionCustomerPriceController,
|
||||
_ => secondOptionCustomerPriceController,
|
||||
};
|
||||
|
||||
final price = double.tryParse(customerPriceController.text.replaceAll('₽', ''));
|
||||
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||
firstOptionTitleController.text = state.planData.options[selectedOptionIndex].short;
|
||||
firstOptionDescriptionController.text = state.planData.options[selectedOptionIndex].description;
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -151,7 +192,6 @@ class _OptionWidgetState extends State<OptionWidget> {
|
||||
hasError: state.validationState.optionsValidationState[state.selectedOptionIndex ?? 0].priceHasError,
|
||||
onChanged: (value) {
|
||||
final price = double.tryParse(value.replaceAll('₽', ''));
|
||||
creatorPriceController.text = '${((price ?? 0) * 0.8).toStringAsFixed(2)}₽';
|
||||
if (price != null) {
|
||||
context.read<CreatePlanCubit>().setPrice(price);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
||||
final RecipeRepository recipeRepository;
|
||||
final GlossaryRepository glossaryRepository;
|
||||
|
||||
CreateRecipeCubit({
|
||||
required this.recipeRepository,
|
||||
required this.glossaryRepository,
|
||||
@@ -192,7 +193,7 @@ class CreateRecipeCubit extends Cubit<CreateRecipeState> {
|
||||
recipe.steps!.map(
|
||||
(e) async => StepEntity(
|
||||
description: e.description,
|
||||
image: await downloadImageAsFile('$baseUrl/storage/${e.image}'),
|
||||
image: e.image.isEmpty ? null : await downloadImageAsFile('$baseUrl/storage/${e.image}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/image_compressing/image_compress_utility.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/diary/models/consultant_review_model.dart';
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:eatvio_app/features/diets/repositories/diet_repository.dart';
|
||||
import 'package:eatvio_app/features/glossary/models/glossary_model.dart';
|
||||
import 'package:eatvio_app/features/glossary/repositories/glossary_repository.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DiaryCubit extends Cubit<DiaryState> {
|
||||
@@ -23,9 +25,17 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
final GlossaryRepository glossaryRepository;
|
||||
final ProfileRepository profileRepository;
|
||||
final DietRepository dietRepository;
|
||||
final ChatRepository chatRepository;
|
||||
final SettingsRepository settingsRepository;
|
||||
int? diaryId;
|
||||
DiaryCubit({required this.diaryRepository, required this.glossaryRepository, required this.profileRepository, required this.dietRepository})
|
||||
: super(const DiaryState.loading()) {
|
||||
DiaryCubit({
|
||||
required this.chatRepository,
|
||||
required this.diaryRepository,
|
||||
required this.glossaryRepository,
|
||||
required this.profileRepository,
|
||||
required this.dietRepository,
|
||||
required this.settingsRepository,
|
||||
}) : super(const DiaryState.loading()) {
|
||||
today = DateTime.now();
|
||||
selectedDay = today;
|
||||
diaryRepository.forceFetchStream.listen((_) {
|
||||
@@ -144,23 +154,36 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
return false;
|
||||
}
|
||||
|
||||
fetchDiary([bool needLoading = false, int? diaryId]) async {
|
||||
fetchDiary([bool needLoading = false, int? diaryId, int? currentDay]) async {
|
||||
this.diaryId ??= diaryId;
|
||||
|
||||
if (needLoading) {
|
||||
emit(const DiaryState.loading());
|
||||
selectedMeal = 1;
|
||||
}
|
||||
try {
|
||||
final DiaryDayModel diaryDay;
|
||||
bool isMoodEnabled = false;
|
||||
bool isWakeUpAndRetireTimeEnabled = false;
|
||||
if (diaryId != null) {
|
||||
diaryDay = await diaryRepository.getDiaryDay(diaryId);
|
||||
} else {
|
||||
final stringDate = selectedDay.format('yyyy-MM-dd');
|
||||
diaryDay = await diaryRepository.getDiaryByDate(stringDate);
|
||||
final profileSettings = await settingsRepository.getProfileSettings();
|
||||
isMoodEnabled = profileSettings.isMoodFillable;
|
||||
isWakeUpAndRetireTimeEnabled = profileSettings.isBedtimeFillable;
|
||||
}
|
||||
ConsultantReviewModel? consultantReview;
|
||||
int? planDuration;
|
||||
int? currentPlanDay;
|
||||
String? planName;
|
||||
if (diaryDay.chosenPlanId != null) {
|
||||
consultantReview = await diaryRepository.getDiaryComments(planId: diaryDay.chosenPlanId!, diaryId: diaryDay.id);
|
||||
final chosenPlanModel = await chatRepository.getChosenPlan(id: diaryDay.chosenPlanId!);
|
||||
planDuration = chosenPlanModel.plan.duration;
|
||||
planName = chosenPlanModel.plan.name;
|
||||
currentPlanDay = currentDay ?? DateTime.now().difference(chosenPlanModel.startDate).inDays + 1;
|
||||
}
|
||||
final nutrition = await getNutrition(diaryDay.id);
|
||||
|
||||
@@ -179,6 +202,11 @@ class DiaryCubit extends Cubit<DiaryState> {
|
||||
consultantReview: consultantReview,
|
||||
isDiaryFinished: isDiaryFinished,
|
||||
isDiaryCommented: isDiaryCommented,
|
||||
chosenPlanCurrentDay: currentPlanDay,
|
||||
chosenPlanDuration: planDuration,
|
||||
chosenPlanName: planName,
|
||||
isMoodEnabled: isMoodEnabled,
|
||||
isWakeUpAndRetireEnabled: isWakeUpAndRetireTimeEnabled
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
|
||||
@@ -20,10 +20,15 @@ abstract class DiaryState with _$DiaryState {
|
||||
required NutritionDataModel nutrition,
|
||||
required DateTime selectedDay,
|
||||
required int selectedMeal,
|
||||
@Default(false) bool isConsultantView,
|
||||
int? chosenPlanCurrentDay,
|
||||
String? chosenPlanName,
|
||||
int? chosenPlanDuration,
|
||||
ConsultantReviewModel? consultantReview,
|
||||
@Default(false) bool isConsultantView,
|
||||
@Default(false) bool isDiaryFinished,
|
||||
@Default(false) bool isDiaryCommented,
|
||||
@Default(false) bool isWakeUpAndRetireEnabled,
|
||||
@Default(false) bool isMoodEnabled,
|
||||
}) = DiaryStateLoaded;
|
||||
|
||||
const factory DiaryState.loadedTomorrowWithDiet({
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:developer';
|
||||
|
||||
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/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
@@ -34,24 +35,33 @@ class DiaryScreen extends StatelessWidget {
|
||||
class UserDiaryScreen extends StatelessWidget {
|
||||
static const path = '/user-diary-screen';
|
||||
static const name = 'userDiaryScreen';
|
||||
const UserDiaryScreen({super.key, required this.diaryId});
|
||||
const UserDiaryScreen({
|
||||
super.key,
|
||||
required this.diaryId,
|
||||
required this.currentPlanDay,
|
||||
});
|
||||
final int diaryId;
|
||||
final int currentPlanDay;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DiaryScreenContent(diaryId: diaryId);
|
||||
return DiaryScreenContent(
|
||||
diaryId: diaryId,
|
||||
currentPlanDay: currentPlanDay,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DiaryScreenContent extends StatelessWidget {
|
||||
const DiaryScreenContent({super.key, this.diaryId});
|
||||
const DiaryScreenContent({super.key, this.diaryId, this.currentPlanDay});
|
||||
final int? diaryId;
|
||||
final int? currentPlanDay;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DiaryCubit>(
|
||||
create: (BuildContext context) => sl<DiaryCubit>()..fetchDiary(true, diaryId),
|
||||
create: (BuildContext context) => sl<DiaryCubit>()..fetchDiary(true, diaryId, currentPlanDay),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => sl<ConsultantCommentsCubit>(),
|
||||
@@ -70,11 +80,19 @@ class DiaryScreenContent extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 0,
|
||||
),
|
||||
appBar: diaryId == null
|
||||
? AppBar(
|
||||
toolbarHeight: 0,
|
||||
)
|
||||
: AppBar(
|
||||
title: Text(
|
||||
state is DiaryStateLoaded ? DateTime.parse(state.diary.date).toLocal().format('d MMMM') : '',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.only(bottom: 16, top: 8),
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -87,13 +105,13 @@ class DiaryScreenContent extends StatelessWidget {
|
||||
],
|
||||
if (state is DiaryStateLoaded)
|
||||
WakeupAndRetireWidget(
|
||||
showRetire: state.diary.typeOfDiary.hasChosenPlan,
|
||||
isEnabled: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isWakeUpAndRetireEnabled,
|
||||
disable: state.isConsultantView || state.isDiaryFinished || state.isDiaryCommented,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state is DiaryStateLoaded)
|
||||
MealWidget(
|
||||
showMood: state.diary.typeOfDiary.hasChosenPlan,
|
||||
showMood: state.diary.typeOfDiary.hasChosenPlan || state.isConsultantView || state.isMoodEnabled,
|
||||
showAddButton: !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented,
|
||||
),
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet)
|
||||
|
||||
@@ -89,7 +89,9 @@ class _CalendarWidgetState extends State<CalendarWidget> {
|
||||
return isSameDay(_selectedDay, day);
|
||||
},
|
||||
onPageChanged: (focusedDay) {
|
||||
_focusedDay = focusedDay;
|
||||
setState(() {
|
||||
_focusedDay = focusedDay;
|
||||
});
|
||||
},
|
||||
onFormatChanged: (format) {
|
||||
if (_calendarFormat != format) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class CaloriesChartWidget extends StatelessWidget {
|
||||
context: context,
|
||||
builder: (context) => const AlertDialog(
|
||||
content: Text(
|
||||
'График отображает сколько калорий вам нужно ежедневно для похуденияи поддержания веса, согласно формуле Харриса-Бенедикта.\nИзменить коэффициент физической нагрузки можно в настройках.',
|
||||
'График отображает сколько калорий Вам нужно ежедневно для поддержания веса, согласно формуле Харриса-Бенедикта. Изменить коэффициент физической нагрузки можно в настройках',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF323F4B),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ChosenPlanWidget extends StatelessWidget {
|
||||
const ChosenPlanWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(builder: (context, state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
if (state.diary.typeOfDiary.hasChosenPlan) {
|
||||
return ChosenPlanWidgetContent(
|
||||
currentPlanDay: state.chosenPlanCurrentDay ?? 0,
|
||||
planName: state.chosenPlanName ?? '',
|
||||
planDuration: state.chosenPlanDuration ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ChosenPlanWidgetContent extends StatelessWidget {
|
||||
const ChosenPlanWidgetContent({
|
||||
super.key,
|
||||
required this.currentPlanDay,
|
||||
required this.planName,
|
||||
required this.planDuration,
|
||||
});
|
||||
final int currentPlanDay;
|
||||
final String planName;
|
||||
final int planDuration;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size.width * 0.4,
|
||||
child: Text(planName, style: TextStyles.textLMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(color: appTheme.dividerColor, borderRadius: BorderRadius.circular(12)),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Text('${s.day} $currentPlanDay из $planDuration', style: TextStyles.textMSemibold.copyWith(color: appTheme.gray)),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/add_product_button_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/chosen_diet_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/chosen_plan_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/comment_container.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/meal_button_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/meal_comment_widget.dart';
|
||||
@@ -58,8 +59,8 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TimeOfMealPickerWidget(),
|
||||
|
||||
const ChosenDietWidget(),
|
||||
const ChosenPlanWidget(),
|
||||
const ProductsListWidget(),
|
||||
if (widget.showAddButton) ...[
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -8,8 +8,8 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import '../../../../core/theme/theme_helper.dart';
|
||||
|
||||
class WakeupAndRetireWidget extends StatefulWidget {
|
||||
const WakeupAndRetireWidget({super.key, required this.showRetire, this.disable = false});
|
||||
final bool showRetire;
|
||||
const WakeupAndRetireWidget({super.key, required this.isEnabled, this.disable = false});
|
||||
final bool isEnabled;
|
||||
final bool disable;
|
||||
@override
|
||||
State<WakeupAndRetireWidget> createState() => _WakeupAndRetireWidgetState();
|
||||
@@ -94,6 +94,9 @@ class _WakeupAndRetireWidgetState extends State<WakeupAndRetireWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(
|
||||
builder: (context, state) {
|
||||
if (!widget.isEnabled) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
if (state is DiaryStateLoaded) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
@@ -157,67 +160,65 @@ class _WakeupAndRetireWidgetState extends State<WakeupAndRetireWidget> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.showRetire) ...[
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/retire.svg'),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Уход ко сну',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/retire.svg'),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Уход ко сну',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: widget.disable ? null : () => showTimePicker(state.diary.id, false),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
color: const Color(0xFFF1F2F6),
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
state.diary.retire.toString().substring(
|
||||
0,
|
||||
state.diary.retire.toString().length - 3,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: widget.disable ? null : () => showTimePicker(state.diary.id, false),
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
color: const Color(0xFFF1F2F6),
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
state.diary.retire.toString().substring(
|
||||
0,
|
||||
state.diary.retire.toString().length - 3,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -318,7 +318,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
path: 'diet/${widget.diet.id}',
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
id: widget.diet.id,
|
||||
diet: widget.diet,
|
||||
@@ -499,7 +499,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
path: 'diet/${widget.diet.id}',
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
id: widget.diet.id,
|
||||
diet: widget.diet,
|
||||
@@ -526,7 +526,7 @@ class _AboutDietScreenState extends State<AboutDietScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
path: 'diet/${widget.diet.id}',
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
id: widget.diet.id,
|
||||
diet: widget.diet,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/features/diets/presentation/widgets/info_bouble.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
@@ -36,7 +37,7 @@ class AuthorCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(author.authorProfileIcon),
|
||||
image: NetworkImage(baseUrl + author.authorProfileIcon),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -20,6 +20,7 @@ abstract class DietsPagination with _$DietsPagination {
|
||||
abstract class Diet with _$Diet {
|
||||
const factory Diet({
|
||||
@JsonKey(name: "id") required int id,
|
||||
@JsonKey(name: "slug") String? slug,
|
||||
@JsonKey(name: "name") required String name,
|
||||
@JsonKey(name: "image") required String image,
|
||||
@JsonKey(name: "duration") required int duration,
|
||||
|
||||
@@ -16,8 +16,8 @@ abstract class Recipe with _$Recipe {
|
||||
@JsonKey(name: 'description_md') required String description,
|
||||
required List<Step>? steps,
|
||||
@JsonKey(name: 'video_url') required String? videoUrl,
|
||||
@JsonKey(name: 'created_at') required DateTime createdAt,
|
||||
@JsonKey(name: 'updated_at') required DateTime updatedAt,
|
||||
@JsonKey(name: 'created_at') DateTime? createdAt,
|
||||
@JsonKey(name: 'updated_at') DateTime? updatedAt,
|
||||
required String? title,
|
||||
required String? type,
|
||||
required String slug,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
|
||||
import 'package:eatvio_app/features/feed/repositories/feed_repository.dart';
|
||||
@@ -77,7 +79,13 @@ class FeedCubit extends Cubit<FeedState> {
|
||||
}
|
||||
}
|
||||
|
||||
likePost(int id) async => await feedRepository.likeBlogPost(id: id);
|
||||
likePost(int id) async {
|
||||
try {
|
||||
await feedRepository.likeBlogPost(id: id);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
unlikedPost(int id) async => await feedRepository.unlikedBlogPost(id: id);
|
||||
|
||||
|
||||
@@ -56,91 +56,95 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
authorName: state.feedBlogPostList[widget.index].author.authorName,
|
||||
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Colors.black12,
|
||||
foregroundImage: NetworkImage(
|
||||
state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.feedBlogPostList[widget.index].author.authorName,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black45,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
|
||||
!state.feedBlogPostList[widget.index].author.authorIsMe)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SubscribeButtonWidget(
|
||||
slug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(
|
||||
'profileScreen',
|
||||
extra: Author(
|
||||
authorSlug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
authorName: state.feedBlogPostList[widget.index].author.authorName,
|
||||
authorProfileIcon: state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
authorIsMe: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor: Colors.black12,
|
||||
foregroundImage: NetworkImage(
|
||||
baseUrl + state.feedBlogPostList[widget.index].author.authorProfileIcon,
|
||||
),
|
||||
CustomPopup(
|
||||
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
complainedContentType: ComplainedContent.posts,
|
||||
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
|
||||
id: state.feedBlogPostList[widget.index].id,
|
||||
feedBlogPostModel: state.feedBlogPostList[widget.index],
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Имя и дата внутри Expanded
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.feedBlogPostList[widget.index].author.authorName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${calculateDifference(state.feedBlogPostList[widget.index].createdAt)} в ${state.feedBlogPostList[widget.index].createdAt.hour.toString().padLeft(2, '0')}:${state.feedBlogPostList[widget.index].createdAt.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Colors.black45,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Кнопка подписки (если нужно)
|
||||
if (!(state.feedBlogPostList[widget.index].author.isEntitySubscribedByMe ?? false) &&
|
||||
!state.feedBlogPostList[widget.index].author.authorIsMe)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: SubscribeButtonWidget(
|
||||
slug: state.feedBlogPostList[widget.index].author.authorSlug,
|
||||
),
|
||||
),
|
||||
// Кнопка меню
|
||||
CustomPopup(
|
||||
isAuthor: state.feedBlogPostList[widget.index].author.authorIsMe,
|
||||
complainedContentType: ComplainedContent.posts,
|
||||
path: 'blog_post/${(state.feedBlogPostList[widget.index].id)}',
|
||||
id: state.feedBlogPostList[widget.index].id,
|
||||
feedBlogPostModel: state.feedBlogPostList[widget.index],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (state.feedBlogPostList[widget.index].title.isNotEmpty || state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
),
|
||||
if (state.feedBlogPostList[widget.index].title.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
state.feedBlogPostList[widget.index].title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(
|
||||
state.feedBlogPostList[widget.index].title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.feedBlogPostList[widget.index].title.isEmpty) const SizedBox(height: 8),
|
||||
if (state.feedBlogPostList[widget.index].title.isNotEmpty && state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
const SizedBox(height: 8),
|
||||
if (state.feedBlogPostList[widget.index].description.isNotEmpty)
|
||||
@@ -245,7 +249,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',),
|
||||
uri: Uri.parse(
|
||||
'$baseUrl/ru/blog_post/${state.feedBlogPostList[widget.index].id}',
|
||||
),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ class BlogPosts extends StatelessWidget {
|
||||
},
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == state.feedBlogPostList.length) {
|
||||
@@ -32,9 +31,7 @@ class BlogPosts extends StatelessWidget {
|
||||
}
|
||||
return BlogPostWidget(index: index);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 8);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
|
||||
),
|
||||
);
|
||||
@@ -53,3 +50,4 @@ class BlogPosts extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
@@ -77,7 +78,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
foregroundImage: NetworkImage(comment.authorProfileIcon),
|
||||
foregroundImage: NetworkImage(baseUrl + comment.authorProfileIcon),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: radius,
|
||||
),
|
||||
|
||||
@@ -66,7 +66,7 @@ class ConsultantWidget extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(
|
||||
foregroundImage: NetworkImage(consultant.photo),
|
||||
foregroundImage: NetworkImage(baseUrl + consultant.photo),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
@@ -90,7 +90,7 @@ class _DietsWidgetState extends State<DietsWidget> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CustomPopup(
|
||||
isAuthor: widget.diet.author.authorIsMe,
|
||||
path: 'diet/${widget.diet.id}',
|
||||
path: 'diet/${widget.diet.slug}',
|
||||
id: widget.diet.id,
|
||||
complainedContentType: ComplainedContent.diet,
|
||||
diet: widget.diet,
|
||||
|
||||
@@ -62,8 +62,9 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '$baseUrl/storage/${widget.plan.image}',
|
||||
fit: BoxFit.fitWidth,
|
||||
fit: BoxFit.cover,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
height: 426,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -95,7 +96,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: CustomPopup(
|
||||
isAuthor: widget.plan.author.authorIsMe,
|
||||
path: 'plan/${widget.plan.id}',
|
||||
path: 'plan/${widget.plan.slug}',
|
||||
id: widget.plan.id,
|
||||
slug: widget.plan.slug,
|
||||
complainedContentType: ComplainedContent.plan,
|
||||
@@ -185,7 +186,7 @@ class _PlanWidgetState extends State<PlanWidget> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(widget.plan.author.authorProfileIcon),
|
||||
image: NetworkImage(baseUrl + widget.plan.author.authorProfileIcon),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
|
||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
final ConsultationsRepository consultationsRepository;
|
||||
ConsultationsArchiveCubit({
|
||||
required this.consultationsRepository,
|
||||
}) : super(
|
||||
ConsultationsArchiveState(
|
||||
statisticModel: StatisticModel(
|
||||
total: 0,
|
||||
canceled: 0,
|
||||
inProgress: 0,
|
||||
finished: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> getConsultations() async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final consultations = await consultationsRepository.getFilteredConsultations(
|
||||
date: state.periodFilterStatus?.value,
|
||||
state: state.consultationFilterStatus?.value,
|
||||
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
|
||||
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
|
||||
? null
|
||||
: state.consultationFilterStatus == ConsultationFilterStatus.inProgress
|
||||
? FiltersComparisonEnum.notInArray
|
||||
: FiltersComparisonEnum.equal,
|
||||
);
|
||||
emit(state.copyWith(consultations: consultations, isLoading: false));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setConsultationsPeriod(PeriodFilterStatus period) async {
|
||||
emit(state.copyWith(periodFilterStatus: period));
|
||||
await getConsultations();
|
||||
}
|
||||
|
||||
Future<void> setConsultationsStatus(ConsultationFilterStatus status) async {
|
||||
emit(state.copyWith(consultationFilterStatus: status));
|
||||
await getConsultations();
|
||||
}
|
||||
|
||||
Future<void> getStatistic() async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final statistic = await consultationsRepository.getStatistic();
|
||||
emit(state.copyWith(statisticModel: statistic, isLoading: false));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ConsultationFilterStatus {
|
||||
all,
|
||||
canceled,
|
||||
finished,
|
||||
inProgress;
|
||||
|
||||
String get name => switch (this) {
|
||||
ConsultationFilterStatus.all => 'Все консультации',
|
||||
ConsultationFilterStatus.canceled => 'Отменён',
|
||||
ConsultationFilterStatus.finished => 'Завершён',
|
||||
ConsultationFilterStatus.inProgress => 'В процессе',
|
||||
};
|
||||
|
||||
String? get value => switch (this) {
|
||||
ConsultationFilterStatus.all => null,
|
||||
ConsultationFilterStatus.canceled => 'canceled_by_the_initiator',
|
||||
ConsultationFilterStatus.finished => 'charge_was_collected',
|
||||
ConsultationFilterStatus.inProgress => 'active',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'consultations_archive_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ConsultationsArchiveState with _$ConsultationsArchiveState {
|
||||
|
||||
factory ConsultationsArchiveState({
|
||||
@Default([]) List<ConsultationModel> consultations,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
@Default(null) ConsultationFilterStatus? consultationFilterStatus,
|
||||
required StatisticModel statisticModel,
|
||||
}) = _ConsultationsArchiveState;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantFinancesCubit extends Cubit<ConsultantFinancesState> {
|
||||
final ConsultationsRepository consultationsRepository;
|
||||
ConsultantFinancesCubit({
|
||||
required this.consultationsRepository,
|
||||
}) : super(
|
||||
ConsultantFinancesState(),
|
||||
);
|
||||
|
||||
Future<void> getConsultations() async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setConsultationsPeriod(PeriodFilterStatus period) async {
|
||||
emit(state.copyWith(periodFilterStatus: period));
|
||||
await getConsultations();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'consultant_finances_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ConsultantFinancesState with _$ConsultantFinancesState {
|
||||
factory ConsultantFinancesState({
|
||||
@Default([]) List<String> financesModels,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
}) = _ConsultantFinancesState;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'statistic_model.freezed.dart';
|
||||
part 'statistic_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class StatisticModel with _$StatisticModel {
|
||||
factory StatisticModel({
|
||||
required int total,
|
||||
@JsonKey(name: 'in_progress') required int inProgress,
|
||||
required int finished,
|
||||
required int canceled,
|
||||
}) = _StatisticModel;
|
||||
|
||||
factory StatisticModel.fromJson(Map<String, dynamic> json) => _$StatisticModelFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
|
||||
enum PeriodFilterStatus {
|
||||
all,
|
||||
month,
|
||||
week;
|
||||
|
||||
String get name => switch (this) {
|
||||
PeriodFilterStatus.all => 'Все периоды',
|
||||
PeriodFilterStatus.month => 'За месяц',
|
||||
PeriodFilterStatus.week => 'За неделю',
|
||||
};
|
||||
|
||||
String? get value => switch (this) {
|
||||
PeriodFilterStatus.all => null,
|
||||
PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// 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/core/utils/date_time_utils.dart';
|
||||
// import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
// import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
|
||||
// import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart';
|
||||
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
|
||||
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart';
|
||||
// import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
// import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/widgets/status_widget.dart';
|
||||
// import 'package:eatvio_app/generated/l10n.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
// class ConsultantFinancesScreen extends StatefulWidget {
|
||||
// const ConsultantFinancesScreen({super.key});
|
||||
|
||||
// @override
|
||||
// State<ConsultantFinancesScreen> createState() => _ConsultantFinancesScreenState();
|
||||
// }
|
||||
|
||||
// class _ConsultantFinancesScreenState extends State<ConsultantFinancesScreen> {
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final s = S.of(context);
|
||||
// return BlocProvider<ConsultantFinancesCubit>(
|
||||
// create: (context) => sl<ConsultantFinancesCubit>(),
|
||||
// child: BlocBuilder<ConsultantFinancesCubit, ConsultantFinancesState>(
|
||||
// builder: (context, state) {
|
||||
// return Scaffold(
|
||||
// backgroundColor: const Color(0xFFF0F2F5),
|
||||
// appBar: AppBar(
|
||||
// shape: const RoundedRectangleBorder(
|
||||
// borderRadius: BorderRadiusDirectional.vertical(
|
||||
// bottom: Radius.circular(12),
|
||||
// ),
|
||||
// ),
|
||||
// title: const Text('Архив консультаций'),
|
||||
// centerTitle: true,
|
||||
// ),
|
||||
// body: state.isLoading
|
||||
// ? const Center(
|
||||
// child: Loader(),
|
||||
// )
|
||||
// : SingleChildScrollView(
|
||||
// padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Material(
|
||||
// color: Colors.white,
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// const SizedBox(height: 16),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
// child: Text(
|
||||
// 'Всего консультаций: ${state.statisticModel.total}',
|
||||
// style: const TextStyle(
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// color: Color(0xFF6D6F71),
|
||||
// height: 1,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 12),
|
||||
// Row(
|
||||
// children: [
|
||||
// const SizedBox(width: 16),
|
||||
// Material(
|
||||
// color: const Color(0xFF0BB450).withAlpha(11),
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
// child: Text(
|
||||
// 'Завершено: ${state.statisticModel.finished}',
|
||||
// style: const TextStyle(
|
||||
// fontSize: 14,
|
||||
// fontWeight: FontWeight.w400,
|
||||
// color: Color(0xFF0BB450),
|
||||
// height: 1,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(width: 8),
|
||||
// Material(
|
||||
// color: const Color(0xFFD01717).withAlpha(11),
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
// child: Text(
|
||||
// 'Отменено: ${state.statisticModel.canceled}',
|
||||
// style: const TextStyle(
|
||||
// fontSize: 14,
|
||||
// fontWeight: FontWeight.w400,
|
||||
// color: Color(0xFFD01717),
|
||||
// height: 1,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 16),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Material(
|
||||
// color: Colors.white,
|
||||
// borderRadius: BorderRadius.circular(12),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
// child: SingleChildScrollView(
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// CustomDropDownButton<PeriodFilterStatus>(
|
||||
// items: PeriodFilterStatus.values
|
||||
// .map(
|
||||
// (value) => DropdownMenuItem(
|
||||
// value: value,
|
||||
// child: Text(
|
||||
// value.name,
|
||||
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// onChanged: (value) {
|
||||
// if (value == null) return;
|
||||
// context.read<ConsultationsArchiveCubit>().setConsultationsPeriod(value);
|
||||
// },
|
||||
// value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
|
||||
// ),
|
||||
// const SizedBox(height: 12),
|
||||
// CustomDropDownButton<ConsultationFilterStatus>(
|
||||
// items: ConsultationFilterStatus.values
|
||||
// .map(
|
||||
// (value) => DropdownMenuItem(
|
||||
// value: value,
|
||||
// child: Text(
|
||||
// value.name,
|
||||
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// onChanged: (value) {
|
||||
// if (value == null) return;
|
||||
// context.read<ConsultationsArchiveCubit>().setConsultationsStatus(value);
|
||||
// },
|
||||
// value: state.consultationFilterStatus,
|
||||
// ),
|
||||
// const SizedBox(height: 12),
|
||||
// if (state.consultations.isNotEmpty)
|
||||
// ListView.separated(
|
||||
// shrinkWrap: true,
|
||||
// physics: const NeverScrollableScrollPhysics(),
|
||||
// itemBuilder: (context, index) {
|
||||
// return Material(
|
||||
// color: const Color(0xFFF6F6F6),
|
||||
// borderRadius: BorderRadius.circular(20),
|
||||
// child: Padding(
|
||||
// padding: const EdgeInsets.all(12),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: [
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text(
|
||||
// state.consultations[index].id.toString(),
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF323F4B),
|
||||
// height: 1,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// Text(
|
||||
// state.consultations[index].startDate.format('yyyy.MM.dd'),
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF6D6F71),
|
||||
// height: 1,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Text(
|
||||
// state.consultations[index].planTitle,
|
||||
// style: TextStyle(
|
||||
// color: appTheme.blueA400,
|
||||
// height: 1,
|
||||
// fontSize: 18,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Text(
|
||||
// 'Покупатель: ${state.consultations[index].userName}',
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF323F4B),
|
||||
// height: 1,
|
||||
// fontSize: 14,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Text(
|
||||
// '${state.consultations[index].duration} ${getDayForm(state.consultations[index].duration, s)}',
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF323F4B),
|
||||
// height: 1,
|
||||
// fontSize: 14,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Container(
|
||||
// height: 1,
|
||||
// width: double.infinity,
|
||||
// decoration: const BoxDecoration(
|
||||
// color: Color(0xFFDEDEDF),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// children: [
|
||||
// Text(
|
||||
// '${state.consultations[index].consultantPayment.toStringAsFixed(2)} ₽',
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF323F4B),
|
||||
// height: 1,
|
||||
// fontSize: 18,
|
||||
// fontWeight: FontWeight.w600,
|
||||
// ),
|
||||
// ),
|
||||
// StatusWidget(
|
||||
// status: state.consultations[index].chosenPlanState,
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
// itemCount: state.financesModels.length,
|
||||
// )
|
||||
// else
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.symmetric(vertical: 40.0),
|
||||
// child: Text(
|
||||
// 'У вас нет таких консультаций',
|
||||
// style: TextStyles.textMMedium,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// },
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,281 @@
|
||||
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/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/widgets/status_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultationsArchiveScreen extends StatefulWidget {
|
||||
const ConsultationsArchiveScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ConsultationsArchiveScreen> createState() => _ConsultationsArchiveScreenState();
|
||||
}
|
||||
|
||||
class _ConsultationsArchiveScreenState extends State<ConsultationsArchiveScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocProvider<ConsultationsArchiveCubit>(
|
||||
create: (context) => sl<ConsultationsArchiveCubit>()
|
||||
..getConsultations()
|
||||
..getStatistic(),
|
||||
child: BlocBuilder<ConsultationsArchiveCubit, ConsultationsArchiveState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
appBar: AppBar(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusDirectional.vertical(
|
||||
bottom: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
title: const Text('Архив консультаций'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(
|
||||
child: Loader(),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Всего консультаций: ${state.statisticModel.total}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Material(
|
||||
color: const Color(0xFF0BB450).withAlpha(11),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Завершено: ${state.statisticModel.finished}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF0BB450),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: const Color(0xFFD01717).withAlpha(11),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Отменено: ${state.statisticModel.canceled}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFFD01717),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
CustomDropDownButton<PeriodFilterStatus>(
|
||||
items: PeriodFilterStatus.values
|
||||
.map(
|
||||
(value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(
|
||||
value.name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
context.read<ConsultationsArchiveCubit>().setConsultationsPeriod(value);
|
||||
},
|
||||
value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomDropDownButton<ConsultationFilterStatus>(
|
||||
items: ConsultationFilterStatus.values
|
||||
.map(
|
||||
(value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(
|
||||
value.name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
context.read<ConsultationsArchiveCubit>().setConsultationsStatus(value);
|
||||
},
|
||||
value: state.consultationFilterStatus ?? ConsultationFilterStatus.values.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state.consultations.isNotEmpty)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Material(
|
||||
color: const Color(0xFFF6F6F6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
state.consultations[index].id.toString(),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
state.consultations[index].startDate.format('yyyy.MM.dd'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.consultations[index].planTitle,
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Покупатель: ${state.consultations[index].userName}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${state.consultations[index].duration} ${getDayForm(state.consultations[index].duration, s)}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFDEDEDF),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${state.consultations[index].consultantPayment.toStringAsFixed(2)} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
StatusWidget(
|
||||
status: state.consultations[index].chosenPlanState,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: state.consultations.length,
|
||||
)
|
||||
else
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40.0),
|
||||
child: Text(
|
||||
'У вас нет таких консультаций',
|
||||
style: TextStyles.textMMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StatusWidget extends StatelessWidget {
|
||||
const StatusWidget({super.key, required this.status});
|
||||
final ChosenPlanStateEnum status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (status == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
return Material(
|
||||
color: appTheme.lightRed,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Отменён',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: appTheme.strongRed,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (status == ChosenPlanStateEnum.stateChargeWasCollected) {
|
||||
return Material(
|
||||
color: const Color(0xFF0BB450).withAlpha(11),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Успешно',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF0BB450),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Material(
|
||||
color: appTheme.transparentBlue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'В процессе',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: appTheme.blueA400,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ 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/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:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
@@ -28,7 +28,7 @@ class MenuScreen extends StatelessWidget {
|
||||
_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(MyRecipesScreen.path)),
|
||||
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)),
|
||||
@@ -47,14 +47,14 @@ class MenuScreen extends StatelessWidget {
|
||||
_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')),
|
||||
_divider(context),
|
||||
_tile(
|
||||
leading: ImageConstant.icnDevTools,
|
||||
title: 'Для разработчиков',
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(DevToolsScreen.name);
|
||||
},
|
||||
),
|
||||
// _divider(context),
|
||||
// _tile(
|
||||
// leading: ImageConstant.icnDevTools,
|
||||
// title: 'Для разработчиков',
|
||||
// onTap: () {
|
||||
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
|
||||
// },
|
||||
// ),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
|
||||
],
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class MyDietWidget extends StatefulWidget {
|
||||
final Diet diet;
|
||||
|
||||
const MyDietWidget({
|
||||
super.key,
|
||||
required this.diet,
|
||||
|
||||
@@ -1,259 +0,0 @@
|
||||
import 'package:collection/collection.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/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:flutter/material.dart';
|
||||
|
||||
class ConsultantFinanceScreen extends StatefulWidget {
|
||||
const ConsultantFinanceScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ConsultantFinanceScreen> createState() => _ConsultantFinanceScreenState();
|
||||
}
|
||||
|
||||
class _ConsultantFinanceScreenState extends State<ConsultantFinanceScreen> {
|
||||
final List<String> periods = ['За всё время', 'За месяц', 'За неделю'];
|
||||
final List<String> statuses = ['Все консультации', 'Отменён', 'Завершён', 'В процессе'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
appBar: AppBar(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusDirectional.vertical(
|
||||
bottom: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
title: const Text('Архив консультаций'),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Всего консультаций: ${testFinanceEntity.totalConsultationsCount}',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
Material(
|
||||
color: const Color(0xFF0BB450).withAlpha(11),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Завершено: ${testFinanceEntity.completedCount}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF0BB450),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: const Color(0xFFD01717).withAlpha(11),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
'Отменено: ${testFinanceEntity.canceledCount}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFFD01717),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Material(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
CustomDropDownButton(
|
||||
items: periods
|
||||
.mapIndexed(
|
||||
(index, value) => DropdownMenuItem(
|
||||
value: periods[index],
|
||||
child: Text(
|
||||
periods[index],
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {},
|
||||
value: periods.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
CustomDropDownButton(
|
||||
items: statuses
|
||||
.mapIndexed(
|
||||
(index, value) => DropdownMenuItem(
|
||||
value: statuses[index],
|
||||
child: Text(
|
||||
statuses[index],
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {},
|
||||
value: statuses.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => Material(
|
||||
color: const Color(0xFFF6F6F6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
testFinanceEntity.consultationsList[index].id,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
testFinanceEntity.consultationsList[index].date.format('dd.MM.yyy', 'RU'),
|
||||
style: const 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,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Покупатель: ${testFinanceEntity.consultationsList[index].clientName}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${testFinanceEntity.consultationsList[index].planDuration} дней',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 1,
|
||||
width: double.infinity,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFDEDEDF),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'${testFinanceEntity.consultationsList[index].price} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: const Color(0xFF0BB450).withAlpha(11),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Text(
|
||||
testFinanceEntity.consultationsList[index].status.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFF0BB450),
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: testFinanceEntity.consultationsList.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ part 'my_plans_service.g.dart';
|
||||
abstract class MyPlansService {
|
||||
factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService;
|
||||
|
||||
@GET('/api/plans/index')
|
||||
@GET('/api/profile/my_plans')
|
||||
Future<PlanPaginationModel> getPlans({
|
||||
@Query('page') required int page,
|
||||
@Query('filters[name][\$startsWith]') String? query,
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
|
||||
sealed class BaseNotificationModel {
|
||||
abstract final NotificationType type;
|
||||
abstract final bool isRead;
|
||||
}
|
||||
|
||||
class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final String chatId;
|
||||
@@ -14,14 +19,16 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.chatId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionStartedToAuthorNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
chatId: json['chat_id'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +36,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
|
||||
class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
|
||||
@@ -36,13 +45,15 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionCanceledToAuthorNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -50,6 +61,8 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
|
||||
class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final int planId;
|
||||
|
||||
@@ -57,13 +70,15 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanStartedToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,58 +86,70 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
|
||||
class ChosenPlanTomorrowReminderToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
|
||||
ChosenPlanTomorrowReminderToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return ChosenPlanTomorrowReminderToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
);
|
||||
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
class DiaryReminderNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
DiaryReminderNotification({required this.type, required this.isRead});
|
||||
|
||||
DiaryReminderNotification({required this.type});
|
||||
|
||||
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return DiaryReminderNotification(
|
||||
type: type,
|
||||
);
|
||||
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return DiaryReminderNotification(type: type, isRead: isRead ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
class ConsultantLeftCommentNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final DateTime date;
|
||||
|
||||
ConsultantLeftCommentNotification({
|
||||
required this.type,
|
||||
required this.date,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return ConsultantLeftCommentNotification(type: type, date: json['diary_date']);
|
||||
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ConsultantLeftCommentNotification(
|
||||
type: type,
|
||||
date: parseDate(json['diary_date']),
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
|
||||
PersonalizedDietIsDefinedNotification({required this.type});
|
||||
PersonalizedDietIsDefinedNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PersonalizedDietIsDefinedNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -130,30 +157,41 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
|
||||
class ChosenPlanFinishedToClientNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
ChosenPlanFinishedToClientNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
return ChosenPlanFinishedToClientNotification(type: type, planName: json['plan_name']);
|
||||
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanFinishedToClientNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PlanSubscriptionCanceledToCustomerNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
PlanSubscriptionCanceledToCustomerNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionCanceledToCustomerNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -161,17 +199,21 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification
|
||||
class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
ChosenPlanStartedToConsultantNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
});
|
||||
|
||||
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanStartedToConsultantNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
);
|
||||
@@ -181,19 +223,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
|
||||
class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
ChosenPlanTomorrowReminderToConsultantNotification({
|
||||
required this.type,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanTomorrowReminderToConsultantNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -201,17 +247,21 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica
|
||||
class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
PlanSubscriptionPaidToAuthorNotification({
|
||||
required this.type,
|
||||
required this.isRead,
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
});
|
||||
|
||||
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return PlanSubscriptionPaidToAuthorNotification(
|
||||
type: type,
|
||||
isRead: isRead ?? false,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['user_name'],
|
||||
);
|
||||
@@ -221,6 +271,8 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel
|
||||
class ChosenPlanFinishedToConsultantNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
final String planName;
|
||||
final String customerName;
|
||||
final int planId;
|
||||
@@ -229,14 +281,16 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
|
||||
required this.planName,
|
||||
required this.customerName,
|
||||
required this.planId,
|
||||
required this.isRead,
|
||||
});
|
||||
|
||||
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
|
||||
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
|
||||
return ChosenPlanFinishedToConsultantNotification(
|
||||
type: type,
|
||||
planName: json['plan_name'],
|
||||
customerName: json['client_name'],
|
||||
planId: json['plan_id'],
|
||||
isRead: isRead ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -244,8 +298,12 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
|
||||
class UnknownNotification implements BaseNotificationModel {
|
||||
@override
|
||||
final NotificationType type;
|
||||
@override
|
||||
final bool isRead;
|
||||
|
||||
UnknownNotification({
|
||||
required this.type,
|
||||
this.isRead = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
@@ -11,22 +12,26 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
final List<BaseNotificationModel> _notifications = [];
|
||||
final WsManager wsManager;
|
||||
final NotificationsRepository notificationsRepository;
|
||||
StreamSubscription? _newNotificationsSubscription;
|
||||
NotificationsCubit({
|
||||
required this.wsManager,
|
||||
required this.notificationsRepository,
|
||||
}) : super(const NotificationsState.empty()) {
|
||||
wsManager.newNotificationsStream?.listen((event) {
|
||||
}) : super(const NotificationsState.empty());
|
||||
|
||||
Future<void> connectToWebSocket() async {
|
||||
await _newNotificationsSubscription?.cancel();
|
||||
_newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) {
|
||||
_handleNotification(event.data);
|
||||
});
|
||||
}
|
||||
|
||||
BaseNotificationModel _parseNotification(Map<String, dynamic> notificationJson, NotificationType type) {
|
||||
try {
|
||||
return switch (type) {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type),
|
||||
@@ -54,6 +59,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -67,10 +73,26 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
NotificationsState.loaded(
|
||||
notifications: _notifications,
|
||||
notificationsCount: _notifications.length,
|
||||
hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty,
|
||||
),
|
||||
);
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
final currentState = state;
|
||||
if (currentState is NotificationsStateLoaded) {
|
||||
try {
|
||||
emit(currentState.copyWith(isLoading: true));
|
||||
await notificationsRepository.markNotificationsAsRead();
|
||||
await getNotifications();
|
||||
emit(currentState.copyWith(isLoading: false, hasUnreadNotifications: false));
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
emit(currentState.copyWith(isLoading: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ abstract class NotificationsState with _$NotificationsState {
|
||||
const factory NotificationsState.empty() = NotificationsStateEmpty;
|
||||
|
||||
const factory NotificationsState.loaded({
|
||||
required List<BaseNotificationModel> notifications,
|
||||
required List<BaseNotificationModel> notifications,
|
||||
required int notificationsCount,
|
||||
@Default(false) bool isLoading,
|
||||
@Default(false) bool hasUnreadNotifications,
|
||||
}) = NotificationsStateLoaded;
|
||||
|
||||
const factory NotificationsState.error({
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_divider.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/widgets/notification_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -11,6 +15,7 @@ class NotificationsScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocBuilder<NotificationsCubit, NotificationsState>(
|
||||
builder: (BuildContext context, NotificationsState state) {
|
||||
return Scaffold(
|
||||
@@ -19,38 +24,58 @@ class NotificationsScreen extends StatelessWidget {
|
||||
title: const Text('Уведомления'),
|
||||
),
|
||||
body: state is NotificationsStateLoaded
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
? Stack(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NotificationWidget(
|
||||
notification: state.notifications[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: state.notifications.length,
|
||||
)
|
||||
],
|
||||
child: state.isLoading
|
||||
? const Center(child: Loader())
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
SliverList.separated(
|
||||
separatorBuilder: (context, index) => const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: NotificationWidget(
|
||||
notification: state.notifications[index],
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: state.notifications.length,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.markAsRead,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle:
|
||||
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
|
||||
onPressed: () async {
|
||||
context.read<NotificationsCubit>().markNotificationsAsRead();
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
])
|
||||
: const Placeholder(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
|
||||
@@ -46,7 +47,7 @@ class NotificationWidget extends StatelessWidget {
|
||||
ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget(
|
||||
notification: notification as ChosenPlanTomorrowReminderToConsultantNotification,
|
||||
),
|
||||
PlanSubscriptionPaidToAuthorNotification() => throw _PlanSubscriptionPaidToAuthorWidget(
|
||||
PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(
|
||||
notification: notification as PlanSubscriptionPaidToAuthorNotification,
|
||||
),
|
||||
ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget(
|
||||
@@ -65,7 +66,8 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +80,8 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -91,7 +94,8 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.planStarted(notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -104,7 +108,8 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.tomorrowYourPlanWillStart(notification.planName),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -116,6 +121,7 @@ class _DiaryReminderWidget extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.fillYasterday,
|
||||
);
|
||||
}
|
||||
@@ -129,7 +135,8 @@ class _ConsultantLeftCommentWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
notificationsText: s.consultantLeftComment(notification.date),
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -151,6 +158,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
|
||||
);
|
||||
}
|
||||
@@ -164,6 +172,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.youCanceledSubscriptionToPlan(notification.planName),
|
||||
);
|
||||
}
|
||||
@@ -177,6 +186,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.planWithUserStarted(notification.planName, notification.customerName),
|
||||
);
|
||||
}
|
||||
@@ -190,6 +200,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName),
|
||||
);
|
||||
}
|
||||
@@ -203,6 +214,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget {
|
||||
final s = S.of(context);
|
||||
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: s.userPaidThePlan(notification.customerName, notification.planName),
|
||||
);
|
||||
}
|
||||
@@ -240,16 +252,32 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
|
||||
|
||||
// CardSettingsScreen
|
||||
class _BaseNotificationWidget extends StatelessWidget {
|
||||
const _BaseNotificationWidget({required this.notificationsText});
|
||||
const _BaseNotificationWidget({required this.notificationsText, required this.isRead});
|
||||
final String notificationsText;
|
||||
final bool isRead;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.6,
|
||||
child: Text(
|
||||
notificationsText,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.6,
|
||||
child: Text(
|
||||
notificationsText,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
if (!isRead) ...[
|
||||
const Spacer(),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.blueA400,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
width: 8,
|
||||
height: 8,
|
||||
)
|
||||
]
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,12 @@ class NotificationsRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
try {
|
||||
await notificationsService.markNotificationsAsRead();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,23 +13,33 @@ class NotificationsService {
|
||||
final notifications = data.map((notificationJson) {
|
||||
try {
|
||||
final stringifiedType = notificationJson['data']['notification_class_name'];
|
||||
final readDate = notificationJson['read_at'];
|
||||
final type = NotificationType.getTypeFromString(stringifiedType);
|
||||
return switch (type) {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionStartedToAuthor =>
|
||||
PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionCanceledToAuthor =>
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanStartedToClientNotification =>
|
||||
ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type),
|
||||
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.personalizedDietIsDefined =>
|
||||
PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanFinishedToClient =>
|
||||
ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionCanceledToCustomer =>
|
||||
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanStartedToConsultantNotification =>
|
||||
ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type),
|
||||
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.planSubscriptionPaidToAuthor =>
|
||||
PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.chosenPlanFinishedToConsultant =>
|
||||
ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -43,4 +53,12 @@ class NotificationsService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> markNotificationsAsRead() async {
|
||||
try {
|
||||
await dio.post('/api/profile/mark_notifications_as_readed');
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,16 @@ enum ChosenPlanStateEnum {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static List<ChosenPlanStateEnum> getActiveStates() {
|
||||
return [
|
||||
ChosenPlanStateEnum.statePaid,
|
||||
ChosenPlanStateEnum.stateStarted,
|
||||
ChosenPlanStateEnum.stateInProgress,
|
||||
ChosenPlanStateEnum.stateLastDayStarted,
|
||||
ChosenPlanStateEnum.stateLastDayFinished,
|
||||
ChosenPlanStateEnum.stateComplained,
|
||||
ChosenPlanStateEnum.stateDone
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.plan.author.authorIsMe,
|
||||
path: 'plan/${widget.plan.id}',
|
||||
path: 'plan/${widget.plan.slug}',
|
||||
complainedContentType: ComplainedContent.plan,
|
||||
slug: widget.plan.slug,
|
||||
),
|
||||
@@ -143,7 +143,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.plan.author.authorIsMe,
|
||||
path: 'plan/${widget.plan.id}',
|
||||
path: 'plan/${widget.plan.slug}',
|
||||
complainedContentType: ComplainedContent.plan,
|
||||
slug: widget.plan.slug,
|
||||
),
|
||||
@@ -362,7 +362,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
|
||||
actions: [
|
||||
CustomPopup(
|
||||
isAuthor: widget.plan.author.authorIsMe,
|
||||
path: 'plan/${widget.plan.id}',
|
||||
path: 'plan/${widget.plan.slug}',
|
||||
complainedContentType: ComplainedContent.plan,
|
||||
slug: widget.plan.slug,
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
|
||||
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/badge_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -50,7 +51,7 @@ abstract class EducationModel with _$EducationModel {
|
||||
@JsonKey(name: "fb_link") String? fbLink,
|
||||
@JsonKey(name: "tg_link") String? tgLink,
|
||||
@JsonKey(name: "personal_site_link") String? personalSiteLink,
|
||||
@JsonKey(name: "request_state") String? requestState,
|
||||
@JsonKey(name: "request_state") required ModerationStatus requestState,
|
||||
}) = _EducationModel;
|
||||
|
||||
factory EducationModel.fromJson(Map<String, dynamic> json) => _$EducationModelFromJson(json);
|
||||
|
||||
@@ -33,6 +33,7 @@ class ProfileCubit extends Cubit<ProfileState> {
|
||||
);
|
||||
} catch (e) {
|
||||
emit(ProfileState.error(errorMessage: e.toString()));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
@@ -92,12 +93,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
|
||||
} else if (state is ProfileStateError) {
|
||||
return DataErrorWidget(
|
||||
onReload: () async {
|
||||
widget.author == null || widget.author!.authorIsMe
|
||||
? await profileCubit.getMyProfile()
|
||||
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
|
||||
},
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: DataErrorWidget(
|
||||
onReload: () async {
|
||||
widget.author == null || widget.author!.authorIsMe
|
||||
? await profileCubit.getMyProfile()
|
||||
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Scaffold(
|
||||
@@ -329,7 +333,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: 378,
|
||||
height: 781,
|
||||
child: PageView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return PlanWidget(
|
||||
@@ -347,6 +351,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_bullets(_currentBullet, state.plansList.length),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -435,12 +440,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'Дипломы\nи сертификаты',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
@@ -449,14 +454,19 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF2F80ED),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
// GestureDetector(
|
||||
// onTap: () {
|
||||
// GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
// },
|
||||
// child: Text(
|
||||
// widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF2F80ED),
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -531,12 +541,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'Образование',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
@@ -544,14 +554,19 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF2F80ED),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
// GestureDetector(
|
||||
// onTap: () {
|
||||
// GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
|
||||
// },
|
||||
// child: Text(
|
||||
// widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
|
||||
// style: const TextStyle(
|
||||
// color: Color(0xFF2F80ED),
|
||||
// fontSize: 16,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -562,7 +577,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
ListView.separated(
|
||||
@@ -584,6 +599,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
|
||||
_educationBlock(String title, String date) {
|
||||
final parsedDate = DateTime.tryParse(date);
|
||||
final formattedDate = parsedDate != null
|
||||
? DateFormat('d MMMM y', 'ru_RU').format(parsedDate)
|
||||
: date;
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Material(
|
||||
@@ -605,7 +625,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
date,
|
||||
formattedDate,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
color: Color(0xFF6D6F71),
|
||||
@@ -620,6 +640,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
_socialLinks(ProfileStateLoaded state) {
|
||||
if (!state.profile.roles.contains('consultant')) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -964,7 +985,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
context.pushTransparentRoute(
|
||||
rootNavigator: true,
|
||||
PhotoViewScreen(
|
||||
imageUrls: [state.profile.profilePhotoUrl],
|
||||
imageUrls: [baseUrl + state.profile.profilePhotoUrl],
|
||||
index: 0,
|
||||
),
|
||||
);
|
||||
@@ -1044,7 +1065,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
tag: state.profile.profilePhotoUrl,
|
||||
child: CircleAvatar(
|
||||
radius: 32,
|
||||
foregroundImage: NetworkImage(state.profile.profilePhotoUrl),
|
||||
foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -66,7 +66,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
|
||||
radius: 20,
|
||||
backgroundColor: Colors.black12,
|
||||
foregroundImage: NetworkImage(
|
||||
state.blogPosts[widget.index].author.authorProfileIcon,
|
||||
baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
|
||||
),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/profile/models/sub_model.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart';
|
||||
@@ -35,7 +36,7 @@ class _SubWidgetState extends State<SubWidget> {
|
||||
child: Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
foregroundImage: NetworkImage(widget.subModel.profilePhotoUrl),
|
||||
foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: 28,
|
||||
),
|
||||
|
||||
@@ -183,7 +183,8 @@ class ProfileRepository {
|
||||
}
|
||||
|
||||
Future<String> getPassKey(String phone) async {
|
||||
return (jsonDecode((await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
|
||||
return (jsonDecode(
|
||||
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
|
||||
}
|
||||
|
||||
Future<String> getMyPhone() async {
|
||||
@@ -194,7 +195,12 @@ class ProfileRepository {
|
||||
return await profileService.sendEmailCode();
|
||||
}
|
||||
|
||||
Future<void> tryVerifyEmailByCode(String code) async {
|
||||
return await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
|
||||
Future<bool> tryVerifyEmailByCode(String code) async {
|
||||
try {
|
||||
await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
lib/features/recipe/models/my_recipe_model.dart
Normal file
36
lib/features/recipe/models/my_recipe_model.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'my_recipe_model.g.dart';
|
||||
|
||||
part 'my_recipe_model.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class MyRecipePaginationModel with _$MyRecipePaginationModel {
|
||||
const factory MyRecipePaginationModel({
|
||||
required List<MyRecipeModel> data,
|
||||
}) = _MyRecipePaginationModel;
|
||||
|
||||
factory MyRecipePaginationModel.fromJson(Map<String, Object?> json) => _$MyRecipePaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class MyRecipeModel with _$MyRecipeModel {
|
||||
const factory MyRecipeModel({
|
||||
required int id,
|
||||
required String image,
|
||||
required String state,
|
||||
@JsonKey(name: 'cooking_time') required int cookingTime,
|
||||
@JsonKey(name: 'active_time') required int activeCookingTime,
|
||||
@JsonKey(name: 'description_md') required String description,
|
||||
required List<Step>? steps,
|
||||
@JsonKey(name: 'video_url') required String? videoUrl,
|
||||
required String? title,
|
||||
required String slug,
|
||||
@JsonKey(name: 'energy_per_serving') required double calories,
|
||||
@JsonKey(name: 'total_likes') required int likesCount,
|
||||
@JsonKey(name: 'total_views') required int viewsCount,
|
||||
}) = _MyRecipeModel;
|
||||
|
||||
factory MyRecipeModel.fromJson(Map<String, Object?> json) => _$MyRecipeModelFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
|
||||
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MyRecipesCubit extends Cubit<MyRecipesState> {
|
||||
final MyRecipesRepository myRecipesRepository;
|
||||
final AboutRecipeRepository aboutRecipeRepository;
|
||||
|
||||
static const String active = 'active';
|
||||
static const String archived = 'archived';
|
||||
|
||||
MyRecipesCubit({
|
||||
required this.myRecipesRepository,
|
||||
required this.aboutRecipeRepository,
|
||||
}) : super(const MyRecipesState.loading());
|
||||
|
||||
getMyRecipes() async {
|
||||
emit(const MyRecipesState.loading());
|
||||
try {
|
||||
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
|
||||
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
|
||||
} catch (e) {
|
||||
emit(const MyRecipesState.error());
|
||||
}
|
||||
}
|
||||
|
||||
setActiveState() async {
|
||||
emit(const MyRecipesState.loading());
|
||||
try {
|
||||
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
|
||||
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
|
||||
} catch (e) {
|
||||
emit(const MyRecipesState.error());
|
||||
}
|
||||
}
|
||||
|
||||
setArchivedState() async {
|
||||
emit(const MyRecipesState.loading());
|
||||
try {
|
||||
final myRecipes = await myRecipesRepository.getMyRecipes();
|
||||
final List<MyRecipeModel> archivedRecipes = myRecipes.where((recipe) => recipe.state == archived).toList();
|
||||
emit(MyRecipesState.loaded(recipes: archivedRecipes, selectedState: archived));
|
||||
} catch (e) {
|
||||
emit(const MyRecipesState.error());
|
||||
}
|
||||
}
|
||||
|
||||
Future<Recipe> getRecipeById(String slug) async {
|
||||
return await aboutRecipeRepository.getRecipe(slug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'my_recipes_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class MyRecipesState with _$MyRecipesState {
|
||||
const factory MyRecipesState() = _MyRecipesState;
|
||||
|
||||
const factory MyRecipesState.loading() = MyRecipesStateLoading;
|
||||
|
||||
const factory MyRecipesState.loaded({
|
||||
required List<MyRecipeModel> recipes,
|
||||
required String selectedState,
|
||||
}) = MyRecipesStateLoaded;
|
||||
|
||||
const factory MyRecipesState.error() = MyRecipesStateError;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MyRecipesScreen extends StatelessWidget {
|
||||
const MyRecipesScreen({super.key});
|
||||
|
||||
static const name = 'myRecipesScreen';
|
||||
static const path = '/my-recipes-screen';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Мои рецепты',
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: true,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: BlocProvider<MyRecipesCubit>(
|
||||
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
|
||||
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
|
||||
builder: (BuildContext context, MyRecipesState state) {
|
||||
if (state is MyRecipesStateLoaded) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: state.recipes.length,
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
218
lib/features/recipe/presentation/widgets/my_recipe_widget.dart
Normal file
218
lib/features/recipe/presentation/widgets/my_recipe_widget.dart
Normal file
@@ -0,0 +1,218 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
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/core/utils/localization_utils.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_html/flutter_html.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class MyRecipeWidget extends StatefulWidget {
|
||||
final MyRecipeModel recipe;
|
||||
|
||||
const MyRecipeWidget({
|
||||
super.key,
|
||||
required this.recipe,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
|
||||
}
|
||||
|
||||
class _MyRecipeWidgetState extends State<MyRecipeWidget> {
|
||||
late final int hours;
|
||||
late final int minutes;
|
||||
late final String timeTitle;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
hours = widget.recipe.cookingTime ~/ 60;
|
||||
minutes = widget.recipe.cookingTime % 60;
|
||||
if (hours > 0) {
|
||||
timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}';
|
||||
} else {
|
||||
timeTitle = '$minutes ${getMinuteForm(minutes)}';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () async => GoRouter.of(context).pushNamed(
|
||||
'aboutRecipesScreen',
|
||||
extra: {
|
||||
'recipe': await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
|
||||
'navigateToComments': false,
|
||||
},
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '$baseUrl/storage/${widget.recipe.image}',
|
||||
fit: BoxFit.fitWidth,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 8,
|
||||
bottom: 8,
|
||||
child: Wrap(
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_iconButton('assets/icons/clock.svg', timeTitle),
|
||||
const SizedBox(width: 8),
|
||||
_iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'),
|
||||
const SizedBox(width: 8),
|
||||
_iconButton('assets/icons/calories.svg', '${widget.recipe.calories.toStringAsFixed(0)} ккал'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
widget.recipe.title!,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
if (widget.recipe.description.isNotEmpty) ...[
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Html(
|
||||
data: widget.recipe.description.length > 200 ? '${widget.recipe.description.substring(0, 200)}...' : widget.recipe.description,
|
||||
style: {
|
||||
"*": Style(
|
||||
margin: Margins.zero,
|
||||
color: appTheme.black,
|
||||
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
|
||||
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final result = await sl<RecipesCubit>().deleteRecipe(widget.recipe.slug);
|
||||
if (!result) {
|
||||
showErrorToast('Не удалось удалить рецепт');
|
||||
return;
|
||||
}
|
||||
showSuccessToast('Рецепт удален');
|
||||
if (context.mounted) context.read<MyRecipesCubit>().getMyRecipes();
|
||||
},
|
||||
text: 'Удалить',
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
onPressed: () async {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
},
|
||||
height: 52,
|
||||
width: 52,
|
||||
buttonStyle: ButtonStyle(
|
||||
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
),
|
||||
text: s.share,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_iconButton(String iconPath, [String? text]) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
height: 32,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
iconPath,
|
||||
),
|
||||
if (text != null) const SizedBox(width: 8),
|
||||
if (text != null)
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/features/recipe/presentation/widgets/my_recipes_tab.dart
Normal file
68
lib/features/recipe/presentation/widgets/my_recipes_tab.dart
Normal 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/core/widgets/animated_option_button.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class MyRecipesTab extends StatefulWidget {
|
||||
const MyRecipesTab({super.key, this.additionalOnTap});
|
||||
|
||||
final void Function()? additionalOnTap;
|
||||
|
||||
@override
|
||||
State<MyRecipesTab> createState() => _MyRecipesTabState();
|
||||
}
|
||||
|
||||
class _MyRecipesTabState extends State<MyRecipesTab> {
|
||||
final List<String> tabTitles = [];
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final s = S.of(context);
|
||||
if (tabTitles.isEmpty) {
|
||||
tabTitles.add(s.published);
|
||||
tabTitles.add(s.drafted);
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.whiteA700,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: [
|
||||
...List.generate(
|
||||
2,
|
||||
(index) =>
|
||||
Expanded(
|
||||
child: AnimatedOptionButton(
|
||||
textStyle: TextStyles.textSMedium,
|
||||
isSelected: false, // state.selectedStateIndex == index,
|
||||
text: tabTitles[index],
|
||||
onTap: () {
|
||||
widget.additionalOnTap?.call();
|
||||
// context.read<MyPlansCubit>().changeSelectedStateIndex(index);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
16
lib/features/recipe/repositories/my_recipes_repository.dart
Normal file
16
lib/features/recipe/repositories/my_recipes_repository.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
|
||||
|
||||
class MyRecipesRepository {
|
||||
final MyRecipesService myRecipesService;
|
||||
|
||||
MyRecipesRepository({required this.myRecipesService});
|
||||
|
||||
Future<List<MyRecipeModel>> getMyRecipes() async {
|
||||
try {
|
||||
return (await myRecipesService.getMyRecipes()).data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
lib/features/recipe/services/my_recipes_service.dart
Normal file
13
lib/features/recipe/services/my_recipes_service.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'my_recipes_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class MyRecipesService {
|
||||
factory MyRecipesService(Dio dio, {String baseUrl}) = _MyRecipesService;
|
||||
|
||||
@GET('/api/recipes/my')
|
||||
Future<MyRecipePaginationModel> getMyRecipes();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user