forked from eatvio/eatvio_app
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
087436da1c | ||
|
|
6ac93d42df | ||
|
|
1a6fb39d84 | ||
|
|
b4776c3be3 | ||
|
|
06e07a92b7 | ||
|
|
0fce3f9d30 | ||
|
|
4bc8cba941 | ||
|
|
8c348f61b1 | ||
|
|
0446e64409 | ||
|
|
74b5c6732d | ||
|
|
b840c936bf | ||
|
|
f3dbbf3537 | ||
|
|
ff49562e86 | ||
|
|
8b7180c2da | ||
|
|
5c911707a3 | ||
|
|
aa604b667b | ||
|
|
0a5d78be1f | ||
|
|
f1f5c79661 | ||
|
|
1c517807c4 | ||
|
|
cad54913d1 | ||
|
|
89f0a46192 | ||
|
|
65542020a7 | ||
|
|
b6b1c8b3c1 | ||
|
|
bf6e23e96a | ||
|
|
952cf76164 | ||
|
|
b665692779 | ||
|
|
54ac20978f | ||
|
|
b9dc5c0136 | ||
|
|
39fce4b0f9 | ||
|
|
73ff67aa13 | ||
|
|
dcf2805df7 | ||
|
|
d13eb78f0e | ||
|
|
5f21a65028 | ||
|
|
12bf9f9e5f | ||
|
|
1680bd2556 | ||
|
|
91d5118f60 | ||
|
|
d8324c1635 | ||
|
|
0f1dbe16f1 | ||
|
|
020682ae80 | ||
|
|
e54476656b | ||
|
|
dd56924076 | ||
|
|
b47da44cf2 | ||
|
|
964550a325 | ||
|
|
7ffa061f3e | ||
|
|
997da4f940 | ||
|
|
ca1777ece0 | ||
|
|
1a427d6f5d | ||
|
|
f190e70f3a | ||
|
|
bc6469a6ae | ||
|
|
e3ff359e1b | ||
|
|
b5edf13bd9 | ||
|
|
c05d027e3d | ||
|
|
bae79b60fd | ||
|
|
6061e413b2 | ||
|
|
5a8fa7089c | ||
|
|
886cfa8f1c | ||
|
|
a416912d2a | ||
|
|
f46392ec8e | ||
|
|
766d06fc4d | ||
|
|
6f4023dd0b | ||
|
|
f892dfed9f | ||
|
|
98803e56cf | ||
|
|
4391870e72 | ||
|
|
7fd24f57e3 | ||
|
|
384a25c4a6 | ||
|
|
88558d62ef | ||
|
|
09b04118aa | ||
|
|
f76b142117 | ||
|
|
13e66edd27 | ||
|
|
5b74ae45b3 | ||
|
|
3d2634b96a | ||
|
|
1281abaf24 | ||
|
|
7dfac3f792 | ||
|
|
9d4687553a | ||
|
|
442da40622 | ||
|
|
ccf586234b | ||
|
|
4f249499a9 | ||
|
|
2f74387df2 | ||
|
|
45774c3875 | ||
|
|
62c0914fe2 | ||
|
|
48f5b64223 | ||
| ed4db5b59e | |||
|
|
91bb5cf483 | ||
|
|
c4dedac3f3 | ||
|
|
8ed243f63c | ||
|
|
a10fe6d2f5 | ||
|
|
35b78bdeee | ||
|
|
ef037eb4d5 | ||
|
|
5a3385cacf | ||
|
|
ef4d91a5d8 | ||
|
|
dca4b4c471 | ||
|
|
cfbd7597fa | ||
|
|
412ac0aeeb | ||
|
|
96e80ced38 |
@@ -21,20 +21,19 @@ jobs:
|
||||
|
||||
- name: Copy .env file
|
||||
run: |
|
||||
mkdir -p env
|
||||
cp env_repo/.env env/.env
|
||||
cp env_repo/.env .env
|
||||
|
||||
- name: Load environment variables from .env
|
||||
run: |
|
||||
set -a
|
||||
source env/.env
|
||||
source .env
|
||||
set +a
|
||||
# Export all variables to GITHUB_ENV
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
||||
echo "$line" >> $GITHUB_ENV
|
||||
fi
|
||||
done < env/.env
|
||||
done < .env
|
||||
|
||||
- name: Unlock login keychain
|
||||
run: |
|
||||
@@ -59,6 +58,8 @@ jobs:
|
||||
flutter pub get
|
||||
flutter pub upgrade
|
||||
flutter pub run build_runner build --delete-conflicting-outputs
|
||||
# Configure Flutter for release mode
|
||||
flutter build ios --config-only --release
|
||||
cd ios
|
||||
pod update
|
||||
fastlane beta
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Eatvio App Android Test
|
||||
on: [ pull_request ]
|
||||
|
||||
env:
|
||||
flutter_version: "3.32.8"
|
||||
flutter_version: "3.35.1"
|
||||
java_version: "21.x"
|
||||
flutter_channel: "stable"
|
||||
my_local_cache_dir: "/opt/hostedtoolcache/"
|
||||
@@ -26,20 +26,19 @@ jobs:
|
||||
|
||||
- name: Copy .env file
|
||||
run: |
|
||||
mkdir -p env
|
||||
cp env_repo/.env env/.env
|
||||
cp env_repo/.env .env
|
||||
|
||||
- name: Load environment variables from .env
|
||||
run: |
|
||||
set -a
|
||||
source env/.env
|
||||
source .env
|
||||
set +a
|
||||
# Export all variables to GITHUB_ENV
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then
|
||||
echo "$line" >> $GITHUB_ENV
|
||||
fi
|
||||
done < env/.env
|
||||
done < .env
|
||||
|
||||
- name: Create key.properties file
|
||||
run: |
|
||||
@@ -144,7 +143,11 @@ jobs:
|
||||
run: flutter test
|
||||
|
||||
- name: Build apk
|
||||
run: flutter build apk --release --dart-define=HIDE_PLANS=false
|
||||
run: |
|
||||
BUILD_DATE=$(TZ=Europe/Moscow date +'%Y-%m-%d %H:%M:%S')
|
||||
flutter build apk --release \
|
||||
--dart-define=HIDE_PLANS=false \
|
||||
--dart-define=BUILD_DATE="$BUILD_DATE"
|
||||
|
||||
- name: Rename and move apk
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -16,6 +16,5 @@
|
||||
*.mocks.dart
|
||||
android/build/
|
||||
ios/XCBuildData/
|
||||
.env
|
||||
env.example
|
||||
env/.env
|
||||
.env
|
||||
@@ -29,7 +29,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.eatvioapp.app"
|
||||
minSdk = 23
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
@@ -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.12-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-all.zip
|
||||
|
||||
@@ -18,11 +18,11 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.7.3" apply false
|
||||
id "com.android.application" version "8.12.0" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id "com.google.gms.google-services" version "4.4.2" apply false
|
||||
id "com.google.gms.google-services" version "4.4.3" apply false
|
||||
// END: FlutterFire Configuration
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.2.10" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
<string>16.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
platform :ios, '16.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -39,7 +39,7 @@ post_install do |installer|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0'
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
## dart: PermissionGroup.camera
|
||||
|
||||
@@ -213,14 +213,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
@@ -287,14 +283,10 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
@@ -378,7 +370,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -396,7 +388,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 67;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -418,6 +410,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -474,7 +467,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -526,7 +519,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
@@ -546,7 +539,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 67;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -568,6 +561,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -584,7 +578,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution: ITVIO, OOO (VL68C36HYS)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 67;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = VL68C36HYS;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -606,6 +600,7 @@
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.eatvioapp.app 1747050044";
|
||||
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>67</string>
|
||||
<string>72</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -44,6 +44,10 @@ platform :ios do
|
||||
|
||||
UI.message "Using auth_key_path: #{auth_key_path}"
|
||||
|
||||
# Configure Flutter for release mode
|
||||
UI.message "Configuring Flutter for release mode..."
|
||||
sh("cd .. && flutter build ios --config-only --release")
|
||||
|
||||
match(type: "appstore", readonly: true)
|
||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||
|
||||
@@ -52,7 +56,6 @@ platform :ios do
|
||||
scheme: "Runner",
|
||||
export_method: "app-store",
|
||||
codesigning_identity: "Apple Distribution",
|
||||
xcargs: "--dart-define=HIDE_PLANS=true"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:eatvio_app/features/consultations/presentation/pages/consultatio
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/blocked_users_screen.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/pages/notifications_settings_screen.dart';
|
||||
import 'package:eatvio_app/features/order_info/presentation/info_by_order_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/diet_preview_screen.dart';
|
||||
@@ -111,23 +112,13 @@ class AppRouter {
|
||||
name: 'authScreen',
|
||||
builder: (context, state) => const AuthScreen(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'registration-screen',
|
||||
name: 'registrationScreen',
|
||||
builder: (context, state) => const RegistrationScreen(),
|
||||
),
|
||||
GoRoute(path: 'registration-screen', name: 'registrationScreen', builder: (context, state) => const RegistrationScreen()),
|
||||
GoRoute(
|
||||
path: 'forget-password-screen',
|
||||
name: 'forgetPasswordScreen',
|
||||
builder: (context, state) => ForgetPasswordScreen(
|
||||
isAuth: state.extra as bool,
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'scan-qr-code-screen',
|
||||
name: 'scanQRCodeScreen',
|
||||
builder: (context, state) => const ScanQRCodeScreen(),
|
||||
builder: (context, state) => ForgetPasswordScreen(isAuth: state.extra as bool),
|
||||
),
|
||||
GoRoute(path: 'scan-qr-code-screen', name: 'scanQRCodeScreen', builder: (context, state) => const ScanQRCodeScreen()),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
@@ -165,19 +156,23 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/diary-screen',
|
||||
name: 'diaryScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(
|
||||
child: DiaryScreen(),
|
||||
),
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: DiaryScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen())),
|
||||
GoRoute(path: '/chat-select-screen', name: 'chatSelectScreen', pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen())),
|
||||
path: ConsultationsScreen.path,
|
||||
name: ConsultationsScreen.name,
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ConsultationsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications-screen',
|
||||
name: 'notificationsScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen())),
|
||||
path: '/chat-select-screen',
|
||||
name: 'chatSelectScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: ChatsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/notifications-screen',
|
||||
name: 'notificationsScreen',
|
||||
pageBuilder: (context, state) => const NoTransitionPage(child: NotificationsScreen()),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/menu-screen',
|
||||
name: 'menuScreen',
|
||||
@@ -194,11 +189,7 @@ class AppRouter {
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
name: 'blockedUsersScreen',
|
||||
path: '/blocked-users-screen',
|
||||
builder: (context, state) => const BlockedUsersScreen(),
|
||||
),
|
||||
GoRoute(name: 'blockedUsersScreen', path: '/blocked-users-screen', builder: (context, state) => const BlockedUsersScreen()),
|
||||
GoRoute(
|
||||
path: '/welcome-screen',
|
||||
name: 'welcomeScreen',
|
||||
@@ -241,26 +232,10 @@ class AppRouter {
|
||||
}
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultationsArchiveScreen.path,
|
||||
name: ConsultationsArchiveScreen.name,
|
||||
builder: (context, state) => const ConsultationsArchiveScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: ConsultantFinancesScreen.path,
|
||||
name: ConsultantFinancesScreen.name,
|
||||
builder: (context, state) => const ConsultantFinancesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: UserPurchasesScreen.path,
|
||||
name: UserPurchasesScreen.name,
|
||||
builder: (context, state) => const UserPurchasesScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/user-finance-screen',
|
||||
name: 'userFinanceScreen',
|
||||
builder: (context, state) => const UserFinanceScreen(),
|
||||
),
|
||||
GoRoute(path: ConsultationsArchiveScreen.path, name: ConsultationsArchiveScreen.name, builder: (context, state) => const ConsultationsArchiveScreen()),
|
||||
GoRoute(path: ConsultantFinancesScreen.path, name: ConsultantFinancesScreen.name, builder: (context, state) => const ConsultantFinancesScreen()),
|
||||
GoRoute(path: UserPurchasesScreen.path, name: UserPurchasesScreen.name, builder: (context, state) => const UserPurchasesScreen()),
|
||||
GoRoute(path: '/user-finance-screen', name: 'userFinanceScreen', builder: (context, state) => const UserFinanceScreen()),
|
||||
GoRoute(
|
||||
path: CardSettingsScreen.path,
|
||||
name: CardSettingsScreen.name,
|
||||
@@ -275,11 +250,7 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
return AboutPlanScreen(
|
||||
slug: slug,
|
||||
navigateToComments: false,
|
||||
index: null,
|
||||
);
|
||||
return AboutPlanScreen(slug: slug, navigateToComments: false, index: null);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
@@ -293,29 +264,23 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
return AboutRecipeScreen(
|
||||
recipeSlug: slug,
|
||||
navigateToComments: false,
|
||||
index: null,
|
||||
);
|
||||
return AboutRecipeScreen(recipeSlug: slug, navigateToComments: false, index: null);
|
||||
}
|
||||
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutRecipeScreen(
|
||||
recipeSlug: data['recipe'] as String,
|
||||
index: data['index'] as int?,
|
||||
navigateToComments: data['navigateToComments'] as bool,
|
||||
);
|
||||
return AboutRecipeScreen(recipeSlug: data['recipe'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: RecipeCreateScreen.path,
|
||||
name: RecipeCreateScreen.name,
|
||||
builder: (context, state) => RecipeCreateScreen(
|
||||
recipe: state.extra != null ? state.extra as Recipe : null,
|
||||
),
|
||||
builder: (context, state) => RecipeCreateScreen(recipe: state.extra != null ? state.extra as Recipe : null),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/profile-screen',
|
||||
name: 'profileScreen',
|
||||
builder: (context, state) => ProfileScreen(author: state.extra as Author),
|
||||
),
|
||||
GoRoute(path: '/profile-screen', name: 'profileScreen', builder: (context, state) => ProfileScreen(author: state.extra as Author)),
|
||||
GoRoute(
|
||||
path: '/subs-screen/:slug',
|
||||
name: 'subsScreen',
|
||||
@@ -327,11 +292,7 @@ class AppRouter {
|
||||
builder: (context, state) {
|
||||
final slug = state.uri.queryParameters['slug'];
|
||||
if (slug != null) {
|
||||
return AboutDietScreen(
|
||||
slug: slug,
|
||||
index: null,
|
||||
navigateToComments: false,
|
||||
);
|
||||
return AboutDietScreen(slug: slug, index: null, navigateToComments: false);
|
||||
}
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
return AboutDietScreen(slug: data['slug'] as String, index: data['index'] as int?, navigateToComments: data['navigateToComments'] as bool);
|
||||
@@ -343,19 +304,18 @@ class AppRouter {
|
||||
builder: (context, state) => BlogPostCreateScreen(feedBlogPostModel: state.extra != null ? state.extra as FeedBlogPostModel : null),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/blog-post-screen',
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
if (parsedId != null) {
|
||||
return BlogPostScreen(
|
||||
id: parsedId,
|
||||
);
|
||||
}
|
||||
path: '/blog-post-screen',
|
||||
name: 'blogPostScreen',
|
||||
builder: (context, state) {
|
||||
final id = state.uri.queryParameters['id'];
|
||||
final parsedId = int.tryParse(id ?? '');
|
||||
if (parsedId != null) {
|
||||
return BlogPostScreen(id: parsedId);
|
||||
}
|
||||
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
}),
|
||||
return BlogPostScreen(id: state.extra as int);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat-screen',
|
||||
name: 'chatScreen',
|
||||
@@ -363,10 +323,7 @@ class AppRouter {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final uuid = data['uuid'] as String;
|
||||
final sendInitialMessage = data['sendInitialMessage'] as bool?;
|
||||
return ChatScreen(
|
||||
uuid: uuid,
|
||||
sendInitialMessage: sendInitialMessage,
|
||||
);
|
||||
return ChatScreen(uuid: uuid, sendInitialMessage: sendInitialMessage);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -399,7 +356,11 @@ class AppRouter {
|
||||
name: 'physicalFormScreen',
|
||||
builder: (context, state) => PhysicalFormScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(path: 'pressure-screen', name: 'pressureScreen', builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity)),
|
||||
GoRoute(
|
||||
path: 'pressure-screen',
|
||||
name: 'pressureScreen',
|
||||
builder: (context, state) => PressureScreen(settingsEntity: state.extra as SettingsEntity),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'change-password-screen',
|
||||
name: 'changePasswordScreen',
|
||||
@@ -416,9 +377,7 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/stepper-become-consultant-screen',
|
||||
name: 'stepperBecomeConsultantScreen',
|
||||
builder: (context, state) => StepperBecomeConsultantScreen(
|
||||
slug: state.extra == null ? null : state.extra as String,
|
||||
),
|
||||
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()),
|
||||
@@ -446,7 +405,11 @@ class AppRouter {
|
||||
GoRoute(path: CreatePlanScreen.path, name: CreatePlanScreen.name, builder: (context, state) => const CreatePlanScreen()),
|
||||
GoRoute(path: MyDietsScreen.path, name: MyDietsScreen.name, builder: (context, state) => const MyDietsScreen()),
|
||||
GoRoute(path: MyPlansScreen.path, name: MyPlansScreen.name, builder: (context, state) => const MyPlansScreen()),
|
||||
GoRoute(path: UserDetailsScreen.path, name: UserDetailsScreen.name, builder: (context, state) => UserDetailsScreen(slug: state.extra as String)),
|
||||
GoRoute(
|
||||
path: UserDetailsScreen.path,
|
||||
name: UserDetailsScreen.name,
|
||||
builder: (context, state) => UserDetailsScreen(slug: state.extra as String),
|
||||
),
|
||||
GoRoute(path: DevToolsScreen.path, name: DevToolsScreen.name, builder: (context, state) => const DevToolsScreen()),
|
||||
GoRoute(path: ConsultantRatingScreen.path, name: ConsultantRatingScreen.name, builder: (context, state) => const ConsultantRatingScreen()),
|
||||
GoRoute(
|
||||
@@ -456,10 +419,7 @@ class AppRouter {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final planId = data['planId'] as int;
|
||||
final successfulPayment = data['successfulPayment'] as bool?;
|
||||
return InfoByOrderScreen(
|
||||
planId: planId,
|
||||
successfulPayment: successfulPayment,
|
||||
);
|
||||
return InfoByOrderScreen(planId: planId, successfulPayment: successfulPayment);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -469,10 +429,7 @@ class AppRouter {
|
||||
final data = state.extra as Map<String, dynamic>;
|
||||
final planId = data['planId'] as int;
|
||||
final url = data['url'] as String;
|
||||
return PaymentScreen(
|
||||
url: url,
|
||||
planId: planId,
|
||||
);
|
||||
return PaymentScreen(url: url, planId: planId);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -482,10 +439,7 @@ class AppRouter {
|
||||
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,
|
||||
);
|
||||
return UserDiaryScreen(diaryId: diaryId, currentPlanDay: currentPlanDay);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -493,16 +447,9 @@ 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(),
|
||||
),
|
||||
GoRoute(path: '/verify-email-screen', name: 'verifyEmailScreen', builder: (context, state) => const VerifyEmailScreen()),
|
||||
GoRoute(path: MyRecipesScreen.path, name: MyRecipesScreen.name, builder: (context, state) => const MyRecipesScreen()),
|
||||
GoRoute(path: '/notifications-settings-screen', name: 'notificationsSettingsScreen', builder: (context, state) => const NotificationsSettingsScreen()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,25 +18,23 @@ void showCommentsActionsBottomSheet(
|
||||
required bool isMyComment,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: appTheme.whiteA700,
|
||||
),
|
||||
child: CommentsActionsBottomSheet(
|
||||
isMyComment: isMyComment,
|
||||
reply: reply,
|
||||
copy: copy,
|
||||
edit: edit,
|
||||
delete: delete,
|
||||
complain: complain,
|
||||
commentText: commentText,
|
||||
),
|
||||
));
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) => Container(
|
||||
margin: EdgeInsets.only(left: 8, right: 8, bottom: MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).padding.bottom + 20),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.whiteA700),
|
||||
child: CommentsActionsBottomSheet(
|
||||
isMyComment: isMyComment,
|
||||
reply: reply,
|
||||
copy: copy,
|
||||
edit: edit,
|
||||
delete: delete,
|
||||
complain: complain,
|
||||
commentText: commentText,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class CommentsActionsBottomSheet extends StatefulWidget {
|
||||
@@ -82,44 +80,33 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: CustomTextFormField(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
onChanged: (value) => setState(() {}),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomTextFormField(autofocus: true, controller: controller, onChanged: (value) => setState(() {})),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.edit(controller.text);
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: Icon(
|
||||
Icons.send_rounded,
|
||||
size: 18,
|
||||
color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
|
||||
const SizedBox(width: 8),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
widget.edit(controller.text);
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: controller.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: Icon(Icons.send_rounded, size: 18, color: controller.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
])
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 7,
|
||||
),
|
||||
const SizedBox(height: 7),
|
||||
const BottomSheetHandlerBar(),
|
||||
if (widget.isMyComment)
|
||||
BottomSheetActionWidget(
|
||||
@@ -131,43 +118,22 @@ class _CommentsActionsBottomSheetState extends State<CommentsActionsBottomSheet>
|
||||
}),
|
||||
)
|
||||
else if (widget.reply != null)
|
||||
BottomSheetActionWidget(
|
||||
text: s.reply,
|
||||
iconPath: 'assets/icons/reply.svg',
|
||||
onTap: widget.reply!,
|
||||
),
|
||||
BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!),
|
||||
if (widget.reply != null) const CustomDivider(),
|
||||
BottomSheetActionWidget(
|
||||
text: s.copy,
|
||||
iconPath: 'assets/icons/copy2.svg',
|
||||
onTap: widget.copy,
|
||||
),
|
||||
BottomSheetActionWidget(text: s.copy, iconPath: 'assets/icons/copy2.svg', onTap: widget.copy),
|
||||
if (widget.isMyComment) ...[
|
||||
if (widget.reply != null) ...[
|
||||
const CustomDivider(),
|
||||
BottomSheetActionWidget(
|
||||
text: s.reply,
|
||||
iconPath: 'assets/icons/reply.svg',
|
||||
onTap: widget.reply!,
|
||||
),
|
||||
BottomSheetActionWidget(text: s.reply, iconPath: 'assets/icons/reply.svg', onTap: widget.reply!),
|
||||
],
|
||||
const CustomDivider(),
|
||||
BottomSheetActionWidget(
|
||||
text: s.deleteComment,
|
||||
iconPath: 'assets/icons/red_trash.svg',
|
||||
onTap: widget.delete,
|
||||
),
|
||||
BottomSheetActionWidget(text: s.deleteComment, iconPath: 'assets/icons/red_trash.svg', onTap: widget.delete),
|
||||
] else ...[
|
||||
const CustomDivider(),
|
||||
BottomSheetActionWidget(
|
||||
text: s.complain,
|
||||
iconPath: 'assets/icons/complain.svg',
|
||||
onTap: widget.complain,
|
||||
),
|
||||
]
|
||||
BottomSheetActionWidget(text: s.complain, iconPath: 'assets/icons/complain.svg', onTap: widget.complain),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/env_config.dart';
|
||||
|
||||
String baseUrl = EnvConfig.baseUrl;
|
||||
bool hidePlans = const bool.fromEnvironment('HIDE_PLANS', defaultValue: false);
|
||||
const buildDate = String.fromEnvironment('BUILD_DATE', defaultValue: '');
|
||||
|
||||
const List<int> durationValues = [7, 14, 21];
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
abstract class CredentialsStorage {
|
||||
static Future<bool> haveCredentialsInStorage() async {
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'email') == null || await const FlutterSecureStorage().read(key: 'password') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<Map<String, String>> getCredentialsFromStorage() async {
|
||||
return {
|
||||
'email': (await const FlutterSecureStorage().read(key: 'email'))!,
|
||||
'password': (await const FlutterSecureStorage().read(key: 'password'))!,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> saveCredentials(String email, String password) async {
|
||||
await const FlutterSecureStorage().write(key: 'email', value: email);
|
||||
await const FlutterSecureStorage().write(key: 'password', value: password);
|
||||
}
|
||||
|
||||
static Future<void> removeCredentials() async {
|
||||
await const FlutterSecureStorage().delete(key: 'email');
|
||||
await const FlutterSecureStorage().delete(key: 'password');
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,9 @@ class EnvConfig {
|
||||
// Sentry Configuration
|
||||
static String get sentryDsn => dotenv.env['SENTRY_DSN'] ?? '';
|
||||
|
||||
// AppMetrica Configuration
|
||||
static String get appMetricaApiKey => dotenv.env['APPMETRICA_KEY'] ?? '';
|
||||
|
||||
// Firebase Configuration
|
||||
static String get firebaseProjectId => dotenv.env['FIREBASE_PROJECT_ID'] ?? '';
|
||||
static String get firebaseDatabaseUrl => dotenv.env['FIREBASE_DATABASE_URL'] ?? '';
|
||||
|
||||
@@ -5,6 +5,9 @@ import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/dio/phone_verify_error_interceptor.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/interceptors/token_interceptor.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/core/views/repository/views_repository.dart';
|
||||
import 'package:eatvio_app/core/views/service/views_service.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_service.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
@@ -20,6 +23,9 @@ import 'package:eatvio_app/features/cancel_plan_bottom_sheet/service/cancel_plan
|
||||
import 'package:eatvio_app/features/complaining/presentation/cubit/complaining_cubit.dart';
|
||||
import 'package:eatvio_app/features/complaining/repositories/complaining_repository.dart';
|
||||
import 'package:eatvio_app/features/complaining/services/complaining_service.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
import 'package:eatvio_app/features/consultations/presentation/cubit/consultations_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart';
|
||||
import 'package:eatvio_app/features/consultations/services/consultation_service.dart';
|
||||
@@ -40,8 +46,11 @@ import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/consultant_finances/consultant_finances_cubit.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/cubit/user_purchases/user_purchases_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/repositories/blocked_users_repository.dart';
|
||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||
import 'package:eatvio_app/features/menu/services/blocked_users_service.dart';
|
||||
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
||||
import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart';
|
||||
import 'package:eatvio_app/features/calendar/presentation/bloc/calendar_cubit.dart';
|
||||
@@ -107,7 +116,6 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se
|
||||
import 'package:eatvio_app/features/settings/repositories/service_repository.dart';
|
||||
import 'package:eatvio_app/features/settings/services/settings_service.dart';
|
||||
import 'package:eatvio_app/features/user_details/presentation/cubit/user_details_cubit.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:eatvio_app/features/profile/presentation/bloc/plans_cubit/plans_cubit.dart' as profile_plans;
|
||||
|
||||
@@ -132,16 +140,18 @@ Future<void> init() async {
|
||||
sl.registerFactory<DietsCubit>(() => DietsCubit(feedRepository: sl(), dietsRepository: sl()));
|
||||
sl.registerFactory<CommentsCubit>(() => CommentsCubit(feedRepository: sl(), commentsRepository: sl()));
|
||||
sl.registerFactory<PlansCubit>(() => PlansCubit(feedRepository: sl(), planRepository: sl()));
|
||||
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl()));
|
||||
sl.registerFactory<SettingsCubit>(() => SettingsCubit(profileRepository: sl(), settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl()));
|
||||
sl.registerFactory<RegisterConsultantCubit>(() => RegisterConsultantCubit(consultantRepository: sl()));
|
||||
sl.registerFactory<DiaryCubit>(() => DiaryCubit(
|
||||
chatRepository: sl(),
|
||||
diaryRepository: sl(),
|
||||
glossaryRepository: sl(),
|
||||
profileRepository: sl(),
|
||||
dietRepository: sl(),
|
||||
settingsRepository: 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()));
|
||||
@@ -170,6 +180,9 @@ Future<void> init() async {
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<BlogPostCubit>(() => BlogPostCubit(sl()));
|
||||
sl.registerFactory<BlockedUsersCubit>(() => BlockedUsersCubit(blockedUsersRepository: sl()));
|
||||
sl.registerFactory<NotificationSettingsCubit>(() => NotificationSettingsCubit(notificationSettingsRepository: sl()));
|
||||
sl.registerFactory<ConsultantRatingCubit>(() => ConsultantRatingCubit(sl()));
|
||||
sl.registerFactory<ViewsCubit>(() => ViewsCubit(viewsRepository: sl()));
|
||||
|
||||
// Repository
|
||||
sl.registerLazySingleton<ChatRepository>(() => ChatRepository(chatService: sl()));
|
||||
@@ -201,6 +214,9 @@ Future<void> init() async {
|
||||
sl.registerFactory<ResetPasswordRepository>(() => ResetPasswordRepository(resetPasswordService: sl()));
|
||||
sl.registerLazySingleton<BlockedUsersRepository>(() => BlockedUsersRepository(blockedUserService: sl()));
|
||||
sl.registerLazySingleton<CommentsRepository>(() => CommentsRepository(service: sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsRepository>(() => NotificationSettingsRepository(sl()));
|
||||
sl.registerFactory<ConsultantRatingRepository>(() => ConsultantRatingRepository(sl()));
|
||||
sl.registerFactory<ViewsRepository>(() => ViewsRepository(viewsService: sl()));
|
||||
|
||||
// Services
|
||||
sl.registerLazySingleton<ChatService>(() => ChatService(sl()));
|
||||
@@ -229,28 +245,19 @@ Future<void> init() async {
|
||||
sl.registerLazySingleton<CancelPlanService>(() => CancelPlanService(sl()));
|
||||
sl.registerLazySingleton<CreatePlanService>(() => CreatePlanService(sl()));
|
||||
sl.registerLazySingleton<NotificationsService>(() => NotificationsService(sl()));
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
sl.registerLazySingleton<FirebaseApi>(() => FirebaseApi());
|
||||
sl.registerLazySingleton<ResetPasswordService>(() => ResetPasswordService(sl()));
|
||||
sl.registerLazySingleton<BlockedUserService>(() => BlockedUserService(sl()));
|
||||
sl.registerLazySingleton<CommentsService>(() => CommentsService(sl()));
|
||||
sl.registerLazySingleton<MyRecipesService>(() => MyRecipesService(sl()));
|
||||
sl.registerLazySingleton<ConsultantRatingService>(() => ConsultantRatingService(sl()));
|
||||
sl.registerLazySingleton<NotificationSettingsService>(() => NotificationSettingsService(sl()));
|
||||
sl.registerLazySingleton<ViewsService>(() => ViewsService(sl()));
|
||||
|
||||
// External
|
||||
sl.registerLazySingleton(
|
||||
() => Dio(
|
||||
BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
headers: {
|
||||
'accept': 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
),
|
||||
)..interceptors.addAll(
|
||||
[
|
||||
TokenInterceptor(),
|
||||
PhoneVerifyErrorInterceptor(),
|
||||
],
|
||||
),
|
||||
() =>
|
||||
Dio(BaseOptions(baseUrl: baseUrl, headers: {'accept': 'application/json', 'content-type': 'application/json'}))
|
||||
..interceptors.addAll([TokenInterceptor(), PhoneVerifyErrorInterceptor()]),
|
||||
);
|
||||
sl.registerLazySingleton(() async => const FlutterSecureStorage());
|
||||
}
|
||||
|
||||
@@ -202,6 +202,8 @@ class PrimaryColors {
|
||||
Color get darkGray => const Color(0XFFBBBCC0);
|
||||
|
||||
Color get commentBg => const Color(0XFFFFFDD8);
|
||||
|
||||
Color get cardNotificationsBG => const Color(0xFFA4A4A4);
|
||||
}
|
||||
|
||||
PrimaryColors get appTheme => ThemeHelper().themeColor();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract class TokenStorage {
|
||||
static Future<bool> haveTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'token') == null) {
|
||||
if (sharedPreferences.getString('token') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@@ -13,19 +14,27 @@ abstract class TokenStorage {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> getTokenFromStorage() async => await const FlutterSecureStorage().read(key: 'token');
|
||||
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
await const FlutterSecureStorage().write(key: 'token', value: token);
|
||||
static Future<String?> getTokenFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
return sharedPreferences.getString('token');
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async => (await const FlutterSecureStorage().delete(key: 'token'));
|
||||
static Future<void> saveTokenInStorage(String token) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setString('token', token);
|
||||
}
|
||||
|
||||
static Future<void> removeTokenInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.remove('token');
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SocketIdStorage {
|
||||
static Future<bool> haveSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
try {
|
||||
if (await const FlutterSecureStorage().read(key: 'socketId') == null) {
|
||||
if (sharedPreferences.getString('socketId') == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
@@ -35,11 +44,18 @@ abstract class SocketIdStorage {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String?> getSocketIdFromStorage() async => await const FlutterSecureStorage().read(key: 'socketId');
|
||||
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
await const FlutterSecureStorage().write(key: 'socketId', value: socketId);
|
||||
static Future<String?> getSocketIdFromStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
return sharedPreferences.getString('socketId');
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async => (await const FlutterSecureStorage().delete(key: 'socketId'));
|
||||
static Future<void> saveSocketIdInStorage(String socketId) async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.setString('socketId', socketId);
|
||||
}
|
||||
|
||||
static Future<void> removeSocketIdInStorage() async {
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
await sharedPreferences.remove('socketId');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,30 @@ import 'package:intl/intl.dart';
|
||||
|
||||
const String dateTimeFormatPattern = 'dd/MM/yyyy';
|
||||
|
||||
/// Преобразует дату из формата гггг-мм-дд чч:мм:сс в дд.мм.гггг
|
||||
String formatBuildDate(String dateString) {
|
||||
if (dateString.isEmpty) return '';
|
||||
|
||||
try {
|
||||
final dateTimeParts = dateString.split(' ');
|
||||
if (dateTimeParts.isNotEmpty) {
|
||||
final datePart = dateTimeParts[0];
|
||||
final dateParts = datePart.split('-');
|
||||
if (dateParts.length == 3) {
|
||||
final year = dateParts[0];
|
||||
final month = dateParts[1];
|
||||
final day = dateParts[2];
|
||||
return '$day.$month.$year';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return dateString;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
extension DateTimeExtension on DateTime {
|
||||
String format([
|
||||
String pattern = dateTimeFormatPattern,
|
||||
String? locale,
|
||||
]) {
|
||||
String format([String pattern = dateTimeFormatPattern, String? locale]) {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
if (locale != null && locale.isNotEmpty) {
|
||||
initializeDateFormatting(locale);
|
||||
@@ -19,17 +38,13 @@ extension DateTimeExtension on DateTime {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
final yesterday = DateTime(now.year, now.month, now.day).subtract(const Duration(days: 1));
|
||||
return localDate.year == yesterday.year &&
|
||||
localDate.month == yesterday.month &&
|
||||
localDate.day == yesterday.day;
|
||||
return localDate.year == yesterday.year && localDate.month == yesterday.month && localDate.day == yesterday.day;
|
||||
}
|
||||
|
||||
bool isToday() {
|
||||
final localDate = isUtc ? toLocal() : this;
|
||||
final now = DateTime.now();
|
||||
return now.year == localDate.year &&
|
||||
now.month == localDate.month &&
|
||||
now.day == localDate.day;
|
||||
return now.year == localDate.year && now.month == localDate.month && now.day == localDate.day;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
128
lib/core/views/cubit/views_cubit.dart
Normal file
128
lib/core/views/cubit/views_cubit.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'dart:async';
|
||||
import 'package:eatvio_app/core/views/cubit/views_state.dart';
|
||||
import 'package:eatvio_app/core/views/repository/views_repository.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ViewsCubit extends Cubit<ViewsCubitState> {
|
||||
final ViewsRepository viewsRepository;
|
||||
ViewsCubit({required this.viewsRepository}) : super(ViewsCubitState()) {
|
||||
_listener = AppLifecycleListener(onPause: _sendViews, onInactive: _sendViews);
|
||||
}
|
||||
|
||||
late final AppLifecycleListener _listener;
|
||||
|
||||
final Set<int> postsIds = <int>{};
|
||||
final Set<int> plansIds = <int>{};
|
||||
final Set<int> dietsIds = <int>{};
|
||||
final Set<int> recipesIds = <int>{};
|
||||
|
||||
final Set<int> _sentPosts = <int>{};
|
||||
final Set<int> _sentPlans = <int>{};
|
||||
final Set<int> _sentDiets = <int>{};
|
||||
final Set<int> _sentRecipes = <int>{};
|
||||
|
||||
final Map<ViewableContentType, Map<int, Timer>> _visibilityTimers = {
|
||||
ViewableContentType.posts: <int, Timer>{},
|
||||
ViewableContentType.plans: <int, Timer>{},
|
||||
ViewableContentType.diets: <int, Timer>{},
|
||||
ViewableContentType.recipes: <int, Timer>{},
|
||||
};
|
||||
|
||||
Timer? _debounceTimer;
|
||||
|
||||
Set<int> _idsForType(ViewableContentType type) {
|
||||
switch (type) {
|
||||
case ViewableContentType.posts:
|
||||
return postsIds;
|
||||
case ViewableContentType.plans:
|
||||
return plansIds;
|
||||
case ViewableContentType.diets:
|
||||
return dietsIds;
|
||||
case ViewableContentType.recipes:
|
||||
return recipesIds;
|
||||
}
|
||||
}
|
||||
|
||||
Set<int> _sentIdsForType(ViewableContentType type) {
|
||||
switch (type) {
|
||||
case ViewableContentType.posts:
|
||||
return _sentPosts;
|
||||
case ViewableContentType.plans:
|
||||
return _sentPlans;
|
||||
case ViewableContentType.diets:
|
||||
return _sentDiets;
|
||||
case ViewableContentType.recipes:
|
||||
return _sentRecipes;
|
||||
}
|
||||
}
|
||||
|
||||
void handleVisibility({required double visibleFraction, required int id, required ViewableContentType type}) {
|
||||
final timers = _visibilityTimers[type]!;
|
||||
final ids = _idsForType(type);
|
||||
final sent = _sentIdsForType(type);
|
||||
|
||||
if (sent.contains(id)) {
|
||||
timers[id]?.cancel();
|
||||
timers.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ids.contains(id)) return;
|
||||
|
||||
if (visibleFraction >= 0.5) {
|
||||
if (timers.containsKey(id)) return;
|
||||
timers[id] = Timer(const Duration(seconds: 1), () {
|
||||
ids.add(id);
|
||||
timers.remove(id);
|
||||
_scheduleDebouncedSend();
|
||||
});
|
||||
} else {
|
||||
timers[id]?.cancel();
|
||||
timers.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _viewContent({required ViewableContentType type, required List<int> ids}) async {
|
||||
await viewsRepository.viewContent(
|
||||
viewModel: ViewModel(type: type.getStringifiedVersion(), ids: ids),
|
||||
);
|
||||
}
|
||||
|
||||
void _scheduleDebouncedSend() {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(seconds: 4), _sendViews);
|
||||
}
|
||||
|
||||
void _send(Set<int> ids, Set<int> sent, ViewableContentType type) {
|
||||
final newIds = ids.difference(sent);
|
||||
if (newIds.isNotEmpty) {
|
||||
_viewContent(type: type, ids: newIds.toList());
|
||||
sent.addAll(newIds);
|
||||
ids.removeAll(newIds);
|
||||
}
|
||||
}
|
||||
|
||||
void _sendViews() {
|
||||
_send(postsIds, _sentPosts, ViewableContentType.posts);
|
||||
_send(plansIds, _sentPlans, ViewableContentType.plans);
|
||||
_send(dietsIds, _sentDiets, ViewableContentType.diets);
|
||||
_send(recipesIds, _sentRecipes, ViewableContentType.recipes);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
for (final map in _visibilityTimers.values) {
|
||||
for (final t in map.values) {
|
||||
t.cancel();
|
||||
}
|
||||
map.clear();
|
||||
}
|
||||
_debounceTimer?.cancel();
|
||||
_listener.dispose();
|
||||
_sendViews();
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
3
lib/core/views/cubit/views_state.dart
Normal file
3
lib/core/views/cubit/views_state.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
class ViewsCubitState {
|
||||
ViewsCubitState();
|
||||
}
|
||||
16
lib/core/views/repository/views_repository.dart
Normal file
16
lib/core/views/repository/views_repository.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/core/views/service/views_service.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
|
||||
class ViewsRepository {
|
||||
final ViewsService viewsService;
|
||||
|
||||
ViewsRepository({required this.viewsService});
|
||||
|
||||
Future<void> viewContent({required ViewModel viewModel}) async {
|
||||
try {
|
||||
await viewsService.viewContent(viewModel: viewModel);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
lib/core/views/service/views_service.dart
Normal file
15
lib/core/views/service/views_service.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'views_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class ViewsService {
|
||||
factory ViewsService(Dio dio, {String baseUrl}) = _ViewsService;
|
||||
|
||||
@POST('/api/views')
|
||||
Future<void> viewContent({
|
||||
@Body() required ViewModel viewModel,
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import 'package:eatvio_app/core/data/selectionPopupModel/selection_popup_model.d
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class CustomDropDown extends StatelessWidget {
|
||||
const CustomDropDown({super.key,
|
||||
const CustomDropDown({
|
||||
super.key,
|
||||
this.alignment,
|
||||
this.width,
|
||||
this.focusNode,
|
||||
@@ -48,12 +49,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return alignment != null
|
||||
? Align(
|
||||
alignment: alignment ?? Alignment.center,
|
||||
child: _buildDropDownWidget(context),
|
||||
)
|
||||
: _buildDropDownWidget(context);
|
||||
return alignment != null ? Align(alignment: alignment ?? Alignment.center, child: _buildDropDownWidget(context)) : _buildDropDownWidget(context);
|
||||
}
|
||||
|
||||
DropdownButtonFormField<SelectionPopupModel> _buildDropDownWidget(BuildContext context) {
|
||||
@@ -61,7 +57,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
focusNode: focusNode ?? FocusNode(),
|
||||
icon: const Icon(Icons.keyboard_arrow_down_rounded),
|
||||
autofocus: autofocus!,
|
||||
value: value,
|
||||
initialValue: value,
|
||||
isDense: true,
|
||||
isExpanded: true,
|
||||
items: items?.map((SelectionPopupModel item) {
|
||||
@@ -69,12 +65,7 @@ class CustomDropDown extends StatelessWidget {
|
||||
value: item,
|
||||
child: Text(
|
||||
item.title,
|
||||
style: hintStyle ??
|
||||
const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: hintStyle ?? const TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.black),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
@@ -90,19 +81,9 @@ class CustomDropDown extends StatelessWidget {
|
||||
|
||||
InputDecoration _buildInputDecoration(BuildContext context) {
|
||||
return InputDecoration(
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
labelStyle: const TextStyle(fontSize: 16, color: Colors.black, fontWeight: FontWeight.w400),
|
||||
hintText: hintText ?? "",
|
||||
hintStyle: hintStyle ??
|
||||
TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.grey.shade400,
|
||||
height: 1,
|
||||
),
|
||||
hintStyle: hintStyle ?? TextStyle(fontSize: 16, fontWeight: FontWeight.w400, color: Colors.grey.shade400, height: 1),
|
||||
prefixIcon: prefix,
|
||||
prefixIconConstraints: prefixConstraints,
|
||||
suffixIcon: suffix,
|
||||
@@ -111,18 +92,9 @@ class CustomDropDown extends StatelessWidget {
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12),
|
||||
fillColor: const Color(0xFFF6F6F6),
|
||||
filled: filled,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
border: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
enabledBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
focusedBorder: const OutlineInputBorder(borderRadius: BorderRadius.all(Radius.circular(20)), borderSide: BorderSide.none),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class CustomDropDownButton<T> extends StatelessWidget {
|
||||
const CustomDropDownButton({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.onChanged,
|
||||
this.value,
|
||||
this.hint,
|
||||
this.hasError = false,
|
||||
});
|
||||
const CustomDropDownButton({super.key, required this.items, required this.onChanged, this.value, this.hint, this.hasError = false});
|
||||
final T? value;
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final void Function(T?)? onChanged;
|
||||
@@ -20,11 +13,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonFormField(
|
||||
hint: hint,
|
||||
icon: SvgPicture.asset(
|
||||
'assets/icons/arrow_down2.svg',
|
||||
width: 12,
|
||||
height: 8,
|
||||
),
|
||||
icon: SvgPicture.asset('assets/icons/arrow_down2.svg', width: 12, height: 8),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
dropdownColor: appTheme.lightGray0,
|
||||
decoration: InputDecoration(
|
||||
@@ -43,7 +32,7 @@ class CustomDropDownButton<T> extends StatelessWidget {
|
||||
fillColor: appTheme.lightGray0,
|
||||
filled: true,
|
||||
),
|
||||
value: value,
|
||||
initialValue: value,
|
||||
items: items,
|
||||
onChanged: onChanged,
|
||||
);
|
||||
|
||||
@@ -47,12 +47,20 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
super.initState();
|
||||
var keyboardVisibilityController = KeyboardVisibilityController();
|
||||
keyboardSubscription = keyboardVisibilityController.onChange.listen((bool visible) {
|
||||
setState(() {
|
||||
isFocused = visible;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isFocused = visible;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
keyboardSubscription.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _reconnectWebSocket(BuildContext context) async {
|
||||
try {
|
||||
await sl<WsManager>().clear();
|
||||
@@ -73,10 +81,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
AppRouter.navigatorKey.currentContext?.go(
|
||||
'/feed-screen',
|
||||
extra: {'needToInitializeWebSocket': false},
|
||||
);
|
||||
AppRouter.navigatorKey.currentContext?.go('/feed-screen', extra: {'needToInitializeWebSocket': false});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -102,11 +107,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
GoRouter.of(context).pushNamed(DevToolsScreen.name);
|
||||
}
|
||||
},
|
||||
child: SvgPicture.asset(
|
||||
ImageConstant.imgLogoEatvio,
|
||||
height: isFocused ? 64 : MediaQuery.of(context).size.width - 64,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
child: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: isFocused ? 64 : MediaQuery.of(context).size.width - 64, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
@@ -137,10 +138,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
hidePassword = !hidePassword;
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
|
||||
color: Colors.grey,
|
||||
),
|
||||
icon: Icon(hidePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -149,11 +147,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
onTap: () => GoRouter.of(context).goNamed('forgetPasswordScreen', extra: false),
|
||||
child: Text(
|
||||
'Забыли пароль?',
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -166,11 +160,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
onTap: () => GoRouter.of(context).goNamed('registrationScreen'),
|
||||
child: Text(
|
||||
'Зарегистрироваться',
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
style: TextStyle(color: appTheme.blueA400, decorationColor: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -181,23 +171,22 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
text: 'Нажимая "Войти" или "Зарегистрироваться", Вы соглашаетесь \n',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.black),
|
||||
children: [
|
||||
const TextSpan(text: 'c '),
|
||||
TextSpan(
|
||||
text: ' Публичной офертой',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
if (!hidePlans) ...[
|
||||
TextSpan(
|
||||
text: ' Публичной офертой',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appTheme.blueA400,
|
||||
decorationColor: appTheme.blueA400,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
recognizer: TapGestureRecognizer()..onTap = () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
),
|
||||
const TextSpan(text: ' и '),
|
||||
const TextSpan(text: ' и '),
|
||||
],
|
||||
TextSpan(
|
||||
text: 'Политикой конфиденциальности\nи защитой персональных данных',
|
||||
style: TextStyle(
|
||||
@@ -223,17 +212,12 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||
onPressed: state is AuthStateLoading
|
||||
? null
|
||||
: () async {
|
||||
final isAuthenticated = await authCubit.getTokenByCredentials(
|
||||
emailController.text,
|
||||
passwordController.text,
|
||||
);
|
||||
final isAuthenticated = await authCubit.getTokenByCredentials(emailController.text, passwordController.text);
|
||||
if (isAuthenticated) {
|
||||
if (context.mounted) {
|
||||
_reconnectWebSocket(context);
|
||||
@@ -242,10 +226,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: Text(
|
||||
'Войти',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
: Text('Войти', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -257,9 +238,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
backgroundColor: state is AuthStateLoading ? Colors.black.withAlpha(50) : appTheme.blueA400,
|
||||
onPressed: state is AuthStateLoading
|
||||
? null
|
||||
@@ -274,14 +253,10 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Где взять QR-код?\n',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const Text('Где взять QR-код?\n', style: TextStyle(fontSize: 18)),
|
||||
Text(
|
||||
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"'),
|
||||
'1. Войдите на сайте $baseUrl в свой аккаунт\n\n2. Перейдите в настройки профиля\n\n3. В разделе "Доп информация" перейдите на "QR Login"',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
@@ -293,15 +268,10 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: const Text(
|
||||
'Отмена',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w400),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -314,15 +284,10 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
child: Ink(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(
|
||||
'Перейти',
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: appTheme.blueA400, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -337,10 +302,7 @@ class _AuthScreenState extends State<AuthScreen> {
|
||||
},
|
||||
label: state is AuthStateLoading
|
||||
? const CupertinoActivityIndicator()
|
||||
: const Icon(
|
||||
Icons.qr_code_scanner_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
: const Icon(Icons.qr_code_scanner_rounded, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/firebase_api.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/utils/toasts.dart';
|
||||
@@ -92,6 +93,7 @@ class _ScanQRCodeScreenState extends State<ScanQRCodeScreen> {
|
||||
await sl<ChatsCubit>().connectToWebSocket();
|
||||
await sl<NotificationsCubit>().getNotifications();
|
||||
await sl<NotificationsCubit>().connectToWebSocket();
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,55 @@
|
||||
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart';
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class ProfileAvatarCubit extends Cubit<ProfileAvatarState> {
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
ProfileAvatarCubit({
|
||||
required this.profileRepository,
|
||||
}) : super(const ProfileAvatarState.loading());
|
||||
ProfileAvatarCubit({required this.profileRepository}) : super(const ProfileAvatarState.loading());
|
||||
|
||||
int _calculateAge(String birthdate) {
|
||||
try {
|
||||
final dateParts = birthdate.split('-');
|
||||
if (dateParts.length == 3) {
|
||||
final birthYear = int.parse(dateParts[0]);
|
||||
final birthMonth = int.parse(dateParts[1]);
|
||||
final birthDay = int.parse(dateParts[2]);
|
||||
|
||||
final now = DateTime.now();
|
||||
int age = now.year - birthYear;
|
||||
|
||||
if (now.month < birthMonth || (now.month == birthMonth && now.day < birthDay)) {
|
||||
age--;
|
||||
}
|
||||
|
||||
return age;
|
||||
}
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Future<void> getMyProfile() async {
|
||||
emit(const ProfileAvatarState.loading());
|
||||
final profile = await profileRepository.getMyProfile();
|
||||
final profileDetails = await profileRepository.getMyProfileDetails();
|
||||
try {
|
||||
emit(
|
||||
ProfileAvatarState.loaded(
|
||||
profile: await profileRepository.getMyProfile(),
|
||||
),
|
||||
);
|
||||
emit(ProfileAvatarState.loaded(profile: profile));
|
||||
final notificationStatus = await Permission.notification.status;
|
||||
final isNotificationEnabled = notificationStatus.isGranted;
|
||||
AppMetricaUserProfile userProfile = AppMetricaUserProfile([
|
||||
AppMetricaNameAttribute.withValue(profile.name),
|
||||
AppMetricaGenderAttribute.withValue(profileDetails.gender == 'male' ? AppMetricaGender.male : AppMetricaGender.female),
|
||||
AppMetricaBirthDateAttribute.withAge(_calculateAge(profileDetails.birthdate)),
|
||||
AppMetricaNotificationEnabledAttribute.withValue(isNotificationEnabled),
|
||||
]);
|
||||
AppMetrica.setUserProfileID(profile.slug);
|
||||
AppMetrica.reportUserProfile(userProfile);
|
||||
} catch (e) {
|
||||
AppRouter.navigatorKey.currentContext?.go('/auth-screen');
|
||||
emit(ProfileAvatarState.error(errorMessage: e.toString()));
|
||||
|
||||
@@ -13,6 +13,8 @@ import 'package:eatvio_app/features/chat/presentation/pages/photo_view_screen.da
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/about_order_button.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_bubble_widget.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/notification_banner.dart';
|
||||
@@ -446,7 +448,27 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
const SizedBox(width: 8),
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
// Проверяем, заблокирован ли пользователь
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == state.chatRoom.users.receiver.slug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (textEditingController.text.isNotEmpty || pickedFiles.isNotEmpty) {
|
||||
chatRoomCubit.sendMessage(
|
||||
data: textEditingController.text.isEmpty ? null : textEditingController.text.trim(),
|
||||
|
||||
@@ -12,6 +12,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/blocked_user_cubit/blocked_users_state.dart';
|
||||
|
||||
class EmptyChatScreen extends StatefulWidget {
|
||||
final Author author;
|
||||
@@ -133,21 +135,45 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () async {
|
||||
// Проверяем, заблокирован ли пользователь
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (textEditingController.text.isNotEmpty) {
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
data: textEditingController.text.trim(),
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
textEditingController.clear();
|
||||
});
|
||||
}
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (context.mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Ink(
|
||||
@@ -175,20 +201,44 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
CupertinoActionSheetAction(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Проверяем, заблокирован ли пользователь
|
||||
final blockedUsersCubit = sl<BlockedUsersCubit>();
|
||||
await blockedUsersCubit.getBlockedUsers();
|
||||
final blockedState = blockedUsersCubit.state;
|
||||
|
||||
if (blockedState is BlockedUsersStateLoaded) {
|
||||
final isBlocked = blockedState.profilesList.any((profile) => profile.slug == widget.author.authorSlug);
|
||||
if (isBlocked) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Вы не можете отправить сообщение заблокированному пользователю'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final picker = ImagePicker();
|
||||
XFile? image = await picker.pickImage(source: ImageSource.camera);
|
||||
if (image != null) {
|
||||
List<XFile>? images = await picker.pickMultiImage();
|
||||
if (images.isNotEmpty) {
|
||||
List<File> files = images.map((image) => File(image.path)).toList();
|
||||
await sl<ChatRepository>().sendMessage(
|
||||
files: [File(image.path)],
|
||||
files: files,
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
@@ -211,12 +261,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
@@ -261,12 +313,14 @@ class _EmptyChatScreenState extends State<EmptyChatScreen> {
|
||||
slug: widget.author.authorSlug,
|
||||
);
|
||||
final chatIdOrError = await sl<ChatRepository>().getChatByUserSlug(widget.author.authorSlug);
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
if (mounted) {
|
||||
AppRouter.navigatorKey.currentContext?.pushReplacementNamed(
|
||||
'chatScreen',
|
||||
extra: {
|
||||
'uuid': chatIdOrError,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Row(
|
||||
|
||||
@@ -25,24 +25,16 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Последний шаг',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
child: Text('Последний шаг', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile(
|
||||
value: checkboxState,
|
||||
title: const Text(
|
||||
'Я ознакомлен и принимаю договор публичной офёрты',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
),
|
||||
title: Text(
|
||||
hidePlans
|
||||
? 'Я ознакомлен и принимаю политику конфиденциальности и защиту персональных данных'
|
||||
: 'Я ознакомлен и принимаю договор публичной офёрты',
|
||||
style: const TextStyle(fontSize: 18, height: 1),
|
||||
),
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
@@ -53,25 +45,14 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
||||
activeColor: appTheme.blueA400,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: _divider(context),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: TextButton(
|
||||
onPressed: () async => await launchUrl(Uri.parse('$baseUrl/oferta_bs.pdf')),
|
||||
child: const Text(
|
||||
'Договор офёрты',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
fontSize: 16,
|
||||
color: Color(0xFF323F4B),
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
onPressed: () async => await launchUrl(Uri.parse(hidePlans ? '$baseUrl/docs/privacy.pdf' : '$baseUrl/oferta_bs.pdf')),
|
||||
child: Text(
|
||||
hidePlans ? 'Политика конфиденциальности и пользовательское соглашение' : 'Договор офёрты',
|
||||
style: const TextStyle(height: 1, fontSize: 16, color: Color(0xFF323F4B), decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -84,10 +65,7 @@ class _LastStepScreenState extends State<LastStepScreen> {
|
||||
return Container(
|
||||
height: 0.4,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,81 +23,105 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'Регистрация в качестве',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
child: Text('Регистрация в качестве', style: TextStyle(fontSize: 24, fontWeight: FontWeight.w700, height: 1.1)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.physical,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Физического лица',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.physical,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Физического лица', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
activeColor: appTheme.blueA400,
|
||||
onChanged: (value) {},
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.selfBusy,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Самозанятого',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.selfBusy,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Самозанятого', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.iP,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Индивидуального предпринимателя',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.iP,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Индивидуального предпринимателя', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
RadioListTile<RegistrationAsType>(
|
||||
value: RegistrationAsType.ooo,
|
||||
groupValue: registrationAsType,
|
||||
title: const Text(
|
||||
'Общества с ограниченной ответственностью',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
Row(
|
||||
children: [
|
||||
Radio<RegistrationAsType>(
|
||||
value: RegistrationAsType.ooo,
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: registrationAsType,
|
||||
activeColor: appTheme.blueA400,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
registrationAsType = value!;
|
||||
});
|
||||
},
|
||||
),
|
||||
const Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text('Общества с ограниченной ответственностью', style: TextStyle(fontSize: 18, height: 1)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onChanged: null,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
child: _divider(context),
|
||||
],
|
||||
),
|
||||
Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), child: _divider(context)),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
'* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся',
|
||||
),
|
||||
child: Text('* На данный момент регистрация доступна только в качестве физического лица. Остальные варианты скоро появятся'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -108,10 +132,7 @@ class _RegistrationAsScreenState extends State<RegistrationAsScreen> {
|
||||
return Container(
|
||||
height: 0.4,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,42 @@
|
||||
import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'rating_model.freezed.dart';
|
||||
part 'rating_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
abstract class RatingPaginationModel with _$RatingPaginationModel {
|
||||
factory RatingPaginationModel({
|
||||
required List<RatingModel> data,
|
||||
}) = _RatingPaginationModel;
|
||||
|
||||
factory RatingPaginationModel.fromJson(Map<String, dynamic> json) => _$RatingPaginationModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class RatingModel with _$RatingModel {
|
||||
factory RatingModel({
|
||||
required DateTime date,
|
||||
required String title,
|
||||
required int amount,
|
||||
@JsonKey(name: 'created_at', fromJson: parseDate) required DateTime date,
|
||||
@JsonKey(name: 'log_data') required RatingEnum type,
|
||||
@JsonKey(name: 'log_scores') required int amount,
|
||||
}) = _RatingModel;
|
||||
|
||||
factory RatingModel.fromJson(Map<String, dynamic> json) => _$RatingModelFromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
enum RatingEnum {
|
||||
@JsonValue('ADDED_RECIPE')
|
||||
addedRecipe,
|
||||
@JsonValue('ADDED_PUBLIC_DIET')
|
||||
addedPublicDiet,
|
||||
@JsonValue('ADDED_BLOG_POST')
|
||||
addedBlogPost,
|
||||
@JsonValue('INVITED_A_NEW_USER')
|
||||
invitedUser,
|
||||
@JsonValue('HAS_MEDICAL_DEGREE')
|
||||
hasMedicalDegree,
|
||||
@JsonValue('CONSULTED')
|
||||
consulted,
|
||||
@JsonValue('PRODUCT_NUTRITINAL_VALUE_APPROVED')
|
||||
addedGlossary;
|
||||
}
|
||||
|
||||
19
lib/features/consultant_rating/models/statistics_model.dart
Normal file
19
lib/features/consultant_rating/models/statistics_model.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'statistics_model.freezed.dart';
|
||||
part 'statistics_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class StatisticsModel with _$StatisticsModel {
|
||||
|
||||
factory StatisticsModel({
|
||||
@JsonKey(name: 'count_referal') int? countReferal,
|
||||
@JsonKey(name: 'count_blog_posts') int? blogPostsCount,
|
||||
@JsonKey(name: 'count_recipes') int? recipesCount,
|
||||
@JsonKey(name: 'count_diets') int? dietsCount,
|
||||
@JsonKey(name: 'count_consultants') int? consultationsCount,
|
||||
@JsonKey(name: 'count_products_nutritional_values') int? qwe,
|
||||
}) = _StatisticsModel;
|
||||
|
||||
factory StatisticsModel.fromJson(Map<String, dynamic> json) => _$StatisticsModelFromJson(json);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_history_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/rating_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_widget.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingScreen extends StatelessWidget {
|
||||
static const name = 'consultantRatingScreen';
|
||||
@@ -15,7 +19,10 @@ class ConsultantRatingScreen extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const ConsultantRatingPageContent();
|
||||
return BlocProvider<ConsultantRatingCubit>(
|
||||
create: (context) => sl<ConsultantRatingCubit>()..getConsultantStatistics()..getScoreLogs(),
|
||||
child: const ConsultantRatingPageContent(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,52 +32,56 @@ class ConsultantRatingPageContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const RatingWidget(
|
||||
rating: 1058,
|
||||
return BlocBuilder<ConsultantRatingCubit, ConsultantRatingState>(
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
backgroundColor: appTheme.gray100,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
s.rating_and_statistics,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.copy_referral_link,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
body: state.error != null
|
||||
? DataErrorWidget(onReload: () async {
|
||||
await context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
if (!context.mounted) return;
|
||||
await context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
})
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
const RatingWidget(
|
||||
rating: 1058,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: CustomOutlinedButton(
|
||||
text: s.copy_referral_link,
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.blueA400),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
StatisticWidget(isLoading: state.isStatisticsLoading, statistics: state.statistics,),
|
||||
const SizedBox(height: 16),
|
||||
RatingHistoryWidget(
|
||||
isLoading: state.isRatingLoading,
|
||||
ratingHistory: state.ratings,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const StatisticWidget(),
|
||||
const SizedBox(height: 16),
|
||||
RatingHistoryWidget(
|
||||
ratingHistory: [
|
||||
RatingModel(date: DateTime.now(), title: ' djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 201300),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 89323),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 20),
|
||||
RatingModel(date: DateTime.now(), title: 'djkasjdasjkdkjasjkdjkaskjdjklasjkld', amount: 1212),
|
||||
RatingModel(date: DateTime.now(), title: 'qweqweeq', amount: 323),
|
||||
RatingModel(date: DateTime.now(), title: 'ee', amount: 1),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_state.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/repositories/consultant_rating_repository.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ConsultantRatingCubit extends Cubit<ConsultantRatingState> {
|
||||
final ConsultantRatingRepository repository;
|
||||
ConsultantRatingCubit(this.repository) : super(ConsultantRatingState());
|
||||
|
||||
Future<void> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isStatisticsLoading: true));
|
||||
final statistics = await repository.getConsultantStatistics(date: date);
|
||||
emit(state.copyWith(isStatisticsLoading: false, statistics: statistics, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isStatisticsLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
emit(state.copyWith(isRatingLoading: true));
|
||||
final scoreLogs = await repository.getScoreLogs(date: date);
|
||||
emit(state.copyWith(isRatingLoading: false, ratings: scoreLogs, error: null));
|
||||
} catch (e, st) {
|
||||
emit(state.copyWith(isRatingLoading: true, error: e.toString()));
|
||||
log(e.toString(), stackTrace: st);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'consultant_rating_state.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ConsultantRatingState with _$ConsultantRatingState {
|
||||
factory ConsultantRatingState({
|
||||
@Default(false) bool isStatisticsLoading,
|
||||
@Default(false) bool isRatingLoading,
|
||||
@Default(null) StatisticsModel? statistics,
|
||||
@Default(null) String? error,
|
||||
@Default(<RatingModel>[]) List<RatingModel> ratings,
|
||||
}) = _ConsultantRatingState;
|
||||
}
|
||||
@@ -1,42 +1,29 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/cubit/consultant_rating_cubit.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PeriodFilterWidget extends StatefulWidget {
|
||||
const PeriodFilterWidget({super.key});
|
||||
|
||||
const PeriodFilterWidget({super.key, required this.isStatistic});
|
||||
final bool isStatistic;
|
||||
@override
|
||||
State<PeriodFilterWidget> createState() => _PeriodFilterWidgetState();
|
||||
}
|
||||
|
||||
class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
final List<String> periodDropDownTitles = [];
|
||||
int periodDropDownIndex = 0;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final s = S.of(context);
|
||||
if (periodDropDownTitles.isEmpty) {
|
||||
periodDropDownTitles.addAll([
|
||||
s.for_whole_period_of_time,
|
||||
s.for_a_year,
|
||||
s.for_a_month,
|
||||
s.for_a_week,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
PeriodFilterStatus _currentState = PeriodFilterStatus.all;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomDropDownButton(
|
||||
CustomDropDownButton<PeriodFilterStatus>(
|
||||
hint: Text(
|
||||
s.choose_diet_duration,
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
@@ -44,24 +31,59 @@ class _PeriodFilterWidgetState extends State<PeriodFilterWidget> {
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
periodDropDownIndex = periodDropDownTitles.indexOf(value);
|
||||
_currentState = value;
|
||||
});
|
||||
if (widget.isStatistic) {
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getConsultantStatistics(date: value.value);
|
||||
return;
|
||||
}
|
||||
if (value == PeriodFilterStatus.all) {
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs();
|
||||
return;
|
||||
}
|
||||
context.read<ConsultantRatingCubit>().getScoreLogs(date: value.value);
|
||||
return;
|
||||
}
|
||||
},
|
||||
items: periodDropDownTitles
|
||||
items: PeriodFilterStatus.values
|
||||
.mapIndexed(
|
||||
(index, value) => DropdownMenuItem(
|
||||
value: periodDropDownTitles[index],
|
||||
value: PeriodFilterStatus.values[index],
|
||||
child: Text(
|
||||
periodDropDownTitles[index],
|
||||
PeriodFilterStatus.values[index].name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
value: periodDropDownTitles[periodDropDownIndex],
|
||||
value: _currentState,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum PeriodFilterStatus {
|
||||
all,
|
||||
month,
|
||||
week,
|
||||
year;
|
||||
|
||||
String get name => switch (this) {
|
||||
PeriodFilterStatus.all => 'Все периоды',
|
||||
PeriodFilterStatus.month => 'За месяц',
|
||||
PeriodFilterStatus.week => 'За неделю',
|
||||
PeriodFilterStatus.year => 'За год',
|
||||
};
|
||||
|
||||
String? get value => switch (this) {
|
||||
PeriodFilterStatus.all => null,
|
||||
PeriodFilterStatus.month => DateTime.now().subtract(const Duration(days: 30)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.week => DateTime.now().subtract(const Duration(days: 7)).format('yyyy-MM-dd'),
|
||||
PeriodFilterStatus.year => DateTime.now().subtract(const Duration(days: 365)).format('yyyy-MM-dd'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.gray100,
|
||||
@@ -29,7 +28,7 @@ class RatingHistoryItem extends StatelessWidget {
|
||||
date.format('dd.MM.yyyy'),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray),
|
||||
),
|
||||
const Spacer(),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
@@ -9,7 +10,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class RatingHistoryWidget extends StatelessWidget {
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory});
|
||||
const RatingHistoryWidget({super.key, required this.ratingHistory, required this.isLoading});
|
||||
final bool isLoading;
|
||||
final List<RatingModel> ratingHistory;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -20,50 +22,60 @@ class RatingHistoryWidget extends StatelessWidget {
|
||||
right: 16,
|
||||
top: 16,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
child: isLoading
|
||||
? const Center(child: Loader())
|
||||
: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
s.rating_history,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
const Spacer(),
|
||||
SvgPicture.asset(
|
||||
'assets/icons/question_mark.svg',
|
||||
height: 22,
|
||||
width: 22,
|
||||
colorFilter: ColorFilter.mode(
|
||||
appTheme.blueA400,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: ratingHistory[index].title,
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
const PeriodFilterWidget(isStatistic: false),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
ListView.builder(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: index == ratingHistory.length - 1 ? EdgeInsets.zero : const EdgeInsets.only(bottom: 12),
|
||||
child: RatingHistoryItem(
|
||||
title: switch (ratingHistory[index].type) {
|
||||
RatingEnum.addedRecipe => 'Добавление рецепта',
|
||||
RatingEnum.addedPublicDiet => 'Добавление публичной диеты',
|
||||
RatingEnum.addedBlogPost => 'Добавление поста',
|
||||
RatingEnum.invitedUser => 'Регистрация по реф. ссылке',
|
||||
RatingEnum.hasMedicalDegree => 'Получение мед. степени',
|
||||
RatingEnum.consulted => 'Консультация',
|
||||
RatingEnum.addedGlossary => 'Добавление продукта в глоссарий',
|
||||
},
|
||||
amount: ratingHistory[index].amount,
|
||||
date: ratingHistory[index].date,
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: ratingHistory.length,
|
||||
shrinkWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/core/widgets/main_background_bouble.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/period_filter_widget.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/widgets/statistic_bouble.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
@@ -8,14 +10,15 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class StatisticWidget extends StatelessWidget {
|
||||
const StatisticWidget({super.key});
|
||||
|
||||
const StatisticWidget({super.key, required this.isLoading, required this.statistics});
|
||||
final bool isLoading;
|
||||
final StatisticsModel? statistics;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
return MainBackgroundBouble(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
child: isLoading ? const Center(child: Loader()) : Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
@@ -38,15 +41,17 @@ class StatisticWidget extends StatelessWidget {
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
const PeriodFilterWidget(),
|
||||
const PeriodFilterWidget(
|
||||
isStatistic: true,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 12,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.invited_people_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.countReferal ?? 0, title: s.invited_people_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_posts_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.blogPostsCount ?? 0, title: s.published_posts_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -54,9 +59,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_recipes_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.recipesCount ?? 0, title: s.published_recipes_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.published_diets_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.dietsCount ?? 0, title: s.published_diets_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -64,9 +69,9 @@ class StatisticWidget extends StatelessWidget {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.consultationsCount ?? 0, title: s.consultations_count)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: StatisticBouble(number: 12, title: s.active_consultations_count)),
|
||||
Expanded(child: StatisticBouble(number: statistics?.qwe ?? 0, title: s.active_consultations_count)),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/services/consultant_rating_service.dart';
|
||||
|
||||
class ConsultantRatingRepository {
|
||||
final ConsultantRatingService service;
|
||||
|
||||
ConsultantRatingRepository(this.service);
|
||||
|
||||
Future<StatisticsModel> getConsultantStatistics({String? date}) async {
|
||||
try {
|
||||
return await service.getConsultantStatistics(date: date);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<RatingModel>> getScoreLogs({String? date}) async {
|
||||
try {
|
||||
final data = (await service.getScoreLogs(date: date)).data;
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/rating_model.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/models/statistics_model.dart';
|
||||
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'consultant_rating_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class ConsultantRatingService {
|
||||
factory ConsultantRatingService(Dio dio, {String baseUrl}) = _ConsultantRatingService;
|
||||
|
||||
@GET('/api/consultants/rating/statistics')
|
||||
Future<StatisticsModel> getConsultantStatistics({@Query('filters[duration][\$gte]') String? date});
|
||||
|
||||
@GET('/api/consultants/score/logs')
|
||||
Future<RatingPaginationModel> getScoreLogs({@Query('filters[duration][\$gte]') String? date});
|
||||
}
|
||||
@@ -50,6 +50,7 @@ class ConsultationService {
|
||||
else if (state != null && stateComparison != null)
|
||||
'filters[last_state][${stateComparison.value}]': state,
|
||||
if (dateComparison != null && date != null) 'filters[start_date][${dateComparison.value}]': date,
|
||||
'sort[]': 'created_at:desc',
|
||||
},
|
||||
);
|
||||
return ConsultationPaginationModel.fromJson(response.data);
|
||||
|
||||
@@ -26,10 +26,7 @@ class DietPreviewScreen extends StatefulWidget {
|
||||
late final List<int> selectedIndexes;
|
||||
static const name = 'createDietScreenPreview';
|
||||
static const path = '/create-diet-screen/preview';
|
||||
DietPreviewScreen({
|
||||
super.key,
|
||||
required this.diet,
|
||||
}) {
|
||||
DietPreviewScreen({super.key, required this.diet}) {
|
||||
selectedIndexes = List.generate(diet.dailySchedule.length, (_) => 0);
|
||||
}
|
||||
|
||||
@@ -65,11 +62,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 12) + (Platform.isIOS ? const EdgeInsets.only(bottom: 16) : EdgeInsets.zero),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
),
|
||||
),
|
||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.1))),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -79,34 +72,16 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
child: FloatingActionButton.extended(
|
||||
heroTag: 'nextButtonRegistration',
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
backgroundColor: appTheme.greenA700,
|
||||
onPressed: () {},
|
||||
label: Text(
|
||||
s.startDiet,
|
||||
style: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
),
|
||||
),
|
||||
child: BookmarkButton(
|
||||
isFavorite: widget.diet.isFavorite,
|
||||
onTap: () {},
|
||||
width: 52,
|
||||
height: 52,
|
||||
size: 24,
|
||||
shape: BoxShape.circle,
|
||||
color: appTheme.gray50,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
BookmarkButton(
|
||||
isFavorite: widget.diet.isFavorite,
|
||||
onTap: () {},
|
||||
width: 52,
|
||||
height: 52,
|
||||
size: 24,
|
||||
shape: BoxShape.circle,
|
||||
color: appTheme.gray50,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -133,24 +108,15 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
filled: true,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFD1D1D6),
|
||||
width: 1,
|
||||
),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFD1D1D6),
|
||||
width: 1,
|
||||
),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFD1D1D6),
|
||||
width: 1,
|
||||
),
|
||||
borderSide: const BorderSide(color: Color(0xFFD1D1D6), width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -160,17 +126,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
decoration: BoxDecoration(color: textEditingController.text.length > 2 ? appTheme.blueA400 : appTheme.gray50, shape: BoxShape.circle),
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: Icon(
|
||||
Icons.send_rounded,
|
||||
size: 18,
|
||||
color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black,
|
||||
),
|
||||
child: Icon(Icons.send_rounded, size: 18, color: textEditingController.text.length > 2 ? appTheme.whiteA700 : appTheme.black),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -212,12 +171,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Image.memory(
|
||||
imageData,
|
||||
height: 300,
|
||||
width: MediaQuery.sizeOf(context).width,
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
Image.memory(imageData, height: 300, width: MediaQuery.sizeOf(context).width, fit: BoxFit.fill),
|
||||
// CachedNetworkImage(
|
||||
// imageUrl: baseUrl + '/storage/' + widget.diet.image,
|
||||
// ),
|
||||
@@ -233,10 +187,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/eye.svg', width: 18, height: 12),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.diet.totalViews.toString(),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(widget.diet.totalViews.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
@@ -252,10 +203,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
colorFilter: widget.diet.wasLikedByMe ? ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn) : null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.diet.totalLikes.toString(),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(widget.diet.totalLikes.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -265,10 +213,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
children: [
|
||||
SvgPicture.asset('assets/icons/comment2.svg', width: 13, height: 12),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
widget.diet.totalComments.toString(),
|
||||
style: TextStyles.textXSSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text(widget.diet.totalComments.toString(), style: TextStyles.textXSSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -279,23 +224,15 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Text(
|
||||
s.about_diet,
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(s.about_diet, style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InfoRow(diet: widget.diet, type: ProductType.diet,),
|
||||
ExpandableText(
|
||||
text: widget.diet.details,
|
||||
),
|
||||
InfoRow(diet: widget.diet, type: ProductType.diet),
|
||||
ExpandableText(text: widget.diet.details),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: AuthorCard(
|
||||
author: widget.diet.author,
|
||||
title: s.diet_author,
|
||||
),
|
||||
child: AuthorCard(author: widget.diet.author, title: s.diet_author),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
@@ -308,9 +245,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${s.diet_plan}(${widget.diet.duration.toString()} ${getDayForm(widget.diet.duration, s)})',
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -320,7 +255,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return _dayOfPlan(widget.diet.dailySchedule[index] ?? {}, index, s);
|
||||
return _dayOfPlan(widget.diet.dailySchedule[index + 1] ?? {}, index, s);
|
||||
},
|
||||
itemCount: widget.diet.dailySchedule.length,
|
||||
),
|
||||
@@ -331,12 +266,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'0 ${getCommentForm(widget.diet.totalComments, s)}',
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.gray500,
|
||||
),
|
||||
),
|
||||
child: Text('0 ${getCommentForm(widget.diet.totalComments, s)}', style: TextStyles.textMMedium.copyWith(color: appTheme.gray500)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
@@ -355,14 +285,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
pressElevation: 0,
|
||||
showCheckmark: false,
|
||||
side: BorderSide.none,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
label: Text((index + 1).toString()),
|
||||
labelStyle: TextStyles.textXSSemibold.copyWith(
|
||||
color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600,
|
||||
),
|
||||
labelStyle: TextStyles.textXSSemibold.copyWith(color: index == widget.selectedIndexes[dayNumber] ? Colors.white : appTheme.gray600),
|
||||
selected: widget.selectedIndexes[dayNumber] == index,
|
||||
backgroundColor: appTheme.gray100,
|
||||
onSelected: (bool selected) {
|
||||
@@ -376,9 +302,7 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: MainBackgroundBouble(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
@@ -386,31 +310,12 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${s.day} ${dayNumber + 1}',
|
||||
style: TextStyles.headingXLSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: CustomDivider(),
|
||||
),
|
||||
Text(
|
||||
s.meal,
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
Text('${s.day} ${dayNumber + 1}', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const Padding(padding: EdgeInsets.symmetric(vertical: 12), child: CustomDivider()),
|
||||
Text(s.meal, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
choiceChipWidget(0),
|
||||
choiceChipWidget(1),
|
||||
choiceChipWidget(2),
|
||||
choiceChipWidget(3),
|
||||
choiceChipWidget(4),
|
||||
],
|
||||
children: [choiceChipWidget(0), choiceChipWidget(1), choiceChipWidget(2), choiceChipWidget(3), choiceChipWidget(4)],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ListView.builder(
|
||||
@@ -443,20 +348,10 @@ class _DietPreviewScreenState extends State<DietPreviewScreen> {
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.sizeOf(context).width * 0.6,
|
||||
child: Text(
|
||||
daily.name,
|
||||
style: TextStyles.textMMedium.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
child: Text(daily.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
const Spacer(),
|
||||
Text(
|
||||
'${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}',
|
||||
style: TextStyles.textMSemibold.copyWith(
|
||||
color: appTheme.black,
|
||||
),
|
||||
),
|
||||
Text('${daily.quantity} ${getLocalizedMeasurementUnits(s, daily.portion)}', style: TextStyles.textMSemibold.copyWith(color: appTheme.black)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -46,21 +46,14 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16) +
|
||||
EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Добавление блюда',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
@@ -73,21 +66,17 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
autofocus: true,
|
||||
hintText: 'Введите название продукта/блюда',
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'glossary-search',
|
||||
const Duration(milliseconds: 250),
|
||||
() async {
|
||||
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
selectedGlossary = null;
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.createDietCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -100,22 +89,20 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
hintText: selectedGlossary == null
|
||||
? '100'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? '100'
|
||||
: '1',
|
||||
? '100'
|
||||
: '1',
|
||||
suffixText: selectedGlossary == null
|
||||
? 'гр'
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 256,
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
@@ -130,28 +117,12 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
setState(() {});
|
||||
FocusManager.instance.primaryFocus?.nextFocus();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
title: Text(
|
||||
glossariesList[i].name,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
|
||||
subtitle: glossariesList[i].grInPortion == 0
|
||||
? null
|
||||
: Text(
|
||||
'${glossariesList[i].grInPortion} г',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.add_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
|
||||
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
);
|
||||
},
|
||||
@@ -166,25 +137,15 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
if (widget.portionIndex != null)
|
||||
InkWell(
|
||||
onTap: () {
|
||||
widget.createDietCubit.removePortion(
|
||||
day: widget.dayIndex,
|
||||
meal: widget.mealIndex,
|
||||
portionIndex: widget.portionIndex!,
|
||||
);
|
||||
widget.createDietCubit.removePortion(day: widget.dayIndex, meal: widget.mealIndex, portionIndex: widget.portionIndex!);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Ink(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0xFFF6F6F6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
|
||||
),
|
||||
),
|
||||
if (widget.portionIndex != null) const SizedBox(width: 8),
|
||||
@@ -194,18 +155,11 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - (widget.portionIndex == null ? 32 : 92),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.portionIndex == null ? 'Добавить' : 'Сохранить',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -222,8 +176,8 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
var measurementUnits = selectedGlossary?.grInPortion == null
|
||||
? Portion.pcs
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? Portion.grams
|
||||
: Portion.pcs;
|
||||
? Portion.grams
|
||||
: Portion.pcs;
|
||||
final glossary = selectedGlossary;
|
||||
if (glossary != null) {
|
||||
final [calories, carbohydrates, proteins, fats] = _getCPFC(glossary, measurementUnits);
|
||||
@@ -235,19 +189,11 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
portion: measurementUnits,
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
widget.createDietCubit.addPortion(
|
||||
dayIndex: widget.dayIndex,
|
||||
mealIndex: widget.mealIndex,
|
||||
portion: portion,
|
||||
);
|
||||
widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion);
|
||||
} else {
|
||||
widget.createDietCubit.updatePortion(
|
||||
dayIndex: widget.dayIndex,
|
||||
mealIndex: widget.mealIndex,
|
||||
portionIndex: portionIndex,
|
||||
portion: portion,
|
||||
);
|
||||
widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portionIndex: portionIndex, portion: portion);
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
final newGlossary = await showModalBottomSheet<GlossaryModel>(
|
||||
@@ -255,13 +201,8 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: GlossaryModerationRequestModalWidget(
|
||||
productName: productNameController.text,
|
||||
diaryCubit: sl<DiaryCubit>(),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: sl<DiaryCubit>()),
|
||||
),
|
||||
);
|
||||
if (newGlossary != null) {
|
||||
@@ -275,18 +216,9 @@ class _AddMealWidgetState extends State<AddMealWidget> {
|
||||
portion: measurementUnits,
|
||||
);
|
||||
if (portionIndex == null) {
|
||||
widget.createDietCubit.addPortion(
|
||||
dayIndex: widget.dayIndex,
|
||||
mealIndex: widget.mealIndex,
|
||||
portion: portion,
|
||||
);
|
||||
widget.createDietCubit.addPortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion);
|
||||
} else {
|
||||
widget.createDietCubit.updatePortion(
|
||||
dayIndex: widget.dayIndex,
|
||||
mealIndex: widget.mealIndex,
|
||||
portion: portion,
|
||||
portionIndex: portionIndex,
|
||||
);
|
||||
widget.createDietCubit.updatePortion(dayIndex: widget.dayIndex, mealIndex: widget.mealIndex, portion: portion, portionIndex: portionIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,7 @@ enum PortionState {
|
||||
|
||||
@freezed
|
||||
abstract class DiaryDayWrapperModel with _$DiaryDayWrapperModel {
|
||||
const factory DiaryDayWrapperModel({
|
||||
@JsonKey(
|
||||
name: "diary",
|
||||
)
|
||||
required DiaryDayModel diary,
|
||||
}) = _DiaryDayWrapperModel;
|
||||
const factory DiaryDayWrapperModel({@JsonKey(name: "diary") required DiaryDayModel diary}) = _DiaryDayWrapperModel;
|
||||
|
||||
factory DiaryDayWrapperModel.fromJson(Map<String, dynamic> json) => _$DiaryDayWrapperModelFromJson(json);
|
||||
}
|
||||
@@ -71,21 +66,13 @@ DiaryDietModel? _parseDiet(dynamic json) {
|
||||
|
||||
@freezed
|
||||
abstract class DiaryDietModel with _$DiaryDietModel {
|
||||
const factory DiaryDietModel({
|
||||
required int id,
|
||||
required int duration,
|
||||
required String name,
|
||||
}) = _DiaryDietModel;
|
||||
const factory DiaryDietModel({required int id, required int duration, required String name}) = _DiaryDietModel;
|
||||
|
||||
factory DiaryDietModel.fromJson(Map<String, dynamic> json) => _$DiaryDietModelFromJson(json);
|
||||
}
|
||||
|
||||
TypeOfDiaryModel _parseDayTypeForFutureDay(dynamic json) {
|
||||
return TypeOfDiaryModel(
|
||||
hasChosenDiet: json['has_chosen_diet'] as bool,
|
||||
hasChosenPlan: json['has_chosen_plan'] as bool,
|
||||
diaryState: DiaryStateEnum.init,
|
||||
);
|
||||
return TypeOfDiaryModel(hasChosenDiet: json['has_chosen_diet'] as bool, hasChosenPlan: json['has_chosen_plan'] as bool, diaryState: DiaryStateEnum.init);
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -115,11 +102,30 @@ abstract class DiaryPhotoModel with _$DiaryPhotoModel {
|
||||
factory DiaryPhotoModel.fromJson(Map<String, dynamic> json) => _$DiaryPhotoModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ProductCaloriesInfoModel with _$ProductCaloriesInfoModel {
|
||||
const factory ProductCaloriesInfoModel({
|
||||
@JsonKey(name: "id") required int id,
|
||||
@JsonKey(name: "product_name") required String productName,
|
||||
@JsonKey(name: "energy_on_100gr") required double calories,
|
||||
@JsonKey(name: "proteins_on_100gr") required double proteins,
|
||||
@JsonKey(name: "fats_on_100gr") required double fats,
|
||||
@JsonKey(name: "carbohydrates_on_100gr") required double carbohydrates,
|
||||
@JsonKey(name: "energy_on_portion") required double energyOnPortion,
|
||||
@JsonKey(name: "proteins_on_portion") required double proteinsOnPortion,
|
||||
@JsonKey(name: "fats_on_portion") required double fatsOnPortion,
|
||||
@JsonKey(name: "carbohydrates_on_portion") required double carbohydratesOnPortion,
|
||||
}) = _ProductCaloriesInfoModel;
|
||||
|
||||
factory ProductCaloriesInfoModel.fromJson(Map<String, dynamic> json) => _$ProductCaloriesInfoModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class PortionModel with _$PortionModel {
|
||||
const factory PortionModel({
|
||||
@JsonKey(name: "id") required int id,
|
||||
@JsonKey(name: "product_nutritional_value_id") int? productNutritionalValueId,
|
||||
ProductCaloriesInfoModel? productCaloriesInfo,
|
||||
@JsonKey(name: "product_name") required String productName,
|
||||
@JsonKey(name: "quantity") required int quantity,
|
||||
@JsonKey(name: "portion") required String portion,
|
||||
|
||||
@@ -14,12 +14,7 @@ class AddProductModal extends StatefulWidget {
|
||||
final DiaryStateLoaded state;
|
||||
final DiaryCubit diaryCubit;
|
||||
|
||||
const AddProductModal({
|
||||
super.key,
|
||||
this.portion,
|
||||
required this.state,
|
||||
required this.diaryCubit,
|
||||
});
|
||||
const AddProductModal({super.key, this.portion, required this.state, required this.diaryCubit});
|
||||
|
||||
@override
|
||||
State<AddProductModal> createState() => _AddProductModalState();
|
||||
@@ -58,75 +53,62 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, void Function(void Function()) setState) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16) +
|
||||
EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
padding: const EdgeInsets.all(16) + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Добавление блюда',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF323F4B),
|
||||
),
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: Color(0xFF323F4B)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.8 - 32,
|
||||
width: MediaQuery.of(context).size.width * 0.76 - 32,
|
||||
child: CustomTextFormField(
|
||||
controller: productNameController,
|
||||
autofocus: true,
|
||||
hintText: 'Введите название продукта/блюда',
|
||||
onChanged: (query) {
|
||||
EasyDebounce.debounce(
|
||||
'glossary-search',
|
||||
const Duration(milliseconds: 250),
|
||||
() async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
setState(() {});
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
|
||||
EasyDebounce.debounce('glossary-search', const Duration(milliseconds: 250), () async {
|
||||
selectedGlossary = null;
|
||||
if (query.isEmpty) {
|
||||
glossariesList = [];
|
||||
setState(() {});
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
glossariesList = await widget.diaryCubit.getGlossaryListByQuery(query);
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.2 - 4,
|
||||
width: MediaQuery.of(context).size.width * 0.24 - 4,
|
||||
child: CustomTextFormField(
|
||||
controller: quantityController,
|
||||
keyboardType: TextInputType.number,
|
||||
hintText: selectedGlossary == null
|
||||
? '100'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? '100'
|
||||
: '1',
|
||||
? '100'
|
||||
: '1',
|
||||
suffixText: selectedGlossary == null
|
||||
? 'гр'
|
||||
: selectedGlossary?.grInPortion == 0
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
? 'гр'
|
||||
: 'шт',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
glossariesList.isEmpty ? const SizedBox(height: 16) : const SizedBox.shrink(),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 256,
|
||||
),
|
||||
constraints: const BoxConstraints(maxHeight: 256),
|
||||
child: ListView.separated(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
@@ -141,28 +123,12 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
setState(() {});
|
||||
FocusManager.instance.primaryFocus?.nextFocus();
|
||||
},
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
title: Text(
|
||||
glossariesList[i].name,
|
||||
style: const TextStyle(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
title: Text(glossariesList[i].name, style: const TextStyle(height: 1)),
|
||||
subtitle: glossariesList[i].grInPortion == 0
|
||||
? null
|
||||
: Text(
|
||||
'${glossariesList[i].grInPortion} г',
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.add_rounded,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
: Text('${glossariesList[i].grInPortion} г', style: TextStyle(height: 1, color: Colors.grey.shade600)),
|
||||
trailing: Icon(Icons.add_rounded, color: Colors.grey.shade400),
|
||||
contentPadding: const EdgeInsets.only(left: 12),
|
||||
);
|
||||
},
|
||||
@@ -188,14 +154,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
child: Ink(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: const Color(0xFFF6F6F6),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.delete_outline_rounded,
|
||||
color: Colors.red,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: const Color(0xFFF6F6F6)),
|
||||
child: const Icon(Icons.delete_outline_rounded, color: Colors.red),
|
||||
),
|
||||
),
|
||||
if (widget.portion != null) const SizedBox(width: 8),
|
||||
@@ -209,8 +169,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
portion: selectedGlossary == null
|
||||
? 'gr'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
quantity: int.parse(quantityController.text),
|
||||
productNutritionalValueId: selectedGlossary?.id,
|
||||
);
|
||||
@@ -223,8 +183,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
portion: selectedGlossary == null
|
||||
? 'gr'
|
||||
: selectedGlossary!.grInPortion == 0
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
? 'gr'
|
||||
: 'pcs',
|
||||
quantity: int.parse(quantityController.text),
|
||||
productNutritionalValueId: selectedGlossary?.id,
|
||||
portionState: widget.portion!.portionState,
|
||||
@@ -237,13 +197,8 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
builder: (context) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: GlossaryModerationRequestModalWidget(
|
||||
productName: productNameController.text,
|
||||
diaryCubit: widget.diaryCubit,
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: GlossaryModerationRequestModalWidget(productName: productNameController.text, diaryCubit: widget.diaryCubit),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -252,18 +207,11 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - (widget.portion == null ? 32 : 92),
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: Center(
|
||||
child: Text(
|
||||
widget.portion == null ? 'Добавить' : 'Сохранить',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/meal_summary_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MealButtonWidget extends StatefulWidget {
|
||||
final int mealNumber;
|
||||
const MealButtonWidget({
|
||||
super.key,
|
||||
required this.mealNumber,
|
||||
});
|
||||
const MealButtonWidget({super.key, required this.mealNumber});
|
||||
|
||||
@override
|
||||
State<MealButtonWidget> createState() => _MealButtonWidgetState();
|
||||
@@ -22,9 +21,7 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(
|
||||
builder: (context, state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull(
|
||||
(element) => element.typeNo == widget.mealNumber,
|
||||
);
|
||||
final consultantComment = state.consultantReview?.consultantComments.mealsComments.firstWhereOrNull((element) => element.typeNo == widget.mealNumber);
|
||||
|
||||
final Color bgColor;
|
||||
if (state.selectedMeal == widget.mealNumber) {
|
||||
@@ -34,55 +31,84 @@ class _MealButtonWidgetState extends State<MealButtonWidget> {
|
||||
} else {
|
||||
bgColor = const Color(0xFFF1F2F6);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
|
||||
final mealIndex = widget.mealNumber - 1;
|
||||
final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : <PortionModel>[];
|
||||
final totals = MealSummaryWidget.calculateMealTotals(portions);
|
||||
final calories = totals['calories']!.round();
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
color: bgColor,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${widget.mealNumber}',
|
||||
style: TextStyle(
|
||||
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(100), color: bgColor),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${widget.mealNumber}',
|
||||
style: TextStyle(
|
||||
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
calories > 0 ? '$calories ккал' : '— ккал',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
|
||||
final mealIndex = widget.mealNumber - 1;
|
||||
final portions = mealIndex < state.diary.meals.length ? state.diary.meals[mealIndex].portions : <PortionModel>[];
|
||||
final totals = MealSummaryWidget.calculateMealTotals(portions);
|
||||
final calories = totals['calories']!.round();
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
context.read<DiaryCubit>().setSelectedMeal(widget.mealNumber);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${widget.mealNumber}',
|
||||
style: TextStyle(
|
||||
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width / 5 - (96 / 5),
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
color: state.selectedMeal == widget.mealNumber ? appTheme.blueA400 : const Color(0xFFF1F2F6),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${widget.mealNumber}',
|
||||
style: TextStyle(
|
||||
color: state.selectedMeal == widget.mealNumber ? Colors.white : const Color(0xFF6D6F71),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
calories > 0 ? '$calories ккал' : '— ккал',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
|
||||
117
lib/features/diary/presentation/widgets/meal_summary_widget.dart
Normal file
117
lib/features/diary/presentation/widgets/meal_summary_widget.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:eatvio_app/features/diary/models/diary_day_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MealSummaryWidget extends StatelessWidget {
|
||||
final List<PortionModel> portions;
|
||||
final int mealNumber;
|
||||
|
||||
const MealSummaryWidget({super.key, required this.portions, required this.mealNumber});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totals = _calculateTotals();
|
||||
|
||||
if (totals['calories'] == 0 && totals['proteins'] == 0 && totals['fats'] == 0 && totals['carbohydrates'] == 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF2F80ED), width: 2),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Прием $mealNumber',
|
||||
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
_buildNutritionChip('К', totals['calories']!.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('Б', totals['proteins']!.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('Ж', totals['fats']!.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('У', totals['carbohydrates']!.round()),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNutritionChip(String label, int value) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(color: const Color(0xFFF6FAFF), borderRadius: BorderRadius.circular(12)),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '$label ',
|
||||
style: const TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
TextSpan(
|
||||
text: '$value',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, double> _calculateTotals() {
|
||||
return calculateMealTotals(portions);
|
||||
}
|
||||
|
||||
static Map<String, double> calculateMealTotals(List<PortionModel> portions) {
|
||||
double totalCalories = 0;
|
||||
double totalProteins = 0;
|
||||
double totalFats = 0;
|
||||
double totalCarbohydrates = 0;
|
||||
|
||||
for (final portion in portions) {
|
||||
if (portion.portionState == PortionState.eatenDeleted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Для продуктов из диеты учитываем только съеденные (с галочкой)
|
||||
if (portion.fromDiet && portion.portionState != PortionState.eatenFromDiet) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final caloriesInfo = portion.productCaloriesInfo;
|
||||
if (caloriesInfo == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double calories, proteins, fats, carbohydrates;
|
||||
|
||||
if (portion.portion == 'gr') {
|
||||
calories = (caloriesInfo.calories * portion.quantity) / 100;
|
||||
proteins = (caloriesInfo.proteins * portion.quantity) / 100;
|
||||
fats = (caloriesInfo.fats * portion.quantity) / 100;
|
||||
carbohydrates = (caloriesInfo.carbohydrates * portion.quantity) / 100;
|
||||
} else {
|
||||
calories = caloriesInfo.energyOnPortion;
|
||||
proteins = caloriesInfo.proteinsOnPortion;
|
||||
fats = caloriesInfo.fatsOnPortion;
|
||||
carbohydrates = caloriesInfo.carbohydratesOnPortion;
|
||||
}
|
||||
|
||||
totalCalories += calories;
|
||||
totalProteins += proteins;
|
||||
totalFats += fats;
|
||||
totalCarbohydrates += carbohydrates;
|
||||
}
|
||||
|
||||
return {'calories': totalCalories, 'proteins': totalProteins, 'fats': totalFats, 'carbohydrates': totalCarbohydrates};
|
||||
}
|
||||
}
|
||||
@@ -40,11 +40,7 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
children: [
|
||||
const Text(
|
||||
'Прием пищи',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Row(
|
||||
@@ -57,23 +53,17 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
MealButtonWidget(mealNumber: 5),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 16),
|
||||
const TimeOfMealPickerWidget(),
|
||||
const ChosenDietWidget(),
|
||||
const ChosenPlanWidget(),
|
||||
const ProductsListWidget(),
|
||||
if (widget.showAddButton) ...[
|
||||
const SizedBox(height: 12),
|
||||
const AddProductButtonWidget(),
|
||||
],
|
||||
if (widget.showAddButton) ...[const SizedBox(height: 12), const AddProductButtonWidget()],
|
||||
const SizedBox(height: 12),
|
||||
const PhotoGalleryWidget(),
|
||||
const SizedBox(height: 12),
|
||||
const SatietyRatingWidget(),
|
||||
if (widget.showMood) ...[
|
||||
const SizedBox(height: 12),
|
||||
const MoodRatingWidget(),
|
||||
],
|
||||
if (widget.showMood) ...[const SizedBox(height: 12), const MoodRatingWidget()],
|
||||
BlocBuilder<DiaryCubit, DiaryState>(
|
||||
builder: (context, state) {
|
||||
if (state is DiaryStateLoaded && state.isConsultantView && !state.isDiaryCommented) {
|
||||
@@ -91,10 +81,7 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
if (consultantCommentOfMeal != null) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'${s.mealComment} ${state.selectedMeal}',
|
||||
style: TextStyles.textLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
Text('${s.mealComment} ${state.selectedMeal}', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 12),
|
||||
CommentContainer(comment: consultantCommentOfMeal.comment),
|
||||
],
|
||||
@@ -103,7 +90,7 @@ class _MealWidgetState extends State<MealWidget> {
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -15,10 +16,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
class ProductWidget extends StatefulWidget {
|
||||
final PortionModel portion;
|
||||
|
||||
const ProductWidget({
|
||||
super.key,
|
||||
required this.portion,
|
||||
});
|
||||
const ProductWidget({super.key, required this.portion});
|
||||
|
||||
@override
|
||||
State<ProductWidget> createState() => _ProductWidgetState();
|
||||
@@ -37,6 +35,60 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
isEaten = widget.portion.portionState == PortionState.eatenFromDiet ? true : false;
|
||||
}
|
||||
|
||||
Widget _buildNutritionalInfo() {
|
||||
if (widget.portion.productNutritionalValueId == null) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(12)),
|
||||
child: const Text(
|
||||
'Продукт отправлен на модерацию',
|
||||
style: TextStyle(color: Color(0xFF2F80ED), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final caloriesInfo = widget.portion.productCaloriesInfo;
|
||||
if (caloriesInfo == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
double calories, proteins, fats, carbohydrates;
|
||||
|
||||
if (widget.portion.portion == 'gr') {
|
||||
calories = (caloriesInfo.calories * widget.portion.quantity) / 100;
|
||||
proteins = (caloriesInfo.proteins * widget.portion.quantity) / 100;
|
||||
fats = (caloriesInfo.fats * widget.portion.quantity) / 100;
|
||||
carbohydrates = (caloriesInfo.carbohydrates * widget.portion.quantity) / 100;
|
||||
} else {
|
||||
calories = caloriesInfo.energyOnPortion;
|
||||
proteins = caloriesInfo.proteinsOnPortion;
|
||||
fats = caloriesInfo.fatsOnPortion;
|
||||
carbohydrates = caloriesInfo.carbohydratesOnPortion;
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
_buildNutritionChip('К', calories.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('Б', proteins.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('Ж', fats.round()),
|
||||
const SizedBox(width: 4),
|
||||
_buildNutritionChip('У', carbohydrates.round()),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNutritionChip(String label, int value) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
|
||||
child: Text(
|
||||
'$label $value',
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ProductWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -48,15 +100,9 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
return showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(12))),
|
||||
isScrollControlled: true,
|
||||
builder: (context) => AddProductModal(
|
||||
state: state,
|
||||
portion: portion,
|
||||
diaryCubit: diaryCubit,
|
||||
),
|
||||
builder: (context) => AddProductModal(state: state, portion: portion, diaryCubit: diaryCubit),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,15 +115,21 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
horizontalTitleGap: 0,
|
||||
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 12) : null,
|
||||
title: Text(widget.portion.productName),
|
||||
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
const SizedBox(height: 4),
|
||||
_buildNutritionalInfo(),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
tileColor: const Color(0xFFF6FAFF),
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoaded) {
|
||||
final isDiaryCommented = state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
|
||||
final isDiaryCommented =
|
||||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.commented ||
|
||||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.lastDayCommented ||
|
||||
state.diary.typeOfDiary.diaryState == DiaryStateEnum.reviewedByUser;
|
||||
final isDiaryFinished =
|
||||
@@ -99,21 +151,20 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
color: Color(0xFF6D6F71),
|
||||
),
|
||||
child: ListTile(
|
||||
onTap: () => diaryCubit.restore(
|
||||
diaryId: state.diary.id,
|
||||
mealId: state.selectedMeal,
|
||||
portionId: widget.portion.id,
|
||||
),
|
||||
trailing: SvgPicture.asset(
|
||||
'assets/icons/revert_product.svg',
|
||||
),
|
||||
onTap: () => diaryCubit.restore(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id),
|
||||
trailing: SvgPicture.asset('assets/icons/revert_product.svg'),
|
||||
horizontalTitleGap: 0,
|
||||
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null,
|
||||
title: Text(widget.portion.productName),
|
||||
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
const SizedBox(height: 4),
|
||||
_buildNutritionalInfo(),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
// ignore: use_full_hex_values_for_flutter_colors
|
||||
tileColor: const Color(0xfff6faff03).withValues(alpha: 0.01),
|
||||
),
|
||||
@@ -122,12 +173,13 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
Widget? trailing;
|
||||
if (state.isConsultantView && !isDiaryCommented) {
|
||||
trailing = GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
showCommentWidget = !showCommentWidget;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset('assets/icons/edit.svg'));
|
||||
onTap: () {
|
||||
setState(() {
|
||||
showCommentWidget = !showCommentWidget;
|
||||
});
|
||||
},
|
||||
child: SvgPicture.asset('assets/icons/edit.svg'),
|
||||
);
|
||||
} else if (!isDiaryCommented && !widget.portion.fromDiet) {
|
||||
trailing = const Icon(Icons.more_vert_rounded);
|
||||
}
|
||||
@@ -147,47 +199,36 @@ class _ProductWidgetState extends State<ProductWidget> {
|
||||
isEaten = value!;
|
||||
});
|
||||
if (isEaten) {
|
||||
diaryCubit.markEaten(
|
||||
diaryId: state.diary.id,
|
||||
mealId: state.selectedMeal,
|
||||
portionId: widget.portion.id,
|
||||
);
|
||||
diaryCubit.markEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id);
|
||||
} else {
|
||||
diaryCubit.unmarkEaten(
|
||||
diaryId: state.diary.id,
|
||||
mealId: state.selectedMeal,
|
||||
portionId: widget.portion.id,
|
||||
);
|
||||
diaryCubit.unmarkEaten(diaryId: state.diary.id, mealId: state.selectedMeal, portionId: widget.portion.id);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
activeColor: appTheme.blueA400,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
side: const BorderSide(
|
||||
color: Color(0xFF809CB9),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
side: const BorderSide(color: Color(0xFF809CB9)),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
horizontalTitleGap: 0,
|
||||
contentPadding: widget.portion.fromDiet ? const EdgeInsets.only(right: 24, left: 4) : null,
|
||||
title: Text(widget.portion.productName),
|
||||
subtitle: Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${widget.portion.quantity} ${widget.portion.portion == 'gr' ? 'гр' : 'шт'}'),
|
||||
const SizedBox(height: 4),
|
||||
_buildNutritionalInfo(),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
tileColor: const Color(0xFFF6FAFF),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: PortionCommentWidget(
|
||||
enabled: showCommentWidget,
|
||||
portionId: widget.portion.id,
|
||||
comment: portionComment?.comment,
|
||||
),
|
||||
)
|
||||
child: PortionCommentWidget(enabled: showCommentWidget, portionId: widget.portion.id, comment: portionComment?.comment),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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/meal_summary_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/product_loader_widget.dart';
|
||||
import 'package:eatvio_app/features/diary/presentation/widgets/product_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -16,33 +17,33 @@ class _ProductsListWidgetState extends State<ProductsListWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DiaryCubit, DiaryState>(
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
builder: (BuildContext context, DiaryState state) {
|
||||
if (state is DiaryStateLoaded) {
|
||||
int mealIndex = state.selectedMeal - 1;
|
||||
final portions = state.diary.meals[mealIndex].portions;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
|
||||
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
|
||||
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
for (int i = 0; i < portions.length; i++) ...[
|
||||
ProductWidget(portion: portions[i]),
|
||||
i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
],
|
||||
if (state.isLoadingAddingProductInMeal)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(top: 8),
|
||||
child: ProductLoaderWidget(),
|
||||
),
|
||||
if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal),
|
||||
if (state.isLoadingAddingProductInMeal) const Padding(padding: EdgeInsets.only(top: 8), child: ProductLoaderWidget()),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (state is DiaryStateLoadedTomorrowWithDiet) {
|
||||
int mealIndex = state.selectedMeal - 1;
|
||||
final portions = state.diary.meals[mealIndex].portions;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (int i = 0; i < state.diary.meals[mealIndex].portions.length; i++) ...[
|
||||
ProductWidget(portion: state.diary.meals[mealIndex].portions[i]),
|
||||
i < state.diary.meals[mealIndex].portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
for (int i = 0; i < portions.length; i++) ...[
|
||||
ProductWidget(portion: portions[i]),
|
||||
i < portions.length - 1 ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
],
|
||||
if (portions.isNotEmpty) MealSummaryWidget(portions: portions, mealNumber: state.selectedMeal),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,55 @@ class DiaryRepository {
|
||||
|
||||
Stream<String> get forceFetchStream => _diaryForceFetchController.stream;
|
||||
|
||||
DiaryRepository({
|
||||
required this.diaryService,
|
||||
});
|
||||
DiaryRepository({required this.diaryService});
|
||||
|
||||
Future<DiaryDayModel> getDiaryByDate(String date) async {
|
||||
try {
|
||||
return (await getDiaryIndexByDate(date)).last;
|
||||
Set<int> ids = {};
|
||||
final DiaryDayModel diaryDay = (await getDiaryIndexByDate(date)).last;
|
||||
for (var meal in diaryDay.meals) {
|
||||
for (var portion in meal.portions) {
|
||||
if (portion.productNutritionalValueId != null) {
|
||||
ids.add(portion.productNutritionalValueId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ids.isNotEmpty) {
|
||||
final glossary = await getGlossariesIndex(ids: ids.toList());
|
||||
final List<MealModel> updatedMeals = List.from(diaryDay.meals);
|
||||
for (var mealIndex = 0; mealIndex < updatedMeals.length; mealIndex++) {
|
||||
final meal = updatedMeals[mealIndex];
|
||||
final updatedPortions = meal.portions.map((portion) {
|
||||
if (portion.productNutritionalValueId != null) {
|
||||
try {
|
||||
final glossaryItem = glossary.glossariesList.firstWhere((g) => g.id == portion.productNutritionalValueId);
|
||||
return portion.copyWith(
|
||||
productCaloriesInfo: ProductCaloriesInfoModel(
|
||||
id: glossaryItem.id,
|
||||
productName: glossaryItem.name,
|
||||
calories: glossaryItem.energyOn100Gr,
|
||||
proteins: glossaryItem.proteinsOn100Gr,
|
||||
fats: glossaryItem.fatsOn100Gr,
|
||||
carbohydrates: glossaryItem.carbohydratesOn100Gr,
|
||||
energyOnPortion: glossaryItem.energyOnPortion,
|
||||
proteinsOnPortion: glossaryItem.proteinsOnPortion,
|
||||
fatsOnPortion: glossaryItem.fatsOnPortion,
|
||||
carbohydratesOnPortion: glossaryItem.carbohydratesOnPortion,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
return portion;
|
||||
}
|
||||
}
|
||||
return portion;
|
||||
}).toList();
|
||||
|
||||
updatedMeals[mealIndex] = meal.copyWith(portions: updatedPortions);
|
||||
}
|
||||
final updatedDiaryDay = diaryDay.copyWith(meals: updatedMeals);
|
||||
return updatedDiaryDay;
|
||||
}
|
||||
return diaryDay;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -75,109 +117,47 @@ class DiaryRepository {
|
||||
int? productNutritionalValueId,
|
||||
required PortionState portionState,
|
||||
}) async {
|
||||
Map<String, Object?> data = ProductInMeal(
|
||||
productName: productName,
|
||||
portion: portion,
|
||||
quantity: quantity,
|
||||
).toJson();
|
||||
Map<String, Object?> data = ProductInMeal(productName: productName, portion: portion, quantity: quantity).toJson();
|
||||
data['product_nutritional_value_id'] = productNutritionalValueId;
|
||||
data['state'] = portionState.name;
|
||||
return await diaryService.updateProductInMeal(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
portionModel: portionId,
|
||||
productInMeal: data,
|
||||
);
|
||||
return await diaryService.updateProductInMeal(diary: diaryId, typeNo: mealId, portionModel: portionId, productInMeal: data);
|
||||
}
|
||||
|
||||
Future<void> markEaten({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required int portionId,
|
||||
}) async {
|
||||
return await diaryService.markEaten(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
portionModel: portionId,
|
||||
);
|
||||
Future<void> markEaten({required int diaryId, required int mealId, required int portionId}) async {
|
||||
return await diaryService.markEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
|
||||
}
|
||||
|
||||
Future<void> unmarkEaten({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required int portionId,
|
||||
}) async {
|
||||
return await diaryService.unmarkEaten(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
portionModel: portionId,
|
||||
);
|
||||
Future<void> unmarkEaten({required int diaryId, required int mealId, required int portionId}) async {
|
||||
return await diaryService.unmarkEaten(diary: diaryId, typeNo: mealId, portionModel: portionId);
|
||||
}
|
||||
|
||||
Future<void> restore({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required int portionId,
|
||||
}) async {
|
||||
return await diaryService.restore(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
portionModel: portionId,
|
||||
);
|
||||
Future<void> restore({required int diaryId, required int mealId, required int portionId}) async {
|
||||
return await diaryService.restore(diary: diaryId, typeNo: mealId, portionModel: portionId);
|
||||
}
|
||||
|
||||
Future<void> deleteProductFromMeal({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required int portionId,
|
||||
}) async {
|
||||
return await diaryService.deleteProductFromMeal(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
portionModel: portionId,
|
||||
);
|
||||
Future<void> deleteProductFromMeal({required int diaryId, required int mealId, required int portionId}) async {
|
||||
return await diaryService.deleteProductFromMeal(diary: diaryId, typeNo: mealId, portionModel: portionId);
|
||||
}
|
||||
|
||||
Future<void> uploadPhotos({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required List<File> files,
|
||||
}) async {
|
||||
Future<void> uploadPhotos({required int diaryId, required int mealId, required List<File> files}) async {
|
||||
try {
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
await diaryService.uploadPhotoOfMeal(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
photo: files[i],
|
||||
);
|
||||
await diaryService.uploadPhotoOfMeal(diary: diaryId, typeNo: mealId, photo: files[i]);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deletePhoto({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required DiaryPhotoModel photo,
|
||||
}) async {
|
||||
Future<void> deletePhoto({required int diaryId, required int mealId, required DiaryPhotoModel photo}) async {
|
||||
try {
|
||||
await diaryService.deletePhotoOfMeal(
|
||||
diaryId: diaryId,
|
||||
typeNo: mealId,
|
||||
userAttachment: photo.id,
|
||||
);
|
||||
await diaryService.deletePhotoOfMeal(diaryId: diaryId, typeNo: mealId, userAttachment: photo.id);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> finishDiaryDay({
|
||||
required int diaryId,
|
||||
required String wakeUp,
|
||||
required String retire,
|
||||
String? comment,
|
||||
}) async {
|
||||
Future<void> finishDiaryDay({required int diaryId, required String wakeUp, required String retire, String? comment}) async {
|
||||
return await diaryService.finishDiaryDay(
|
||||
diary: diaryId,
|
||||
finishDiaryDayModel: FinishDiaryDayModel(wakeUp: wakeUp, retire: retire, comment: comment),
|
||||
@@ -192,87 +172,45 @@ class DiaryRepository {
|
||||
required double carbohydrates,
|
||||
}) async {
|
||||
return await diaryService.sendProductToModeration(
|
||||
data: ProductModerationModel(
|
||||
name: name,
|
||||
energy: energy,
|
||||
proteins: proteins,
|
||||
fats: fats,
|
||||
carbohydrates: carbohydrates,
|
||||
).toJson(),
|
||||
data: ProductModerationModel(name: name, energy: energy, proteins: proteins, fats: fats, carbohydrates: carbohydrates).toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> fillSatiety({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required int satiety,
|
||||
}) async {
|
||||
Future<void> fillSatiety({required int diaryId, required int mealId, required int satiety}) async {
|
||||
try {
|
||||
return await diaryService.fillSatiety(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
satiety: satiety,
|
||||
);
|
||||
return await diaryService.fillSatiety(diary: diaryId, typeNo: mealId, satiety: satiety);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fillMood({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required int mood,
|
||||
}) async {
|
||||
Future<void> fillMood({required int diaryId, required int mealId, required int mood}) async {
|
||||
try {
|
||||
return await diaryService.fillMood(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
mood: mood,
|
||||
);
|
||||
return await diaryService.fillMood(diary: diaryId, typeNo: mealId, mood: mood);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fillTime({
|
||||
required int diaryId,
|
||||
required int mealId,
|
||||
required String time,
|
||||
}) async {
|
||||
Future<void> fillTime({required int diaryId, required int mealId, required String time}) async {
|
||||
try {
|
||||
return await diaryService.fillTime(
|
||||
diary: diaryId,
|
||||
typeNo: mealId,
|
||||
time: time,
|
||||
);
|
||||
return await diaryService.fillTime(diary: diaryId, typeNo: mealId, time: time);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fillWakeup({
|
||||
required int diaryId,
|
||||
required String time,
|
||||
}) async {
|
||||
Future<void> fillWakeup({required int diaryId, required String time}) async {
|
||||
try {
|
||||
return await diaryService.fillWakeup(
|
||||
diary: diaryId,
|
||||
wakeUp: time,
|
||||
);
|
||||
return await diaryService.fillWakeup(diary: diaryId, wakeUp: time);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> fillRetire({
|
||||
required int diaryId,
|
||||
required String time,
|
||||
}) async {
|
||||
Future<void> fillRetire({required int diaryId, required String time}) async {
|
||||
try {
|
||||
return await diaryService.fillRetire(
|
||||
diary: diaryId,
|
||||
retire: time,
|
||||
);
|
||||
return await diaryService.fillRetire(diary: diaryId, retire: time);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
@@ -287,10 +225,7 @@ class DiaryRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ConsultantReviewModel> getDiaryComments({
|
||||
required int planId,
|
||||
required int diaryId,
|
||||
}) async {
|
||||
Future<ConsultantReviewModel> getDiaryComments({required int planId, required int diaryId}) async {
|
||||
try {
|
||||
return await diaryService.getDiaryComments(planId: planId, diaryId: diaryId);
|
||||
} catch (e) {
|
||||
@@ -305,4 +240,18 @@ class DiaryRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<GlossaryPaginationModel> getGlossariesIndex({required List<int> ids}) async {
|
||||
Map<String, dynamic> filters = {};
|
||||
if (ids.isNotEmpty) {
|
||||
for (int i = 0; i < ids.length; i++) {
|
||||
filters['filters[id][\$in][$i]'] = ids[i];
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await diaryService.getGlossariesIndex(filters: filters);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,32 +15,20 @@ abstract class DiaryService {
|
||||
factory DiaryService(Dio dio, {String baseUrl}) = _DiaryService;
|
||||
|
||||
@GET('/api/diary/index/{date}')
|
||||
Future<List<DiaryDayModel>> getDiaryIndexByDate({
|
||||
@Path('date') required String date,
|
||||
});
|
||||
Future<List<DiaryDayModel>> getDiaryIndexByDate({@Path('date') required String date});
|
||||
|
||||
@GET('/api/diary/index/{date}')
|
||||
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({
|
||||
@Path('date') required String date,
|
||||
});
|
||||
Future<List<DiaryFutureDietDayModel>> getDiaryIndexByDateForDietOnly({@Path('date') required String date});
|
||||
|
||||
@GET('/api/diary/{diary}/show')
|
||||
Future<DiaryDayWrapperModel> getDiaryDay({
|
||||
@Path('diary') required int diary,
|
||||
});
|
||||
Future<DiaryDayWrapperModel> getDiaryDay({@Path('diary') required int diary});
|
||||
|
||||
@GET('/api/diary/{diary}/dailyNutritionData')
|
||||
Future<NutritionDataModel> getNutrition({
|
||||
@Path('diary') required int diary,
|
||||
});
|
||||
Future<NutritionDataModel> getNutrition({@Path('diary') required int diary});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/photo/upload')
|
||||
@MultiPart()
|
||||
Future<void> uploadPhotoOfMeal({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Part(name: 'photo') required File photo,
|
||||
});
|
||||
Future<void> uploadPhotoOfMeal({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Part(name: 'photo') required File photo});
|
||||
|
||||
@DELETE('/api/diary/{diary}/meals/{type_no}/photo/{user_attachment}/delete')
|
||||
Future<void> deletePhotoOfMeal({
|
||||
@@ -68,25 +56,13 @@ abstract class DiaryService {
|
||||
});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/mark_eaten')
|
||||
Future<void> markEaten({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Path('portionModel') required int portionModel,
|
||||
});
|
||||
Future<void> markEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/{portionModel}/unmark_eaten')
|
||||
Future<void> unmarkEaten({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Path('portionModel') required int portionModel,
|
||||
});
|
||||
Future<void> unmarkEaten({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/restore')
|
||||
Future<void> restore({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Path('portionModel') required int portionModel,
|
||||
});
|
||||
Future<void> restore({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Path('portionModel') required int portionModel});
|
||||
|
||||
@DELETE('/api/diary/{diary}/meals/{type_no}/portions/{portionModel}/delete')
|
||||
Future<void> deleteProductFromMeal({
|
||||
@@ -96,63 +72,35 @@ abstract class DiaryService {
|
||||
});
|
||||
|
||||
@POST('/api/diary/{diary}/finishDay')
|
||||
Future<void> finishDiaryDay({
|
||||
@Path('diary') required int diary,
|
||||
@Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel,
|
||||
});
|
||||
Future<void> finishDiaryDay({@Path('diary') required int diary, @Body(nullToAbsent: true) required FinishDiaryDayModel finishDiaryDayModel});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/satiety/fill')
|
||||
Future<void> fillSatiety({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Field() required int satiety,
|
||||
});
|
||||
Future<void> fillSatiety({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int satiety});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/mood/fill')
|
||||
Future<void> fillMood({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Field() required int mood,
|
||||
});
|
||||
Future<void> fillMood({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required int mood});
|
||||
|
||||
@POST('/api/diary/{diary}/meals/{type_no}/time/fill')
|
||||
Future<void> fillTime({
|
||||
@Path('diary') required int diary,
|
||||
@Path('type_no') required int typeNo,
|
||||
@Field() required String time,
|
||||
});
|
||||
Future<void> fillTime({@Path('diary') required int diary, @Path('type_no') required int typeNo, @Field() required String time});
|
||||
|
||||
@POST('/api/diary/{diary}/wakeUp/fill')
|
||||
Future<void> fillWakeup({
|
||||
@Path('diary') required int diary,
|
||||
@Field() required String wakeUp,
|
||||
});
|
||||
Future<void> fillWakeup({@Path('diary') required int diary, @Field() required String wakeUp});
|
||||
|
||||
@POST('/api/diary/{diary}/retire/fill')
|
||||
Future<void> fillRetire({
|
||||
@Path('diary') required int diary,
|
||||
@Field() required String retire,
|
||||
});
|
||||
Future<void> fillRetire({@Path('diary') required int diary, @Field() required String retire});
|
||||
|
||||
@POST('/api/diary/product_nutritional_values/sendToModeration')
|
||||
Future<GlossaryModerationModel> sendProductToModeration({
|
||||
@Body() required Map<String, dynamic> data,
|
||||
});
|
||||
Future<GlossaryModerationModel> sendProductToModeration({@Body() required Map<String, dynamic> data});
|
||||
|
||||
@POST('/api/diary/{id}/consultant/comment')
|
||||
Future<void> commentUserDay({
|
||||
@Path('id') required int id,
|
||||
@Body() required ConsultantCommentModel body,
|
||||
});
|
||||
Future<void> commentUserDay({@Path('id') required int id, @Body() required ConsultantCommentModel body});
|
||||
|
||||
@GET('/api/chosen_plans/{planId}/{diaryId}/comments/show')
|
||||
Future<ConsultantReviewModel> getDiaryComments({
|
||||
@Path() required int planId,
|
||||
@Path() required int diaryId,
|
||||
});
|
||||
Future<ConsultantReviewModel> getDiaryComments({@Path() required int planId, @Path() required int diaryId});
|
||||
|
||||
@POST('/api/diary/{diaryId}/consultant/comment/view')
|
||||
Future<void> viewCommentOfDiaryDayByConsultant({
|
||||
@Path() required int diaryId,
|
||||
});
|
||||
Future<void> viewCommentOfDiaryDayByConsultant({@Path() required int diaryId});
|
||||
|
||||
@GET('/api/glossaries/index')
|
||||
Future<GlossaryPaginationModel> getGlossariesIndex({@Queries() Map<String, dynamic>? filters});
|
||||
}
|
||||
|
||||
16
lib/features/feed/models/view_model.dart
Normal file
16
lib/features/feed/models/view_model.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'view_model.freezed.dart';
|
||||
part 'view_model.g.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ViewModel with _$ViewModel {
|
||||
|
||||
factory ViewModel({
|
||||
required String type,
|
||||
List<int>? ids,
|
||||
List<String>? slugs,
|
||||
}) = _ViewModel;
|
||||
|
||||
factory ViewModel.fromJson(Map<String, dynamic> json) => _$ViewModelFromJson(json);
|
||||
}
|
||||
@@ -12,70 +12,57 @@ class FeedCubit extends Cubit<FeedState> {
|
||||
bool isLoading = false;
|
||||
String? _query;
|
||||
|
||||
FeedCubit({
|
||||
required this.feedRepository,
|
||||
}) : super(const FeedState.loading());
|
||||
FeedCubit({required this.feedRepository}) : super(const FeedState.loading());
|
||||
|
||||
void updateFeedBlogPostList(FeedBlogPostModel? feedBlogPostModel, bool wasLikedByMe) {
|
||||
if (feedBlogPostModel == null) return;
|
||||
if (feedBlogPostModel == null || isClosed) return;
|
||||
List<FeedBlogPostModel> feedBlogPostList = [...(state as FeedStateLoaded).feedBlogPostList];
|
||||
final int index = feedBlogPostList.indexOf(feedBlogPostModel);
|
||||
feedBlogPostList[index] = feedBlogPostModel.copyWith(
|
||||
wasLikedByMe: wasLikedByMe,
|
||||
totalLikes: wasLikedByMe == false ? feedBlogPostList[index].totalLikes - 1 : feedBlogPostList[index].totalLikes + 1,
|
||||
);
|
||||
emit(
|
||||
FeedState.loaded(
|
||||
feedBlogPostList: feedBlogPostList,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(FeedState.loaded(feedBlogPostList: feedBlogPostList, isLoading: isLoading));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadFeedBlogPostList({
|
||||
String? query,
|
||||
}) async {
|
||||
Future<void> loadFeedBlogPostList({String? query}) async {
|
||||
try {
|
||||
_query = query;
|
||||
final feedBlogPosts = await feedRepository.getFeedBlogPosts(query: query);
|
||||
lastPage = feedBlogPosts.meta.lastPage;
|
||||
emit(
|
||||
FeedState.loaded(
|
||||
feedBlogPostList: feedBlogPosts.feedBlogPostList,
|
||||
isLoading: false,
|
||||
),
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(FeedState.loaded(feedBlogPostList: feedBlogPosts.feedBlogPostList, isLoading: false));
|
||||
}
|
||||
} catch (e) {
|
||||
emit(FeedState.error(errorMessage: e.toString()));
|
||||
if (!isClosed) {
|
||||
emit(FeedState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadNextPage() async {
|
||||
if (currentPage < lastPage && !isLoading) {
|
||||
if (currentPage < lastPage && !isLoading && !isClosed) {
|
||||
isLoading = true;
|
||||
|
||||
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
|
||||
if (!isClosed) {
|
||||
emit((state as FeedStateLoaded).copyWith(isLoading: isLoading));
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentPage < lastPage) {
|
||||
if (currentPage < lastPage && !isClosed) {
|
||||
currentPage++;
|
||||
final feedBlogPosts = await feedRepository.getFeedBlogPosts(
|
||||
page: currentPage,
|
||||
query: _query,
|
||||
);
|
||||
final feedBlogPosts = await feedRepository.getFeedBlogPosts(page: currentPage, query: _query);
|
||||
isLoading = false;
|
||||
emit(
|
||||
FeedState.loaded(
|
||||
feedBlogPostList: [
|
||||
...(state as FeedStateLoaded).feedBlogPostList,
|
||||
...feedBlogPosts.feedBlogPostList,
|
||||
],
|
||||
isLoading: isLoading,
|
||||
),
|
||||
);
|
||||
if (!isClosed) {
|
||||
emit(FeedState.loaded(feedBlogPostList: [...(state as FeedStateLoaded).feedBlogPostList, ...feedBlogPosts.feedBlogPostList], isLoading: isLoading));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
emit(FeedState.error(errorMessage: e.toString()));
|
||||
if (!isClosed) {
|
||||
emit(FeedState.error(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,4 +81,5 @@ class FeedCubit extends Cubit<FeedState> {
|
||||
Future<void> viewRecipe(String slug) async => await feedRepository.viewRecipe(slug);
|
||||
Future<void> viewDiet(int id) async => await feedRepository.viewDiet(id);
|
||||
Future<void> viewPlan(String slug) async => await feedRepository.viewPlan(slug);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:easy_debounce/easy_debounce.dart';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/views/cubit/views_cubit.dart';
|
||||
import 'package:eatvio_app/core/widgets/add_item_button.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_search_bar.dart';
|
||||
import 'package:eatvio_app/features/create_diet/presentation/create_diet_screen.dart';
|
||||
@@ -66,12 +67,9 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
final dietsScrollController = ScrollController();
|
||||
final plansScrollController = ScrollController();
|
||||
final consultantsScrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Initialize chipsLabelList based on hidePlans setting
|
||||
chipsLabelList = hidePlans ? ['Все', 'Рецепты', 'Диеты', 'Консультанты'] : ['Все', 'Рецепты', 'Диеты', 'Планы питания', 'Консультанты'];
|
||||
|
||||
consultantsScrollController.addListener(_onScrollConsultants);
|
||||
super.initState();
|
||||
}
|
||||
@@ -80,6 +78,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
if (consultantsScrollController.offset >= consultantsScrollController.position.maxScrollExtent && !consultantsScrollController.position.outOfRange) {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
blogPostsScrollController.dispose();
|
||||
@@ -90,6 +90,8 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
FlutterNativeSplash.remove();
|
||||
@@ -228,10 +230,10 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
];
|
||||
},
|
||||
body: switch (_selectedChipIndex) {
|
||||
0 => const BlogPosts(),
|
||||
1 => const Recipes(),
|
||||
2 => const Diets(),
|
||||
3 when !hidePlans => const Plans(),
|
||||
0 => BlogPosts(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
1 => Recipes(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
2 => Diets(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
3 when !hidePlans => Plans(handleVisibility: context.read<ViewsCubit>().handleVisibility),
|
||||
3 when hidePlans => const Consultants(),
|
||||
4 when !hidePlans => const Consultants(),
|
||||
_ => Center(child: Text(chipsLabelList[_selectedChipIndex])),
|
||||
@@ -240,3 +242,20 @@ class _FeedScreenContentState extends State<FeedScreenContent> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewableContentType {
|
||||
plans,
|
||||
posts,
|
||||
diets,
|
||||
recipes;
|
||||
|
||||
String getStringifiedVersion() => switch (this) {
|
||||
ViewableContentType.plans => 'plans',
|
||||
|
||||
ViewableContentType.posts => 'blog_posts',
|
||||
|
||||
ViewableContentType.diets => 'diets',
|
||||
|
||||
ViewableContentType.recipes => 'recipes',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,9 +245,9 @@ class _BlogPostWidgetState extends State<BlogPostWidget> {
|
||||
const SizedBox(width: 8),
|
||||
FeedIconButton(
|
||||
svgIcon: SvgPicture.asset(
|
||||
height: 20,
|
||||
width: 20,
|
||||
'assets/icons/share.svg',
|
||||
height: 15,
|
||||
width: 12,
|
||||
'assets/icons/repost.svg',
|
||||
colorFilter: ColorFilter.mode(appTheme.gray600, BlendMode.srcIn),
|
||||
),
|
||||
onTap: () {
|
||||
|
||||
@@ -3,13 +3,16 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/blog_post_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class BlogPosts extends StatelessWidget {
|
||||
const BlogPosts({super.key});
|
||||
|
||||
const BlogPosts({super.key, required this.handleVisibility});
|
||||
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
@@ -29,25 +32,26 @@ class BlogPosts extends StatelessWidget {
|
||||
if (index == state.feedBlogPostList.length) {
|
||||
return const Loader();
|
||||
}
|
||||
return BlogPostWidget(index: index);
|
||||
return VisibilityDetector(
|
||||
onVisibilityChanged: (info) {
|
||||
final id = state.feedBlogPostList[index].id;
|
||||
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.posts);
|
||||
},
|
||||
key: ValueKey(index),
|
||||
child: BlogPostWidget(index: index),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemCount: state.isLoading ? state.feedBlogPostList.length + 1 : state.feedBlogPostList.length,
|
||||
),
|
||||
);
|
||||
} else if (state is FeedStateError) {
|
||||
return DataErrorWidget(
|
||||
onReload: () => context.read<FeedCubit>().loadFeedBlogPostList(),
|
||||
);
|
||||
return DataErrorWidget(onReload: () => context.read<FeedCubit>().loadFeedBlogPostList());
|
||||
} else {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: const Loader(),
|
||||
);
|
||||
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,7 @@ class CommentsWidget extends StatefulWidget {
|
||||
final CommentModel comment;
|
||||
final int? replyId;
|
||||
|
||||
const CommentsWidget({
|
||||
super.key,
|
||||
required this.id,
|
||||
required this.commentSourceType,
|
||||
required this.comment,
|
||||
this.replyId,
|
||||
});
|
||||
const CommentsWidget({super.key, required this.id, required this.commentSourceType, required this.comment, this.replyId});
|
||||
|
||||
@override
|
||||
State<CommentsWidget> createState() => _CommentsWidgetState();
|
||||
@@ -47,10 +41,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
_comment(s, widget.comment.data[i]),
|
||||
if (widget.comment.data[i].childrenComments.isNotEmpty) const SizedBox(height: 8),
|
||||
for (int j = 0; j < widget.comment.data[i].childrenComments.length; j++)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 40),
|
||||
child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true),
|
||||
),
|
||||
Padding(padding: const EdgeInsets.only(left: 40), child: _comment(s, widget.comment.data[i].childrenComments[j], 16, true)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
const SizedBox(height: 48),
|
||||
@@ -72,7 +63,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
: null,
|
||||
copy: () {
|
||||
Clipboard.setData(ClipboardData(text: comment.body));
|
||||
showInfoToast(s.link_was_copied);
|
||||
showInfoToast(s.text_was_copied);
|
||||
},
|
||||
edit: (String text) {
|
||||
context.read<CommentsCubit>().updateComment(id: comment.id, body: text);
|
||||
@@ -84,10 +75,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => ComplainingBottomSheet(
|
||||
complainedContentType: ComplainedContent.comment,
|
||||
id: comment.id,
|
||||
),
|
||||
builder: (context) => ComplainingBottomSheet(complainedContentType: ComplainedContent.comment, id: comment.id),
|
||||
);
|
||||
},
|
||||
isMyComment: isMe,
|
||||
@@ -117,9 +105,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(
|
||||
foregroundImage: NetworkImage(
|
||||
comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon,
|
||||
),
|
||||
foregroundImage: NetworkImage(comment.authorProfileIcon.contains('https') ? comment.authorProfileIcon : baseUrl + comment.authorProfileIcon),
|
||||
backgroundImage: const AssetImage('assets/images/no_avatar.png'),
|
||||
radius: radius,
|
||||
),
|
||||
@@ -131,24 +117,14 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
children: [
|
||||
Text(
|
||||
comment.authorName,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
width: isChild ? MediaQuery.of(context).size.width * 0.72 : MediaQuery.of(context).size.width * 0.8,
|
||||
child: Text(
|
||||
comment.body,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: TextStyle(height: 1, color: appTheme.black, fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
@@ -156,12 +132,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
children: [
|
||||
Text(
|
||||
comment.createdAt.format('dd MMMM', 'ru'),
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
if (!isChild)
|
||||
@@ -171,12 +142,7 @@ class _CommentsWidgetState extends State<CommentsWidget> {
|
||||
},
|
||||
child: Text(
|
||||
s.answer,
|
||||
style: TextStyle(
|
||||
height: 1,
|
||||
color: appTheme.gray,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(height: 1, color: appTheme.gray, fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/diets_cubit/diets_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/diet_filter_bottom_sheet.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/diets_widget.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class Diets extends StatelessWidget {
|
||||
const Diets({super.key});
|
||||
|
||||
const Diets({super.key, required this.handleVisibility});
|
||||
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
@@ -19,10 +21,7 @@ class Diets extends StatelessWidget {
|
||||
child: BlocBuilder<DietsCubit, DietsState>(
|
||||
builder: (BuildContext context, DietsState state) {
|
||||
if (state.isLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: const Loader(),
|
||||
);
|
||||
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
|
||||
} else if (state.errorMessage != null) {
|
||||
return DataErrorWidget(onReload: () => context.read<DietsCubit>().loadDiets());
|
||||
} else {
|
||||
@@ -39,7 +38,14 @@ class Diets extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return DietsWidget(diet: state.dietsList[index], index: index);
|
||||
return VisibilityDetector(
|
||||
key: ValueKey(index),
|
||||
onVisibilityChanged: (info) {
|
||||
final id = state.dietsList[index].id;
|
||||
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.diets);
|
||||
},
|
||||
child: DietsWidget(diet: state.dietsList[index], index: index),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const SizedBox(height: 8);
|
||||
@@ -50,11 +56,7 @@ class Diets extends StatelessWidget {
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 74,
|
||||
child: FilterBadge(
|
||||
filterBottomSheet: DietFilterBottomSheet(
|
||||
dietsCubit: context.read<DietsCubit>(),
|
||||
),
|
||||
),
|
||||
child: FilterBadge(filterBottomSheet: DietFilterBottomSheet(dietsCubit: context.read<DietsCubit>())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class DietsWidget extends StatefulWidget {
|
||||
class DietsWidget extends StatefulWidget {
|
||||
final Diet diet;
|
||||
final int index;
|
||||
const DietsWidget({
|
||||
|
||||
@@ -3,15 +3,17 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/filter_badge.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/plan_widget.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/plans_filter_bottom_sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class Plans extends StatelessWidget {
|
||||
const Plans({super.key});
|
||||
|
||||
const Plans({super.key, required this.handleVisibility});
|
||||
final Function({required double visibleFraction, required int id, required ViewableContentType type}) handleVisibility;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
@@ -19,10 +21,7 @@ class Plans extends StatelessWidget {
|
||||
child: BlocBuilder<PlansCubit, PlansState>(
|
||||
builder: (BuildContext context, PlansState state) {
|
||||
if (state.isLoading) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
body: const Loader(),
|
||||
);
|
||||
return Scaffold(backgroundColor: Colors.grey.shade200, body: const Loader());
|
||||
} else if (state.errorMessage != null) {
|
||||
return DataErrorWidget(onReload: () => context.read<PlansCubit>().loadPlans());
|
||||
}
|
||||
@@ -39,9 +38,13 @@ class Plans extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return PlanWidget(
|
||||
plan: state.plans[index],
|
||||
index: index,
|
||||
return VisibilityDetector(
|
||||
key: ValueKey(index),
|
||||
onVisibilityChanged: (info) {
|
||||
final id = state.plans[index].id;
|
||||
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.plans);
|
||||
},
|
||||
child: PlanWidget(plan: state.plans[index], index: index),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
@@ -53,11 +56,7 @@ class Plans extends StatelessWidget {
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: 74,
|
||||
child: FilterBadge(
|
||||
filterBottomSheet: PlansFilterBottomSheet(
|
||||
plansCubit: context.read<PlansCubit>(),
|
||||
),
|
||||
),
|
||||
child: FilterBadge(filterBottomSheet: PlansFilterBottomSheet(plansCubit: context.read<PlansCubit>())),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -3,13 +3,15 @@ import 'package:eatvio_app/core/widgets/data_error_widget.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/bloc/recipes_cubit/recipes_state.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/pages/feed_screen.dart';
|
||||
import 'package:eatvio_app/features/feed/presentation/widgets/recipes_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
class Recipes extends StatelessWidget {
|
||||
const Recipes({super.key});
|
||||
|
||||
const Recipes({super.key, required this.handleVisibility});
|
||||
final Function({required double visibleFraction,required int id, required ViewableContentType type}) handleVisibility;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
@@ -40,9 +42,16 @@ class Recipes extends StatelessWidget {
|
||||
shrinkWrap: true,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return RecipesWidget(
|
||||
recipe: state.recipesList[index],
|
||||
index: index,
|
||||
return VisibilityDetector(
|
||||
key: ValueKey(index),
|
||||
onVisibilityChanged: (info) {
|
||||
final id = state.recipesList[index].id;
|
||||
handleVisibility(visibleFraction: info.visibleFraction, id: id, type: ViewableContentType.recipes);
|
||||
},
|
||||
child: RecipesWidget(
|
||||
recipe: state.recipesList[index],
|
||||
index: index,
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
|
||||
@@ -231,7 +231,7 @@ class _RecipesWidgetState extends State<RecipesWidget> {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
SharePlus.instance.share(
|
||||
ShareParams(
|
||||
uri: Uri.parse('$baseUrl/ru/blog_post/${widget.recipe.id}'),
|
||||
uri: Uri.parse('$baseUrl/ru/recipe/${widget.recipe.id}'),
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,103 +5,55 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
|
||||
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/recipes_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:eatvio_app/features/feed/services/feed_service.dart';
|
||||
|
||||
class FeedRepository {
|
||||
final FeedService feedService;
|
||||
|
||||
FeedRepository({
|
||||
required this.feedService,
|
||||
});
|
||||
FeedRepository({required this.feedService});
|
||||
|
||||
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({
|
||||
String? query,
|
||||
int page = 1,
|
||||
}) async {
|
||||
Future<FeedBlogPostSettingsModel> getFeedBlogPosts({String? query, int page = 1}) async {
|
||||
try {
|
||||
return (await feedService.getBlogPostList(
|
||||
page: page,
|
||||
query: query,
|
||||
));
|
||||
return (await feedService.getBlogPostList(page: page, query: query));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<FeedBlogPostModel> getBlogPost({
|
||||
required int id
|
||||
}) async {
|
||||
Future<FeedBlogPostModel> getBlogPost({required int id}) async {
|
||||
try {
|
||||
return await feedService.getBlogPost(
|
||||
postId: id
|
||||
);
|
||||
return await feedService.getBlogPost(postId: id);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Recipe>> getRecipes({
|
||||
String? query,
|
||||
List<int>? tags,
|
||||
int page = 1,
|
||||
}) async {
|
||||
Future<List<Recipe>> getRecipes({String? query, List<int>? tags, int page = 1}) async {
|
||||
try {
|
||||
return (await feedService.getRecipes(
|
||||
query: query,
|
||||
tags: tags,
|
||||
page: page,
|
||||
))
|
||||
.recipesList;
|
||||
return (await feedService.getRecipes(query: query, tags: tags, page: page)).recipesList;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<DietsPagination> getDiets({
|
||||
int page = 1,
|
||||
String? query,
|
||||
String? types,
|
||||
int? duration,
|
||||
String? sorting,
|
||||
}) async {
|
||||
Future<DietsPagination> getDiets({int page = 1, String? query, String? types, int? duration, String? sorting}) async {
|
||||
try {
|
||||
return (await feedService.getDiets(
|
||||
page: page,
|
||||
query: query,
|
||||
types: types,
|
||||
duration: duration,
|
||||
sorting: sorting,
|
||||
));
|
||||
return (await feedService.getDiets(page: page, query: query, types: types, duration: duration, sorting: sorting));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<PlanPaginationModel> getPlans({
|
||||
int page = 1,
|
||||
String? query,
|
||||
String? types,
|
||||
int? duration,
|
||||
int? minPrice,
|
||||
int? maxPrice,
|
||||
}) async {
|
||||
Future<PlanPaginationModel> getPlans({int page = 1, String? query, String? types, int? duration, int? minPrice, int? maxPrice}) async {
|
||||
try {
|
||||
return (await feedService.getPlans(
|
||||
page: page,
|
||||
query: query,
|
||||
types: types,
|
||||
duration: duration,
|
||||
minPrice: minPrice,
|
||||
maxPrice: maxPrice,
|
||||
));
|
||||
return (await feedService.getPlans(page: page, query: query, types: types, duration: duration, minPrice: minPrice, maxPrice: maxPrice));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<CommentModel> getBlogPostComments({
|
||||
required String id,
|
||||
}) async {
|
||||
Future<CommentModel> getBlogPostComments({required String id}) async {
|
||||
try {
|
||||
return (await feedService.getBlogPostComments(id: id));
|
||||
} catch (e) {
|
||||
@@ -109,25 +61,15 @@ class FeedRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendBlogPostComment({
|
||||
required String id,
|
||||
required String commentText,
|
||||
int? parentCommentId,
|
||||
}) async {
|
||||
Future<void> sendBlogPostComment({required String id, required String commentText, int? parentCommentId}) async {
|
||||
try {
|
||||
await feedService.sendBlogPostComment(
|
||||
id: id,
|
||||
body: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedService.sendBlogPostComment(id: id, body: commentText, parentCommentId: parentCommentId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<CommentModel> getDietsComments({
|
||||
required String id,
|
||||
}) async {
|
||||
Future<CommentModel> getDietsComments({required String id}) async {
|
||||
try {
|
||||
return (await feedService.getDietsComments(id: id));
|
||||
} catch (e) {
|
||||
@@ -135,25 +77,15 @@ class FeedRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendDietsComment({
|
||||
required String id,
|
||||
required String commentText,
|
||||
int? parentCommentId,
|
||||
}) async {
|
||||
Future<void> sendDietsComment({required String id, required String commentText, int? parentCommentId}) async {
|
||||
try {
|
||||
await feedService.sendDietsComment(
|
||||
id: id,
|
||||
body: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedService.sendDietsComment(id: id, body: commentText, parentCommentId: parentCommentId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<CommentModel> getRecipesComments({
|
||||
required String id,
|
||||
}) async {
|
||||
Future<CommentModel> getRecipesComments({required String id}) async {
|
||||
try {
|
||||
return (await feedService.getRecipesComments(id: id));
|
||||
} catch (e) {
|
||||
@@ -161,25 +93,15 @@ class FeedRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendRecipesComment({
|
||||
required String id,
|
||||
required String commentText,
|
||||
int? parentCommentId,
|
||||
}) async {
|
||||
Future<void> sendRecipesComment({required String id, required String commentText, int? parentCommentId}) async {
|
||||
try {
|
||||
await feedService.sendRecipesComment(
|
||||
id: id,
|
||||
body: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedService.sendRecipesComment(id: id, body: commentText, parentCommentId: parentCommentId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<CommentModel> getPlansComments({
|
||||
required String slug,
|
||||
}) async {
|
||||
Future<CommentModel> getPlansComments({required String slug}) async {
|
||||
try {
|
||||
return (await feedService.getPlansComments(slug: slug));
|
||||
} catch (e) {
|
||||
@@ -187,25 +109,15 @@ class FeedRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendPlansComment({
|
||||
required String slug,
|
||||
required String commentText,
|
||||
int? parentCommentId,
|
||||
}) async {
|
||||
Future<void> sendPlansComment({required String slug, required String commentText, int? parentCommentId}) async {
|
||||
try {
|
||||
await feedService.sendPlansComment(
|
||||
slug: slug,
|
||||
body: commentText,
|
||||
parentCommentId: parentCommentId,
|
||||
);
|
||||
await feedService.sendPlansComment(slug: slug, body: commentText, parentCommentId: parentCommentId);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> likeBlogPost({
|
||||
required int id,
|
||||
}) async {
|
||||
Future<void> likeBlogPost({required int id}) async {
|
||||
try {
|
||||
await feedService.likeBlogPost(id: id);
|
||||
} catch (e) {
|
||||
@@ -213,9 +125,7 @@ class FeedRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> unlikedBlogPost({
|
||||
required int id,
|
||||
}) async {
|
||||
Future<void> unlikedBlogPost({required int id}) async {
|
||||
try {
|
||||
await feedService.unlikedBlogPost(id: id);
|
||||
} catch (e) {
|
||||
@@ -262,4 +172,12 @@ class FeedRepository {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> viewContent({required ViewModel viewModel}) async {
|
||||
try {
|
||||
await feedService.viewContent(viewModel: viewModel);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.da
|
||||
import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart';
|
||||
import 'package:eatvio_app/features/feed/models/view_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'feed_service.g.dart';
|
||||
@@ -131,4 +132,9 @@ abstract class FeedService {
|
||||
Future<void> viewPlan({
|
||||
@Path() required String slug,
|
||||
});
|
||||
|
||||
@POST('/api/views')
|
||||
Future<void> viewContent({
|
||||
@Body() required ViewModel viewModel,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
dateComparison: state.periodFilterStatus == PeriodFilterStatus.all ? null : FiltersComparisonEnum.greaterThanOrEqual,
|
||||
stateComparison: state.consultationFilterStatus == ConsultationFilterStatus.all
|
||||
? null
|
||||
: state.consultationFilterStatus == ConsultationFilterStatus.inProgress
|
||||
? FiltersComparisonEnum.notInArray
|
||||
: FiltersComparisonEnum.equal,
|
||||
: FiltersComparisonEnum.equal,
|
||||
);
|
||||
emit(state.copyWith(consultations: consultations, isLoading: false));
|
||||
} catch (e) {
|
||||
@@ -67,20 +65,17 @@ class ConsultationsArchiveCubit extends Cubit<ConsultationsArchiveState> {
|
||||
enum ConsultationFilterStatus {
|
||||
all,
|
||||
canceled,
|
||||
finished,
|
||||
inProgress;
|
||||
finished;
|
||||
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class _ConsultationsArchiveScreenState extends State<ConsultationsArchiveScreen>
|
||||
),
|
||||
),
|
||||
Text(
|
||||
state.consultations[index].startDate.format('yyyy.MM.dd'),
|
||||
state.consultations[index].startDate.format('dd.MM.yy'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
|
||||
@@ -33,21 +33,12 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF0F2F5),
|
||||
appBar: AppBar(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadiusDirectional.vertical(
|
||||
bottom: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
'Финансы',
|
||||
style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black),
|
||||
),
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))),
|
||||
title: Text('Финансы', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: state.isLoading
|
||||
? const Center(
|
||||
child: Loader(),
|
||||
)
|
||||
? const Center(child: Loader())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
@@ -67,10 +58,7 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
.map(
|
||||
(value) => DropdownMenuItem(
|
||||
value: value,
|
||||
child: Text(
|
||||
value.name,
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.black),
|
||||
),
|
||||
child: Text(value.name, style: TextStyles.textMMedium.copyWith(color: appTheme.black)),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
@@ -105,60 +93,41 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Builder(builder: (context) {
|
||||
final idLen = state.purchasesModels[index].bankId.length;
|
||||
return Text(
|
||||
'${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
);
|
||||
}),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final idLen = state.purchasesModels[index].bankId.length;
|
||||
return Text(
|
||||
'${state.purchasesModels[index].bankId.substring(0, 4)}...${state.purchasesModels[index].bankId.substring(idLen - 4, idLen)} ',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.black),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(
|
||||
state.purchasesModels[index].startDate.format('yyyy.MM.dd'),
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF6D6F71),
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
state.purchasesModels[index].startDate.format('dd.MM.yyyy'),
|
||||
style: const TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.purchasesModels[index].title,
|
||||
style: TextStyle(
|
||||
color: appTheme.blueA400,
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Продавец: ${state.purchasesModels[index].consultantName}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${state.purchasesModels[index].duration} ${getDayForm(state.purchasesModels[index].duration, s)}',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
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),
|
||||
),
|
||||
decoration: const BoxDecoration(color: Color(0xFFDEDEDF)),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@@ -166,16 +135,9 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
children: [
|
||||
Text(
|
||||
'${state.purchasesModels[index].paymentPrice} ₽',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF323F4B),
|
||||
height: 1,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
StatusWidget(
|
||||
status: purchaseStatus,
|
||||
style: const TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
StatusWidget(status: purchaseStatus),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -189,10 +151,7 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
else
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40.0),
|
||||
child: Text(
|
||||
'У вас нет таких консультаций',
|
||||
style: TextStyles.textMMedium,
|
||||
),
|
||||
child: Text('У вас нет таких консультаций', style: TextStyles.textMMedium),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -200,7 +159,8 @@ class _UserPurchasesScreen extends State<UserPurchasesScreen> {
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
39
lib/features/menu/models/notification_settings_model.dart
Normal file
39
lib/features/menu/models/notification_settings_model.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'notification_settings_model.g.dart';
|
||||
part 'notification_settings_model.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class NotificationSettingsModel with _$NotificationSettingsModel {
|
||||
const factory NotificationSettingsModel({
|
||||
@JsonKey(name: 'new_updates_notification') required bool newUpdatesNotification,
|
||||
@JsonKey(name: 'weight_measurements_notification') required bool weightMeasurementsNotification,
|
||||
@JsonKey(name: 'is_email_notifications_enabled') required bool isEmailNotificationsEnabled,
|
||||
@JsonKey(name: 'is_push_notifications_enabled') required bool isPushNotificationsEnabled,
|
||||
@JsonKey(name: 'diary_reminder_notifications') required DiaryReminderNotifications diaryReminderNotifications,
|
||||
}) = _NotificationSettingsModel;
|
||||
|
||||
factory NotificationSettingsModel.fromJson(Map<String, Object?> json) => _$NotificationSettingsModelFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class DiaryReminderNotifications with _$DiaryReminderNotifications {
|
||||
const factory DiaryReminderNotifications({
|
||||
@JsonKey(name: 'first_reminder') required ReminderSettings firstReminder,
|
||||
@JsonKey(name: 'second_reminder') required ReminderSettings secondReminder,
|
||||
@JsonKey(name: 'third_reminder') required ReminderSettings thirdReminder,
|
||||
}) = _DiaryReminderNotifications;
|
||||
|
||||
factory DiaryReminderNotifications.fromJson(Map<String, Object?> json) => _$DiaryReminderNotificationsFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class ReminderSettings with _$ReminderSettings {
|
||||
const factory ReminderSettings({
|
||||
required bool enabled,
|
||||
@JsonKey(name: 'local_time_hours') required int localTimeHours,
|
||||
@JsonKey(name: 'local_time_mins') required int localTimeMins,
|
||||
}) = _ReminderSettings;
|
||||
|
||||
factory ReminderSettings.fromJson(Map<String, Object?> json) => _$ReminderSettingsFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
|
||||
import 'package:eatvio_app/features/menu/repositories/notification_settings_repository.dart';
|
||||
|
||||
class NotificationSettingsCubit extends Cubit<NotificationSettingsState> {
|
||||
final NotificationSettingsRepository _notificationSettingsRepository;
|
||||
|
||||
NotificationSettingsCubit({required NotificationSettingsRepository notificationSettingsRepository})
|
||||
: _notificationSettingsRepository = notificationSettingsRepository,
|
||||
super(const NotificationSettingsStateLoading());
|
||||
|
||||
Future<void> getNotificationSettings() async {
|
||||
emit(const NotificationSettingsStateLoading());
|
||||
try {
|
||||
final notificationSettings = await _notificationSettingsRepository.getNotificationSettings();
|
||||
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
|
||||
} catch (e, stackTrace) {
|
||||
log('Error getting notification settings: $e', stackTrace: stackTrace);
|
||||
emit(NotificationSettingsStateError(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateNotificationSettings(Map<String, dynamic> data) async {
|
||||
try {
|
||||
final notificationSettings = await _notificationSettingsRepository.updateNotificationSettings(data);
|
||||
emit(NotificationSettingsStateLoaded(notificationSettings: notificationSettings));
|
||||
} catch (e, stackTrace) {
|
||||
log('Error updating notification settings: $e', stackTrace: stackTrace);
|
||||
emit(NotificationSettingsStateError(errorMessage: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
|
||||
|
||||
abstract class NotificationSettingsState extends Equatable {
|
||||
const NotificationSettingsState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class NotificationSettingsStateLoading extends NotificationSettingsState {
|
||||
const NotificationSettingsStateLoading();
|
||||
}
|
||||
|
||||
class NotificationSettingsStateLoaded extends NotificationSettingsState {
|
||||
final NotificationSettingsModel notificationSettings;
|
||||
|
||||
const NotificationSettingsStateLoaded({required this.notificationSettings});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [notificationSettings];
|
||||
}
|
||||
|
||||
class NotificationSettingsStateError extends NotificationSettingsState {
|
||||
final String errorMessage;
|
||||
|
||||
const NotificationSettingsStateError({required this.errorMessage});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [errorMessage];
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/core/utils/image_constant.dart';
|
||||
import 'package:eatvio_app/core/utils/date_time_utils.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/consultant_rating/presentation/consultant_rating_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/consultant_finances_screen.dart';
|
||||
import 'package:eatvio_app/features/finances_and_consultations_archive/presentation/user_purchases_screen.dart';
|
||||
import 'package:eatvio_app/features/my_diets/presentation/my_diets_screen.dart';
|
||||
@@ -53,9 +55,20 @@ class MenuScreen extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
],
|
||||
if (profileModel.roles.contains('consultant')) _divider(context),
|
||||
if (profileModel.roles.contains('consultant'))
|
||||
_tile(
|
||||
leading: 'assets/icons/finance.svg',
|
||||
title: 'Мой рейтинг',
|
||||
onTap: () {
|
||||
GoRouter.of(context).push(ConsultantRatingScreen.path);
|
||||
},
|
||||
),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')),
|
||||
_divider(context),
|
||||
_tile(leading: 'assets/icons/notifications.svg', title: 'Уведомления', onTap: () => GoRouter.of(context).pushNamed('notificationsSettingsScreen')),
|
||||
_divider(context),
|
||||
_tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')),
|
||||
_divider(context),
|
||||
_tile(leading: 'assets/icons/blocked_users.svg', title: 'Чёрный список', onTap: () => GoRouter.of(context).pushNamed('blockedUsersScreen')),
|
||||
@@ -156,7 +169,13 @@ class _AppInfoState extends State<AppInfo> {
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 16),
|
||||
child: Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Версия приложения: $version ($buildNumber)', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
|
||||
if (buildDate != '') Text('Дата сборки: ${formatBuildDate(buildDate)}', style: TextStyles.textXSSemibold.copyWith(color: appTheme.gray)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_cubit.dart';
|
||||
import 'package:eatvio_app/features/menu/presentation/bloc/notification_settings_cubit/notification_settings_state.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class NotificationsSettingsScreen extends StatelessWidget {
|
||||
const NotificationsSettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(create: (context) => sl<NotificationSettingsCubit>()..getNotificationSettings(), child: const NotificationsSettingsContent());
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationsSettingsContent extends StatelessWidget {
|
||||
const NotificationsSettingsContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(s.notifications)),
|
||||
body: BlocBuilder<NotificationSettingsCubit, NotificationSettingsState>(
|
||||
builder: (context, state) {
|
||||
if (state is NotificationSettingsStateLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is NotificationSettingsStateError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Ошибка: ${state.errorMessage}'),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<NotificationSettingsCubit>().getNotificationSettings();
|
||||
},
|
||||
child: const Text('Повторить'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else if (state is NotificationSettingsStateLoaded) {
|
||||
return _buildSettingsContent(context, state.notificationSettings);
|
||||
}
|
||||
return const Center(child: Text('Нет данных'));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsContent(BuildContext context, NotificationSettingsModel notificationSettings) {
|
||||
final cubit = context.read<NotificationSettingsCubit>();
|
||||
return Column(
|
||||
children: [
|
||||
_tile(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onTap: () {},
|
||||
title: 'Новые обновления',
|
||||
subtitle: null,
|
||||
trailing: CupertinoSwitch(
|
||||
value: notificationSettings.newUpdatesNotification,
|
||||
activeTrackColor: appTheme.blueA400,
|
||||
onChanged: (newValue) {
|
||||
_updateNotificationSetting(cubit, {'new_updates_notification': newValue});
|
||||
},
|
||||
),
|
||||
),
|
||||
_divider(context),
|
||||
_tile(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onTap: () {},
|
||||
title: 'Напоминать о взвешивании',
|
||||
subtitle: null,
|
||||
trailing: CupertinoSwitch(
|
||||
value: notificationSettings.weightMeasurementsNotification,
|
||||
activeTrackColor: appTheme.blueA400,
|
||||
onChanged: (newValue) {
|
||||
_updateNotificationSetting(cubit, {'weight_measurements_notification': newValue});
|
||||
},
|
||||
),
|
||||
),
|
||||
_divider(context),
|
||||
_tile(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onTap: () {},
|
||||
title: 'Email уведомления',
|
||||
subtitle: null,
|
||||
trailing: CupertinoSwitch(
|
||||
value: notificationSettings.isEmailNotificationsEnabled,
|
||||
activeTrackColor: appTheme.blueA400,
|
||||
onChanged: (newValue) {
|
||||
_updateNotificationSetting(cubit, {'is_email_notifications_enabled': newValue});
|
||||
},
|
||||
),
|
||||
),
|
||||
_divider(context),
|
||||
_tile(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
onTap: () {},
|
||||
title: 'Push уведомления',
|
||||
subtitle: null,
|
||||
trailing: CupertinoSwitch(
|
||||
value: notificationSettings.isPushNotificationsEnabled,
|
||||
activeTrackColor: appTheme.blueA400,
|
||||
onChanged: (newValue) {
|
||||
_updateNotificationSetting(cubit, {'is_push_notifications_enabled': newValue});
|
||||
},
|
||||
),
|
||||
),
|
||||
_divider(context),
|
||||
_buildDiaryRemindersSection(context, notificationSettings.diaryReminderNotifications, cubit),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDiaryRemindersSection(BuildContext context, DiaryReminderNotifications diaryReminderNotifications, NotificationSettingsCubit cubit) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Напоминание о заполнении дневника',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: appTheme.cardNotificationsBG),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildReminderTile(context, '№1 Напоминание о приеме пищи', diaryReminderNotifications.firstReminder, 'first_reminder', cubit),
|
||||
const SizedBox(height: 8),
|
||||
_buildReminderTile(context, '№2 Напоминание о приеме пищи', diaryReminderNotifications.secondReminder, 'second_reminder', cubit),
|
||||
const SizedBox(height: 8),
|
||||
_buildReminderTile(context, '№3 Напоминание о приеме пищи', diaryReminderNotifications.thirdReminder, 'third_reminder', cubit),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateNotificationSetting(NotificationSettingsCubit cubit, Map<String, dynamic> data) {
|
||||
cubit.updateNotificationSettings(data);
|
||||
}
|
||||
|
||||
void _updateReminderSetting(NotificationSettingsCubit cubit, String reminderType, Map<String, dynamic> data) {
|
||||
final updateData = {
|
||||
'diary_reminder_notifications': {reminderType: data},
|
||||
};
|
||||
cubit.updateNotificationSettings(updateData);
|
||||
}
|
||||
|
||||
Widget _buildReminderTile(BuildContext context, String title, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12), color: appTheme.dividerColor),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
reminderType == 'third_reminder' ? SvgPicture.asset('assets/icons/retire.svg') : SvgPicture.asset('assets/icons/wakeup.svg'),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: reminder.enabled
|
||||
? () {
|
||||
_showTimePicker(context, reminder, reminderType, cubit);
|
||||
}
|
||||
: null,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(color: reminder.enabled ? Colors.white : Colors.white.withAlpha((255 * 0.7).toInt()), borderRadius: BorderRadius.circular(48)),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${reminder.localTimeHours.toString().padLeft(2, '0')}:${reminder.localTimeMins.toString().padLeft(2, '0')}',
|
||||
style: TextStyle(fontSize: 17, color: appTheme.gray, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CupertinoSwitch(
|
||||
value: reminder.enabled,
|
||||
activeTrackColor: appTheme.blueA400,
|
||||
onChanged: (newValue) {
|
||||
_updateReminderSetting(cubit, reminderType, {
|
||||
'enabled': newValue,
|
||||
'local_time_hours': reminder.localTimeHours,
|
||||
'local_time_mins': reminder.localTimeMins,
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showTimePicker(BuildContext context, ReminderSettings reminder, String reminderType, NotificationSettingsCubit cubit) {
|
||||
int selectedHour = reminder.localTimeHours;
|
||||
int selectedMinute = reminder.localTimeMins;
|
||||
|
||||
showCupertinoModalPopup<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return Container(
|
||||
height: 300,
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
margin: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
color: CupertinoColors.systemBackground.resolveFrom(context),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
CupertinoButton(child: const Text('Отмена'), onPressed: () => Navigator.of(context).pop()),
|
||||
CupertinoButton(
|
||||
child: const Text('Готово'),
|
||||
onPressed: () {
|
||||
_updateReminderSetting(cubit, reminderType, {
|
||||
'enabled': reminder.enabled,
|
||||
'local_time_hours': selectedHour,
|
||||
'local_time_mins': selectedMinute,
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoDatePicker(
|
||||
mode: CupertinoDatePickerMode.time,
|
||||
use24hFormat: true,
|
||||
initialDateTime: DateTime(2024, 1, 1, selectedHour, selectedMinute),
|
||||
onDateTimeChanged: (DateTime newDateTime) {
|
||||
selectedHour = newDateTime.hour;
|
||||
selectedMinute = newDateTime.minute;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ListTile _tile({required String title, required Function()? onTap, Widget? trailing, EdgeInsets? padding, Widget? subtitle}) {
|
||||
return ListTile(
|
||||
contentPadding: padding,
|
||||
onTap: onTap,
|
||||
title: Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, height: 1)),
|
||||
subtitle: subtitle,
|
||||
trailing: trailing ?? const Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38),
|
||||
);
|
||||
}
|
||||
|
||||
Center _divider(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
height: 0.4,
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
|
||||
import 'package:eatvio_app/features/menu/services/notification_settings_service.dart';
|
||||
|
||||
class NotificationSettingsRepository {
|
||||
final NotificationSettingsService _notificationSettingsService;
|
||||
|
||||
NotificationSettingsRepository(this._notificationSettingsService);
|
||||
|
||||
Future<NotificationSettingsModel> getNotificationSettings() async {
|
||||
return await _notificationSettingsService.getNotificationSettings();
|
||||
}
|
||||
|
||||
Future<NotificationSettingsModel> updateNotificationSettings(Map<String, dynamic> data) async {
|
||||
return await _notificationSettingsService.updateNotificationSettings(data: data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eatvio_app/features/menu/models/notification_settings_model.dart';
|
||||
import 'package:retrofit/retrofit.dart';
|
||||
|
||||
part 'notification_settings_service.g.dart';
|
||||
|
||||
@RestApi()
|
||||
abstract class NotificationSettingsService {
|
||||
factory NotificationSettingsService(Dio dio, {String baseUrl}) = _NotificationSettingsService;
|
||||
|
||||
@GET('/api/profile/notification_settings')
|
||||
Future<NotificationSettingsModel> getNotificationSettings();
|
||||
|
||||
@POST('/api/profile/update_notification_settings')
|
||||
Future<NotificationSettingsModel> updateNotificationSettings({@Body() required Map<String, dynamic> data});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -66,12 +66,6 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionCanceledToCustomer => PlanSubscriptionCanceledToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
@@ -96,12 +90,6 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanLastDayFinishedForUser => ChosenPlanLastDayFinishedForUser.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
@@ -113,6 +101,166 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
// Новые типы уведомлений
|
||||
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.mealReminder => MealReminderNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
// New notification types
|
||||
NotificationType.dietCancelled => DietCancelledNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToClient => ChosenPlanFinishedToClientNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanFinishedToConsultant => ChosenPlanFinishedToConsultantNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.phoneChanged => PhoneChangedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.subscription => SubscriptionNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: isRead,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(notificationJson, type, isRead: isRead, createdAt: createdAt),
|
||||
NotificationType.unknown => UnknownNotification(type: type),
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -122,8 +270,36 @@ class NotificationsCubit extends Cubit<NotificationsState> {
|
||||
|
||||
void _handleNotification(String notificationData) {
|
||||
final json = jsonDecode(notificationData) as Map<String, dynamic>;
|
||||
final stringifiedType = json['notification_class_name'];
|
||||
final type = NotificationType.getTypeFromString(stringifiedType);
|
||||
|
||||
// Улучшенная логика определения типа уведомления
|
||||
String? stringifiedType;
|
||||
|
||||
// Сначала проверяем поле 'type' в корне JSON
|
||||
if (json.containsKey('type') && json['type'] != null) {
|
||||
stringifiedType = json['type'].toString();
|
||||
log('Found type in root: $stringifiedType');
|
||||
}
|
||||
// Затем проверяем поле 'notification_class_name' в корне JSON
|
||||
else if (json.containsKey('notification_class_name') && json['notification_class_name'] != null) {
|
||||
stringifiedType = json['notification_class_name'].toString();
|
||||
log('Found notification_class_name in root: $stringifiedType');
|
||||
}
|
||||
// Если есть поле 'data', проверяем внутри него
|
||||
else if (json.containsKey('data') && json['data'] is Map<String, dynamic>) {
|
||||
final data = json['data'] as Map<String, dynamic>;
|
||||
if (data.containsKey('type') && data['type'] != null) {
|
||||
stringifiedType = data['type'].toString();
|
||||
log('Found type in data: $stringifiedType');
|
||||
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
|
||||
stringifiedType = data['notification_class_name'].toString();
|
||||
log('Found notification_class_name in data: $stringifiedType');
|
||||
}
|
||||
}
|
||||
|
||||
// Если тип не найден, используем unknown
|
||||
final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown;
|
||||
log('Final type: $type, stringifiedType: $stringifiedType');
|
||||
|
||||
final notification = _parseNotification(json, type);
|
||||
_notifications.insert(0, notification);
|
||||
|
||||
|
||||
@@ -47,6 +47,72 @@ class NotificationWidget extends StatelessWidget {
|
||||
CommentReplied() => _CommentRepliedWidget(notification: notification as CommentReplied),
|
||||
ScoreAdded() => _ScoreAddedWidget(notification: notification as ScoreAdded),
|
||||
EntityCommented() => _EntityCommentedWidget(notification: notification as EntityCommented),
|
||||
// Новые типы уведомлений
|
||||
PlanSubscriptionExpiredNotification() => _PlanSubscriptionExpiredWidget(notification: notification as PlanSubscriptionExpiredNotification),
|
||||
PlanSubscriptionRenewedNotification() => _PlanSubscriptionRenewedWidget(notification: notification as PlanSubscriptionRenewedNotification),
|
||||
ConsultantAssignedNotification() => _ConsultantAssignedWidget(notification: notification as ConsultantAssignedNotification),
|
||||
ConsultantUnassignedNotification() => _ConsultantUnassignedWidget(notification: notification as ConsultantUnassignedNotification),
|
||||
PaymentFailedNotification() => _PaymentFailedWidget(notification: notification as PaymentFailedNotification),
|
||||
PaymentSuccessfulNotification() => _PaymentSuccessfulWidget(notification: notification as PaymentSuccessfulNotification),
|
||||
GoalAchievedNotification() => _GoalAchievedWidget(notification: notification as GoalAchievedNotification),
|
||||
WorkoutReminderNotification() => _WorkoutReminderWidget(notification: notification as WorkoutReminderNotification),
|
||||
MealReminderNotification() => _MealReminderWidget(notification: notification as MealReminderNotification),
|
||||
// New notification types
|
||||
DietCancelledNotification() => _DietCancelledWidget(notification: notification as DietCancelledNotification),
|
||||
DietStartedNotification() => _DietStartedWidget(notification: notification as DietStartedNotification),
|
||||
DietExpiredNotification() => _DietExpiredWidget(notification: notification as DietExpiredNotification),
|
||||
DietWillStartedInNotification() => _DietWillStartedInWidget(notification: notification as DietWillStartedInNotification),
|
||||
DietCanNotBeStartedNotification() => _DietCanNotBeStartedWidget(notification: notification as DietCanNotBeStartedNotification),
|
||||
PersonalDietCreationNeededNotification() => _PersonalDietCreationNeededWidget(notification: notification as PersonalDietCreationNeededNotification),
|
||||
ChosenPlanClientLastDayFinishedNotification() => _ChosenPlanClientLastDayFinishedWidget(
|
||||
notification: notification as ChosenPlanClientLastDayFinishedNotification,
|
||||
),
|
||||
ChosenPlanClientLastDayStartedNotification() => _ChosenPlanClientLastDayStartedWidget(
|
||||
notification: notification as ChosenPlanClientLastDayStartedNotification,
|
||||
),
|
||||
StayOnTrackToAuthorNotification() => _StayOnTrackToAuthorWidget(notification: notification as StayOnTrackToAuthorNotification),
|
||||
ClientStartPlanSubscriptionToSubscriberNotification() => _ClientStartPlanSubscriptionToSubscriberWidget(
|
||||
notification: notification as ClientStartPlanSubscriptionToSubscriberNotification,
|
||||
),
|
||||
ConsultantConfirmPlanSubscriptionToSubscriberNotification() => _ConsultantConfirmPlanSubscriptionToSubscriberWidget(
|
||||
notification: notification as ConsultantConfirmPlanSubscriptionToSubscriberNotification,
|
||||
),
|
||||
ClientPaidPlanSubscriptionToAuthorNotification() => _ClientPaidPlanSubscriptionToAuthorWidget(
|
||||
notification: notification as ClientPaidPlanSubscriptionToAuthorNotification,
|
||||
),
|
||||
ConsultantLeftDiaryCommentNotification() => _ConsultantLeftDiaryCommentWidget(notification: notification as ConsultantLeftDiaryCommentNotification),
|
||||
ConsultantConfirmPlanSubscriptionToAuthorNotification() => _ConsultantConfirmPlanSubscriptionToAuthorWidget(
|
||||
notification: notification as ConsultantConfirmPlanSubscriptionToAuthorNotification,
|
||||
),
|
||||
ClientPaidPlanSubscriptionToSubscriberNotification() => _ClientPaidPlanSubscriptionToSubscriberWidget(
|
||||
notification: notification as ClientPaidPlanSubscriptionToSubscriberNotification,
|
||||
),
|
||||
ClientStartPlanSubscriptionToAuthorNotification() => _ClientStartPlanSubscriptionToAuthorWidget(
|
||||
notification: notification as ClientStartPlanSubscriptionToAuthorNotification,
|
||||
),
|
||||
ClientCancelPlanSubscriptionToAuthorNotification() => _ClientCancelPlanSubscriptionToAuthorWidget(
|
||||
notification: notification as ClientCancelPlanSubscriptionToAuthorNotification,
|
||||
),
|
||||
ClientCancelPlanSubscriptionToSubscriberNotification() => _ClientCancelPlanSubscriptionToSubscriberWidget(
|
||||
notification: notification as ClientCancelPlanSubscriptionToSubscriberNotification,
|
||||
),
|
||||
UserDailyReportCompletedNotification() => _UserDailyReportCompletedWidget(notification: notification as UserDailyReportCompletedNotification),
|
||||
StayOnTrackToCustomerNotification() => _StayOnTrackToCustomerWidget(notification: notification as StayOnTrackToCustomerNotification),
|
||||
PhoneChangedNotification() => _PhoneChangedWidget(notification: notification as PhoneChangedNotification),
|
||||
SubscriptionNotification() => _SubscriptionWidget(notification: notification as SubscriptionNotification),
|
||||
BadgeAwardedNotification() => _BadgeAwardedWidget(notification: notification as BadgeAwardedNotification),
|
||||
BadgeRevokedNotification() => _BadgeRevokedWidget(notification: notification as BadgeRevokedNotification),
|
||||
ConsultantRequestStartedNotification() => _ConsultantRequestStartedWidget(notification: notification as ConsultantRequestStartedNotification),
|
||||
ClientCloseComplaintSuccessNotification() => _ClientCloseComplaintSuccessWidget(notification: notification as ClientCloseComplaintSuccessNotification),
|
||||
ClientRefundSuccessNotification() => _ClientRefundSuccessWidget(notification: notification as ClientRefundSuccessNotification),
|
||||
ConsultantRefundSuccessNotification() => _ConsultantRefundSuccessWidget(notification: notification as ConsultantRefundSuccessNotification),
|
||||
ClientRejectSuccessNotification() => _ClientRejectSuccessWidget(notification: notification as ClientRejectSuccessNotification),
|
||||
ConsultantRejectSuccessNotification() => _ConsultantRejectSuccessWidget(notification: notification as ConsultantRejectSuccessNotification),
|
||||
ScoreSubstractedNotification() => _ScoreSubstractedWidget(notification: notification as ScoreSubstractedNotification),
|
||||
ProductStatusUpdatedForAuthorNotification() => _ProductStatusUpdatedForAuthorWidget(
|
||||
notification: notification as ProductStatusUpdatedForAuthorNotification,
|
||||
),
|
||||
DiaryReminderForUsersNotification() => _DiaryReminderForUsersWidget(notification: notification as DiaryReminderForUsersNotification),
|
||||
UnknownNotification() => _UnknownNotificationWidget(notification: notification as UnknownNotification),
|
||||
};
|
||||
}
|
||||
@@ -443,3 +509,547 @@ class _UnknownNotificationWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Новые виджеты для дополнительных типов уведомлений
|
||||
class _PlanSubscriptionExpiredWidget extends StatelessWidget {
|
||||
const _PlanSubscriptionExpiredWidget({required this.notification});
|
||||
final PlanSubscriptionExpiredNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка на план "${notification.planName}" истекла',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PlanSubscriptionRenewedWidget extends StatelessWidget {
|
||||
const _PlanSubscriptionRenewedWidget({required this.notification});
|
||||
final PlanSubscriptionRenewedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка на план "${notification.planName}" продлена до ${notification.renewalDate}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantAssignedWidget extends StatelessWidget {
|
||||
const _ConsultantAssignedWidget({required this.notification});
|
||||
final ConsultantAssignedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'К плану "${notification.planName}" назначен консультант ${notification.consultantName}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantUnassignedWidget extends StatelessWidget {
|
||||
const _ConsultantUnassignedWidget({required this.notification});
|
||||
final ConsultantUnassignedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Консультант ${notification.consultantName} отстранен от плана "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaymentFailedWidget extends StatelessWidget {
|
||||
const _PaymentFailedWidget({required this.notification});
|
||||
final PaymentFailedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Ошибка оплаты плана "${notification.planName}": ${notification.errorMessage}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PaymentSuccessfulWidget extends StatelessWidget {
|
||||
const _PaymentSuccessfulWidget({required this.notification});
|
||||
final PaymentSuccessfulNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Успешная оплата плана "${notification.planName}" на сумму ${notification.amount} ${notification.currency}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GoalAchievedWidget extends StatelessWidget {
|
||||
const _GoalAchievedWidget({required this.notification});
|
||||
final GoalAchievedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Достигнута цель "${notification.goalName}": ${notification.achievedValue}/${notification.targetValue}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WorkoutReminderWidget extends StatelessWidget {
|
||||
const _WorkoutReminderWidget({required this.notification});
|
||||
final WorkoutReminderNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Напоминание о тренировке "${notification.workoutName}" (${notification.duration} мин)',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MealReminderWidget extends StatelessWidget {
|
||||
const _MealReminderWidget({required this.notification});
|
||||
final MealReminderNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Напоминание о приеме пищи "${notification.mealName}" (${notification.calories} ккал)',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// New notification widget classes
|
||||
class _DietCancelledWidget extends StatelessWidget {
|
||||
const _DietCancelledWidget({required this.notification});
|
||||
final DietCancelledNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Диета "${notification.dietName}" отменена',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DietStartedWidget extends StatelessWidget {
|
||||
const _DietStartedWidget({required this.notification});
|
||||
final DietStartedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Диета "${notification.dietName}" началась',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DietExpiredWidget extends StatelessWidget {
|
||||
const _DietExpiredWidget({required this.notification});
|
||||
final DietExpiredNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Диета "${notification.dietName}" истекла',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DietWillStartedInWidget extends StatelessWidget {
|
||||
const _DietWillStartedInWidget({required this.notification});
|
||||
final DietWillStartedInNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Диета "${notification.dietName}" начнется ${notification.startDate}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DietCanNotBeStartedWidget extends StatelessWidget {
|
||||
const _DietCanNotBeStartedWidget({required this.notification});
|
||||
final DietCanNotBeStartedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Диета "${notification.dietName}" не может быть начата: ${notification.reason}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PersonalDietCreationNeededWidget extends StatelessWidget {
|
||||
const _PersonalDietCreationNeededWidget({required this.notification});
|
||||
final PersonalDietCreationNeededNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Требуется создание персональной диеты для клиента ${notification.clientName} по плану "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChosenPlanClientLastDayFinishedWidget extends StatelessWidget {
|
||||
const _ChosenPlanClientLastDayFinishedWidget({required this.notification});
|
||||
final ChosenPlanClientLastDayFinishedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Последний день плана "${notification.planName}" завершен',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChosenPlanClientLastDayStartedWidget extends StatelessWidget {
|
||||
const _ChosenPlanClientLastDayStartedWidget({required this.notification});
|
||||
final ChosenPlanClientLastDayStartedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Последний день плана "${notification.planName}" начался',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StayOnTrackToAuthorWidget extends StatelessWidget {
|
||||
const _StayOnTrackToAuthorWidget({required this.notification});
|
||||
final StayOnTrackToAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Клиент ${notification.clientName} остается на плане "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientStartPlanSubscriptionToSubscriberWidget extends StatelessWidget {
|
||||
const _ClientStartPlanSubscriptionToSubscriberWidget({required this.notification});
|
||||
final ClientStartPlanSubscriptionToSubscriberNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка на план "${notification.planName}" началась',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantConfirmPlanSubscriptionToSubscriberWidget extends StatelessWidget {
|
||||
const _ConsultantConfirmPlanSubscriptionToSubscriberWidget({required this.notification});
|
||||
final ConsultantConfirmPlanSubscriptionToSubscriberNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientPaidPlanSubscriptionToAuthorWidget extends StatelessWidget {
|
||||
const _ClientPaidPlanSubscriptionToAuthorWidget({required this.notification});
|
||||
final ClientPaidPlanSubscriptionToAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Клиент ${notification.clientName} оплатил подписку на план "${notification.planName}" на сумму ${notification.amount}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantLeftDiaryCommentWidget extends StatelessWidget {
|
||||
const _ConsultantLeftDiaryCommentWidget({required this.notification});
|
||||
final ConsultantLeftDiaryCommentNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Консультант ${notification.consultantName} оставил комментарий к дневнику по плану "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantConfirmPlanSubscriptionToAuthorWidget extends StatelessWidget {
|
||||
const _ConsultantConfirmPlanSubscriptionToAuthorWidget({required this.notification});
|
||||
final ConsultantConfirmPlanSubscriptionToAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Консультант ${notification.consultantName} подтвердил подписку на план "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientPaidPlanSubscriptionToSubscriberWidget extends StatelessWidget {
|
||||
const _ClientPaidPlanSubscriptionToSubscriberWidget({required this.notification});
|
||||
final ClientPaidPlanSubscriptionToSubscriberNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Оплачена подписка на план "${notification.planName}" на сумму ${notification.amount}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientStartPlanSubscriptionToAuthorWidget extends StatelessWidget {
|
||||
const _ClientStartPlanSubscriptionToAuthorWidget({required this.notification});
|
||||
final ClientStartPlanSubscriptionToAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Клиент ${notification.clientName} начал подписку на план "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientCancelPlanSubscriptionToAuthorWidget extends StatelessWidget {
|
||||
const _ClientCancelPlanSubscriptionToAuthorWidget({required this.notification});
|
||||
final ClientCancelPlanSubscriptionToAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Клиент ${notification.clientName} отменил подписку на план "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientCancelPlanSubscriptionToSubscriberWidget extends StatelessWidget {
|
||||
const _ClientCancelPlanSubscriptionToSubscriberWidget({required this.notification});
|
||||
final ClientCancelPlanSubscriptionToSubscriberNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка на план "${notification.planName}" отменена',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserDailyReportCompletedWidget extends StatelessWidget {
|
||||
const _UserDailyReportCompletedWidget({required this.notification});
|
||||
final UserDailyReportCompletedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Клиент ${notification.clientName} завершил ежедневный отчет по плану "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StayOnTrackToCustomerWidget extends StatelessWidget {
|
||||
const _StayOnTrackToCustomerWidget({required this.notification});
|
||||
final StayOnTrackToCustomerNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Консультант ${notification.consultantName} остается на плане "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PhoneChangedWidget extends StatelessWidget {
|
||||
const _PhoneChangedWidget({required this.notification});
|
||||
final PhoneChangedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Номер телефона изменен с ${notification.oldPhone} на ${notification.newPhone}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SubscriptionWidget extends StatelessWidget {
|
||||
const _SubscriptionWidget({required this.notification});
|
||||
final SubscriptionNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Подписка ${notification.subscriptionType} на план "${notification.planName}"',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BadgeAwardedWidget extends StatelessWidget {
|
||||
const _BadgeAwardedWidget({required this.notification});
|
||||
final BadgeAwardedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Получен значок "${notification.badgeName}": ${notification.badgeDescription}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BadgeRevokedWidget extends StatelessWidget {
|
||||
const _BadgeRevokedWidget({required this.notification});
|
||||
final BadgeRevokedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Отозван значок "${notification.badgeName}": ${notification.reason}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantRequestStartedWidget extends StatelessWidget {
|
||||
const _ConsultantRequestStartedWidget({required this.notification});
|
||||
final ConsultantRequestStartedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Запрос к консультанту ${notification.consultantName} начат',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientCloseComplaintSuccessWidget extends StatelessWidget {
|
||||
const _ClientCloseComplaintSuccessWidget({required this.notification});
|
||||
final ClientCloseComplaintSuccessNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Жалоба на ${notification.complaintType} "${notification.entityName}" успешно закрыта',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientRefundSuccessWidget extends StatelessWidget {
|
||||
const _ClientRefundSuccessWidget({required this.notification});
|
||||
final ClientRefundSuccessNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Возврат средств по плану "${notification.planName}" на сумму ${notification.amount}: ${notification.reason}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantRefundSuccessWidget extends StatelessWidget {
|
||||
const _ConsultantRefundSuccessWidget({required this.notification});
|
||||
final ConsultantRefundSuccessNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Возврат средств клиенту ${notification.clientName} по плану "${notification.planName}" на сумму ${notification.amount}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClientRejectSuccessWidget extends StatelessWidget {
|
||||
const _ClientRejectSuccessWidget({required this.notification});
|
||||
final ClientRejectSuccessNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Отклонение по плану "${notification.planName}": ${notification.reason}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsultantRejectSuccessWidget extends StatelessWidget {
|
||||
const _ConsultantRejectSuccessWidget({required this.notification});
|
||||
final ConsultantRejectSuccessNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Отклонение клиентом ${notification.clientName} по плану "${notification.planName}": ${notification.reason}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScoreSubstractedWidget extends StatelessWidget {
|
||||
const _ScoreSubstractedWidget({required this.notification});
|
||||
final ScoreSubstractedNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Вычтено ${notification.scoreValue} баллов за ${notification.scorableName}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProductStatusUpdatedForAuthorWidget extends StatelessWidget {
|
||||
const _ProductStatusUpdatedForAuthorWidget({required this.notification});
|
||||
final ProductStatusUpdatedForAuthorNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(
|
||||
isRead: notification.isRead,
|
||||
notificationsText: 'Статус продукта "${notification.productName}" изменен с ${notification.oldStatus} на ${notification.newStatus}',
|
||||
createdAt: notification.createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiaryReminderForUsersWidget extends StatelessWidget {
|
||||
const _DiaryReminderForUsersWidget({required this.notification});
|
||||
final DiaryReminderForUsersNotification notification;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _BaseNotificationWidget(isRead: notification.isRead, notificationsText: 'Напоминание о заполнении дневника', createdAt: notification.createdAt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,30 @@ class NotificationsService {
|
||||
|
||||
final notifications = data.map((notificationJson) {
|
||||
try {
|
||||
final stringifiedType = notificationJson['data']['notification_class_name'];
|
||||
// Улучшенная логика определения типа уведомления
|
||||
String? stringifiedType;
|
||||
|
||||
// Сначала проверяем поле 'type' в корне JSON
|
||||
if (notificationJson.containsKey('type') && notificationJson['type'] != null) {
|
||||
stringifiedType = notificationJson['type'].toString();
|
||||
}
|
||||
// Затем проверяем поле 'notification_class_name' в корне JSON
|
||||
else if (notificationJson.containsKey('notification_class_name') && notificationJson['notification_class_name'] != null) {
|
||||
stringifiedType = notificationJson['notification_class_name'].toString();
|
||||
}
|
||||
// Если есть поле 'data', проверяем внутри него
|
||||
else if (notificationJson.containsKey('data') && notificationJson['data'] is Map<String, dynamic>) {
|
||||
final data = notificationJson['data'] as Map<String, dynamic>;
|
||||
if (data.containsKey('type') && data['type'] != null) {
|
||||
stringifiedType = data['type'].toString();
|
||||
} else if (data.containsKey('notification_class_name') && data['notification_class_name'] != null) {
|
||||
stringifiedType = data['notification_class_name'].toString();
|
||||
}
|
||||
}
|
||||
|
||||
final readDate = notificationJson['read_at'];
|
||||
final createdAt = notificationJson['created_at'];
|
||||
final type = NotificationType.getTypeFromString(stringifiedType);
|
||||
final type = stringifiedType != null ? NotificationType.getTypeFromString(stringifiedType) : NotificationType.unknown;
|
||||
|
||||
return switch (type) {
|
||||
NotificationType.planSubscriptionStartedToAuthor => PlanSubscriptionStartedToAuthorNotification.fromJson(
|
||||
@@ -119,6 +139,274 @@ class NotificationsService {
|
||||
NotificationType.commentReplied => CommentReplied.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
NotificationType.scoreAdded => ScoreAdded.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
NotificationType.entityCommented => EntityCommented.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
// Новые типы уведомлений
|
||||
NotificationType.planSubscriptionExpired => PlanSubscriptionExpiredNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.planSubscriptionRenewed => PlanSubscriptionRenewedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantAssigned => ConsultantAssignedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantUnassigned => ConsultantUnassignedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.paymentFailed => PaymentFailedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.paymentSuccessful => PaymentSuccessfulNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.goalAchieved => GoalAchievedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.workoutReminder => WorkoutReminderNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.mealReminder => MealReminderNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
// New notification types
|
||||
NotificationType.dietCancelled => DietCancelledNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.dietStarted => DietStartedNotification.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
NotificationType.dietExpired => DietExpiredNotification.fromJson(notificationJson['data'], type, isRead: readDate != null, createdAt: createdAt),
|
||||
NotificationType.dietWillStartedIn => DietWillStartedInNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.dietCanNotBeStarted => DietCanNotBeStartedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.personalDietCreationNeeded => PersonalDietCreationNeededNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanClientLastDayFinished => ChosenPlanClientLastDayFinishedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanClientLastDayStarted => ChosenPlanClientLastDayStartedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.stayOnTrackToAuthor => StayOnTrackToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToClient => ChosenPlanStartedToClientNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientStartPlanSubscriptionToSubscriber => ClientStartPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantConfirmPlanSubscriptionToSubscriber => ConsultantConfirmPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientPaidPlanSubscriptionToAuthor => ClientPaidPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToClient => ChosenPlanTomorrowReminderToClientNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanTomorrowReminderToConsultant => ChosenPlanTomorrowReminderToConsultantNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.chosenPlanStartedToConsultant => ChosenPlanStartedToConsultantNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantLeftDiaryComment => ConsultantLeftDiaryCommentNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantConfirmPlanSubscriptionToAuthor => ConsultantConfirmPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientPaidPlanSubscriptionToSubscriber => ClientPaidPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientStartPlanSubscriptionToAuthor => ClientStartPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientCancelPlanSubscriptionToAuthor => ClientCancelPlanSubscriptionToAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientCancelPlanSubscriptionToSubscriber => ClientCancelPlanSubscriptionToSubscriberNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.userDailyReportCompleted => UserDailyReportCompletedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.stayOnTrackToCustomer => StayOnTrackToCustomerNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.phoneChanged => PhoneChangedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.subscription => SubscriptionNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.badgeAwarded => BadgeAwardedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.badgeRevoked => BadgeRevokedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantRequestStarted => ConsultantRequestStartedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientCloseComplaintSuccess => ClientCloseComplaintSuccessNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientRefundSuccess => ClientRefundSuccessNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantRefundSuccess => ConsultantRefundSuccessNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.clientRejectSuccess => ClientRejectSuccessNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.consultantRejectSuccess => ConsultantRejectSuccessNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.scoreSubstracted => ScoreSubstractedNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.productStatusUpdatedForAuthor => ProductStatusUpdatedForAuthorNotification.fromJson(
|
||||
notificationJson['data'],
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.diaryReminderForUsers => DiaryReminderForUsersNotification.fromJson(
|
||||
notificationJson,
|
||||
type,
|
||||
isRead: readDate != null,
|
||||
createdAt: createdAt,
|
||||
),
|
||||
NotificationType.unknown => UnknownNotification(type: type, createdAt: parseDate(createdAt)),
|
||||
};
|
||||
} catch (e) {
|
||||
|
||||
@@ -19,6 +19,7 @@ abstract class ProfileModel with _$ProfileModel {
|
||||
@JsonKey(name: "updated_at") DateTime? updatedAt,
|
||||
@JsonKey(name: "is_public_profile", fromJson: parseBool) required bool isPublicProfile,
|
||||
@JsonKey(name: "is_blocked_by_me", fromJson: parseBool) required bool isBlockedByMe,
|
||||
@JsonKey(name: "is_blocked_by_user", fromJson: parseBool, defaultValue: false) required bool isBlockedByUser,
|
||||
@JsonKey(name: 'is_entity_subscribed_by_me') required bool isEntitySubscribedByMe,
|
||||
@JsonKey(name: "slug") required String slug,
|
||||
@JsonKey(name: "profile_photo_url") required String profilePhotoUrl,
|
||||
|
||||
@@ -93,6 +93,31 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
}
|
||||
isFirstFetch = false;
|
||||
}
|
||||
|
||||
// Проверяем, заблокировал ли пользователь меня
|
||||
if (state.profile.isBlockedByUser) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.block, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Пользователь ограничил доступ к своей странице',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return widget.author == null || widget.author!.authorIsMe ? _myProfileScreen(state) : _profileScreen(state);
|
||||
} else if (state is ProfileStateError) {
|
||||
return Scaffold(
|
||||
@@ -174,10 +199,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
),
|
||||
_subscriptionsAndSubscribers(state),
|
||||
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _socialLinks(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _education(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null) _diplomasAndCertificates(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans) _activePlans(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _socialLinks(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _education(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !state.profile.isBlockedByMe) _diplomasAndCertificates(state),
|
||||
if (state.profile.roles.contains('consultant') && state.consultantInfo != null && !hidePlans && !state.profile.isBlockedByMe) _activePlans(state),
|
||||
if (state.blogPosts.isNotEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 8, top: 16, bottom: 8),
|
||||
child: Text(
|
||||
'Блог',
|
||||
style: TextStyle(color: Color(0xFF323F4B), fontSize: 24, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
_blogPosts(state),
|
||||
],
|
||||
),
|
||||
@@ -244,9 +277,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
_subscriptionsAndSubscribers(state),
|
||||
if (state.profile.badges.isNotEmpty) Padding(padding: const EdgeInsets.only(bottom: 16), child: _badges(state)),
|
||||
if (state.profile.roles.contains('consultant')) _socialLinks(state),
|
||||
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
|
||||
if (state.profile.roles.contains('consultant')) _education(state),
|
||||
if (state.profile.roles.contains('consultant')) _diplomasAndCertificates(state),
|
||||
Padding(padding: const EdgeInsets.only(bottom: 16), child: _charts()),
|
||||
if (state.profile.roles.contains('consultant') && !hidePlans) _activePlans(state),
|
||||
_newBlogPostButton(),
|
||||
_blogPosts(state),
|
||||
@@ -479,6 +512,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
children: [
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(0),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => _educationBlock(
|
||||
state.consultantInfo!.educationModel.completedCourses![index].values.first.title,
|
||||
@@ -733,7 +767,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
|
||||
ListView _blogPosts(ProfileStateLoaded state) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) => ProfileBlogPostWidget(index: index, profileCubit: profileCubit),
|
||||
|
||||
@@ -12,9 +12,7 @@ import 'package:eatvio_app/features/profile/services/profile_service.dart';
|
||||
class ProfileRepository {
|
||||
final ProfileService profileService;
|
||||
|
||||
ProfileRepository({
|
||||
required this.profileService,
|
||||
});
|
||||
ProfileRepository({required this.profileService});
|
||||
|
||||
Future<ProfileModel> getProfileBySlug(String slug) async {
|
||||
try {
|
||||
@@ -48,11 +46,7 @@ class ProfileRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addCircumference({
|
||||
required int chestCircumference,
|
||||
required int waistCircumference,
|
||||
required int hipCircumference,
|
||||
}) async {
|
||||
Future<void> addCircumference({required int chestCircumference, required int waistCircumference, required int hipCircumference}) async {
|
||||
try {
|
||||
return (await profileService.addCircumference(
|
||||
chestCircumference: chestCircumference,
|
||||
@@ -64,9 +58,7 @@ class ProfileRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addWeight({
|
||||
required double weight,
|
||||
}) async {
|
||||
Future<void> addWeight({required double weight}) async {
|
||||
try {
|
||||
return (await profileService.addWeight(weight: weight));
|
||||
} catch (e) {
|
||||
@@ -74,52 +66,32 @@ class ProfileRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<FeedBlogPostModel>> getBlogPostsByAuthorSlug({
|
||||
required String slug,
|
||||
}) async {
|
||||
Future<List<FeedBlogPostModel>> getBlogPostsByAuthorSlug({required String slug}) async {
|
||||
try {
|
||||
return (await profileService.getBlogPostByAuthorSlug(
|
||||
slug: slug,
|
||||
));
|
||||
return (await profileService.getBlogPostByAuthorSlug(slug: slug));
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<WeightModel>> getWeight({
|
||||
required String period,
|
||||
String? slug,
|
||||
}) async {
|
||||
Future<List<WeightModel>> getWeight({required String period, String? slug}) async {
|
||||
try {
|
||||
if (slug != null) {
|
||||
return (await profileService.getWeightBySlug(
|
||||
period: period,
|
||||
slug: slug,
|
||||
));
|
||||
return (await profileService.getWeightBySlug(period: period, slug: slug));
|
||||
} else {
|
||||
return (await profileService.getWeight(
|
||||
period: period,
|
||||
));
|
||||
return (await profileService.getWeight(period: period));
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<CircumferenceModel>> getCircumferences({
|
||||
required String period,
|
||||
String? slug,
|
||||
}) async {
|
||||
Future<List<CircumferenceModel>> getCircumferences({required String period, String? slug}) async {
|
||||
try {
|
||||
if (slug != null) {
|
||||
return (await profileService.getCircumferencesBySlug(
|
||||
period: period,
|
||||
slug: slug,
|
||||
));
|
||||
return (await profileService.getCircumferencesBySlug(period: period, slug: slug));
|
||||
} else {
|
||||
return (await profileService.getCircumferences(
|
||||
period: period,
|
||||
));
|
||||
return (await profileService.getCircumferences(period: period));
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
@@ -184,15 +156,20 @@ 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']!;
|
||||
(await profileService.getPassKey(json: '{"route_name": "user_detailz.change-phone-verify", "route_params": {"phone": "$phone"}}')),
|
||||
))['key_is']!;
|
||||
}
|
||||
|
||||
Future<String> getMyPhone() async {
|
||||
return (jsonDecode(await profileService.getMyPhone())['phone']);
|
||||
}
|
||||
|
||||
Future<void> sendEmailCode() async {
|
||||
return await profileService.sendEmailCode();
|
||||
Future<void> sendEmailCode({String? newEmail}) async {
|
||||
if (newEmail != null) {
|
||||
return await profileService.sendEmailCode(data: {'new_email': newEmail});
|
||||
} else {
|
||||
return await profileService.sendEmailCode();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> tryVerifyEmailByCode(String code) async {
|
||||
@@ -206,7 +183,7 @@ class ProfileRepository {
|
||||
|
||||
Future<void> deleteAccount() async {
|
||||
try {
|
||||
await profileService.deleteAccount();
|
||||
await profileService.deleteAccount();
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ abstract class ProfileService {
|
||||
factory ProfileService(Dio dio, {String baseUrl}) = _ProfileService;
|
||||
|
||||
@GET('/api/profile/show/{slug}')
|
||||
Future<ProfileModel> getProfileBySlug({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<ProfileModel> getProfileBySlug({@Path() required String slug});
|
||||
|
||||
@GET('/api/profile/index')
|
||||
Future<ProfileModel> getMyProfile();
|
||||
@@ -27,9 +25,7 @@ abstract class ProfileService {
|
||||
Future<ProfileDetailsModel> getMyProfileDetails();
|
||||
|
||||
@GET('/api/profile/detailz/{slug}')
|
||||
Future<ProfileDetailsModel> getProfileDetailsBySlug({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<ProfileDetailsModel> getProfileDetailsBySlug({@Path() required String slug});
|
||||
|
||||
@POST('/api/profile/addCircumference')
|
||||
Future<void> addCircumference({
|
||||
@@ -39,64 +35,38 @@ abstract class ProfileService {
|
||||
});
|
||||
|
||||
@POST('/api/profile/addWeight')
|
||||
Future<void> addWeight({
|
||||
@Part() required double weight,
|
||||
});
|
||||
Future<void> addWeight({@Part() required double weight});
|
||||
|
||||
@GET('/api/blogPosts/search_by_author/{slug}')
|
||||
Future<List<FeedBlogPostModel>> getBlogPostByAuthorSlug({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<List<FeedBlogPostModel>> getBlogPostByAuthorSlug({@Path() required String slug});
|
||||
|
||||
@GET('/api/profile/measure/weight/{period}')
|
||||
Future<List<WeightModel>> getWeight({
|
||||
@Path() required String period,
|
||||
});
|
||||
Future<List<WeightModel>> getWeight({@Path() required String period});
|
||||
|
||||
@GET('/api/profile/measure/weight/{period}/{slug}')
|
||||
Future<List<WeightModel>> getWeightBySlug({
|
||||
@Path() required String period,
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<List<WeightModel>> getWeightBySlug({@Path() required String period, @Path() required String slug});
|
||||
|
||||
@GET('/api/profile/measure/circumferences/{period}')
|
||||
Future<List<CircumferenceModel>> getCircumferences({
|
||||
@Path() required String period,
|
||||
});
|
||||
Future<List<CircumferenceModel>> getCircumferences({@Path() required String period});
|
||||
|
||||
@GET('/api/profile/measure/circumferences/{period}/{user}')
|
||||
Future<List<CircumferenceModel>> getCircumferencesBySlug({
|
||||
@Path() required String period,
|
||||
@Path() required String slug,
|
||||
});
|
||||
@GET('/api/profile/measure/circumferences/{period}/{slug}')
|
||||
Future<List<CircumferenceModel>> getCircumferencesBySlug({@Path() required String period, @Path() required String slug});
|
||||
|
||||
@GET('/api/profile/{slug}/subscribers')
|
||||
Future<SubPaginationModel> getSubscribers({
|
||||
@Path() required String slug,
|
||||
@Query('filters[name][\$startsWith]') String? query,
|
||||
});
|
||||
Future<SubPaginationModel> getSubscribers({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query});
|
||||
|
||||
@GET('/api/profile/{slug}/subscriptions')
|
||||
Future<SubPaginationModel> getSubscriptions({
|
||||
@Path() required String slug,
|
||||
@Query('filters[name][\$startsWith]') String? query,
|
||||
});
|
||||
Future<SubPaginationModel> getSubscriptions({@Path() required String slug, @Query('filters[name][\$startsWith]') String? query});
|
||||
|
||||
@POST('/api/profile/subscribe/{slug}')
|
||||
Future<void> subscribe({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<void> subscribe({@Path() required String slug});
|
||||
|
||||
@POST('/api/profile/unsubscribe/{slug}')
|
||||
Future<void> unsubscribe({
|
||||
@Path() required String slug,
|
||||
});
|
||||
Future<void> unsubscribe({@Path() required String slug});
|
||||
|
||||
@POST('/api/profile/photo/upload')
|
||||
@MultiPart()
|
||||
Future<void> uploadPhoto({
|
||||
@Part(name: 'photo') required File photo,
|
||||
});
|
||||
Future<void> uploadPhoto({@Part(name: 'photo') required File photo});
|
||||
|
||||
@DELETE('/api/profile/photo/delete')
|
||||
Future<void> deletePhoto();
|
||||
@@ -105,20 +75,16 @@ abstract class ProfileService {
|
||||
Future<ChosenDietsPaginationModel> getChosenDiets();
|
||||
|
||||
@POST('/api/profile/make_pass_key')
|
||||
Future<String> getPassKey({
|
||||
@Body() required String json,
|
||||
});
|
||||
Future<String> getPassKey({@Body() required String json});
|
||||
|
||||
@GET('/api/profile/my_phone')
|
||||
Future<String> getMyPhone();
|
||||
|
||||
@POST('/api/profile/send_email_code')
|
||||
Future<void> sendEmailCode();
|
||||
Future<void> sendEmailCode({@Body() Map<String, dynamic> data = const {}});
|
||||
|
||||
@POST('/api/profile/verificate_email_code')
|
||||
Future<void> tryVerifyEmailByCode({
|
||||
@Body() required String code,
|
||||
});
|
||||
Future<void> tryVerifyEmailByCode({@Body() required String code});
|
||||
|
||||
@DELETE('/api/profile/delete_my_account')
|
||||
Future<void> deleteAccount();
|
||||
|
||||
@@ -8,62 +8,61 @@ import 'package:eatvio_app/core/token_storage.dart';
|
||||
import 'package:eatvio_app/features/registration/models/user_registration_model.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/registration_cubit/registration_state.dart';
|
||||
import 'package:eatvio_app/features/registration/repositories/registration_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class RegistrationCubit extends Cubit<RegistrationState> {
|
||||
final RegistrationRepository registrationRepository;
|
||||
|
||||
RegistrationCubit({
|
||||
required this.registrationRepository,
|
||||
}) : super(
|
||||
const RegistrationState(
|
||||
sheet: UserRegistrationSheet(
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
birthdate: '',
|
||||
gender: '',
|
||||
location: '',
|
||||
responseBody: '{}',
|
||||
lang: 'Русский',
|
||||
weight: 0,
|
||||
height: 0,
|
||||
chestCircumference: 0,
|
||||
waistCircumference: 0,
|
||||
hipCircumference: 0,
|
||||
bloodPressureUp: 0,
|
||||
bloodPressureDown: 0,
|
||||
hasHypo: 0,
|
||||
hasHyper: 0,
|
||||
aboutAllergy: '',
|
||||
aboutTakingPils: '',
|
||||
aboutSportHobbies: '',
|
||||
favoriteFoods: '',
|
||||
unfavoriteFoods: '',
|
||||
additionalInfo: '',
|
||||
phone: '',
|
||||
),
|
||||
RegistrationCubit({required this.registrationRepository})
|
||||
: super(
|
||||
const RegistrationState(
|
||||
sheet: UserRegistrationSheet(
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirmation: '',
|
||||
birthdate: '',
|
||||
gender: '',
|
||||
location: '',
|
||||
responseBody: '{}',
|
||||
lang: 'Русский',
|
||||
weight: 0,
|
||||
height: 0,
|
||||
chestCircumference: 0,
|
||||
waistCircumference: 0,
|
||||
hipCircumference: 0,
|
||||
bloodPressureUp: 0,
|
||||
bloodPressureDown: 0,
|
||||
hasHypo: 0,
|
||||
hasHyper: 0,
|
||||
aboutAllergy: '',
|
||||
aboutTakingPils: '',
|
||||
aboutSportHobbies: '',
|
||||
favoriteFoods: '',
|
||||
unfavoriteFoods: '',
|
||||
additionalInfo: '',
|
||||
phone: '',
|
||||
),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
void updateSheet(UserRegistrationSheet sheet) => emit(RegistrationState(sheet: sheet));
|
||||
|
||||
Future<void> tryRegistration() async {
|
||||
try {
|
||||
final token = await registrationRepository.tryRegister(
|
||||
state.sheet.copyWith(
|
||||
phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''),
|
||||
responseBody: '{}',
|
||||
),
|
||||
state.sheet.copyWith(phone: state.sheet.phone.replaceAll(' ', '').replaceAll('(', '').replaceAll(')', ''), responseBody: '{}'),
|
||||
);
|
||||
await TokenStorage.saveTokenInStorage(token);
|
||||
await sl<FirebaseApi>().registerPushToken();
|
||||
RegistrationAnalyticsService.reportRegistrationComplete();
|
||||
AppRouter.navigatorKey.currentContext?.replaceNamed('verifyEmailScreen');
|
||||
} catch (e, st) {
|
||||
log(e.toString(), stackTrace: st);
|
||||
RegistrationAnalyticsService.reportRegistrationError(e.toString());
|
||||
emit(RegistrationState(sheet: state.sheet, error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:eatvio_app/features/profile/repositories/profile_repository.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class VerifyEmailCubit extends Cubit<VerifyEmailState> {
|
||||
final ProfileRepository profileRepository;
|
||||
|
||||
VerifyEmailCubit({
|
||||
required this.profileRepository,
|
||||
}) : super(const VerifyEmailState.loading());
|
||||
VerifyEmailCubit({required this.profileRepository}) : super(const VerifyEmailState.loading());
|
||||
|
||||
Future<void> sendEmailCode() async {
|
||||
try {
|
||||
@@ -24,6 +23,7 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
|
||||
emit(const VerifyEmailState.verifyLoading());
|
||||
final result = await profileRepository.tryVerifyEmailByCode(code);
|
||||
if (result == true) {
|
||||
RegistrationAnalyticsService.reportEmailVerification();
|
||||
emit(const VerifyEmailState.verifySuccess());
|
||||
} else {
|
||||
emit(const VerifyEmailState.verifyError());
|
||||
@@ -32,4 +32,16 @@ class VerifyEmailCubit extends Cubit<VerifyEmailState> {
|
||||
emit(const VerifyEmailState.verifyError());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changeEmail(String newEmail) async {
|
||||
try {
|
||||
emit(const VerifyEmailState.changeEmailLoading());
|
||||
await profileRepository.sendEmailCode(newEmail: newEmail);
|
||||
emit(const VerifyEmailState.changeEmailSuccess());
|
||||
// Обновляем email в состоянии после успешной смены
|
||||
emit(VerifyEmailState.loaded(email: newEmail));
|
||||
} catch (e) {
|
||||
emit(const VerifyEmailState.changeEmailError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,9 @@ part 'verify_email_state.freezed.dart';
|
||||
abstract class VerifyEmailState with _$VerifyEmailState {
|
||||
const factory VerifyEmailState.loading() = VerifyEmailStateLoading;
|
||||
|
||||
const factory VerifyEmailState.loaded({
|
||||
required String email,
|
||||
}) = VerifyEmailStateLoaded;
|
||||
const factory VerifyEmailState.loaded({required String email}) = VerifyEmailStateLoaded;
|
||||
|
||||
const factory VerifyEmailState.error({
|
||||
required String errorMessage,
|
||||
}) = VerifyEmailStateError;
|
||||
const factory VerifyEmailState.error({required String errorMessage}) = VerifyEmailStateError;
|
||||
|
||||
const factory VerifyEmailState() = _VerifyEmailState;
|
||||
|
||||
@@ -21,4 +17,10 @@ abstract class VerifyEmailState with _$VerifyEmailState {
|
||||
const factory VerifyEmailState.verifySuccess() = VerifyEmailStateVerifySuccess;
|
||||
|
||||
const factory VerifyEmailState.verifyError() = VerifyEmailStateVerifyError;
|
||||
|
||||
const factory VerifyEmailState.changeEmailLoading() = VerifyEmailStateChangeEmailLoading;
|
||||
|
||||
const factory VerifyEmailState.changeEmailSuccess() = VerifyEmailStateChangeEmailSuccess;
|
||||
|
||||
const factory VerifyEmailState.changeEmailError() = VerifyEmailStateChangeEmailError;
|
||||
}
|
||||
|
||||
@@ -27,48 +27,46 @@ class _GenderScreenState extends State<GenderScreen> {
|
||||
padding: const EdgeInsets.only(bottom: 80, top: 16, left: 16, right: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
RadioListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: const Text('Мужской'),
|
||||
value: 'male',
|
||||
groupValue: currentValue,
|
||||
onChanged: (newValue) {
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'male'));
|
||||
setState(() {
|
||||
currentValue = 'male';
|
||||
});
|
||||
},
|
||||
secondary: Image.asset(
|
||||
'assets/images/img_male.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: 'male',
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: currentValue,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (newValue) {
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'male'));
|
||||
setState(() {
|
||||
currentValue = 'male';
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Row(children: [Image.asset('assets/images/img_male.png', width: 40, height: 40), const SizedBox(width: 8), const Text('Мужской')]),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
RadioListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: const Text('Женский'),
|
||||
value: 'female',
|
||||
groupValue: currentValue,
|
||||
onChanged: (newValue) {
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'female'));
|
||||
setState(() {
|
||||
currentValue = 'female';
|
||||
});
|
||||
},
|
||||
secondary: Image.asset(
|
||||
'assets/images/img_female.png',
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Radio<String>(
|
||||
value: 'female',
|
||||
// ignore: deprecated_member_use
|
||||
groupValue: currentValue,
|
||||
// ignore: deprecated_member_use
|
||||
onChanged: (newValue) {
|
||||
final registrationCubit = context.read<RegistrationCubit>();
|
||||
registrationCubit.updateSheet(registrationCubit.state.sheet.copyWith(gender: 'female'));
|
||||
setState(() {
|
||||
currentValue = 'female';
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Row(children: [Image.asset('assets/images/img_female.png', width: 40, height: 40), const SizedBox(width: 8), const Text('Женский')]),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -17,6 +17,7 @@ import 'package:eatvio_app/features/registration/presentation/pages/medicine_int
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/name_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/phone_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/pages/weight_screen.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
@@ -39,6 +40,12 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
final pageController = PageController();
|
||||
final RegistrationCubit registrationCubit = sl<RegistrationCubit>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
RegistrationAnalyticsService.reportRegistrationStep(step);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<RegistrationCubit>(
|
||||
@@ -51,12 +58,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка при регистрации'),
|
||||
content: Text(state.error!),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -76,22 +78,14 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: const Color(0xFFEAF2FD),
|
||||
onPressed: () async {
|
||||
setState(() {
|
||||
pageController.previousPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
pageController.previousPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
||||
});
|
||||
},
|
||||
label: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
label: Icon(Icons.arrow_back_ios_new_rounded, color: appTheme.blueA400),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
@@ -103,24 +97,16 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
highlightElevation: 0,
|
||||
elevation: 0,
|
||||
focusElevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
backgroundColor: appTheme.blueA400,
|
||||
onPressed: () async {
|
||||
if (await canGoNext(state, context)) {
|
||||
setState(() {
|
||||
pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
pageController.nextPage(duration: const Duration(milliseconds: 350), curve: Curves.easeInOut);
|
||||
});
|
||||
}
|
||||
},
|
||||
label: Text(
|
||||
'Продолжить',
|
||||
style: theme.textTheme.titleSmall!.copyWith(color: Colors.white),
|
||||
),
|
||||
label: Text('Продолжить', style: theme.textTheme.titleSmall!.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -128,23 +114,11 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
appBar: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
leading: Builder(
|
||||
builder: (context) => IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () => GoRouter.of(context).pop(),
|
||||
),
|
||||
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 из 15'),
|
||||
),
|
||||
],
|
||||
title: SvgPicture.asset(ImageConstant.imgLogoEatvio, height: 48, fit: BoxFit.contain),
|
||||
actions: [Padding(padding: const EdgeInsets.only(right: 16), child: Text('шаг $step из 15'))],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(MediaQuery.of(context).size.width, 32),
|
||||
child: Padding(
|
||||
@@ -152,10 +126,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: const Duration(milliseconds: 350),
|
||||
curve: Curves.easeInOut,
|
||||
tween: Tween<double>(
|
||||
begin: 1 / 15,
|
||||
end: progressValue,
|
||||
),
|
||||
tween: Tween<double>(begin: 1 / 15, end: progressValue),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
value: value,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -175,6 +146,8 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
step = index + 1;
|
||||
progressValue = step / 15;
|
||||
});
|
||||
// Report analytics for step completion
|
||||
RegistrationAnalyticsService.reportRegistrationStep(step);
|
||||
},
|
||||
children: [
|
||||
// LanguageSelectScreen(sheet: state.sheet),
|
||||
@@ -211,12 +184,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: const Text('Введите корректный номер телефона'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
@@ -224,12 +192,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
return true;
|
||||
}
|
||||
case 2:
|
||||
List<String> errors = validateCredentialsFields(
|
||||
state.sheet.email,
|
||||
state.sheet.password,
|
||||
state.sheet.passwordConfirmation,
|
||||
context,
|
||||
);
|
||||
List<String> errors = validateCredentialsFields(state.sheet.email, state.sheet.password, state.sheet.passwordConfirmation, context);
|
||||
if (errors.isEmpty) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -244,22 +207,13 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
case 3:
|
||||
List<String> errors = validateNameFields(
|
||||
state.sheet.firstName.toString(),
|
||||
state.sheet.lastName.toString(),
|
||||
context,
|
||||
);
|
||||
List<String> errors = validateNameFields(state.sheet.firstName.toString(), state.sheet.lastName.toString(), context);
|
||||
if (errors.isEmpty) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -274,12 +228,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
@@ -293,12 +242,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: const Text('Выберете пол'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
@@ -312,12 +256,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: const Text('Заполните все поля'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
@@ -343,12 +282,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Ошибка'),
|
||||
content: Text(errorMessage),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('OK'))],
|
||||
),
|
||||
);
|
||||
return false;
|
||||
@@ -359,10 +293,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
barrierDismissible: false,
|
||||
useSafeArea: false,
|
||||
context: context,
|
||||
builder: (context) => const PopScope(
|
||||
canPop: false,
|
||||
child: Loader(),
|
||||
),
|
||||
builder: (context) => const PopScope(canPop: false, child: Loader()),
|
||||
);
|
||||
await registrationCubit.tryRegistration();
|
||||
if (context.mounted) {
|
||||
@@ -373,9 +304,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
Navigator.pop(context);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => const AlertDialog(
|
||||
title: Text('Ошибка при попытке регистрации'),
|
||||
),
|
||||
builder: (context) => const AlertDialog(title: Text('Ошибка при попытке регистрации')),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -385,12 +314,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
List<String> validateCircumferencesFields(
|
||||
String chestCircumference,
|
||||
String waistCircumference,
|
||||
String hipCircumference,
|
||||
BuildContext context,
|
||||
) {
|
||||
List<String> validateCircumferencesFields(String chestCircumference, String waistCircumference, String hipCircumference, BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
List<String> errors = [];
|
||||
@@ -406,12 +330,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
return errors;
|
||||
}
|
||||
|
||||
List<String> validateCredentialsFields(
|
||||
String email,
|
||||
String password,
|
||||
String passwordConfirmation,
|
||||
BuildContext context,
|
||||
) {
|
||||
List<String> validateCredentialsFields(String email, String password, String passwordConfirmation, BuildContext context) {
|
||||
final s = S.of(context);
|
||||
|
||||
List<String> errors = [];
|
||||
@@ -435,11 +354,7 @@ class _RegistrationScreenState extends State<RegistrationScreen> {
|
||||
return errors;
|
||||
}
|
||||
|
||||
List<String> validateNameFields(
|
||||
String firstName,
|
||||
String lastName,
|
||||
BuildContext context,
|
||||
) {
|
||||
List<String> validateNameFields(String firstName, String lastName, BuildContext context) {
|
||||
List<String> errors = [];
|
||||
|
||||
if (firstName.isEmpty) {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/widgets/change_email_dialog.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/widgets/code_not_received_dialog.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -18,11 +26,17 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
final List<TextEditingController> _controllers = List.generate(4, (_) => TextEditingController());
|
||||
String email = '';
|
||||
late final GoRouter goRouter;
|
||||
late Timer _timer;
|
||||
int _secondsRemaining = 60;
|
||||
bool _canSendAgain = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
goRouter = GoRouter.of(context);
|
||||
_startTimer();
|
||||
// Report email verification screen opened
|
||||
RegistrationAnalyticsService.reportRegistrationStep(16); // Email verification step
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -36,6 +50,23 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _startTimer() {
|
||||
setState(() {
|
||||
_canSendAgain = false;
|
||||
_secondsRemaining = 60;
|
||||
});
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||
setState(() {
|
||||
if (_secondsRemaining > 0) {
|
||||
_secondsRemaining--;
|
||||
} else {
|
||||
_canSendAgain = true;
|
||||
_timer.cancel();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _checkAndSubmitCode(BuildContext context) {
|
||||
final code = _controllers.map((c) => c.text).join();
|
||||
if (code.length == 4) {
|
||||
@@ -47,48 +78,96 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<VerifyEmailCubit>(
|
||||
create: (_) => sl<VerifyEmailCubit>()..sendEmailCode(),
|
||||
child: BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
|
||||
listener: (context, state) {
|
||||
if (state is VerifyEmailStateLoaded) {
|
||||
setState(() {
|
||||
email = state.email;
|
||||
});
|
||||
} else if (state is VerifyEmailStateVerifySuccess) {
|
||||
showSuccessToast('Почта успешно подтверждена');
|
||||
goRouter.pushNamed('feedScreen');
|
||||
} else if (state is VerifyEmailStateVerifyError) {
|
||||
showErrorToast("Неверный код. Попробуйте снова.");
|
||||
for (var controller in _controllers) {
|
||||
controller.clear();
|
||||
}
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Подтвердите почту")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: List.generate(4, (index) => _buildCodeField(context, index)),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return BlocConsumer<VerifyEmailCubit, VerifyEmailState>(
|
||||
listener: (context, state) {
|
||||
if (state is VerifyEmailStateLoaded) {
|
||||
setState(() {
|
||||
email = state.email;
|
||||
});
|
||||
_startTimer();
|
||||
} else if (state is VerifyEmailStateVerifySuccess) {
|
||||
showSuccessToast('Почта успешно подтверждена');
|
||||
goRouter.pushNamed('feedScreen');
|
||||
} else if (state is VerifyEmailStateVerifyError) {
|
||||
showErrorToast("Неверный код. Попробуйте снова.");
|
||||
for (var controller in _controllers) {
|
||||
controller.clear();
|
||||
}
|
||||
FocusScope.of(context).requestFocus(_focusNodes[0]);
|
||||
} else if (state is VerifyEmailStateChangeEmailSuccess) {
|
||||
showSuccessToast('Код подтверждения отправлен на новый email');
|
||||
// Диалог закроется автоматически в ChangeEmailDialog при успешной смене
|
||||
} else if (state is VerifyEmailStateChangeEmailError) {
|
||||
showErrorToast('Ошибка при смене email. Попробуйте снова.');
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text("Подтвердите почту")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("Код отправлен на почту\n$email", textAlign: TextAlign.center),
|
||||
const SizedBox(height: 20),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: List.generate(4, (index) => _buildCodeField(context, index))),
|
||||
const SizedBox(height: 16),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
final verifyEmailCubit = context.read<VerifyEmailCubit>();
|
||||
showDialog(context: context, builder: (context) => CodeNotReceivedDialog(
|
||||
currentEmail: email,
|
||||
verifyEmailCubit: verifyEmailCubit,
|
||||
));
|
||||
},
|
||||
child: Text(
|
||||
'Не приходит код?',
|
||||
style: TextStyles.textSMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
CustomOutlinedButton(
|
||||
text: 'Запросить код повторно',
|
||||
height: 52,
|
||||
isDisabled: !_canSendAgain,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(_canSendAgain ? appTheme.blueA400 : appTheme.gray),
|
||||
),
|
||||
onPressed: () async {
|
||||
await context.read<VerifyEmailCubit>().sendEmailCode();
|
||||
_startTimer();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (!_canSendAgain)
|
||||
Text("Зпросить код повторно можно через $_secondsRemaining с.", style: TextStyles.textSMedium.copyWith(color: appTheme.gray)),
|
||||
const SizedBox(height: 16),
|
||||
CustomOutlinedButton(
|
||||
text: 'Сменить email',
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.blueA400),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: WidgetStatePropertyAll(BorderSide(color: appTheme.blueA400)),
|
||||
backgroundColor: const WidgetStatePropertyAll(Colors.transparent),
|
||||
),
|
||||
onPressed: () {
|
||||
final verifyEmailCubit = context.read<VerifyEmailCubit>();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ChangeEmailDialog(currentEmail: email, verifyEmailCubit: verifyEmailCubit),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("Запросить код повторно", style: TextStyle(color: Colors.grey)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<VerifyEmailCubit>().sendEmailCode();
|
||||
},
|
||||
child: const Text("Не приходит код?"),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/widgets/loader.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
@@ -9,10 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart';
|
||||
class VerifyScreen extends StatefulWidget {
|
||||
final String passKey;
|
||||
|
||||
const VerifyScreen({
|
||||
super.key,
|
||||
required this.passKey,
|
||||
});
|
||||
const VerifyScreen({super.key, required this.passKey});
|
||||
|
||||
@override
|
||||
State<VerifyScreen> createState() => _VerifyScreenState();
|
||||
@@ -25,6 +23,8 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Report phone verification screen opened
|
||||
RegistrationAnalyticsService.reportRegistrationStep(17); // Phone verification step
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
@@ -36,6 +36,8 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
},
|
||||
onNavigationRequest: (NavigationRequest request) async {
|
||||
if (request.url.contains('user_detailz') && !request.url.contains('change-phone-verify') || request.url.contains('email/verify')) {
|
||||
// Report phone verification success
|
||||
RegistrationAnalyticsService.reportPhoneVerification();
|
||||
GoRouter.of(context).pop();
|
||||
await showModalBottomSheet(
|
||||
useRootNavigator: true,
|
||||
@@ -50,27 +52,15 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
const Text(
|
||||
'Номер телефона\nуспешно привязан',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 20, height: 1, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Lottie.asset(
|
||||
'assets/lottie_animations/success.json',
|
||||
width: 160,
|
||||
height: 160,
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: TextStyle(fontSize: 16, height: 1, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
InkWell(
|
||||
@@ -80,18 +70,11 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
child: Ink(
|
||||
width: MediaQuery.of(context).size.width - 32,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: appTheme.blueA400,
|
||||
),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Закрыть',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -117,10 +100,7 @@ class _VerifyScreenState extends State<VerifyScreen> {
|
||||
child: Stack(
|
||||
children: [
|
||||
WebViewWidget(controller: _controller),
|
||||
if (_isLoading)
|
||||
const Center(
|
||||
child: Loader(),
|
||||
),
|
||||
if (_isLoading) const Center(child: Loader()),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:developer';
|
||||
import 'package:eatvio_app/core/const.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/services/registration_analytics_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
@@ -24,6 +25,8 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
authCubit = sl<AuthCubit>();
|
||||
// Report webview registration started
|
||||
RegistrationAnalyticsService.reportRegistrationStep(18); // Webview registration step
|
||||
_controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..addJavaScriptChannel(
|
||||
@@ -43,10 +46,9 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
if (request.url.contains('profile')) {
|
||||
authCubit.getTokenByCredentials(
|
||||
email!,
|
||||
password!,
|
||||
);
|
||||
// Report webview registration completion
|
||||
RegistrationAnalyticsService.reportRegistrationComplete();
|
||||
authCubit.getTokenByCredentials(email!, password!);
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
@@ -86,12 +88,8 @@ class _WebViewRegistrationScreenState extends State<WebViewRegistrationScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
toolbarHeight: 0,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: WebViewWidget(controller: _controller),
|
||||
),
|
||||
appBar: AppBar(toolbarHeight: 0),
|
||||
body: SafeArea(child: WebViewWidget(controller: _controller)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
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/toasts.dart';
|
||||
import 'package:eatvio_app/core/widgets/custom_outlined_button.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ChangeEmailDialog extends StatefulWidget {
|
||||
final String currentEmail;
|
||||
final VerifyEmailCubit verifyEmailCubit;
|
||||
|
||||
const ChangeEmailDialog({super.key, required this.currentEmail, required this.verifyEmailCubit});
|
||||
|
||||
@override
|
||||
State<ChangeEmailDialog> createState() => _ChangeEmailDialogState();
|
||||
}
|
||||
|
||||
class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final FocusNode _emailFocusNode = FocusNode();
|
||||
bool _isLoading = false;
|
||||
String? _errorText;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_emailController.text = widget.currentEmail;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_emailFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _isValidEmail(String email) {
|
||||
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
|
||||
}
|
||||
|
||||
void _validateEmail() {
|
||||
final email = _emailController.text.trim();
|
||||
if (email.isEmpty) {
|
||||
setState(() {
|
||||
_errorText = 'Введите email';
|
||||
});
|
||||
} else if (!_isValidEmail(email)) {
|
||||
setState(() {
|
||||
_errorText = 'Введите корректный email';
|
||||
});
|
||||
} else if (email == widget.currentEmail) {
|
||||
setState(() {
|
||||
_errorText = 'Новый email должен отличаться от текущего';
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_errorText = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _changeEmail() async {
|
||||
_validateEmail();
|
||||
if (_errorText != null) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await widget.verifyEmailCubit.changeEmail(_emailController.text.trim());
|
||||
// Диалог закроется автоматически при успешной смене email через основной экран
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
showErrorToast('Ошибка при смене email. Попробуйте снова.');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: widget.verifyEmailCubit,
|
||||
child: BlocListener<VerifyEmailCubit, VerifyEmailState>(
|
||||
listener: (context, state) {
|
||||
if (state is VerifyEmailStateChangeEmailSuccess) {
|
||||
Navigator.of(context).pop(); // Закрываем диалог при успешной смене
|
||||
} else if (state is VerifyEmailStateChangeEmailError) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Dialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Сменить email', style: TextStyles.headingXLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 16),
|
||||
Text('Введите новый email для получения кода подтверждения', style: TextStyles.textSMedium.copyWith(color: appTheme.gray)),
|
||||
const SizedBox(height: 20),
|
||||
TextField(
|
||||
controller: _emailController,
|
||||
focusNode: _emailFocusNode,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Введите новый email',
|
||||
hintStyle: TextStyles.textSMedium.copyWith(color: appTheme.gray),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: appTheme.gray),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: appTheme.blueA400),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: appTheme.red),
|
||||
),
|
||||
errorText: _errorText,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
),
|
||||
onChanged: (_) => _validateEmail(),
|
||||
onSubmitted: (_) => _changeEmail(),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
text: 'Отмена',
|
||||
height: 52,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.black),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(appTheme.lightGray0),
|
||||
),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: CustomOutlinedButton(
|
||||
text: _isLoading ? 'Отправка...' : 'Сменить',
|
||||
height: 52,
|
||||
isDisabled: _isLoading,
|
||||
buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700),
|
||||
buttonStyle: ButtonStyle(
|
||||
side: const WidgetStatePropertyAll(BorderSide.none),
|
||||
backgroundColor: WidgetStatePropertyAll(_isLoading ? appTheme.gray : appTheme.blueA400),
|
||||
),
|
||||
onPressed: _isLoading ? null : _changeEmail,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/gestures.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/registration/presentation/widgets/change_email_dialog.dart';
|
||||
import 'package:eatvio_app/features/registration/presentation/bloc/verify_email_cubit/verify_email_cubit.dart';
|
||||
|
||||
class CodeNotReceivedDialog extends StatelessWidget {
|
||||
final String currentEmail;
|
||||
final VerifyEmailCubit verifyEmailCubit;
|
||||
|
||||
const CodeNotReceivedDialog({super.key, required this.currentEmail, required this.verifyEmailCubit});
|
||||
|
||||
void _showChangeEmailDialog(BuildContext context) {
|
||||
Navigator.of(context).pop(); // Закрываем текущий диалог
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ChangeEmailDialog(currentEmail: currentEmail, verifyEmailCubit: verifyEmailCubit),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
backgroundColor: Colors.transparent,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16.0)),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Почему может не приходить код?', style: TextStyles.textLSemibold.copyWith(color: appTheme.black)),
|
||||
const SizedBox(height: 16),
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.gray600),
|
||||
children: [
|
||||
const TextSpan(text: 'Опечатка в email: Если адрес введён неправильно, код не придёт. ', style: TextStyles.textFS13FW400),
|
||||
TextSpan(
|
||||
text: 'Ввести email повторно.',
|
||||
style: TextStyles.textMMedium.copyWith(color: appTheme.blueA400, decoration: TextDecoration.underline),
|
||||
recognizer: TapGestureRecognizer()..onTap = () => _showChangeEmailDialog(context),
|
||||
),
|
||||
const TextSpan(
|
||||
style: TextStyles.textFS13FW400,
|
||||
text:
|
||||
'\n\nПроверьте спам/промо/мусорную папку: Код может попасть туда из-за фильтров почтового провайдера\n\nПодождите 1-2 минуты: Иногда email задерживается из-за сетевых проблем.\n\nУбедитесь в стабильном соединении: Проверьте интернет и уведомления на устройстве.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: appTheme.blueA400,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
child: Text('Понятно', style: TextStyles.textMSemibold.copyWith(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:appmetrica_plugin/appmetrica_plugin.dart';
|
||||
|
||||
class RegistrationAnalyticsService {
|
||||
static const String _eventPrefix = 'registration_step';
|
||||
|
||||
/// Reports registration step completion
|
||||
static void reportRegistrationStep(int stepNumber) {
|
||||
AppMetrica.reportEvent('$_eventPrefix $stepNumber');
|
||||
}
|
||||
|
||||
/// Reports email verification completion
|
||||
static void reportEmailVerification() {
|
||||
AppMetrica.reportEvent('registration_email_verify');
|
||||
}
|
||||
|
||||
/// Reports phone verification completion
|
||||
static void reportPhoneVerification() {
|
||||
AppMetrica.reportEvent('registration_phone_verify');
|
||||
}
|
||||
|
||||
/// Reports registration completion
|
||||
static void reportRegistrationComplete() {
|
||||
AppMetrica.reportEvent('registration_complete');
|
||||
}
|
||||
|
||||
/// Reports registration error
|
||||
static void reportRegistrationError(String error) {
|
||||
AppMetrica.reportEvent('registration_error');
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user