forked from eatvio/eatvio_app
Compare commits
88 Commits
diary
...
consultant
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96e80ced38 | ||
|
|
fa1192c5e5 | ||
|
|
41f948c8f7 | ||
|
|
896b2783ae | ||
|
|
9c004f8593 | ||
|
|
a577a8d58a | ||
|
|
3b2c4fdff5 | ||
|
|
44d6487709 | ||
|
|
319fd2dfe3 | ||
|
|
a98043758b | ||
|
|
a84d2d7daa | ||
|
|
4acb861b69 | ||
|
|
f163608a90 | ||
| 6bbec1f022 | |||
|
|
c0311bf213 | ||
|
|
9d1e621769 | ||
|
|
11b695cefd | ||
|
|
926f0de450 | ||
|
|
dc81085ae4 | ||
|
|
8c0efc1a34 | ||
|
|
2c3cc199c0 | ||
|
|
2584a70a09 | ||
|
|
483cf6d8ee | ||
|
|
87d3f058f6 | ||
|
|
7a3c6e6b07 | ||
|
|
e6bad9d35d | ||
|
|
72cf203658 | ||
|
|
086fd1cac4 | ||
| 88e33765d3 | |||
|
|
6fa4bffd7e | ||
|
|
70620d8d9e | ||
|
|
0b2464ea19 | ||
|
|
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
|
||||
11
ios/Podfile
11
ios/Podfile
@@ -40,6 +40,17 @@ post_install do |installer|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
## dart: PermissionGroup.camera
|
||||
'PERMISSION_CAMERA=1',
|
||||
## dart: PermissionGroup.photos
|
||||
'PERMISSION_PHOTOS=1',
|
||||
'PERMISSION_NOTIFICATIONS=1',
|
||||
## dart: PermissionGroup.mediaLibrary
|
||||
'PERMISSION_MEDIA_LIBRARY=1',
|
||||
]
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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 = 27;
|
||||
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 = 27;
|
||||
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 = 27;
|
||||
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>27</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
@@ -32,11 +32,11 @@
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Нужно для загрузки фото</string>
|
||||
<string>Предоставление доступа позволит Вам делать фотографии и добавлять к записям</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Нужно для загрузки фото</string>
|
||||
<string>Предоставление доступа позволит Вам добавлять фото к записям</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/pages/auth_screen.dart';
|
||||
@@ -16,6 +20,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/become_consultant_screen.dart';
|
||||
import 'package:eatvio_app/features/consultant/become_consultant/presentation/pages/stepper_become_consultant_screen.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/pages/consultations_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
|
||||
@@ -32,7 +38,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 +46,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 +84,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 +106,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 +115,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,
|
||||
@@ -143,6 +160,23 @@ class AppRouter {
|
||||
name: 'welcomeScreen',
|
||||
builder: (context, state) => const WelcomeScreen(),
|
||||
redirect: (context, state) async {
|
||||
try {
|
||||
if (await TokenStorage.haveTokenInStorage()) {
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
final fbToken = await firebaseApi.getToken();
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
await sl<Dio>().post(
|
||||
'/api/profile/notification/token/register',
|
||||
data: {
|
||||
'platform': Platform.isIOS ? 'ios': 'android',
|
||||
'hw_id': Platform.isIOS ? (await deviceInfo.iosInfo).identifierForVendor : (await deviceInfo.androidInfo).id,
|
||||
'token': fbToken,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final welcomeScreenWasShowed = sharedPreferences.getBool('welcomeScreenWasShowed');
|
||||
if (welcomeScreenWasShowed == true) {
|
||||
@@ -170,9 +204,19 @@ class AppRouter {
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/consultant-finance-screen',
|
||||
name: 'consultantFinanceScreen',
|
||||
builder: (context, state) => const ConsultantFinanceScreen(),
|
||||
path: ConsultationsArchiveScreen.path,
|
||||
name: ConsultationsArchiveScreen.name,
|
||||
builder: (context, state) => const ConsultationsArchiveScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultantFinancesScreen.path,
|
||||
name: ConsultantFinancesScreen.name,
|
||||
builder: (context, state) => const ConsultantFinancesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: UserPurchasesScreen.path,
|
||||
name: UserPurchasesScreen.name,
|
||||
builder: (context, state) => const UserPurchasesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/user-finance-screen',
|
||||
@@ -201,7 +245,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 +332,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 +397,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 +411,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);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
|
||||
class FirebaseApi {
|
||||
|
||||
final _firebaseMessaging = FirebaseMessaging.instance;
|
||||
|
||||
Future<String?> getToken() async{
|
||||
Future<String?> getToken() async {
|
||||
await _firebaseMessaging.requestPermission();
|
||||
return await _firebaseMessaging.getToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_service.dart';
|
||||
@@ -13,9 +14,13 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
|
||||
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
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';
|
||||
@@ -29,6 +34,8 @@ import 'package:eatvio_app/features/blog_post_create/bloc/blog_post_create_cubit
|
||||
import 'package:eatvio_app/features/blog_post_create/repositories/blog_post_repository.dart';
|
||||
import 'package:eatvio_app/features/blog_post_create/services/blog_post_service.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.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/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
|
||||
@@ -81,9 +88,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 +126,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 +156,12 @@ 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()));
|
||||
sl.registerFactory<ConsultantFinancesCubit>(() => ConsultantFinancesCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<UserPurchasesCubit>(() => UserPurchasesCubit(consultationsRepository: sl()));
|
||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
@@ -165,6 +189,8 @@ 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()));
|
||||
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
@@ -193,6 +219,9 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
|
||||
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
|
||||
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
|
||||
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
|
||||
23
lib/core/permission_handler/permission_handler.dart
Normal file
23
lib/core/permission_handler/permission_handler.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:eatvio_app/core/widgets/show_error_dialog.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PermissionHandler {
|
||||
static Future<void> checkPermission({
|
||||
required Permission permission,
|
||||
required String title,
|
||||
required String description,
|
||||
required Future<void> Function() onGranted,
|
||||
required BuildContext context,
|
||||
}) async {
|
||||
final permissionState = await permission.request();
|
||||
if (permissionState.isGranted) {
|
||||
await onGranted();
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
showErrorDialog(context, title: title, description: description, buttonTitle: S.of(context).allowAccess, needToOpenSettings: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -8,31 +8,39 @@ extension DateTimeExtension on DateTime {
|
||||
String pattern = dateTimeFormatPattern,
|
||||
String? locale,
|
||||
]) {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
if (locale != null && locale.isNotEmpty) {
|
||||
initializeDateFormatting(locale);
|
||||
}
|
||||
return DateFormat(pattern, locale).format(this);
|
||||
return DateFormat(pattern, locale).format(localDate);
|
||||
}
|
||||
|
||||
bool isYesterday(DateTime date) {
|
||||
final DateTime localDate = date.isUtc ? date.toLocal() : date;
|
||||
final yesterday = DateTime.now().subtract(const Duration(days: 1));
|
||||
return yesterday.day == localDate.day && yesterday.month == localDate.month && yesterday.year == localDate.year;
|
||||
}
|
||||
|
||||
bool isToday(DateTime date) {
|
||||
final DateTime localDate = date.isUtc ? date.toLocal() : date;
|
||||
bool isYesterday() {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
return now.day == localDate.day && now.month == localDate.month && now.year == localDate.year;
|
||||
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
|
||||
return localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day;
|
||||
}
|
||||
|
||||
bool isToday() {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
return now.year == localDate.year &&
|
||||
now.month == localDate.month &&
|
||||
now.day == localDate.day;
|
||||
}
|
||||
}
|
||||
|
||||
String calculateDifference(DateTime dateTime) {
|
||||
if (dateTime.isToday(dateTime)) {
|
||||
final localDateTime = dateTime.isUtc ? dateTime.toLocal() : dateTime;
|
||||
|
||||
if (localDateTime.isToday()) {
|
||||
return 'Сегодня';
|
||||
} else if (dateTime.isYesterday(dateTime)) {
|
||||
} else if (localDateTime.isYesterday()) {
|
||||
return 'Вчера';
|
||||
} else {
|
||||
return dateTime.format('d MMMM', 'ru');
|
||||
return localDateTime.format('d MMMM', 'ru');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,8 +4,9 @@ import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
Future<void> showErrorDialog(BuildContext context, {String? title, String? description}) async {
|
||||
Future<void> showErrorDialog(BuildContext context, {String? title, String? description, String? buttonTitle, bool? needToOpenSettings}) async {
|
||||
return await showDialog<void>(
|
||||
context: context,
|
||||
barrierColor: Colors.transparent,
|
||||
@@ -43,8 +44,13 @@ Future<void> showErrorDialog(BuildContext context, {String? title, String? descr
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: () => AppRouter.navigatorKey.currentContext?.pop(),
|
||||
child: Text(s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||
onPressed: () {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
if (needToOpenSettings ?? false) {
|
||||
openAppSettings();
|
||||
}
|
||||
},
|
||||
child: Text(buttonTitle ?? s.ok, style: TextStyles.textLSemibold.copyWith(color: appTheme.blueA400)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
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/auth/presentation/bloc/auth_state.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/dev_tools/presentation/dev_tools_screen.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';
|
||||
@@ -20,8 +23,6 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../bloc/auth_state.dart';
|
||||
|
||||
class AuthScreen extends StatefulWidget {
|
||||
const AuthScreen({super.key});
|
||||
|
||||
@@ -38,6 +39,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
late StreamSubscription<bool> keyboardSubscription;
|
||||
bool isFocused = false;
|
||||
int tapsCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -49,6 +51,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);
|
||||
@@ -66,11 +82,8 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
tapsCount++;
|
||||
if (tapsCount == 6) {
|
||||
tapsCount = 0;
|
||||
// showModalBottomSheet(context: context, builder: (context) => );
|
||||
}
|
||||
EasyDebounce.debounce('change env', Durations.medium4, () {
|
||||
tapsCount = 0;
|
||||
});
|
||||
GoRouter.of(context).pushNamed(DevToolsScreen.name);
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
@@ -136,7 +149,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
child: RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: 'Нажимая "Войти", Вы соглашаетесь \n',
|
||||
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
@@ -153,6 +166,17 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
const TextSpan(text: ' и '),
|
||||
TextSpan(
|
||||
text: 'Политикой конфиденциальности\nи защитой персональных данных',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/docs/privacy.pdf')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -179,8 +203,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'),
|
||||
),
|
||||
@@ -215,7 +215,10 @@ class _BlogPostScreenState extends State<BlogPostScreen> {
|
||||
child: Html(data: widget.blogPost.description, style: {"*": Style(color: Colors.black87)}),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CachedNetworkImage(imageUrl: '$baseUrl/storage/${widget.blogPost.image}'),
|
||||
if (widget.blogPost.image.isNotEmpty)
|
||||
CachedNetworkImage(
|
||||
imageUrl: '$baseUrl/storage/${widget.blogPost.image}',
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/core/theme/app_decoration.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';
|
||||
@@ -17,6 +19,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class BlogPostCreateScreen extends StatefulWidget {
|
||||
final FeedBlogPostModel? feedBlogPostModel;
|
||||
@@ -80,47 +83,65 @@ class _BlogPostCreateScreenState extends State<BlogPostCreateScreen> {
|
||||
_showImagePicker() {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (context) => CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
builder: (context) {
|
||||
final s = S.of(context);
|
||||
return CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.camera,
|
||||
title: s.allowAccessToCamera,
|
||||
description: s.allowAccessToCameraDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.photos,
|
||||
title: s.allowAcessToGalery,
|
||||
description: s.allowAcessToGaleryDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
if (!context.mounted) return;
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ class BlogPostRepository {
|
||||
'video_url': videoUrl,
|
||||
'image': image != null
|
||||
? await MultipartFile.fromFile((await ImageCompressUtility.compressFile(File(image.path))).path, filename: image.name)
|
||||
: oldImageUrl != null
|
||||
: oldImageUrl != '' && oldImageUrl != null
|
||||
? await MultipartFile.fromFile((await downloadImage(oldImageUrl))?.path ?? '', filename: oldImageUrl.split('/').last)
|
||||
: null,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -9,7 +9,6 @@ abstract class ChatUserModel with _$ChatUserModel {
|
||||
required int id,
|
||||
required String name,
|
||||
required String slug,
|
||||
@JsonKey(name: 'profile_photo_path') String? profilePhotoPath,
|
||||
@JsonKey(name: 'profile_photo_url') String? profilePhotoUrl,
|
||||
}) = _ChatUserModel;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,12 +43,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 +72,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
_listener.dispose();
|
||||
chatRoomCubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -95,7 +108,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 +117,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;
|
||||
@@ -302,17 +315,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,6 +1,5 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:dismissible_page/dismissible_page.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/utils/date_time_utils.dart';
|
||||
@@ -40,7 +39,7 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4, bottom: 4),
|
||||
child: Text(
|
||||
sendTime.format('hh:mm'),
|
||||
sendTime.format('HH:mm', 'ru'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF7B8794),
|
||||
height: 1,
|
||||
@@ -81,7 +80,7 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
contentPadding: const EdgeInsets.all(4),
|
||||
horizontalTitleGap: 8,
|
||||
onTap: () async {
|
||||
if (!await launchUrl(Uri.parse('$baseUrl/storage/${mediaData[index]}'))) {
|
||||
if (!await launchUrl(Uri.parse('${mediaData[index]}'))) {
|
||||
throw Exception('Could not launch ${mediaData[index].toString()}');
|
||||
}
|
||||
},
|
||||
@@ -108,18 +107,18 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
onTap: () {
|
||||
context.pushTransparentRoute(
|
||||
PhotoViewScreen(
|
||||
imageUrls: mediaData.map((e) => '$baseUrl/storage/$e').toList(),
|
||||
imageUrls: mediaData.map((e) => '$e').toList(),
|
||||
index: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Hero(
|
||||
tag: '$baseUrl/storage/${mediaData[index]}',
|
||||
tag: '${mediaData[index]}',
|
||||
transitionOnUserGestures: true,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: '$baseUrl/storage/${mediaData[index]}',
|
||||
imageUrl: '${mediaData[index]}',
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -145,7 +144,7 @@ class ChatBubbleWidget extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 4),
|
||||
child: Text(
|
||||
sendTime.format('hh:mm'),
|
||||
sendTime.format('HH:mm', 'ru'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF7B8794),
|
||||
height: 1,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'rating_model.freezed.dart';
|
||||
part 'rating_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
abstract class RatingPaginationModel with _$RatingPaginationModel {
|
||||
factory RatingPaginationModel({
|
||||
required List<RatingModel> data,
|
||||
}) = _RatingPaginationModel;
|
||||
|
||||
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
factory RatingModel({
|
||||
required DateTime date,
|
||||
required String title,
|
||||
required int amount,
|
||||
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date,
|
||||
@JsonKey(name: 'log_data') required RatingEnum type,
|
||||
@JsonKey(name: 'log_scores') required int amount,
|
||||
}) = _RatingModel;
|
||||
|
||||
factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
enum RatingEnum {
|
||||
@JsonValue('ADDED_RECIPE')
|
||||
addedRecipe,
|
||||
@JsonValue('ADDED_PUBLIC_DIET')
|
||||
addedPublicDiet,
|
||||
@JsonValue('ADDED_BLOG_POST')
|
||||
addedBlogPost,
|
||||
@JsonValue('INVITED_A_NEW_USER')
|
||||
invitedUser,
|
||||
@JsonValue('HAS_MEDICAL_DEGREE')
|
||||
hasMedicalDegree,
|
||||
@JsonValue('CONSULTED')
|
||||
consulted,
|
||||
@JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED')
|
||||
addedGlossary;
|
||||
}
|
||||
|
||||
19
lib/features/consultant_rating/models/statistics_model.dart
Normal file
19
lib/features/consultant_rating/models/statistics_model.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'statistics_model.freezed.dart';
|
||||
part 'statistics_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class StatisticsModel with _$StatisticsModel {
|
||||
|
||||
factory StatisticsModel({
|
||||
@JsonKey(name: 'count_referal') int? countReferal,
|
||||
@JsonKey(name: 'count_blog_posts') int? blogPostsCount,
|
||||
@JsonKey(name: 'count_recipes') int? recipesCount,
|
||||
@JsonKey(name: 'count_diets') int? dietsCount,
|
||||
@JsonKey(name: 'count_consultants') int? consultationsCount,
|
||||
@JsonKey(name: 'count_products_nutritional_values') int? qwe,
|
||||
}) = _StatisticsModel;
|
||||
|
||||
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
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/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingScreen extends StatelessWidget {
|
||||
static const name = 'consultantRatingScreen';
|
||||
@@ -15,7 +19,10 @@ class ConsultantRatingScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const ConsultantRatingPageContent();
|
||||
return BlocProvider<ConsultantRatingCubit>(
|
||||
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
|
||||
child: const ConsultantRatingPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,52 +32,56 @@ class ConsultantRatingPageContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const RatingWidget(
|
||||
rating: 1058,
|
||||
return BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.copy_referral_link,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
body: state.error != null
|
||||
? DataErrorWidget(onReload: () async {
|
||||
await context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
if (!context.mounted) return;
|
||||
await context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
})
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const RatingWidget(
|
||||
rating: 1058,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.copy_referral_link,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,),
|
||||
const SizedBox(height: 16),
|
||||
RatingHistoryWidget(
|
||||
isLoading: state.isRatingLoading,
|
||||
ratingHistory: state.ratings,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const StatisticWidget(),
|
||||
const SizedBox(height: 16),
|
||||
RatingHistoryWidget(
|
||||
ratingHistory: [
|
||||
RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212),
|
||||
RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323),
|
||||
RatingModel(date: DateTime.now(), title: 'ee', amount: 1),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
|
||||
final ConsultantRatingRepository repository;
|
||||
ConsultantRatingCubit(this.repository) : super(ConsultantRatingState());
|
||||
|
||||
Future<void> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isStatisticsLoading: true));
|
||||
final statistics = await repository.getConsultantStatistics(date: date);
|
||||
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isRatingLoading: true));
|
||||
final scoreLogs = await repository.getScoreLogs(date: date);
|
||||
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'consultant_rating_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ConsultantRatingState with _$ConsultantRatingState {
|
||||
factory ConsultantRatingState({
|
||||
@Default(false) bool isStatisticsLoading,
|
||||
@Default(false) bool isRatingLoading,
|
||||
@Default(null) StatisticsModel? statistics,
|
||||
@Default(null) String? error,
|
||||
@Default(<RatingModel>[]) List<RatingModel> ratings,
|
||||
}) = _ConsultantRatingState;
|
||||
}
|
||||
@@ -1,42 +1,29 @@
|
||||
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/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PeriodFilterWidget extends StatefulWidget {
|
||||
const PeriodFilterWidget({super.key});
|
||||
|
||||
const PeriodFilterWidget({super.key, required this.isStatistic});
|
||||
final bool isStatistic;
|
||||
@override
|
||||
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
|
||||
}
|
||||
|
||||
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
final List<String> periodDropDownTitles = [];
|
||||
int periodDropDownIndex = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final s = S.of(context);
|
||||
if (periodDropDownTitles.isEmpty) {
|
||||
periodDropDownTitles.addAll([
|
||||
s.for_whole_period_of_time,
|
||||
s.for_a_year,
|
||||
s.for_a_month,
|
||||
s.for_a_week,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
PeriodFilterStatus _currentState = PeriodFilterStatus.all;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomDropDownButton(
|
||||
CustomDropDownButton<PeriodFilterStatus>(
|
||||
hint: Text(
|
||||
s.choose_diet_duration,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
@@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
periodDropDownIndex = periodDropDownTitles.indexOf(value);
|
||||
_currentState = value;
|
||||
});
|
||||
if (widget.isStatistic) {
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics(date: value.value);
|
||||
return;
|
||||
}
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs(date: value.value);
|
||||
return;
|
||||
}
|
||||
},
|
||||
items: periodDropDownTitles
|
||||
items: PeriodFilterStatus.values
|
||||
.mapIndexed(
|
||||
(index, value) => DropdownMenuItem(
|
||||
value: periodDropDownTitles[index],
|
||||
value: PeriodFilterStatus.values[index],
|
||||
child: Text(
|
||||
periodDropDownTitles[index],
|
||||
PeriodFilterStatus.values[index].name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: periodDropDownTitles[periodDropDownIndex],
|
||||
value: _currentState,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PeriodFilterStatus {
|
||||
all,
|
||||
month,
|
||||
week,
|
||||
year;
|
||||
|
||||
String get name => switch (this) {
|
||||
PeriodFilterStatus.all => 'Все периоды',
|
||||
PeriodFilterStatus.month => 'За месяц',
|
||||
PeriodFilterStatus.week => 'За неделю',
|
||||
PeriodFilterStatus.year => 'За год',
|
||||
};
|
||||
|
||||
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'),
|
||||
PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray100,
|
||||
@@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
date.format('dd.MM.yyyy'),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
|
||||
@@ -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/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
@@ -9,7 +10,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class RatingHistoryWidget extends StatelessWidget {
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory});
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading});
|
||||
final bool isLoading;
|
||||
final List<RatingModel> ratingHistory;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget {
|
||||
right: 16,
|
||||
top: 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
child: isLoading
|
||||
? const Center(child: Loader())
|
||||
: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: ratingHistory[index].title,
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const PeriodFilterWidget(isStatistic: false),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: switch (ratingHistory[index].type) {
|
||||
RatingEnum.addedRecipe => 'Добавление рецепта',
|
||||
RatingEnum.addedPublicDiet => 'Добавление публичной диеты',
|
||||
RatingEnum.addedBlogPost => 'Добавление поста',
|
||||
RatingEnum.invitedUser => 'Регистрация по реф. ссылке',
|
||||
RatingEnum.hasMedicalDegree => 'Получение мед. степени',
|
||||
RatingEnum.consulted => 'Консультация',
|
||||
RatingEnum.addedGlossary => 'Добавление продукта в глоссарий',
|
||||
},
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
@@ -8,14 +10,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class StatisticWidget extends StatelessWidget {
|
||||
const StatisticWidget({super.key});
|
||||
|
||||
const StatisticWidget({super.key, required this.isLoading, required this.statistics});
|
||||
final bool isLoading;
|
||||
final StatisticsModel? statistics;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
child: isLoading ? const Center(child: Loader()) : Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const PeriodFilterWidget(
|
||||
isStatistic: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
|
||||
class ConsultantRatingRepository {
|
||||
final ConsultantRatingService service;
|
||||
|
||||
ConsultantRatingRepository(this.service);
|
||||
|
||||
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
return await service.getConsultantStatistics(date: date);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RatingModel>> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
final data = (await service.getScoreLogs(date: date)).data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'consultant_rating_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class ConsultantRatingService {
|
||||
factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService;
|
||||
|
||||
@GET('/api/consultants/rating/statistics')
|
||||
Future<StatisticsModel> getConsultantStatistics({@Query('filters[duration][\$gte]') String? date});
|
||||
|
||||
@GET('/api/consultants/score/logs')
|
||||
Future<RatingPaginationModel> getScoreLogs({@Query('filters[duration][\$gte]') String? date});
|
||||
}
|
||||
@@ -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,12 +1,15 @@
|
||||
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';
|
||||
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_state.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.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 +45,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(ConsultationsArchiveScreen.name);
|
||||
},
|
||||
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(
|
||||
@@ -183,6 +185,8 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: List.generate(widget.consultationsModel.duration, (index) {
|
||||
final day = givenDaysWithDayNumber.firstWhereOrNull((e) => e.$1 == index);
|
||||
final hasActivity = day != null && day.$2.state != DiaryStateEnum.init;
|
||||
final canConsultantOpenThisDay = day != null &&
|
||||
(day.$2.state != DiaryStateEnum.init && day.$2.state != DiaryStateEnum.started && day.$2.state != DiaryStateEnum.lastDayStarted);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Column(
|
||||
@@ -190,12 +194,15 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
if (!hasActivity) {
|
||||
if (!hasActivity || !canConsultantOpenThisDay) {
|
||||
showErrorDialog(context,
|
||||
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 +279,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 +298,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,9 @@
|
||||
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/consultant_finance_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
|
||||
|
||||
class ConsultationsRepository {
|
||||
final ConsultationService consultationService;
|
||||
@@ -13,4 +17,47 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ConsultantFinanceModel>> getConsultantFinances(String? date, FiltersComparisonEnum? dateComparison) async {
|
||||
try {
|
||||
return await consultationService.getConsultantFinances(date, dateComparison);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<UserPurchasesModel>> getUserPurchases(String? date, FiltersComparisonEnum? dateComparison) async {
|
||||
try {
|
||||
return await consultationService.getUserPurchases(date, dateComparison);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,109 @@
|
||||
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/consultant_finance_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_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;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ConsultantFinanceModel>> getConsultantFinances(
|
||||
String? date,
|
||||
FiltersComparisonEnum? dateComparison,
|
||||
) async {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'/api/finances/my_finances',
|
||||
queryParameters: {
|
||||
if (dateComparison != null && date != null) 'filters[end_date]': date,
|
||||
},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final consultations = data['data'] as List<dynamic>;
|
||||
return consultations.map((consultation) => ConsultantFinanceModel.fromJson(consultation as Map<String, dynamic>)).toList();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<UserPurchasesModel>> getUserPurchases(
|
||||
String? date,
|
||||
FiltersComparisonEnum? dateComparison,
|
||||
) async {
|
||||
try {
|
||||
final response = await dio.get(
|
||||
'/api/finances/my_purchases',
|
||||
queryParameters: {
|
||||
if (dateComparison != null && date != null) 'filters[start_date]': date,
|
||||
},
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final purchases = data['data'] as List<dynamic>;
|
||||
return purchases.map((purchase) => UserPurchasesModel.fromJson(purchase as Map<String, dynamic>)).toList();
|
||||
} 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),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.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/create_diet/presentation/cubit/create_diet_cubit.dart';
|
||||
@@ -11,7 +13,9 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class DietImagePickerWidget extends StatefulWidget {
|
||||
const DietImagePickerWidget({super.key});
|
||||
@@ -28,11 +32,18 @@ class _DietImagePickerWidgetState extends State<DietImagePickerWidget> {
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.camera);
|
||||
cubit.setImage(image);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.camera,
|
||||
title: s.allowAccessToCamera,
|
||||
description: s.allowAccessToCameraDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.camera);
|
||||
cubit.setImage(image);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -44,11 +55,18 @@ class _DietImagePickerWidgetState extends State<DietImagePickerWidget> {
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.gallery);
|
||||
cubit.setImage(image);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.photos,
|
||||
title: s.allowAcessToGalery,
|
||||
description: s.allowAcessToGaleryDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.gallery);
|
||||
cubit.setImage(image);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.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/create_diet/presentation/widgets/error_widget.dart';
|
||||
@@ -11,7 +13,9 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PlanImagePickerWidget extends StatefulWidget {
|
||||
const PlanImagePickerWidget({super.key});
|
||||
@@ -28,11 +32,18 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.camera);
|
||||
cubit.setImage(image);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.camera,
|
||||
title: s.allowAccessToCamera,
|
||||
description: s.allowAccessToCameraDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.camera);
|
||||
cubit.setImage(image);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
@@ -44,11 +55,18 @@ class _PlanImagePickerWidgetState extends State<PlanImagePickerWidget> {
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.gallery);
|
||||
cubit.setImage(image);
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.photos,
|
||||
title: s.allowAcessToGalery,
|
||||
description: s.allowAcessToGaleryDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.gallery);
|
||||
cubit.setImage(image);
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -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}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class ImageWidget extends StatefulWidget {
|
||||
const ImageWidget({super.key, required this.image});
|
||||
@@ -67,55 +72,72 @@ class _ImageWidgetState extends State<ImageWidget> {
|
||||
_showImagePicker() {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (context) => CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
createRecipeCubit.replaceImage(File(image.path));
|
||||
createRecipeCubit.validateImage();
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
builder: (context) {
|
||||
final s = S.of(context);
|
||||
return CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.camera,
|
||||
title: s.allowAccessToCamera,
|
||||
description: s.allowAccessToCameraDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
createRecipeCubit.replaceImage(File(image.path));
|
||||
createRecipeCubit.validateImage();
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
createRecipeCubit.replaceImage(File(image.path));
|
||||
createRecipeCubit.validateImage();
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.photos,
|
||||
title: s.allowAcessToGalery,
|
||||
description: s.allowAcessToGaleryDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
createRecipeCubit.replaceImage(File(image.path));
|
||||
createRecipeCubit.validateImage();
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recipe_cubit/create_recipe_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class StepImageWidget extends StatefulWidget {
|
||||
final CreateRecipeCubit createRecipeCubit;
|
||||
@@ -12,7 +17,8 @@ class StepImageWidget extends StatefulWidget {
|
||||
|
||||
const StepImageWidget({
|
||||
super.key,
|
||||
required this.index, required this.createRecipeCubit,
|
||||
required this.index,
|
||||
required this.createRecipeCubit,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -79,53 +85,70 @@ class _StepImageWidgetState extends State<StepImageWidget> {
|
||||
_showImagePicker() {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (context) => CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
builder: (context) {
|
||||
final s = S.of(context);
|
||||
return CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.camera,
|
||||
title: s.allowAccessToCamera,
|
||||
description: s.allowAccessToCameraDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.camera);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.photos,
|
||||
title: s.allowAcessToGalery,
|
||||
description: s.allowAcessToGaleryDescription,
|
||||
onGranted: () async {
|
||||
final picker = ImagePicker();
|
||||
image = await picker.pickImage(source: ImageSource.gallery);
|
||||
setState(() {});
|
||||
if (image != null) {
|
||||
widget.createRecipeCubit.replaceImageInStep(File(image!.path), widget.index);
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/permission_handler/permission_handler.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.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/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class PhotoGalleryWidget extends StatefulWidget {
|
||||
const PhotoGalleryWidget({super.key});
|
||||
@@ -32,55 +37,75 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
|
||||
showImagePicker(DiaryDayModel diaryDay) {
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (context) => CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
final picker = ImagePicker();
|
||||
XFile? image = await picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
diaryCubit.uploadPhotos(
|
||||
files: [File(image.path)],
|
||||
diaryId: diaryDay.id,
|
||||
builder: (context) {
|
||||
final s = S.of(context);
|
||||
return CupertinoActionSheet(
|
||||
actions: [
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.camera,
|
||||
title: s.allowAccessToCamera,
|
||||
description: s.allowAccessToCameraDescription,
|
||||
onGranted: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
|
||||
final picker = ImagePicker();
|
||||
XFile? image = await picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
diaryCubit.uploadPhotos(
|
||||
files: [File(image.path)],
|
||||
diaryId: diaryDay.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo_camera),
|
||||
SizedBox(width: 8),
|
||||
Text('Фото'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
final picker = ImagePicker();
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
diaryCubit.uploadPhotos(
|
||||
files: images.map((f) => File(f.path)).toList(),
|
||||
diaryId: diaryDay.id,
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
PermissionHandler.checkPermission(
|
||||
permission: Permission.photos,
|
||||
title: s.allowAcessToGalery,
|
||||
description: s.allowAcessToGaleryDescription,
|
||||
onGranted: () async {
|
||||
AppRouter.navigatorKey.currentContext?.pop();
|
||||
final picker = ImagePicker();
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
diaryCubit.uploadPhotos(
|
||||
files: images.map((f) => File(f.path)).toList(),
|
||||
diaryId: diaryDay.id,
|
||||
);
|
||||
}
|
||||
},
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
},
|
||||
child: const Row(
|
||||
children: [
|
||||
Icon(Icons.photo),
|
||||
SizedBox(width: 8),
|
||||
Text('Галерея'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
],
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -92,7 +117,9 @@ class _PhotoGalleryWidgetState extends State<PhotoGalleryWidget> {
|
||||
int mealIndex = state.selectedMeal - 1;
|
||||
if (state.diary.meals[mealIndex].photos.isEmpty && !state.isConsultantView && !state.isDiaryFinished && !state.isDiaryCommented) {
|
||||
return GestureDetector(
|
||||
onTap: () => showImagePicker(state.diary),
|
||||
onTap: () async {
|
||||
showImagePicker(state.diary);
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
'assets/no_one_image_was_added.svg',
|
||||
|
||||
@@ -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") required 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,35 @@
|
||||
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/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> getConsultantFinances() async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final consultantFinances = await consultationsRepository.getConsultantFinances(
|
||||
state.periodFilterStatus?.value,
|
||||
state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
|
||||
);
|
||||
emit(state.copyWith(financesModels: consultantFinances, isLoading: false));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(financesModels: [], isLoading: false));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setFinancesPeriod(PeriodFilterStatus period) async {
|
||||
emit(state.copyWith(periodFilterStatus: period));
|
||||
await getConsultantFinances();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/consultant_finance_model.dart';
|
||||
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<ConsultantFinanceModel> financesModels,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
}) = _ConsultantFinancesState;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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/user_purchases/user_purchases_state.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class UserPurchasesCubit extends Cubit<UserPurchasesState> {
|
||||
final ConsultationsRepository consultationsRepository;
|
||||
UserPurchasesCubit({
|
||||
required this.consultationsRepository,
|
||||
}) : super(
|
||||
UserPurchasesState(),
|
||||
);
|
||||
|
||||
Future<void> getUserPurchases() async {
|
||||
try {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
final purchasesModels = await consultationsRepository.getUserPurchases(
|
||||
state.periodFilterStatus?.value,
|
||||
state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
|
||||
);
|
||||
|
||||
emit(state.copyWith(purchasesModels: purchasesModels, isLoading: false));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(purchasesModels: [], isLoading: false));
|
||||
log(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setPurchasesPeriod(PeriodFilterStatus period) async {
|
||||
emit(state.copyWith(periodFilterStatus: period));
|
||||
await getUserPurchases();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/models/user_purchases_model.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'user_purchases_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class UserPurchasesState with _$UserPurchasesState {
|
||||
factory UserPurchasesState({
|
||||
@Default([]) List<UserPurchasesModel> purchasesModels,
|
||||
@Default(false) isLoading,
|
||||
@Default(null) String? error,
|
||||
@Default(null) PeriodFilterStatus? periodFilterStatus,
|
||||
}) = _UserPurchasesState;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'consultant_finance_model.freezed.dart';
|
||||
part 'consultant_finance_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ConsultantFinanceModel with _$ConsultantFinanceModel {
|
||||
factory ConsultantFinanceModel({
|
||||
@JsonKey(name: 'start_date', fromJson: parseDate) required DateTime startDate,
|
||||
@JsonKey(name: 'bank_id') required String bankId,
|
||||
@JsonKey(name: 'title') required String title,
|
||||
@JsonKey(name: 'clientName') required String clientName,
|
||||
@JsonKey(name: 'duration') required int duration,
|
||||
@JsonKey(name: 'payment_price', fromJson: _parsePayment) required String paymentPrice,
|
||||
required PaymentStatus? paymentStatus,
|
||||
}) = _ConsultantFinanceModel;
|
||||
|
||||
factory ConsultantFinanceModel.fromJson(Map<String, dynamic> json) => _$ConsultantFinanceModelFromJson(json);
|
||||
}
|
||||
|
||||
String _parsePayment(json) {
|
||||
return ((json as int) / 100).toStringAsFixed(2);
|
||||
}
|
||||
|
||||
enum PaymentStatus {
|
||||
@JsonValue('COMPLETED')
|
||||
completed,
|
||||
@JsonValue('REJECTED')
|
||||
rejected,
|
||||
@JsonValue('CHECKED')
|
||||
checked
|
||||
}
|
||||
@@ -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,21 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'user_purchases_model.freezed.dart';
|
||||
part 'user_purchases_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class UserPurchasesModel with _$UserPurchasesModel {
|
||||
factory UserPurchasesModel({
|
||||
@JsonKey(name: 'start_date', fromJson: parseDate) required DateTime startDate,
|
||||
@JsonKey(name: 'bank_id') required String bankId,
|
||||
@JsonKey(name: 'title') required String title,
|
||||
@JsonKey(name: 'consultantName') required String consultantName,
|
||||
@JsonKey(name: 'duration') required int duration,
|
||||
@JsonKey(name: 'price') required int paymentPrice,
|
||||
@JsonKey(name: 'chosen_plan_last_state') required ChosenPlanStateEnum chosenPlanState,
|
||||
}) = _UserPurchasesModel;
|
||||
|
||||
factory UserPurchasesModel.fromJson(Map<String, dynamic> json) => _$UserPurchasesModelFromJson(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,225 @@
|
||||
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/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/models/consultant_finance_model.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/features/my_finance/presentation/pages/card_settings.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class ConsultantFinancesScreen extends StatefulWidget {
|
||||
const ConsultantFinancesScreen({super.key});
|
||||
static const name = 'consultantFinancesScreen';
|
||||
static const path = '/consultant-finances-screen';
|
||||
@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>()..getConsultantFinances(),
|
||||
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: Text(
|
||||
'Финансы',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(
|
||||
child: Loader(),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
GoRouter.of(context).pushNamed(CardSettingsScreen.name);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
child: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Text('Управление картами', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
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<ConsultantFinancesCubit>().setFinancesPeriod(value);
|
||||
},
|
||||
value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state.financesModels.isNotEmpty)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final Status modelStatus = switch (state.financesModels[index].paymentStatus) {
|
||||
null => Status.inProgress,
|
||||
PaymentStatus.completed => Status.success,
|
||||
PaymentStatus.rejected => Status.canceled,
|
||||
PaymentStatus.checked => Status.success,
|
||||
};
|
||||
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: [
|
||||
Builder(builder: (context) {
|
||||
final idLen = state.financesModels[index].bankId.length;
|
||||
return Text(
|
||||
'${state.financesModels[index].bankId.substring(0, 4)}...${state.financesModels[index].bankId.substring(idLen - 4, idLen)} ',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
);
|
||||
}),
|
||||
Text(
|
||||
state.financesModels[index].startDate.format('yyyy.MM.dd'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.financesModels[index].title,
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Покупатель: ${state.financesModels[index].clientName}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${state.financesModels[index].duration} ${getDayForm(state.financesModels[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.financesModels[index].paymentPrice} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
StatusWidget(
|
||||
status: modelStatus,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,292 @@
|
||||
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/features/plans/enums/chosen_plan_state_enum.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});
|
||||
static const name = 'consultationsArchiveScreen';
|
||||
static const path = '/consultations-archive-screen';
|
||||
@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) {
|
||||
final Status consultationStatus;
|
||||
if (state.consultations[index].chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
consultationStatus = Status.canceled;
|
||||
} else if (state.consultations[index].chosenPlanState == ChosenPlanStateEnum.stateChargeWasCollected) {
|
||||
consultationStatus = Status.success;
|
||||
} else {
|
||||
consultationStatus = Status.inProgress;
|
||||
}
|
||||
|
||||
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: consultationStatus,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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,209 @@
|
||||
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/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_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/features/plans/enums/chosen_plan_state_enum.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class UserPurchasesScreen extends StatefulWidget {
|
||||
const UserPurchasesScreen({super.key});
|
||||
static const name = 'userPurchasesScreen';
|
||||
static const path = '/user-purchases-screen';
|
||||
@override
|
||||
State<UserPurchasesScreen> createState() => _UserPurchasesScreen();
|
||||
}
|
||||
|
||||
class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return BlocProvider<UserPurchasesCubit>(
|
||||
create: (context) => sl<UserPurchasesCubit>()..getUserPurchases(),
|
||||
child: BlocBuilder<UserPurchasesCubit, UserPurchasesState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
appBar: AppBar(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusDirectional.vertical(
|
||||
bottom: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Финансы',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(
|
||||
child: Loader(),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
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<UserPurchasesCubit>().setPurchasesPeriod(value);
|
||||
},
|
||||
value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (state.purchasesModels.isNotEmpty)
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final Status purchaseStatus;
|
||||
if (state.purchasesModels[index].chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
|
||||
purchaseStatus = Status.canceled;
|
||||
} else if (state.purchasesModels[index].chosenPlanState == ChosenPlanStateEnum.stateChargeWasCollected) {
|
||||
purchaseStatus = Status.success;
|
||||
} else {
|
||||
purchaseStatus = Status.inProgress;
|
||||
}
|
||||
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: [
|
||||
Builder(builder: (context) {
|
||||
final idLen = state.purchasesModels[index].bankId.length;
|
||||
return Text(
|
||||
'${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
);
|
||||
}),
|
||||
Text(
|
||||
state.purchasesModels[index].startDate.format('yyyy.MM.dd'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.purchasesModels[index].title,
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Продавец: ${state.purchasesModels[index].consultantName}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[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.purchasesModels[index].paymentPrice} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
StatusWidget(
|
||||
status: purchaseStatus,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemCount: state.purchasesModels.length,
|
||||
)
|
||||
else
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40.0),
|
||||
child: Text(
|
||||
'У вас нет таких консультаций',
|
||||
style: TextStyles.textMMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user