Compare commits

...

58 Commits

Author SHA1 Message Date
Username1548
2c3cc199c0 fix: change baseUrl 2025-06-09 19:53:26 +07:00
Username1548
2584a70a09 fix(diary): fixed app review issues 2025-06-09 19:51:32 +07:00
Username1548
69359b01c5 Merge remote-tracking branch 'upstream/main' 2025-06-05 04:29:06 +07:00
Username1548
ff40e74491 feat(chats): added refresh timer for chat room, feat(diary): fixed plan duration in plan widget and added date to app bar) 2025-06-05 04:28:43 +07:00
mso7nueh
0270c940fe Merge pull request 'fix: email verify + consultant registration' (#490) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#490
2025-06-04 12:27:15 +00:00
mso7nueh
cacfaff4cf fix: email verify + consultant registration 2025-06-04 15:26:34 +03:00
mso7nueh
e728b0951a Merge pull request 'fix: upd ver to 23' (#488) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#488
2025-06-04 09:14:42 +00:00
mso7nueh
cf6ad1eafe fix: upd ver to 23 2025-06-04 12:14:09 +03:00
mso7nueh
bbba15b3c7 Merge pull request 'Issue-483' (#484) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#484
2025-06-04 08:28:44 +00:00
mso7nueh
c5e0d233f0 Merge pull request 'feat(plans): fixed plans price field in CreatePlanScreen, feat(menu): removed dev tools, feat(chats,notifications): added ws reconection' (#486) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#486
2025-06-04 08:28:34 +00:00
Username1548
da8d21bb08 feat(plans): fixed plans price field in CreatePlanScreen, feat(menu): removed dev tools, feat(chats,notifications): added ws reconection 2025-06-03 23:19:15 +07:00
mso7nueh
c361e12d35 Issue-483: WIP* moderation consultant request status and message 2025-06-03 18:17:03 +03:00
mso7nueh
7c354cc0a3 feat(consultant): ***WIP: consultant request status in settings 2025-06-02 17:51:27 +03:00
mso7nueh
f0a184b800 feat(consultant): update consultant info 2025-06-02 16:05:47 +03:00
mso7nueh
8a9a913420 fix(recipe): infinity loading recipe edit 2025-06-02 13:38:19 +03:00
mso7nueh
74ecbbdbff fix: plan widget in profile image size overflow 2025-06-02 13:31:00 +03:00
mso7nueh
c95b610c35 Merge pull request 'fix(notifications): fixed consultantLeftComment notifications issue' (#482) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#482
2025-06-02 07:18:59 +00:00
Username1548
35fc032d31 fix(notifications): fixed consultantLeftComment notifications issue 2025-05-30 21:19:49 +07:00
mso7nueh
1cb824573b Merge pull request 'fix: upd ver' (#481) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#481
2025-05-30 14:01:21 +00:00
mso7nueh
77c52a50e1 fix: upd ver 2025-05-30 17:00:01 +03:00
mso7nueh
7b05c7bd36 Merge pull request 'fix(profile): education' (#479) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#479
2025-05-30 13:09:42 +00:00
mso7nueh
27cea212b0 fix(profile): education 2025-05-30 16:09:23 +03:00
mso7nueh
252538f52d Merge pull request 'feat(consultations): consultations archive' (#472) from misha/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#472
2025-05-30 13:06:33 +00:00
Username1548
88bf79def4 removed unused variable 2025-05-30 20:05:31 +07:00
Username1548
7f29ade548 fixed consultation archive filters issues 2025-05-30 20:04:47 +07:00
Username1548
d405be1ea4 merged upstram/main 2025-05-30 19:49:24 +07:00
Username1548
10ef59f6ea feat(consultations): consultations archive. feat(diary): plan and diet widget 2025-05-30 19:38:33 +07:00
mso7nueh
ee7b03c504 Merge pull request 'fix(consultation): state filter, avatar' (#478) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#478
2025-05-30 12:36:03 +00:00
mso7nueh
9f0ce55d47 fix(consultation): state filter, avatar 2025-05-30 15:33:32 +03:00
mso7nueh
5d1a1dccc8 Merge pull request 'fix: upd ver' (#475) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#475
2025-05-30 09:59:19 +00:00
mso7nueh
a90efa8caa fix: upd ver 2025-05-30 12:58:37 +03:00
mso7nueh
1db528838b Merge pull request 'fix(consultation): some fixes' (#474) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#474
2025-05-29 14:15:09 +00:00
mso7nueh
3addd9d238 fix(consultation): some fixes 2025-05-29 17:14:02 +03:00
Username1548
d645910554 fix(analyzer): fixed analyzer issues 2025-05-29 01:00:28 +07:00
Username1548
820b4d0a8b Merge remote-tracking branch 'upstream/main' 2025-05-29 00:57:55 +07:00
Username1548
cd4ca7288c feat(consultations): updated consultations archive screen, and added buisines logic 2025-05-29 00:57:37 +07:00
mso7nueh
ac46cd159b Merge pull request 'fix(feed): blogPosts' (#469) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#469
2025-05-28 13:56:49 +00:00
mso7nueh
dc1e8d5b71 fix(feed): blogPosts overflow 2025-05-28 16:56:04 +03:00
mso7nueh
db73865278 Merge pull request 'main' (#3) from eatvio/eatvio_app:main into main
Reviewed-on: mso7nueh/eatvio_app#3
2025-05-28 13:11:11 +00:00
mso7nueh
2cd4054f29 Merge pull request 'feat(diary): New state for future days with diet in diary' (#465) from misha/eatvio_app:diary into main
Reviewed-on: eatvio/eatvio_app#465
2025-05-28 13:10:50 +00:00
mso7nueh
b9611bb5a1 Merge pull request 'feat(notifications): unread notifications handling' (#467) from misha/eatvio_app:notifications into main
Reviewed-on: eatvio/eatvio_app#467
2025-05-28 13:10:47 +00:00
mso7nueh
036aca7d08 Merge pull request 'recipes, plans, email and phone verify' (#463) from mso7nueh/eatvio_app:main into main
Reviewed-on: eatvio/eatvio_app#463
2025-05-28 12:59:21 +00:00
mso7nueh
8f9aeb7b63 Merge branch 'main' into main 2025-05-28 12:59:12 +00:00
mso7nueh
38031c89c2 feat(recipe): my recipes screen complete 2025-05-28 15:56:40 +03:00
64bf6e33d7 Merge pull request 'feat(consultations): added new data fields to consultations model' (#462) from misha/eatvio_app:consultations into main
Reviewed-on: eatvio/eatvio_app#462
2025-05-28 12:04:09 +00:00
Username1548
b80b4e3a9e added logic of unread notifications 2025-05-28 00:30:05 +07:00
mso7nueh
566a96b18e feat(recipe): my recipes screen 2025-05-27 17:38:46 +03:00
mso7nueh
242cb474cc feat(email): add alert for phone verify and email verify after registration 2025-05-27 16:50:36 +03:00
mso7nueh
67bf7b0510 feat(email): verify screen 2025-05-26 17:56:29 +03:00
mso7nueh
51d3c1ad2f fix: avatars 2025-05-26 14:26:12 +03:00
mso7nueh
f38eead280 fix: consultant profile 2025-05-26 13:48:55 +03:00
mso7nueh
cf5b459756 fix: phone-verify redirect 2025-05-26 13:37:33 +03:00
mso7nueh
29b3de325c fix: analyzer 2025-05-24 16:52:23 +03:00
mso7nueh
1e1005a09c feat(email) add email verify screen, add my recipe screen, plans/index -> my_plans 2025-05-23 16:52:50 +03:00
Username1548
7df1323f7d Merge remote-tracking branch 'upstream/main' into consultations 2025-05-23 20:36:13 +07:00
Username1548
8f70e9620b Merge remote-tracking branch 'upstream/main' into consultations 2025-05-23 20:35:13 +07:00
mso7nueh
50b63fea7d Merge pull request 'main' (#2) from eatvio/eatvio_app:main into main
Reviewed-on: mso7nueh/eatvio_app#2
2025-05-23 10:15:12 +00:00
Username1548
1dfaa7cd93 feat(consultations): added new data to consultation model 2025-05-23 12:52:20 +07:00
112 changed files with 3187 additions and 1005 deletions

View File

@@ -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"

View File

@@ -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/"

View File

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

View File

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

View File

@@ -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
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -1,2 +1,3 @@
extensions:
- provider: true
- provider: true
- shared_preferences: true

View File

@@ -388,7 +388,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -538,7 +538,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@@ -576,7 +576,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 16;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_TEAM = VL68C36HYS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (

View File

@@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>16</string>
<string>24</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@@ -32,7 +32,7 @@ import 'package:eatvio_app/features/glossary/presentation/pages/glossary_screen.
import 'package:eatvio_app/features/menu/presentation/pages/menu_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/consultant_finance_screen.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultations_archive_screen.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/user_finance_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_screen.dart';
@@ -40,9 +40,11 @@ import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
import 'package:eatvio_app/features/plans/presentation/pages/about_plan_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/profile_screen.dart';
import 'package:eatvio_app/features/profile/presentation/pages/subs_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/about_recipe_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/about_recipe_screen.dart';
import 'package:eatvio_app/features/create_recipe/presentation/pages/recipe_create_screen.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/registration_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_email_screen.dart';
import 'package:eatvio_app/features/registration/presentation/pages/verify_screen.dart';
import 'package:eatvio_app/features/settings/entities/settings_entity.dart';
import 'package:eatvio_app/features/settings/presentation/pages/change_email_screen.dart';
@@ -76,8 +78,11 @@ class AppRouter {
name: 'authScreen',
builder: (context, state) => const AuthScreen(),
routes: [
// GoRoute(path: 'webView-registration-screen', name: 'webViewRegistrationScreen', builder: (context, state) => const WebViewRegistrationScreen()),
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
GoRoute(
path: 'registration-screen',
name: 'registrationScreen',
builder: (context, state) => const RegistrationScreen(),
),
],
),
ShellRoute(
@@ -95,10 +100,6 @@ class AppRouter {
GoRoute(
path: '/my-profile-screen',
name: 'myProfileScreen',
redirect: (context, state) async {
await sl<WsManager>().initialize();
return null;
},
pageBuilder: (context, state) {
return const NoTransitionPage(child: ProfileScreen());
},
@@ -108,11 +109,21 @@ class AppRouter {
name: 'feedScreen',
pageBuilder: (context, state) => const NoTransitionPage(child: FeedScreen()),
redirect: (context, state) async {
await sl<WsManager>().initialize();
return null;
try {
await sl<WsManager>().initialize();
} catch (e) {
rethrow;
}
return '/feed-screen';
},
),
GoRoute(path: '/diary-screen', name: 'diaryScreen', pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen())),
GoRoute(
path: '/diary-screen',
name: 'diaryScreen',
pageBuilder: (context, state) => const NoTransitionPage(
child: DiaryScreen(),
),
),
GoRoute(
path: ConsultationsScreen.path,
name: ConsultationsScreen.name,
@@ -172,7 +183,7 @@ class AppRouter {
GoRoute(
path: '/consultant-finance-screen',
name: 'consultantFinanceScreen',
builder: (context, state) => const ConsultantFinanceScreen(),
builder: (context, state) => const ConsultationsArchiveScreen(),
),
GoRoute(
path: '/user-finance-screen',
@@ -201,7 +212,11 @@ class AppRouter {
name: 'aboutRecipesScreen',
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
return AboutRecipeScreen(recipe: data['recipe'] as Recipe, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
return AboutRecipeScreen(
recipe: data['recipe'] as Recipe,
index: data['index'] as int?,
navigateToComments: data['navigateToComments'] as bool,
);
},
),
GoRoute(
@@ -284,7 +299,12 @@ class AppRouter {
),
GoRoute(path: '/become-consultant-screen', name: 'becomeConsultantScreen', builder: (context, state) => const BecomeConsultantScreen()),
GoRoute(
path: '/stepper-become-consultant-screen', name: 'stepperBecomeConsultantScreen', builder: (context, state) => const StepperBecomeConsultantScreen()),
path: '/stepper-become-consultant-screen',
name: 'stepperBecomeConsultantScreen',
builder: (context, state) => StepperBecomeConsultantScreen(
slug: state.extra == null ? null : state.extra as String,
),
),
GoRoute(path: '/calendar-screen', name: 'calendarScreen', builder: (context, state) => const CalendarScreen()),
GoRoute(path: '/support-screen', name: 'supportScreen', builder: (context, state) => const SupportScreen()),
GoRoute(
@@ -344,9 +364,12 @@ class AppRouter {
path: UserDiaryScreen.path,
name: UserDiaryScreen.name,
builder: (context, state) {
final diaryId = state.extra as int;
final data = state.extra as Map<String, int>;
final diaryId = data['diaryId'] as int;
final currentPlanDay = data['currentDiaryDay'] as int;
return UserDiaryScreen(
diaryId: diaryId,
currentPlanDay: currentPlanDay,
);
},
),
@@ -355,6 +378,16 @@ class AppRouter {
name: 'verifyScreen',
builder: (context, state) => VerifyScreen(passKey: state.extra as String),
),
GoRoute(
path: '/verify-email-screen',
name: 'verifyEmailScreen',
builder: (context, state) => const VerifyEmailScreen(),
),
GoRoute(
path: MyRecipesScreen.path,
name: MyRecipesScreen.name,
builder: (context, state) => const MyRecipesScreen(),
),
],
);
}

View File

@@ -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(

View 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);
}

View File

@@ -16,6 +16,7 @@ import 'package:eatvio_app/features/complaining/services/complaining_service.dar
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
import 'package:eatvio_app/features/create_diet/presentation/cubit/create_diet_cubit.dart';
import 'package:eatvio_app/features/create_diet/repositories/create_diet_repository.dart';
import 'package:eatvio_app/features/create_diet/services/create_diet_service.dart';
@@ -81,9 +82,13 @@ import 'package:eatvio_app/features/create_recipe/presentation/bloc/create_recip
import 'package:eatvio_app/features/create_recipe/reporitories/recipe_repository.dart';
import 'package:eatvio_app/features/create_recipe/services/recipe_service.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/about_recipe_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:eatvio_app/features/recipe/services/about_recipe_service.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_cubit.dart';
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
import 'package:eatvio_app/features/registration/services/registration_service.dart';
import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/settings_cubit.dart';
@@ -115,9 +120,16 @@ Future<void> init() async {
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl()));
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl()));
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit(diaryRepository: sl(), glossaryRepository: sl(), profileRepository: sl(), dietRepository: sl()));
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
chatRepository: sl(),
diaryRepository: sl(),
glossaryRepository: sl(),
profileRepository: sl(),
dietRepository: sl(),
settingsRepository: sl(),
));
sl.registerFactory<AboutDietCubit>(() => AboutDietCubit(dietsRepository: sl(), profileRepository: sl()));
sl.registerFactory<BlogPostCreateCubit>(() => BlogPostCreateCubit(blogPostRepository: sl()));
sl.registerFactory<SubCubit>(() => SubCubit(profileRepository: sl()));
@@ -138,6 +150,9 @@ Future<void> init() async {
sl.registerFactory<ConsultationsCubit>(() => ConsultationsCubit(consultationsRepository: sl()));
sl.registerFactory<ConsultantCommentsCubit>(() => ConsultantCommentsCubit(diaryRepository: sl()));
sl.registerFactory<CancelPlanCubit>(() => CancelPlanCubit(cancelPlanRepository: sl()));
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
sl.registerFactory<MyRecipesCubit>(() => MyRecipesCubit(myRecipesRepository: sl(), aboutRecipeRepository: sl()));
sl.registerFactory<ConsultationsArchiveCubit>(() => ConsultationsArchiveCubit(consultationsRepository: sl()));
// Repository
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
@@ -165,6 +180,7 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanRepository>(() => CancelPlanRepository(cancelService: sl()));
sl.registerLazySingleton<CreatePlanRepository>(() => CreatePlanRepository(createPlanService: sl()));
sl.registerLazySingleton<NotificationsRepository>(() => NotificationsRepository(notificationsService: sl()));
sl.registerFactory<MyRecipesRepository>(() => MyRecipesRepository(myRecipesService: sl()));
// Services
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
@@ -193,6 +209,7 @@ Future<void> init() async {
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
// External
sl.registerLazySingleton(

View File

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

View File

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

View File

@@ -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('План удален, обновите ленту');

View File

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

View File

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

View File

@@ -5,10 +5,13 @@ import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
import 'package:eatvio_app/core/widgets/custom_text_form_field.dart';
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit.dart';
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
@@ -49,6 +52,20 @@ class _AuthScreenState extends State<AuthScreen> {
});
}
Future<void> _reconnectWebSocket(BuildContext context) async {
await sl<WsManager>().clear();
await sl<WsManager>().initialize();
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
context.read<ChatsCubit>().connectToWebSocket();
context.read<NotificationsCubit>().getNotifications();
context.read<NotificationsCubit>().connectToWebSocket();
showSuccessToast('Успешный вход');
GoRouter.of(context).go('/feed-screen', extra: {'needToInitializeWebSocket': false});
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
@@ -179,8 +196,7 @@ class _AuthScreenState extends State<AuthScreen> {
);
if (isAuthenticated) {
if (context.mounted) {
context.read<ProfileAvatarCubit>().getMyProfile();
context.read<ChatsCubit>().getChatRoomsWithLoading();
_reconnectWebSocket(context);
}
}
},

View File

@@ -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'),
),

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import 'dart:io';
import 'dart:math';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
@@ -43,12 +44,24 @@ class _ChatScreenState extends State<ChatScreen> {
final List<({int id, int index})> unreadMessagesIds = [];
final List<({int id, int index})> unreadMessagesIdsCopy = [];
final showableChatBadgeStates = ChosenPlanStateEnum.getShowableChatBadgeStates();
late AppLifecycleListener _listener;
void _onHide() {
chatRoomCubit.cancelTimer();
}
void _onResume() {
chatRoomCubit.updateChosenPlanState();
chatRoomCubit.setTimer();
}
@override
void initState() {
super.initState();
scrollController.addListener(_onScroll);
chatRoomCubit.initChat(widget.chatId, sendInitialMessage: widget.sendInitialMessage ?? false);
chatRoomCubit.setTimer();
_listener = AppLifecycleListener(onHide: _onHide, onResume: _onResume);
}
_onScroll() {
@@ -60,6 +73,7 @@ class _ChatScreenState extends State<ChatScreen> {
@override
void dispose() {
scrollController.dispose();
_listener.dispose();
chatRoomCubit.close();
super.dispose();
}
@@ -95,7 +109,7 @@ class _ChatScreenState extends State<ChatScreen> {
final showAboutOrderButton =
state.chatRoom.chosenPlan != null && state.chatRoom.chosenPlan!.chosenPlanState != ChosenPlanStateEnum.stateCanceledByTheInitiator;
BannerType? bannerType;
if (chosenPlan == null) {
if (chosenPlan == null || chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
bannerType = null;
} else if (amIConsultant && !state.hasCard) {
bannerType = BannerType.card;
@@ -104,7 +118,7 @@ class _ChatScreenState extends State<ChatScreen> {
} else if (!amIConsultant &&
(chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateStartDateHasBeenSet || chosenPlan.chosenPlanState == ChosenPlanStateEnum.statePendingPayment)) {
bannerType = BannerType.payment;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone) {
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateDone && amIConsultant) {
bannerType = BannerType.collectCharge;
} else if (chosenPlan.chosenPlanState == ChosenPlanStateEnum.stateLastDayFinished && !amIConsultant) {
bannerType = BannerType.finishPlan;
@@ -141,7 +155,7 @@ class _ChatScreenState extends State<ChatScreen> {
if (state.chatRoom.users.receiver.profilePhotoUrl != null)
CircleAvatar(
radius: 20,
foregroundImage: NetworkImage(state.chatRoom.users.receiver.profilePhotoUrl!),
foregroundImage: NetworkImage(baseUrl + state.chatRoom.users.receiver.profilePhotoUrl!),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
if (state.chatRoom.users.receiver.profilePhotoUrl == null)
@@ -302,17 +316,17 @@ class _ChatScreenState extends State<ChatScreen> {
},
),
),
if (bannerType != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: NotificationBanner(
bannerType: bannerType,
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
),
),
],
if (bannerType != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: NotificationBanner(
bannerType: bannerType,
chosenPlanId: chosenPlan?.id,
planName: chosenPlan?.plan.name,
planPrice: chosenOption?.price.toDouble(),
),
),
],
),
),

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
@@ -41,7 +42,7 @@ class _ChatEntryWidgetState extends State<ChatEntryWidget> {
CircleAvatar(
radius: 24,
foregroundImage: NetworkImage(
widget.chatRoom.users.toChatUser.profilePhotoUrl,
baseUrl + widget.chatRoom.users.toChatUser.profilePhotoUrl,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
backgroundColor: Colors.white,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
@@ -7,6 +8,7 @@ import 'package:eatvio_app/features/consultations/presentation/cubit/consultatio
import 'package:eatvio_app/features/consultations/presentation/widgets/consultation_card_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class ConsultationsScreen extends StatelessWidget {
static const path = '/consultations-screen';
@@ -42,17 +44,53 @@ class ConsultationsContent extends StatelessWidget {
),
body: switch (state.state) {
BaseState.loading => const Loader(),
BaseState.error => DataErrorWidget(onReload: () async {}),
BaseState.error => DataErrorWidget(onReload: () async {
context.read<ConsultationsCubit>().loadConsultations();
}),
BaseState.initial => const Loader(),
BaseState.success => ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 12),
itemBuilder: (context, index) => ConsultationCardWidget(
consultationsModel: state.consultations[index],
BaseState.success => SingleChildScrollView(
child: Column(
children: [
ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 12),
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => ConsultationCardWidget(
consultationsModel: state.consultations[index],
),
separatorBuilder: (context, index) => const SizedBox(
height: 12,
),
shrinkWrap: true,
itemCount: state.consultations.length,
),
const SizedBox(height: 20),
InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
GoRouter.of(context).pushNamed('consultantFinanceScreen');
},
child: Ink(
width: MediaQuery.of(context).size.width - 32,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: appTheme.blueA400,
),
child: const Center(
child: Text(
'Архив консультаций',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 20),
],
),
separatorBuilder: (context, index) => const SizedBox(
height: 12,
),
itemCount: state.consultations.length,
),
});
},

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
@@ -71,8 +72,9 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
children: [
Row(
children: [
const CircleAvatar(
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(baseUrl + widget.consultationsModel.userProfilePhotoUrl),
),
const SizedBox(width: 12),
Column(
@@ -195,7 +197,10 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
title: s.you_can_not_open_this_day, description: '${widget.consultationsModel.userName} ${s.has_not_filled_it_yet}');
return;
}
GoRouter.of(context).push(UserDiaryScreen.path, extra: day.$2.id);
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
'diaryId': day.$2.id,
'currentDiaryDay': index + 1,
});
},
borderRadius: BorderRadius.circular(100),
child: Ink(
@@ -272,7 +277,7 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
const CustomDivider(),
const SizedBox(height: 12),
Text(
'${widget.consultationsModel.consultantPayment.toStringAsFixed(0)} ₽',
'${widget.consultationsModel.consultationOptionPrice.toStringAsFixed(0)} ₽',
style: const TextStyle(
color: Color(0xFF019A3E),
fontSize: 20,
@@ -291,6 +296,10 @@ class _ConsultationCardWidgetState extends State<ConsultationCardWidget> {
)
.lastOrNull;
if (lastActiveDay != null) {
GoRouter.of(context).push(UserDiaryScreen.path, extra: {
'diaryId': lastActiveDay.$2.id,
'currentDiaryDay': lastActiveDay.$1,
});
GoRouter.of(context).push(UserDiaryScreen.path, extra: lastActiveDay.$2.id);
return;
}

View File

@@ -1,5 +1,7 @@
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
class ConsultationsRepository {
final ConsultationService consultationService;
@@ -13,4 +15,31 @@ class ConsultationsRepository {
rethrow;
}
}
Future<List<ConsultationModel>> getFilteredConsultations({
String? state,
String? date,
FiltersComparisonEnum? stateComparison,
FiltersComparisonEnum? dateComparison,
}) async {
try {
final model = await consultationService.getFilteredConsultations(
state: state,
date: date,
stateComparison: stateComparison,
dateComparison: dateComparison,
);
return model.data;
} catch (e) {
rethrow;
}
}
Future<StatisticModel> getStatistic() async {
try {
return await consultationService.getStatistics();
} catch (e) {
rethrow;
}
}
}

View File

@@ -1,13 +1,69 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/core/enums/filters_comparison_enum.dart';
import 'package:eatvio_app/features/consultations/models/consultation_model.dart';
import 'package:retrofit/retrofit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/models/statistic_model.dart';
part 'consultation_service.g.dart';
class ConsultationService {
final Dio dio;
ConsultationService(this.dio);
@RestApi()
abstract class ConsultationService {
factory ConsultationService(Dio dio, {String baseUrl}) = _ConsultationService;
Future<ConsultationPaginationModel> getConsultations() async {
try {
final response = await dio.get('/api/consultations', queryParameters: {
'filters[last_state][\$notIn][]': const [
'initied',
'in_coordination',
'start_date_has_been_set',
'pending_payment',
'canceled_by_the_initiator',
'charge_was_collected',
],
'sort[]': 'created_at:desc'
});
return ConsultationPaginationModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
@GET('/api/consultations')
Future<ConsultationPaginationModel> getConsultations();
Future<ConsultationPaginationModel> getFilteredConsultations({
String? state,
String? date,
FiltersComparisonEnum? stateComparison,
FiltersComparisonEnum? dateComparison,
}) async {
try {
final response = await dio.get(
'/api/consultations',
queryParameters: {
if (stateComparison == FiltersComparisonEnum.notInArray)
'filters[last_state][\$notIn][]': const [
'initied',
'in_coordination',
'start_date_has_been_set',
'pending_payment',
'canceled_by_the_initiator',
'charge_was_collected',
]
else if (state != null && stateComparison != null)
'filters[last_state][${stateComparison.value}]': state,
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
},
);
return ConsultationPaginationModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<StatisticModel> getStatistics() async {
try {
final response = await dio.get(
'/api/consultations/statistics',
);
return StatisticModel.fromJson(response.data);
} catch (e) {
rethrow;
}
}
}

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

@@ -20,6 +20,7 @@ abstract class DietsPagination with _$DietsPagination {
abstract class Diet with _$Diet {
const factory Diet({
@JsonKey(name: "id") required int id,
@JsonKey(name: "slug") String? slug,
@JsonKey(name: "name") required String name,
@JsonKey(name: "image") required String image,
@JsonKey(name: "duration") required int duration,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
import 'dart:developer';
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultantFinancesCubit extends Cubit<ConsultantFinancesState> {
final ConsultationsRepository consultationsRepository;
ConsultantFinancesCubit({
required this.consultationsRepository,
}) : super(
ConsultantFinancesState(),
);
Future<void> getConsultations() async {
try {
emit(state.copyWith(isLoading: true));
} catch (e) {
emit(state.copyWith(isLoading: false));
log(e.toString());
}
}
Future<void> setConsultationsPeriod(PeriodFilterStatus period) async {
emit(state.copyWith(periodFilterStatus: period));
await getConsultations();
}
}

View File

@@ -0,0 +1,15 @@
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'consultant_finances_state.freezed.dart';
@freezed
abstract class ConsultantFinancesState with _$ConsultantFinancesState {
factory ConsultantFinancesState({
@Default([]) List<String> financesModels,
@Default(false) isLoading,
@Default(null) String? error,
@Default(null) PeriodFilterStatus? periodFilterStatus,
}) = _ConsultantFinancesState;
}

View File

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

View File

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

View File

@@ -0,0 +1,281 @@
// import 'package:eatvio_app/core/locator_service.dart';
// import 'package:eatvio_app/core/theme/custom_text_style.dart';
// import 'package:eatvio_app/core/theme/theme_helper.dart';
// import 'package:eatvio_app/core/utils/date_time_utils.dart';
// import 'package:eatvio_app/core/utils/localization_utils.dart';
// import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
// import 'package:eatvio_app/core/widgets/loader.dart';
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart';
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
// import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_state.dart';
// import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
// import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/widgets/status_widget.dart';
// import 'package:eatvio_app/generated/l10n.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_bloc/flutter_bloc.dart';
// class ConsultantFinancesScreen extends StatefulWidget {
// const ConsultantFinancesScreen({super.key});
// @override
// State<ConsultantFinancesScreen> createState() => _ConsultantFinancesScreenState();
// }
// class _ConsultantFinancesScreenState extends State<ConsultantFinancesScreen> {
// @override
// Widget build(BuildContext context) {
// final s = S.of(context);
// return BlocProvider<ConsultantFinancesCubit>(
// create: (context) => sl<ConsultantFinancesCubit>(),
// child: BlocBuilder<ConsultantFinancesCubit, ConsultantFinancesState>(
// builder: (context, state) {
// return Scaffold(
// backgroundColor: const Color(0xFFF0F2F5),
// appBar: AppBar(
// shape: const RoundedRectangleBorder(
// borderRadius: BorderRadiusDirectional.vertical(
// bottom: Radius.circular(12),
// ),
// ),
// title: const Text('Архив консультаций'),
// centerTitle: true,
// ),
// body: state.isLoading
// ? const Center(
// child: Loader(),
// )
// : SingleChildScrollView(
// padding: const EdgeInsets.symmetric(vertical: 8),
// child: Column(
// children: [
// Material(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// const SizedBox(height: 16),
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 16),
// child: Text(
// 'Всего консультаций: ${state.statisticModel.total}',
// style: const TextStyle(
// fontSize: 16,
// fontWeight: FontWeight.w500,
// color: Color(0xFF6D6F71),
// height: 1,
// ),
// ),
// ),
// const SizedBox(height: 12),
// Row(
// children: [
// const SizedBox(width: 16),
// Material(
// color: const Color(0xFF0BB450).withAlpha(11),
// borderRadius: BorderRadius.circular(8),
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// child: Text(
// 'Завершено: ${state.statisticModel.finished}',
// style: const TextStyle(
// fontSize: 14,
// fontWeight: FontWeight.w400,
// color: Color(0xFF0BB450),
// height: 1,
// ),
// ),
// ),
// ),
// const SizedBox(width: 8),
// Material(
// color: const Color(0xFFD01717).withAlpha(11),
// borderRadius: BorderRadius.circular(8),
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
// child: Text(
// 'Отменено: ${state.statisticModel.canceled}',
// style: const TextStyle(
// fontSize: 14,
// fontWeight: FontWeight.w400,
// color: Color(0xFFD01717),
// height: 1,
// ),
// ),
// ),
// ),
// ],
// ),
// const SizedBox(height: 16),
// ],
// ),
// ),
// const SizedBox(height: 8),
// Material(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// child: Padding(
// padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
// child: SingleChildScrollView(
// physics: const NeverScrollableScrollPhysics(),
// child: Column(
// children: [
// CustomDropDownButton<PeriodFilterStatus>(
// items: PeriodFilterStatus.values
// .map(
// (value) => DropdownMenuItem(
// value: value,
// child: Text(
// value.name,
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
// ),
// ),
// )
// .toList(),
// onChanged: (value) {
// if (value == null) return;
// context.read<ConsultationsArchiveCubit>().setConsultationsPeriod(value);
// },
// value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
// ),
// const SizedBox(height: 12),
// CustomDropDownButton<ConsultationFilterStatus>(
// items: ConsultationFilterStatus.values
// .map(
// (value) => DropdownMenuItem(
// value: value,
// child: Text(
// value.name,
// style: TextStyles.textMMedium.copyWith(color: appTheme.black),
// ),
// ),
// )
// .toList(),
// onChanged: (value) {
// if (value == null) return;
// context.read<ConsultationsArchiveCubit>().setConsultationsStatus(value);
// },
// value: state.consultationFilterStatus,
// ),
// const SizedBox(height: 12),
// if (state.consultations.isNotEmpty)
// ListView.separated(
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// itemBuilder: (context, index) {
// return Material(
// color: const Color(0xFFF6F6F6),
// borderRadius: BorderRadius.circular(20),
// child: Padding(
// padding: const EdgeInsets.all(12),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// state.consultations[index].id.toString(),
// style: const TextStyle(
// color: Color(0xFF323F4B),
// height: 1,
// fontWeight: FontWeight.w500,
// ),
// ),
// Text(
// state.consultations[index].startDate.format('yyyy.MM.dd'),
// style: const TextStyle(
// color: Color(0xFF6D6F71),
// height: 1,
// fontWeight: FontWeight.w500,
// ),
// ),
// ],
// ),
// const SizedBox(height: 8),
// Text(
// state.consultations[index].planTitle,
// style: TextStyle(
// color: appTheme.blueA400,
// height: 1,
// fontSize: 18,
// fontWeight: FontWeight.w600,
// ),
// ),
// const SizedBox(height: 8),
// Text(
// 'Покупатель: ${state.consultations[index].userName}',
// style: const TextStyle(
// color: Color(0xFF323F4B),
// height: 1,
// fontSize: 14,
// fontWeight: FontWeight.w500,
// ),
// ),
// const SizedBox(height: 8),
// Text(
// '${state.consultations[index].duration} ${getDayForm(state.consultations[index].duration, s)}',
// style: const TextStyle(
// color: Color(0xFF323F4B),
// height: 1,
// fontSize: 14,
// fontWeight: FontWeight.w500,
// ),
// ),
// const SizedBox(height: 8),
// Container(
// height: 1,
// width: double.infinity,
// decoration: const BoxDecoration(
// color: Color(0xFFDEDEDF),
// ),
// ),
// const SizedBox(height: 8),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
// children: [
// Text(
// '${state.consultations[index].consultantPayment.toStringAsFixed(2)} ₽',
// style: const TextStyle(
// color: Color(0xFF323F4B),
// height: 1,
// fontSize: 18,
// fontWeight: FontWeight.w600,
// ),
// ),
// StatusWidget(
// status: state.consultations[index].chosenPlanState,
// ),
// ],
// ),
// ],
// ),
// ),
// );
// },
// separatorBuilder: (context, index) => const SizedBox(height: 12),
// itemCount: state.financesModels.length,
// )
// else
// const Padding(
// padding: EdgeInsets.symmetric(vertical: 40.0),
// child: Text(
// 'У вас нет таких консультаций',
// style: TextStyles.textMMedium,
// ),
// ),
// ],
// ),
// ),
// ),
// ),
// ],
// ),
// ),
// );
// },
// ),
// );
// }
// }

View File

@@ -0,0 +1,281 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_cubit.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/archive/consultations_archive_state.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/period_enum.dart';
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/widgets/status_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ConsultationsArchiveScreen extends StatefulWidget {
const ConsultationsArchiveScreen({super.key});
@override
State<ConsultationsArchiveScreen> createState() => _ConsultationsArchiveScreenState();
}
class _ConsultationsArchiveScreenState extends State<ConsultationsArchiveScreen> {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocProvider<ConsultationsArchiveCubit>(
create: (context) => sl<ConsultationsArchiveCubit>()
..getConsultations()
..getStatistic(),
child: BlocBuilder<ConsultationsArchiveCubit, ConsultationsArchiveState>(
builder: (context, state) {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.vertical(
bottom: Radius.circular(12),
),
),
title: const Text('Архив консультаций'),
centerTitle: true,
),
body: state.isLoading
? const Center(
child: Loader(),
)
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Всего консультаций: ${state.statisticModel.total}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF6D6F71),
height: 1,
),
),
),
const SizedBox(height: 12),
Row(
children: [
const SizedBox(width: 16),
Material(
color: const Color(0xFF0BB450).withAlpha(11),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Завершено: ${state.statisticModel.finished}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF0BB450),
height: 1,
),
),
),
),
const SizedBox(width: 8),
Material(
color: const Color(0xFFD01717).withAlpha(11),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Отменено: ${state.statisticModel.canceled}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFFD01717),
height: 1,
),
),
),
),
],
),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 8),
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
children: [
CustomDropDownButton<PeriodFilterStatus>(
items: PeriodFilterStatus.values
.map(
(value) => DropdownMenuItem(
value: value,
child: Text(
value.name,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
onChanged: (value) {
if (value == null) return;
context.read<ConsultationsArchiveCubit>().setConsultationsPeriod(value);
},
value: state.periodFilterStatus ?? PeriodFilterStatus.values.first,
),
const SizedBox(height: 12),
CustomDropDownButton<ConsultationFilterStatus>(
items: ConsultationFilterStatus.values
.map(
(value) => DropdownMenuItem(
value: value,
child: Text(
value.name,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
onChanged: (value) {
if (value == null) return;
context.read<ConsultationsArchiveCubit>().setConsultationsStatus(value);
},
value: state.consultationFilterStatus ?? ConsultationFilterStatus.values.first,
),
const SizedBox(height: 12),
if (state.consultations.isNotEmpty)
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return Material(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
state.consultations[index].id.toString(),
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontWeight: FontWeight.w500,
),
),
Text(
state.consultations[index].startDate.format('yyyy.MM.dd'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Text(
state.consultations[index].planTitle,
style: TextStyle(
color: appTheme.blueA400,
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'Покупатель: ${state.consultations[index].userName}',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'${state.consultations[index].duration} ${getDayForm(state.consultations[index].duration, s)}',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Container(
height: 1,
width: double.infinity,
decoration: const BoxDecoration(
color: Color(0xFFDEDEDF),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${state.consultations[index].consultantPayment.toStringAsFixed(2)} ₽',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
StatusWidget(
status: state.consultations[index].chosenPlanState,
),
],
),
],
),
),
);
},
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemCount: state.consultations.length,
)
else
const Padding(
padding: EdgeInsets.symmetric(vertical: 40.0),
child: Text(
'У вас нет таких консультаций',
style: TextStyles.textMMedium,
),
),
],
),
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,63 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:flutter/material.dart';
class StatusWidget extends StatelessWidget {
const StatusWidget({super.key, required this.status});
final ChosenPlanStateEnum status;
@override
Widget build(BuildContext context) {
if (status == ChosenPlanStateEnum.stateCanceledByTheInitiator) {
return Material(
color: appTheme.lightRed,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Отменён',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: appTheme.strongRed,
height: 1,
),
),
),
);
} else if (status == ChosenPlanStateEnum.stateChargeWasCollected) {
return Material(
color: const Color(0xFF0BB450).withAlpha(11),
borderRadius: BorderRadius.circular(8),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Успешно',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF0BB450),
height: 1,
),
),
),
);
}
return Material(
color: appTheme.transparentBlue,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'В процессе',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: appTheme.blueA400,
height: 1,
),
),
),
);
}
}

View File

@@ -4,10 +4,10 @@ import 'package:eatvio_app/core/token_storage.dart';
import 'package:eatvio_app/core/utils/image_constant.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/dev_tools/presentation/dev_tools_screen.dart';
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
import 'package:eatvio_app/features/my_plans/presentation/my_plans_screen.dart';
import 'package:eatvio_app/features/profile/models/profile_model.dart';
import 'package:eatvio_app/features/recipe/presentation/pages/my_recipes_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -28,7 +28,7 @@ class MenuScreen extends StatelessWidget {
_tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push('/recipe-select-screen')),
_tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push(MyRecipesScreen.path)),
if (profileModel.roles.contains('consultant')) _divider(context),
if (profileModel.roles.contains('consultant'))
_tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)),
@@ -47,14 +47,14 @@ class MenuScreen extends StatelessWidget {
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
_divider(context),
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
_divider(context),
_tile(
leading: ImageConstant.icnDevTools,
title: 'Для разработчиков',
onTap: () {
GoRouter.of(context).pushNamed(DevToolsScreen.name);
},
),
// _divider(context),
// _tile(
// leading: ImageConstant.icnDevTools,
// title: 'Для разработчиков',
// onTap: () {
// GoRouter.of(context).pushNamed(DevToolsScreen.name);
// },
// ),
_divider(context),
_tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)),
],

View File

@@ -26,6 +26,7 @@ import 'package:share_plus/share_plus.dart';
class MyDietWidget extends StatefulWidget {
final Diet diet;
const MyDietWidget({
super.key,
required this.diet,

View File

@@ -1,259 +0,0 @@
import 'package:collection/collection.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
import 'package:eatvio_app/features/my_finance/entities/consultant_finance_entity.dart';
import 'package:flutter/material.dart';
class ConsultantFinanceScreen extends StatefulWidget {
const ConsultantFinanceScreen({super.key});
@override
State<ConsultantFinanceScreen> createState() => _ConsultantFinanceScreenState();
}
class _ConsultantFinanceScreenState extends State<ConsultantFinanceScreen> {
final List<String> periods = ['За всё время', 'За месяц', 'За неделю'];
final List<String> statuses = ['Все консультации', 'Отменён', 'Завершён', 'В процессе'];
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF0F2F5),
appBar: AppBar(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadiusDirectional.vertical(
bottom: Radius.circular(12),
),
),
title: const Text('Архив консультаций'),
centerTitle: true,
),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Всего консультаций: ${testFinanceEntity.totalConsultationsCount}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF6D6F71),
height: 1,
),
),
),
const SizedBox(height: 12),
Row(
children: [
const SizedBox(width: 16),
Material(
color: const Color(0xFF0BB450).withAlpha(11),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Завершено: ${testFinanceEntity.completedCount}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF0BB450),
height: 1,
),
),
),
),
const SizedBox(width: 8),
Material(
color: const Color(0xFFD01717).withAlpha(11),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
'Отменено: ${testFinanceEntity.canceledCount}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFFD01717),
height: 1,
),
),
),
),
],
),
const SizedBox(height: 16),
],
),
),
const SizedBox(height: 8),
Material(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 16),
child: SingleChildScrollView(
physics: const NeverScrollableScrollPhysics(),
child: Column(
children: [
CustomDropDownButton(
items: periods
.mapIndexed(
(index, value) => DropdownMenuItem(
value: periods[index],
child: Text(
periods[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
onChanged: (value) {},
value: periods.first,
),
const SizedBox(height: 12),
CustomDropDownButton(
items: statuses
.mapIndexed(
(index, value) => DropdownMenuItem(
value: statuses[index],
child: Text(
statuses[index],
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
)
.toList(),
onChanged: (value) {},
value: statuses.first,
),
const SizedBox(height: 12),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Material(
color: const Color(0xFFF6F6F6),
borderRadius: BorderRadius.circular(20),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
testFinanceEntity.consultationsList[index].id,
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontWeight: FontWeight.w500,
),
),
Text(
testFinanceEntity.consultationsList[index].date.format('dd.MM.yyy', 'RU'),
style: const TextStyle(
color: Color(0xFF6D6F71),
height: 1,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 8),
Text(
testFinanceEntity.consultationsList[index].title,
style: TextStyle(
color: appTheme.blueA400,
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Text(
'Покупатель: ${testFinanceEntity.consultationsList[index].clientName}',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'${testFinanceEntity.consultationsList[index].planDuration} дней',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Container(
height: 1,
width: double.infinity,
decoration: const BoxDecoration(
color: Color(0xFFDEDEDF),
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${testFinanceEntity.consultationsList[index].price} ₽',
style: const TextStyle(
color: Color(0xFF323F4B),
height: 1,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Material(
color: const Color(0xFF0BB450).withAlpha(11),
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Text(
testFinanceEntity.consultationsList[index].status.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF0BB450),
height: 1,
),
),
),
),
],
),
],
),
),
),
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemCount: testFinanceEntity.consultationsList.length,
),
],
),
),
),
),
],
),
),
);
}
}

View File

@@ -8,7 +8,7 @@ part 'my_plans_service.g.dart';
abstract class MyPlansService {
factory MyPlansService(Dio dio, {String baseUrl}) = _MyPlansService;
@GET('/api/plans/index')
@GET('/api/profile/my_plans')
Future<PlanPaginationModel> getPlans({
@Query('page') required int page,
@Query('filters[name][\$startsWith]') String? query,

View File

@@ -1,10 +1,15 @@
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
sealed class BaseNotificationModel {
abstract final NotificationType type;
abstract final bool isRead;
}
class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
final String chatId;
@@ -14,14 +19,16 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
required this.planName,
required this.customerName,
required this.chatId,
required this.isRead,
});
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory PlanSubscriptionStartedToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return PlanSubscriptionStartedToAuthorNotification(
type: type,
planName: json['plan_name'],
customerName: json['user_name'],
chatId: json['chat_id'],
isRead: isRead ?? false,
);
}
}
@@ -29,6 +36,8 @@ class PlanSubscriptionStartedToAuthorNotification implements BaseNotificationMod
class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
@@ -36,13 +45,15 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
required this.type,
required this.planName,
required this.customerName,
required this.isRead,
});
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory PlanSubscriptionCanceledToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return PlanSubscriptionCanceledToAuthorNotification(
type: type,
planName: json['plan_name'],
customerName: json['user_name'],
isRead: isRead ?? false,
);
}
}
@@ -50,6 +61,8 @@ class PlanSubscriptionCanceledToAuthorNotification implements BaseNotificationMo
class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final int planId;
@@ -57,13 +70,15 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
required this.type,
required this.planName,
required this.planId,
required this.isRead,
});
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory ChosenPlanStartedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanStartedToClientNotification(
type: type,
planName: json['plan_name'],
planId: json['plan_id'],
isRead: isRead ?? false,
);
}
}
@@ -71,58 +86,70 @@ class ChosenPlanStartedToClientNotification implements BaseNotificationModel {
class ChosenPlanTomorrowReminderToClientNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
ChosenPlanTomorrowReminderToClientNotification({
required this.type,
required this.planName,
required this.isRead,
});
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
return ChosenPlanTomorrowReminderToClientNotification(
type: type,
planName: json['plan_name'],
);
factory ChosenPlanTomorrowReminderToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanTomorrowReminderToClientNotification(type: type, planName: json['plan_name'], isRead: isRead ?? false);
}
}
class DiaryReminderNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
DiaryReminderNotification({required this.type, required this.isRead});
DiaryReminderNotification({required this.type});
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
return DiaryReminderNotification(
type: type,
);
factory DiaryReminderNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return DiaryReminderNotification(type: type, isRead: isRead ?? false);
}
}
class ConsultantLeftCommentNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final DateTime date;
ConsultantLeftCommentNotification({
required this.type,
required this.date,
required this.isRead,
});
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
return ConsultantLeftCommentNotification(type: type, date: json['diary_date']);
factory ConsultantLeftCommentNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ConsultantLeftCommentNotification(
type: type,
date: parseDate(json['diary_date']),
isRead: isRead ?? false,
);
}
}
class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
PersonalizedDietIsDefinedNotification({required this.type});
PersonalizedDietIsDefinedNotification({
required this.type,
required this.isRead,
});
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory PersonalizedDietIsDefinedNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return PersonalizedDietIsDefinedNotification(
type: type,
isRead: isRead ?? false,
);
}
}
@@ -130,30 +157,41 @@ class PersonalizedDietIsDefinedNotification implements BaseNotificationModel {
class ChosenPlanFinishedToClientNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
ChosenPlanFinishedToClientNotification({
required this.type,
required this.planName,
required this.isRead,
});
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
return ChosenPlanFinishedToClientNotification(type: type, planName: json['plan_name']);
factory ChosenPlanFinishedToClientNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanFinishedToClientNotification(
type: type,
planName: json['plan_name'],
isRead: isRead ?? false,
);
}
}
class PlanSubscriptionCanceledToCustomerNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
PlanSubscriptionCanceledToCustomerNotification({
required this.type,
required this.planName,
required this.isRead,
});
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory PlanSubscriptionCanceledToCustomerNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return PlanSubscriptionCanceledToCustomerNotification(
type: type,
planName: json['plan_name'],
isRead: isRead ?? false,
);
}
}
@@ -161,17 +199,21 @@ class PlanSubscriptionCanceledToCustomerNotification implements BaseNotification
class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
ChosenPlanStartedToConsultantNotification({
required this.type,
required this.isRead,
required this.planName,
required this.customerName,
});
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory ChosenPlanStartedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanStartedToConsultantNotification(
type: type,
isRead: isRead ?? false,
planName: json['plan_name'],
customerName: json['client_name'],
);
@@ -181,19 +223,23 @@ class ChosenPlanStartedToConsultantNotification implements BaseNotificationModel
class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
ChosenPlanTomorrowReminderToConsultantNotification({
required this.type,
required this.planName,
required this.customerName,
required this.isRead,
});
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory ChosenPlanTomorrowReminderToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanTomorrowReminderToConsultantNotification(
type: type,
planName: json['plan_name'],
customerName: json['client_name'],
isRead: isRead ?? false,
);
}
}
@@ -201,17 +247,21 @@ class ChosenPlanTomorrowReminderToConsultantNotification implements BaseNotifica
class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
PlanSubscriptionPaidToAuthorNotification({
required this.type,
required this.isRead,
required this.planName,
required this.customerName,
});
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory PlanSubscriptionPaidToAuthorNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return PlanSubscriptionPaidToAuthorNotification(
type: type,
isRead: isRead ?? false,
planName: json['plan_name'],
customerName: json['user_name'],
);
@@ -221,6 +271,8 @@ class PlanSubscriptionPaidToAuthorNotification implements BaseNotificationModel
class ChosenPlanFinishedToConsultantNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
final String planName;
final String customerName;
final int planId;
@@ -229,14 +281,16 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
required this.planName,
required this.customerName,
required this.planId,
required this.isRead,
});
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type) {
factory ChosenPlanFinishedToConsultantNotification.fromJson(Map<String, dynamic> json, NotificationType type, {bool? isRead}) {
return ChosenPlanFinishedToConsultantNotification(
type: type,
planName: json['plan_name'],
customerName: json['client_name'],
planId: json['plan_id'],
isRead: isRead ?? false,
);
}
}
@@ -244,8 +298,12 @@ class ChosenPlanFinishedToConsultantNotification implements BaseNotificationMode
class UnknownNotification implements BaseNotificationModel {
@override
final NotificationType type;
@override
final bool isRead;
UnknownNotification({
required this.type,
this.isRead = true,
});
}

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:developer';
@@ -11,22 +12,26 @@ class NotificationsCubit extends Cubit<NotificationsState> {
final List<BaseNotificationModel> _notifications = [];
final WsManager wsManager;
final NotificationsRepository notificationsRepository;
StreamSubscription? _newNotificationsSubscription;
NotificationsCubit({
required this.wsManager,
required this.notificationsRepository,
}) : super(const NotificationsState.empty()) {
wsManager.newNotificationsStream?.listen((event) {
}) : super(const NotificationsState.empty());
Future<void> connectToWebSocket() async {
await _newNotificationsSubscription?.cancel();
_newNotificationsSubscription = wsManager.newNotificationsStream?.listen((event) {
_handleNotification(event.data);
});
}
BaseNotificationModel _parseNotification(Map<String, dynamic> notificationJson, NotificationType type) {
try {
return switch (type) {
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson, type),
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
NotificationType.chosenPlanTomorrowReminderToClientNotification => ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson, type),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson, type),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson, type),
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson, type),
@@ -54,6 +59,7 @@ class NotificationsCubit extends Cubit<NotificationsState> {
NotificationsState.loaded(
notifications: _notifications,
notificationsCount: _notifications.length,
hasUnreadNotifications: true,
),
);
}
@@ -67,10 +73,26 @@ class NotificationsCubit extends Cubit<NotificationsState> {
NotificationsState.loaded(
notifications: _notifications,
notificationsCount: _notifications.length,
hasUnreadNotifications: (notifications ?? []).where((elem) => elem.isRead == false).isNotEmpty,
),
);
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
Future<void> markNotificationsAsRead() async {
final currentState = state;
if (currentState is NotificationsStateLoaded) {
try {
emit(currentState.copyWith(isLoading: true));
await notificationsRepository.markNotificationsAsRead();
await getNotifications();
emit(currentState.copyWith(isLoading: false, hasUnreadNotifications: false));
} catch (e, st) {
log(e.toString(), stackTrace: st);
emit(currentState.copyWith(isLoading: false));
}
}
}
}

View File

@@ -10,8 +10,10 @@ abstract class NotificationsState with _$NotificationsState {
const factory NotificationsState.empty() = NotificationsStateEmpty;
const factory NotificationsState.loaded({
required List<BaseNotificationModel> notifications,
required List<BaseNotificationModel> notifications,
required int notificationsCount,
@Default(false) bool isLoading,
@Default(false) bool hasUnreadNotifications,
}) = NotificationsStateLoaded;
const factory NotificationsState.error({

View File

@@ -1,8 +1,12 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/custom_divider.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_state.dart';
import 'package:eatvio_app/features/notifications/presentation/widgets/notification_widget.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -11,6 +15,7 @@ class NotificationsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final s = S.of(context);
return BlocBuilder<NotificationsCubit, NotificationsState>(
builder: (BuildContext context, NotificationsState state) {
return Scaffold(
@@ -19,38 +24,58 @@ class NotificationsScreen extends StatelessWidget {
title: const Text('Уведомления'),
),
body: state is NotificationsStateLoaded
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
? Stack(children: [
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Container(
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
),
child: CustomScrollView(
slivers: [
SliverList.separated(
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: CustomDivider(),
),
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: NotificationWidget(
notification: state.notifications[index],
),
);
},
itemCount: state.notifications.length,
)
],
child: state.isLoading
? const Center(child: Loader())
: CustomScrollView(
slivers: [
SliverList.separated(
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: CustomDivider(),
),
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: NotificationWidget(
notification: state.notifications[index],
),
);
},
itemCount: state.notifications.length,
)
],
),
),
),
)
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
child: CustomOutlinedButton(
text: s.markAsRead,
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
buttonStyle:
ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)),
onPressed: () async {
context.read<NotificationsCubit>().markNotificationsAsRead();
},
),
),
)
])
: const Placeholder(),
);
},

View File

@@ -1,5 +1,6 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/date_time_utils.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart';
import 'package:eatvio_app/features/notifications/models/notification_model.dart';
@@ -46,7 +47,7 @@ class NotificationWidget extends StatelessWidget {
ChosenPlanTomorrowReminderToConsultantNotification() => _ChosenPlanTomorrowReminderToConsultantWidget(
notification: notification as ChosenPlanTomorrowReminderToConsultantNotification,
),
PlanSubscriptionPaidToAuthorNotification() => throw _PlanSubscriptionPaidToAuthorWidget(
PlanSubscriptionPaidToAuthorNotification() => _PlanSubscriptionPaidToAuthorWidget(
notification: notification as PlanSubscriptionPaidToAuthorNotification,
),
ChosenPlanFinishedToConsultantNotification() => _ChosenPlanFinishedToConsultantWidget(
@@ -65,7 +66,8 @@ class _PlanSubscriptionStartedToAuthorWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
notificationsText: s.userSubscribedToPlan(notification.customerName, notification.planName),
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
}
}
@@ -78,7 +80,8 @@ class _PlanSubscriptionCanceledToAuthorWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
notificationsText: s.userCanceldThePlanSubscription(notification.customerName, notification.planName),
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
}
}
@@ -91,7 +94,8 @@ class _ChosenPlanStartedToClientWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
notificationsText: s.planStarted(notification.planName),
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
}
}
@@ -104,7 +108,8 @@ class _ChosenPlanTomorrowReminderToClientWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
notificationsText: s.tomorrowYourPlanWillStart(notification.planName),
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
}
}
@@ -116,6 +121,7 @@ class _DiaryReminderWidget extends StatelessWidget {
Widget build(BuildContext context) {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.fillYasterday,
);
}
@@ -129,7 +135,8 @@ class _ConsultantLeftCommentWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
notificationsText: s.consultantLeftComment(notification.date),
isRead: notification.isRead,
notificationsText: s.consultantLeftComment(notification.date.format('dd.MM.yyyy')),
);
}
}
@@ -151,6 +158,7 @@ class _ChosenPlanFinishedToClientWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: '${s.congratsYouHaveFinishedPlan} «${notification.planName}»',
);
}
@@ -164,6 +172,7 @@ class _PlanSubscriptionCanceledToCustomerWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.youCanceledSubscriptionToPlan(notification.planName),
);
}
@@ -177,6 +186,7 @@ class _ChosenPlanStartedToConsultantWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.planWithUserStarted(notification.planName, notification.customerName),
);
}
@@ -190,6 +200,7 @@ class _ChosenPlanTomorrowReminderToConsultantWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.tommorowPlanWithUserWillStart(notification.planName, notification.customerName),
);
}
@@ -203,6 +214,7 @@ class _PlanSubscriptionPaidToAuthorWidget extends StatelessWidget {
final s = S.of(context);
return _BaseNotificationWidget(
isRead: notification.isRead,
notificationsText: s.userPaidThePlan(notification.customerName, notification.planName),
);
}
@@ -240,16 +252,32 @@ class _ChosenPlanFinishedToConsultantWidget extends StatelessWidget {
// CardSettingsScreen
class _BaseNotificationWidget extends StatelessWidget {
const _BaseNotificationWidget({required this.notificationsText});
const _BaseNotificationWidget({required this.notificationsText, required this.isRead});
final String notificationsText;
final bool isRead;
@override
Widget build(BuildContext context) {
return SizedBox(
width: MediaQuery.sizeOf(context).width * 0.6,
child: Text(
notificationsText,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
return Row(
children: [
SizedBox(
width: MediaQuery.sizeOf(context).width * 0.6,
child: Text(
notificationsText,
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
),
),
if (!isRead) ...[
const Spacer(),
Container(
decoration: BoxDecoration(
color: appTheme.blueA400,
shape: BoxShape.circle,
),
width: 8,
height: 8,
)
]
],
);
}
}

View File

@@ -12,4 +12,12 @@ class NotificationsRepository {
rethrow;
}
}
Future<void> markNotificationsAsRead() async {
try {
await notificationsService.markNotificationsAsRead();
} catch (e) {
rethrow;
}
}
}

View File

@@ -13,23 +13,33 @@ class NotificationsService {
final notifications = data.map((notificationJson) {
try {
final stringifiedType = notificationJson['data']['notification_class_name'];
final readDate = notificationJson['read_at'];
final type = NotificationType.getTypeFromString(stringifiedType);
return switch (type) {
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type),
NotificationType.planSubscriptionCanceledToAuthor => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type),
NotificationType.chosenPlanStartedToClientNotification => ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type),
NotificationType.planSubscriptionStartedToAuthor =>
PlanSubscriptionStartedToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.planSubscriptionCanceledToAuthor =>
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanStartedToClientNotification =>
ChosenPlanStartedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanTomorrowReminderToClientNotification =>
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type),
NotificationType.personalizedDietIsDefined => PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type),
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type),
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type),
NotificationType.chosenPlanStartedToConsultantNotification => ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type),
ChosenPlanTomorrowReminderToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.diaryReminderNotification => DiaryReminderNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.consultantLeftComment => ConsultantLeftCommentNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.personalizedDietIsDefined =>
PersonalizedDietIsDefinedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanFinishedToClient =>
ChosenPlanFinishedToClientNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.planSubscriptionCanceledToCustomer =>
PlanSubscriptionCanceledToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanStartedToConsultantNotification =>
ChosenPlanStartedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanTomorrowReminderToConsultantNotification =>
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type),
NotificationType.planSubscriptionPaidToAuthor => PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type),
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type),
ChosenPlanTomorrowReminderToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.planSubscriptionPaidToAuthor =>
PlanSubscriptionPaidToAuthorNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.chosenPlanFinishedToConsultant =>
ChosenPlanFinishedToConsultantNotification.fromJson(notificationJson['data'], type, isRead: readDate != null),
NotificationType.unknown => UnknownNotification(type: type),
};
} catch (e) {
@@ -43,4 +53,12 @@ class NotificationsService {
rethrow;
}
}
Future<void> markNotificationsAsRead() async {
try {
await dio.post('/api/profile/mark_notifications_as_readed');
} catch (e) {
rethrow;
}
}
}

View File

@@ -55,4 +55,16 @@ enum ChosenPlanStateEnum {
return true;
}
}
static List<ChosenPlanStateEnum> getActiveStates() {
return [
ChosenPlanStateEnum.statePaid,
ChosenPlanStateEnum.stateStarted,
ChosenPlanStateEnum.stateInProgress,
ChosenPlanStateEnum.stateLastDayStarted,
ChosenPlanStateEnum.stateLastDayFinished,
ChosenPlanStateEnum.stateComplained,
ChosenPlanStateEnum.stateDone
];
}
}

View File

@@ -113,7 +113,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
complainedContentType: ComplainedContent.plan,
slug: widget.plan.slug,
),
@@ -143,7 +143,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
complainedContentType: ComplainedContent.plan,
slug: widget.plan.slug,
),
@@ -362,7 +362,7 @@ class _AboutPlanScreenState extends State<AboutPlanScreen> {
actions: [
CustomPopup(
isAuthor: widget.plan.author.authorIsMe,
path: 'plan/${widget.plan.id}',
path: 'plan/${widget.plan.slug}',
complainedContentType: ComplainedContent.plan,
slug: widget.plan.slug,
),

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/features/consultant/become_consultant/models/consultation_request_model.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/profile/models/badge_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -50,7 +51,7 @@ abstract class EducationModel with _$EducationModel {
@JsonKey(name: "fb_link") String? fbLink,
@JsonKey(name: "tg_link") String? tgLink,
@JsonKey(name: "personal_site_link") String? personalSiteLink,
@JsonKey(name: "request_state") String? requestState,
@JsonKey(name: "request_state") required ModerationStatus requestState,
}) = _EducationModel;
factory EducationModel.fromJson(Map<String, dynamic> json) => _$EducationModelFromJson(json);

View File

@@ -33,6 +33,7 @@ class ProfileCubit extends Cubit<ProfileState> {
);
} catch (e) {
emit(ProfileState.error(errorMessage: e.toString()));
rethrow;
}
}

View File

@@ -33,6 +33,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -92,12 +93,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
} else if (state is ProfileStateError) {
return DataErrorWidget(
onReload: () async {
widget.author == null || widget.author!.authorIsMe
? await profileCubit.getMyProfile()
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
},
return Scaffold(
appBar: AppBar(),
body: DataErrorWidget(
onReload: () async {
widget.author == null || widget.author!.authorIsMe
? await profileCubit.getMyProfile()
: await profileCubit.getProfileBySlug(widget.author!.authorSlug);
},
),
);
} else {
return Scaffold(
@@ -329,7 +333,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(height: 8),
SizedBox(
height: 378,
height: 781,
child: PageView.builder(
itemBuilder: (BuildContext context, int index) {
return PlanWidget(
@@ -347,6 +351,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(height: 8),
_bullets(_currentBullet, state.plansList.length),
const SizedBox(height: 8),
],
);
}
@@ -435,12 +440,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.only(bottom: 16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
'Дипломы\nи сертификаты',
style: TextStyle(
color: Color(0xFF323F4B),
@@ -449,14 +454,19 @@ class _ProfileScreenState extends State<ProfileScreen> {
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
// GestureDetector(
// onTap: () {
// GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
// },
// child: Text(
// widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
// style: const TextStyle(
// color: Color(0xFF2F80ED),
// fontSize: 16,
// fontWeight: FontWeight.w500,
// ),
// ),
// ),
],
),
),
@@ -531,12 +541,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
padding: const EdgeInsets.only(bottom: 16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
'Образование',
style: TextStyle(
color: Color(0xFF323F4B),
@@ -544,14 +554,19 @@ class _ProfileScreenState extends State<ProfileScreen> {
fontWeight: FontWeight.w600,
),
),
Text(
widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
style: const TextStyle(
color: Color(0xFF2F80ED),
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
// GestureDetector(
// onTap: () {
// GoRouter.of(context).pushNamed('stepperBecomeConsultantScreen', extra: state.profile.slug);
// },
// child: Text(
// widget.author == null || widget.author!.authorIsMe ? 'Редактировать' : '',
// style: const TextStyle(
// color: Color(0xFF2F80ED),
// fontSize: 16,
// fontWeight: FontWeight.w500,
// ),
// ),
// ),
],
),
),
@@ -562,7 +577,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16, top: 20),
padding: const EdgeInsets.all(16),
child: Column(
children: [
ListView.separated(
@@ -584,6 +599,11 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
_educationBlock(String title, String date) {
final parsedDate = DateTime.tryParse(date);
final formattedDate = parsedDate != null
? DateFormat('d MMMM y', 'ru_RU').format(parsedDate)
: date;
return SizedBox(
width: double.infinity,
child: Material(
@@ -605,7 +625,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
const SizedBox(height: 4),
Text(
date,
formattedDate,
style: const TextStyle(
height: 1,
color: Color(0xFF6D6F71),
@@ -620,6 +640,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
}
_socialLinks(ProfileStateLoaded state) {
if (!state.profile.roles.contains('consultant')) {
return const SizedBox.shrink();
@@ -964,7 +985,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
context.pushTransparentRoute(
rootNavigator: true,
PhotoViewScreen(
imageUrls: [state.profile.profilePhotoUrl],
imageUrls: [baseUrl + state.profile.profilePhotoUrl],
index: 0,
),
);
@@ -1044,7 +1065,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
tag: state.profile.profilePhotoUrl,
child: CircleAvatar(
radius: 32,
foregroundImage: NetworkImage(state.profile.profilePhotoUrl),
foregroundImage: NetworkImage(baseUrl + state.profile.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),
),

View File

@@ -66,7 +66,7 @@ class _ProfileBlogPostWidgetState extends State<ProfileBlogPostWidget> {
radius: 20,
backgroundColor: Colors.black12,
foregroundImage: NetworkImage(
state.blogPosts[widget.index].author.authorProfileIcon,
baseUrl + state.blogPosts[widget.index].author.authorProfileIcon,
),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
),

View File

@@ -1,3 +1,4 @@
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/profile/models/sub_model.dart';
import 'package:eatvio_app/features/profile/presentation/widgets/sub_action_button_widget.dart';
@@ -35,7 +36,7 @@ class _SubWidgetState extends State<SubWidget> {
child: Row(
children: [
CircleAvatar(
foregroundImage: NetworkImage(widget.subModel.profilePhotoUrl),
foregroundImage: NetworkImage(baseUrl + widget.subModel.profilePhotoUrl),
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
radius: 28,
),

View File

@@ -183,7 +183,8 @@ class ProfileRepository {
}
Future<String> getPassKey(String phone) async {
return (jsonDecode((await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
return (jsonDecode(
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}'))))['key_is']!;
}
Future<String> getMyPhone() async {
@@ -194,7 +195,12 @@ class ProfileRepository {
return await profileService.sendEmailCode();
}
Future<void> tryVerifyEmailByCode(String code) async {
return await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
Future<bool> tryVerifyEmailByCode(String code) async {
try {
await profileService.tryVerifyEmailByCode(code: "{\"code\": \"$code\"}");
return true;
} catch (e) {
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_recipe_model.g.dart';
part 'my_recipe_model.freezed.dart';
@freezed
abstract class MyRecipePaginationModel with _$MyRecipePaginationModel {
const factory MyRecipePaginationModel({
required List<MyRecipeModel> data,
}) = _MyRecipePaginationModel;
factory MyRecipePaginationModel.fromJson(Map<String, Object?> json) => _$MyRecipePaginationModelFromJson(json);
}
@freezed
abstract class MyRecipeModel with _$MyRecipeModel {
const factory MyRecipeModel({
required int id,
required String image,
required String state,
@JsonKey(name: 'cooking_time') required int cookingTime,
@JsonKey(name: 'active_time') required int activeCookingTime,
@JsonKey(name: 'description_md') required String description,
required List<Step>? steps,
@JsonKey(name: 'video_url') required String? videoUrl,
required String? title,
required String slug,
@JsonKey(name: 'energy_per_serving') required double calories,
@JsonKey(name: 'total_likes') required int likesCount,
@JsonKey(name: 'total_views') required int viewsCount,
}) = _MyRecipeModel;
factory MyRecipeModel.fromJson(Map<String, Object?> json) => _$MyRecipeModelFromJson(json);
}

View File

@@ -0,0 +1,56 @@
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
import 'package:eatvio_app/features/recipe/repositories/about_recipe_repository.dart';
import 'package:eatvio_app/features/recipe/repositories/my_recipes_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyRecipesCubit extends Cubit<MyRecipesState> {
final MyRecipesRepository myRecipesRepository;
final AboutRecipeRepository aboutRecipeRepository;
static const String active = 'active';
static const String archived = 'archived';
MyRecipesCubit({
required this.myRecipesRepository,
required this.aboutRecipeRepository,
}) : super(const MyRecipesState.loading());
getMyRecipes() async {
emit(const MyRecipesState.loading());
try {
final myRecipes = await myRecipesRepository.getMyRecipes();
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
} catch (e) {
emit(const MyRecipesState.error());
}
}
setActiveState() async {
emit(const MyRecipesState.loading());
try {
final myRecipes = await myRecipesRepository.getMyRecipes();
final List<MyRecipeModel> activeRecipes = myRecipes.where((recipe) => recipe.state == active).toList();
emit(MyRecipesState.loaded(recipes: activeRecipes, selectedState: active));
} catch (e) {
emit(const MyRecipesState.error());
}
}
setArchivedState() async {
emit(const MyRecipesState.loading());
try {
final myRecipes = await myRecipesRepository.getMyRecipes();
final List<MyRecipeModel> archivedRecipes = myRecipes.where((recipe) => recipe.state == archived).toList();
emit(MyRecipesState.loaded(recipes: archivedRecipes, selectedState: archived));
} catch (e) {
emit(const MyRecipesState.error());
}
}
Future<Recipe> getRecipeById(String slug) async {
return await aboutRecipeRepository.getRecipe(slug);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'my_recipes_state.freezed.dart';
@freezed
abstract class MyRecipesState with _$MyRecipesState {
const factory MyRecipesState() = _MyRecipesState;
const factory MyRecipesState.loading() = MyRecipesStateLoading;
const factory MyRecipesState.loaded({
required List<MyRecipeModel> recipes,
required String selectedState,
}) = MyRecipesStateLoaded;
const factory MyRecipesState.error() = MyRecipesStateError;
}

View File

@@ -0,0 +1,49 @@
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recipes_state.dart';
import 'package:eatvio_app/features/recipe/presentation/widgets/my_recipe_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MyRecipesScreen extends StatelessWidget {
const MyRecipesScreen({super.key});
static const name = 'myRecipesScreen';
static const path = '/my-recipes-screen';
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: appTheme.gray100,
appBar: AppBar(
centerTitle: true,
title: Text(
'Мои рецепты',
maxLines: 2,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
softWrap: true,
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
),
),
body: BlocProvider<MyRecipesCubit>(
create: (BuildContext context) => sl<MyRecipesCubit>()..getMyRecipes(),
child: BlocBuilder<MyRecipesCubit, MyRecipesState>(
builder: (BuildContext context, MyRecipesState state) {
if (state is MyRecipesStateLoaded) {
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 16),
itemBuilder: (context, index) => MyRecipeWidget(recipe: state.recipes[index]),
separatorBuilder: (context, index) => const SizedBox(height: 12),
itemCount: state.recipes.length,
);
}
return Container();
},
),
),
);
}
}

View File

@@ -0,0 +1,218 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:eatvio_app/core/const.dart';
import 'package:eatvio_app/core/locator_service.dart';
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/utils/localization_utils.dart';
import 'package:eatvio_app/core/utils/toasts.dart';
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/presentation/cubit/my_recipes_cubit/my_recieps_cubit.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:share_plus/share_plus.dart';
class MyRecipeWidget extends StatefulWidget {
final MyRecipeModel recipe;
const MyRecipeWidget({
super.key,
required this.recipe,
});
@override
State<MyRecipeWidget> createState() => _MyRecipeWidgetState();
}
class _MyRecipeWidgetState extends State<MyRecipeWidget> {
late final int hours;
late final int minutes;
late final String timeTitle;
@override
void initState() {
super.initState();
hours = widget.recipe.cookingTime ~/ 60;
minutes = widget.recipe.cookingTime % 60;
if (hours > 0) {
timeTitle = '$hours ${getHourForm(hours)} $minutes ${getMinuteForm(minutes)}';
} else {
timeTitle = '$minutes ${getMinuteForm(minutes)}';
}
}
@override
Widget build(BuildContext context) {
final s = S.of(context);
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
InkWell(
onTap: () async => GoRouter.of(context).pushNamed(
'aboutRecipesScreen',
extra: {
'recipe': await context.read<MyRecipesCubit>().getRecipeById(widget.recipe.slug),
'navigateToComments': false,
},
),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
child: CachedNetworkImage(
imageUrl: '$baseUrl/storage/${widget.recipe.image}',
fit: BoxFit.fitWidth,
width: MediaQuery.of(context).size.width,
),
),
),
Positioned(
left: 8,
bottom: 8,
child: Wrap(
runSpacing: 8,
children: [
_iconButton('assets/icons/clock.svg', timeTitle),
const SizedBox(width: 8),
_iconButton('assets/icons/steps.svg', '${widget.recipe.steps?.length ?? 0} ${getStepForm(widget.recipe.steps?.length ?? 0)}'),
const SizedBox(width: 8),
_iconButton('assets/icons/calories.svg', '${widget.recipe.calories.toStringAsFixed(0)} ккал'),
],
),
),
],
),
const SizedBox(height: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
widget.recipe.title!,
style: TextStyles.textMSemibold.copyWith(color: appTheme.black),
),
),
if (widget.recipe.description.isNotEmpty) ...[
const SizedBox(
height: 12,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Html(
data: widget.recipe.description.length > 200 ? '${widget.recipe.description.substring(0, 200)}...' : widget.recipe.description,
style: {
"*": Style(
margin: Margins.zero,
color: appTheme.black,
fontSize: FontSize(TextStyles.textSMedium.fontSize ?? 14),
fontWeight: TextStyles.textSMedium.fontWeight ?? FontWeight.w400,
),
},
),
),
],
],
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
final result = await sl<RecipesCubit>().deleteRecipe(widget.recipe.slug);
if (!result) {
showErrorToast('Не удалось удалить рецепт');
return;
}
showSuccessToast('Рецепт удален');
if (context.mounted) context.read<MyRecipesCubit>().getMyRecipes();
},
text: 'Удалить',
height: 52,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
side: const WidgetStatePropertyAll(BorderSide.none),
),
),
),
const SizedBox(width: 8),
Expanded(
child: CustomOutlinedButton(
onPressed: () async {
final box = context.findRenderObject() as RenderBox?;
await SharePlus.instance.share(
ShareParams(
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.slug}'),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
),
);
},
height: 52,
width: 52,
buttonStyle: ButtonStyle(
shape: WidgetStatePropertyAll(RoundedRectangleBorder(borderRadius: BorderRadius.circular(20))),
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
side: const WidgetStatePropertyAll(BorderSide.none),
),
text: s.share,
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
),
),
],
),
const SizedBox(height: 8),
],
),
),
],
),
);
}
_iconButton(String iconPath, [String? text]) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(16),
),
height: 32,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
iconPath,
),
if (text != null) const SizedBox(width: 8),
if (text != null)
Text(
text,
style: const TextStyle(
fontSize: 12,
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:eatvio_app/core/theme/custom_text_style.dart';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/animated_option_button.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/widgets.dart';
class MyRecipesTab extends StatefulWidget {
const MyRecipesTab({super.key, this.additionalOnTap});
final void Function()? additionalOnTap;
@override
State<MyRecipesTab> createState() => _MyRecipesTabState();
}
class _MyRecipesTabState extends State<MyRecipesTab> {
final List<String> tabTitles = [];
@override
void didChangeDependencies() {
final s = S.of(context);
if (tabTitles.isEmpty) {
tabTitles.add(s.published);
tabTitles.add(s.drafted);
}
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.centerLeft,
child: Row(
children: [
Expanded(
child: Container(
height: 48,
decoration: BoxDecoration(
color: appTheme.whiteA700,
borderRadius: BorderRadius.circular(100),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
...List.generate(
2,
(index) =>
Expanded(
child: AnimatedOptionButton(
textStyle: TextStyles.textSMedium,
isSelected: false, // state.selectedStateIndex == index,
text: tabTitles[index],
onTap: () {
widget.additionalOnTap?.call();
// context.read<MyPlansCubit>().changeSelectedStateIndex(index);
},
),
),
),
],
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:eatvio_app/features/recipe/services/my_recipes_service.dart';
class MyRecipesRepository {
final MyRecipesService myRecipesService;
MyRecipesRepository({required this.myRecipesService});
Future<List<MyRecipeModel>> getMyRecipes() async {
try {
return (await myRecipesService.getMyRecipes()).data;
} catch (e) {
rethrow;
}
}
}

View File

@@ -0,0 +1,13 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/recipe/models/my_recipe_model.dart';
import 'package:retrofit/retrofit.dart';
part 'my_recipes_service.g.dart';
@RestApi()
abstract class MyRecipesService {
factory MyRecipesService(Dio dio, {String baseUrl}) = _MyRecipesService;
@GET('/api/recipes/my')
Future<MyRecipePaginationModel> getMyRecipes();
}

Some files were not shown because too many files have changed in this diff Show More